prithivMLmods commited on
Commit
16e11ee
·
verified ·
1 Parent(s): 27215dc

upload app

Browse files
Files changed (9) hide show
  1. Dockerfile +33 -0
  2. Home.tsx +863 -0
  3. index.css +630 -0
  4. index.html +17 -0
  5. index.tsx +11 -0
  6. metadata.json +5 -0
  7. package.json +24 -0
  8. tsconfig.json +29 -0
  9. vite.config.ts +23 -0
Dockerfile ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Stage 1: Build the frontend, and install server dependencies
2
+ FROM node:22 AS builder
3
+
4
+ WORKDIR /app
5
+
6
+ # Copy all files from the current directory
7
+ COPY . ./
8
+ RUN echo "API_KEY=PLACEHOLDER" > ./.env
9
+ RUN echo "GEMINI_API_KEY=PLACEHOLDER" >> ./.env
10
+
11
+ # Install server dependencies
12
+ WORKDIR /app/server
13
+ RUN npm install
14
+
15
+ # Install dependencies and build the frontend
16
+ WORKDIR /app
17
+ RUN mkdir dist
18
+ RUN bash -c 'if [ -f package.json ]; then npm install && npm run build; fi'
19
+
20
+
21
+ # Stage 2: Build the final server image
22
+ FROM node:22
23
+
24
+ WORKDIR /app
25
+
26
+ #Copy server files
27
+ COPY --from=builder /app/server .
28
+ # Copy built frontend assets from the builder stage
29
+ COPY --from=builder /app/dist ./dist
30
+
31
+ EXPOSE 3000
32
+
33
+ CMD ["node", "server.js"]
Home.tsx ADDED
@@ -0,0 +1,863 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+ /* tslint:disable */
6
+ import {GoogleGenAI, Modality} from '@google/genai';
7
+ import {
8
+ ChevronDown,
9
+ Download,
10
+ ImageUp,
11
+ Info,
12
+ LoaderCircle,
13
+ Moon,
14
+ Paintbrush,
15
+ Redo2,
16
+ Sparkles,
17
+ Sun,
18
+ Trash2,
19
+ Undo2,
20
+ X,
21
+ } from 'lucide-react';
22
+ import {useState, useRef, useEffect} from 'react';
23
+
24
+ const ai = new GoogleGenAI({apiKey: process.env.API_KEY});
25
+
26
+ const aspectRatios = ['1:1', '16:9', '9:16', '4:3', '3:4'];
27
+
28
+ type Mode = 'text-to-image' | 'image-to-image' | 'draw-to-image';
29
+ type Theme = 'light' | 'dark';
30
+
31
+ function parseError(error: any): string {
32
+ if (error instanceof Error) {
33
+ const match = error.message.match(/"message":\s*"(.*?)"/);
34
+ if (match && match[1]) {
35
+ return match[1];
36
+ }
37
+ return error.message;
38
+ }
39
+ if (typeof error === 'string') {
40
+ return error;
41
+ }
42
+ return 'An unexpected error occurred.';
43
+ }
44
+
45
+ export default function Home() {
46
+ const [mode, setMode] = useState<Mode>('text-to-image');
47
+ const [prompt, setPrompt] = useState('');
48
+ const [sourceImages, setSourceImages] = useState<string[]>([]);
49
+ const [resultImages, setResultImages] = useState<string[]>([]);
50
+ const [selectedImageIndex, setSelectedImageIndex] = useState(0);
51
+ const [isLoading, setIsLoading] = useState(false);
52
+ const [errorMessage, setErrorMessage] = useState('');
53
+ const [showAdvanced, setShowAdvanced] = useState(true);
54
+ const [aspectRatio, setAspectRatio] = useState('1:1');
55
+ const [downloadType, setDownloadType] = useState<'png' | 'jpeg'>('png');
56
+ const [numberOfImages, setNumberOfImages] = useState(1);
57
+ const [isDropdownOpen, setIsDropdownOpen] = useState(false);
58
+ const [theme, setTheme] = useState<Theme>('light');
59
+ const dropdownRef = useRef<HTMLDivElement>(null);
60
+ const fileInputRef = useRef<HTMLInputElement>(null);
61
+
62
+ // Canvas state
63
+ const canvasRef = useRef<HTMLCanvasElement>(null);
64
+ const [isDrawing, setIsDrawing] = useState(false);
65
+ const [canvasHistory, setCanvasHistory] = useState<string[]>([]);
66
+ const [historyIndex, setHistoryIndex] = useState(-1);
67
+
68
+ useEffect(() => {
69
+ document.documentElement.setAttribute('data-theme', theme);
70
+ }, [theme]);
71
+
72
+ useEffect(() => {
73
+ function handleClickOutside(event: MouseEvent) {
74
+ if (
75
+ dropdownRef.current &&
76
+ !dropdownRef.current.contains(event.target as Node)
77
+ ) {
78
+ setIsDropdownOpen(false);
79
+ }
80
+ }
81
+ document.addEventListener('mousedown', handleClickOutside);
82
+ return () => {
83
+ document.removeEventListener('mousedown', handleClickOutside);
84
+ };
85
+ }, []);
86
+
87
+ const initCanvas = () => {
88
+ const canvas = canvasRef.current;
89
+ if (canvas) {
90
+ const ctx = canvas.getContext('2d');
91
+ if (ctx) {
92
+ ctx.fillStyle = '#FFFFFF';
93
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
94
+ const dataUrl = canvas.toDataURL();
95
+ setCanvasHistory([dataUrl]);
96
+ setHistoryIndex(0);
97
+ }
98
+ }
99
+ };
100
+
101
+ // Initialize canvas when mode changes to draw-to-image
102
+ useEffect(() => {
103
+ if (mode === 'draw-to-image') {
104
+ // A small delay to ensure canvas is in the DOM
105
+ setTimeout(initCanvas, 50);
106
+ }
107
+ }, [mode]);
108
+
109
+ // Load generated image back to canvas in draw mode
110
+ useEffect(() => {
111
+ if (mode === 'draw-to-image' && resultImages.length > 0 && canvasRef.current) {
112
+ const canvas = canvasRef.current;
113
+ const ctx = canvas.getContext('2d');
114
+ const img = new Image();
115
+ img.onload = () => {
116
+ if (ctx) {
117
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
118
+ ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
119
+ saveCanvasState();
120
+ }
121
+ };
122
+ img.src = resultImages[0];
123
+ }
124
+ }, [resultImages]);
125
+
126
+
127
+ const toggleTheme = () => {
128
+ setTheme((prevTheme) => (prevTheme === 'light' ? 'dark' : 'light'));
129
+ };
130
+
131
+ const handleModeChange = (newMode: Mode) => {
132
+ if (mode !== newMode) {
133
+ setMode(newMode);
134
+ setResultImages([]);
135
+ setSelectedImageIndex(0);
136
+ setErrorMessage('');
137
+ setPrompt('');
138
+ setSourceImages([]);
139
+ setNumberOfImages(1);
140
+ }
141
+ setIsDropdownOpen(false);
142
+ };
143
+
144
+ const handleClear = () => {
145
+ setPrompt('');
146
+ setSourceImages([]);
147
+ setResultImages([]);
148
+ setSelectedImageIndex(0);
149
+ setErrorMessage('');
150
+ setShowAdvanced(true);
151
+ setAspectRatio('1:1');
152
+ setDownloadType('png');
153
+ setNumberOfImages(1);
154
+ if (mode === 'draw-to-image') {
155
+ initCanvas();
156
+ }
157
+ };
158
+
159
+ // Canvas history functions
160
+ const saveCanvasState = () => {
161
+ if (!canvasRef.current) return;
162
+ const canvas = canvasRef.current;
163
+ const dataUrl = canvas.toDataURL();
164
+ const newHistory = canvasHistory.slice(0, historyIndex + 1);
165
+ newHistory.push(dataUrl);
166
+ setCanvasHistory(newHistory);
167
+ setHistoryIndex(newHistory.length - 1);
168
+ };
169
+
170
+ const restoreCanvasState = (index: number) => {
171
+ if (!canvasRef.current || !canvasHistory[index]) return;
172
+ const canvas = canvasRef.current;
173
+ const ctx = canvas.getContext('2d');
174
+ const dataUrl = canvasHistory[index];
175
+ const img = new window.Image();
176
+ img.onload = () => {
177
+ if(ctx) {
178
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
179
+ ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
180
+ }
181
+ };
182
+ img.src = dataUrl;
183
+ };
184
+
185
+ const handleUndo = () => {
186
+ if (historyIndex > 0) {
187
+ const newIndex = historyIndex - 1;
188
+ setHistoryIndex(newIndex);
189
+ restoreCanvasState(newIndex);
190
+ }
191
+ };
192
+
193
+ const handleRedo = () => {
194
+ if (historyIndex < canvasHistory.length - 1) {
195
+ const newIndex = historyIndex + 1;
196
+ setHistoryIndex(newIndex);
197
+ restoreCanvasState(newIndex);
198
+ }
199
+ };
200
+
201
+ const getCoordinates = (e: React.MouseEvent | React.TouchEvent) => {
202
+ const canvas = canvasRef.current;
203
+ if(!canvas) return { x: 0, y: 0 };
204
+ const rect = canvas.getBoundingClientRect();
205
+ const scaleX = canvas.width / rect.width;
206
+ const scaleY = canvas.height / rect.height;
207
+
208
+ let clientX, clientY;
209
+ if ('touches' in e.nativeEvent) {
210
+ clientX = e.nativeEvent.touches[0].clientX;
211
+ clientY = e.nativeEvent.touches[0].clientY;
212
+ } else {
213
+ clientX = e.nativeEvent.clientX;
214
+ clientY = e.nativeEvent.clientY;
215
+ }
216
+
217
+ return {
218
+ x: (clientX - rect.left) * scaleX,
219
+ y: (clientY - rect.top) * scaleY,
220
+ };
221
+ };
222
+
223
+ const startDrawing = (e: React.MouseEvent | React.TouchEvent) => {
224
+ if ('touches' in e.nativeEvent) e.preventDefault();
225
+ const canvas = canvasRef.current;
226
+ if(!canvas) return;
227
+ const ctx = canvas.getContext('2d');
228
+ if(!ctx) return;
229
+ const {x, y} = getCoordinates(e);
230
+ ctx.beginPath();
231
+ ctx.moveTo(x, y);
232
+ setIsDrawing(true);
233
+ };
234
+
235
+ const draw = (e: React.MouseEvent | React.TouchEvent) => {
236
+ if (!isDrawing) return;
237
+ if ('touches' in e.nativeEvent) e.preventDefault();
238
+ const canvas = canvasRef.current;
239
+ if(!canvas) return;
240
+ const ctx = canvas.getContext('2d');
241
+ if(!ctx) return;
242
+ const {x, y} = getCoordinates(e);
243
+ ctx.lineWidth = 5;
244
+ ctx.lineCap = 'round';
245
+ ctx.strokeStyle = '#000000';
246
+ ctx.lineTo(x, y);
247
+ ctx.stroke();
248
+ };
249
+
250
+ const stopDrawing = () => {
251
+ if (!isDrawing) return;
252
+ setIsDrawing(false);
253
+ saveCanvasState();
254
+ };
255
+
256
+ useEffect(() => {
257
+ const canvas = canvasRef.current;
258
+ if (!canvas) return;
259
+
260
+ const preventDefault = (e: TouchEvent) => {
261
+ if (isDrawing) {
262
+ e.preventDefault();
263
+ }
264
+ };
265
+
266
+ canvas.addEventListener('touchstart', preventDefault, { passive: false });
267
+ canvas.addEventListener('touchmove', preventDefault, { passive: false });
268
+
269
+ return () => {
270
+ canvas.removeEventListener('touchstart', preventDefault);
271
+ canvas.removeEventListener('touchmove', preventDefault);
272
+ };
273
+ }, [isDrawing]);
274
+
275
+
276
+ const processFiles = (files: FileList) => {
277
+ if (!files || files.length === 0) return;
278
+ const imageFiles = Array.from(files).filter((file) =>
279
+ file.type.startsWith('image/'),
280
+ );
281
+ if (imageFiles.length === 0) return;
282
+
283
+ const readers = imageFiles.map((file) => {
284
+ return new Promise<string>((resolve, reject) => {
285
+ const reader = new FileReader();
286
+ reader.onload = () => resolve(reader.result as string);
287
+ reader.onerror = (error) => reject(error);
288
+ reader.readAsDataURL(file);
289
+ });
290
+ });
291
+
292
+ Promise.all(readers)
293
+ .then((newImages) => {
294
+ setSourceImages((prev) => [...prev, ...newImages]);
295
+ setResultImages([]);
296
+ setSelectedImageIndex(0);
297
+ })
298
+ .catch(() => {
299
+ setErrorMessage('Failed to read one or more files.');
300
+ });
301
+ };
302
+
303
+ const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
304
+ if (e.target.files) {
305
+ processFiles(e.target.files);
306
+ }
307
+ e.target.value = '';
308
+ };
309
+
310
+ const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
311
+ e.preventDefault();
312
+ e.stopPropagation();
313
+ e.currentTarget.classList.remove('dragover');
314
+ if (e.dataTransfer.files) {
315
+ processFiles(e.dataTransfer.files);
316
+ }
317
+ };
318
+
319
+ const removeImage = (indexToRemove: number) => {
320
+ setSourceImages((prev) =>
321
+ prev.filter((_, index) => index !== indexToRemove),
322
+ );
323
+ };
324
+
325
+ const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
326
+ e.preventDefault();
327
+ e.stopPropagation();
328
+ };
329
+
330
+ const handleDragEnter = (e: React.DragEvent<HTMLDivElement>) => {
331
+ e.preventDefault();
332
+ e.stopPropagation();
333
+ e.currentTarget.classList.add('dragover');
334
+ };
335
+
336
+ const handleDragLeave = (e: React.DragEvent<HTMLDivElement>) => {
337
+ e.preventDefault();
338
+ e.stopPropagation();
339
+ e.currentTarget.classList.remove('dragover');
340
+ };
341
+
342
+ const handleSubmit = async (e: React.FormEvent) => {
343
+ e.preventDefault();
344
+ if (!prompt) {
345
+ setErrorMessage('Please enter a prompt to continue.');
346
+ return;
347
+ }
348
+ if (mode === 'image-to-image' && sourceImages.length === 0) {
349
+ setErrorMessage('Please upload at least one source image for editing.');
350
+ return;
351
+ }
352
+
353
+ setIsLoading(true);
354
+ setResultImages([]);
355
+ setSelectedImageIndex(0);
356
+ setErrorMessage('');
357
+
358
+ try {
359
+ if ((mode === 'image-to-image' && sourceImages.length > 0) || mode === 'draw-to-image') {
360
+
361
+ const parts = [];
362
+ if (mode === 'image-to-image') {
363
+ const imageParts = sourceImages.map((imgData) => {
364
+ const mimeType = imgData.substring(
365
+ imgData.indexOf(':') + 1,
366
+ imgData.indexOf(';'),
367
+ );
368
+ const imageB64 = imgData.split(',')[1];
369
+ return {inlineData: {data: imageB64, mimeType}};
370
+ });
371
+ parts.push(...imageParts);
372
+ } else if (mode === 'draw-to-image' && canvasRef.current) {
373
+ const imageB64 = canvasRef.current.toDataURL('image/png').split(',')[1];
374
+ parts.push({inlineData: {data: imageB64, mimeType: 'image/png'}});
375
+ }
376
+
377
+ const textPart = {text: prompt};
378
+ parts.push(textPart);
379
+
380
+ const response = await ai.models.generateContent({
381
+ model: 'gemini-2.5-flash-image',
382
+ contents: {parts},
383
+ config: {
384
+ responseModalities: [Modality.IMAGE, Modality.TEXT],
385
+ },
386
+ });
387
+
388
+ let foundImage = false;
389
+ if(response.candidates && response.candidates[0].content.parts) {
390
+ for (const part of response.candidates[0].content.parts) {
391
+ if (part.inlineData) {
392
+ const imageUrl = `data:${part.inlineData.mimeType};base64,${part.inlineData.data}`;
393
+ setResultImages([imageUrl]);
394
+ foundImage = true;
395
+ break;
396
+ }
397
+ }
398
+ }
399
+
400
+ if (!foundImage) {
401
+ const textMessage = response.text;
402
+ setErrorMessage(
403
+ textMessage ||
404
+ 'The model did not return an image. Please try a different prompt.',
405
+ );
406
+ }
407
+ } else { // Text-to-image
408
+ const response = await ai.models.generateImages({
409
+ model: 'imagen-4.0-generate-001',
410
+ prompt: prompt,
411
+ config: {
412
+ numberOfImages: numberOfImages,
413
+ aspectRatio: aspectRatio as any,
414
+ outputMimeType:
415
+ `image/${downloadType}` as 'image/png' | 'image/jpeg',
416
+ },
417
+ });
418
+
419
+ if (response.generatedImages && response.generatedImages.length > 0) {
420
+ const imageUrls = response.generatedImages.map((img) => {
421
+ const base64Image = img.image.imageBytes;
422
+ return `data:image/${downloadType};base64,${base64Image}`;
423
+ });
424
+ setResultImages(imageUrls);
425
+ } else {
426
+ setErrorMessage(
427
+ 'The model did not return an image. Please try again.',
428
+ );
429
+ }
430
+ }
431
+ } catch (error) {
432
+ console.error('Error during API call:', error);
433
+ setErrorMessage(parseError(error));
434
+ } finally {
435
+ setIsLoading(false);
436
+ }
437
+ };
438
+
439
+ const handleDownload = async (imageUrl: string) => {
440
+ const targetMimeType = `image/${downloadType}`;
441
+ const targetExtension = downloadType;
442
+ const sourceMimeType = imageUrl.match(/data:(image\/.*?);/)?.[1];
443
+
444
+ let finalImageUrl = imageUrl;
445
+
446
+ if (sourceMimeType && sourceMimeType !== targetMimeType) {
447
+ try {
448
+ finalImageUrl = await new Promise((resolve, reject) => {
449
+ const img = new Image();
450
+ img.onload = () => {
451
+ const canvas = document.createElement('canvas');
452
+ canvas.width = img.width;
453
+ canvas.height = img.height;
454
+ const ctx = canvas.getContext('2d');
455
+ if (!ctx) {
456
+ reject(new Error('Could not get canvas context'));
457
+ return;
458
+ }
459
+ if (targetMimeType === 'image/jpeg') {
460
+ ctx.fillStyle = '#FFFFFF';
461
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
462
+ }
463
+ ctx.drawImage(img, 0, 0);
464
+ resolve(canvas.toDataURL(targetMimeType, 0.9));
465
+ };
466
+ img.onerror = () => {
467
+ reject(new Error('Failed to load image for conversion.'));
468
+ };
469
+ img.src = imageUrl;
470
+ });
471
+ } catch (error) {
472
+ setErrorMessage(
473
+ error instanceof Error ? error.message : 'Image conversion failed.',
474
+ );
475
+ return;
476
+ }
477
+ }
478
+
479
+ const link = document.createElement('a');
480
+ link.href = finalImageUrl;
481
+ link.download = `gemini-studio-image.${targetExtension}`;
482
+ document.body.appendChild(link);
483
+ link.click();
484
+ document.body.removeChild(link);
485
+ };
486
+
487
+ return (
488
+ <div className="app-container">
489
+ <header className="app-header">
490
+ <div>
491
+ <h1>Gemini-Image-Studio</h1>
492
+ <p>State-of-the-art image generation and editing model.</p>
493
+ </div>
494
+ <button
495
+ onClick={toggleTheme}
496
+ className="button theme-toggle"
497
+ aria-label="Toggle theme">
498
+ {theme === 'light' ? (
499
+ <Moon className="w-6 h-6" />
500
+ ) : (
501
+ <Sun className="w-6 h-6" />
502
+ )}
503
+ </button>
504
+ </header>
505
+ <main className="app-main">
506
+ <div className="main-grid">
507
+ {/* Input Card */}
508
+ <div className="card">
509
+ <div className="head">
510
+ <span>INPUT</span>
511
+ <div
512
+ className="relative inline-block text-left"
513
+ ref={dropdownRef}>
514
+ <button
515
+ type="button"
516
+ className="button"
517
+ onClick={() => setIsDropdownOpen(!isDropdownOpen)}>
518
+ {mode === 'text-to-image'
519
+ ? 'Text-to-Image'
520
+ : mode === 'image-to-image'
521
+ ? 'Image-to-Image'
522
+ : 'Draw-to-Image'}
523
+ <ChevronDown className="-mr-1 h-5 w-5" />
524
+ </button>
525
+ {isDropdownOpen && (
526
+ <div className="dropdown-panel">
527
+ <div
528
+ onClick={() => handleModeChange('text-to-image')}
529
+ className={`dropdown-item ${
530
+ mode === 'text-to-image' ? 'active' : ''
531
+ }`}
532
+ role="menuitem">
533
+ Text-to-Image
534
+ </div>
535
+ <div
536
+ onClick={() => handleModeChange('image-to-image')}
537
+ className={`dropdown-item ${
538
+ mode === 'image-to-image' ? 'active' : ''
539
+ }`}
540
+ role="menuitem">
541
+ Image-to-Image
542
+ </div>
543
+ <div
544
+ onClick={() => handleModeChange('draw-to-image')}
545
+ className={`dropdown-item ${
546
+ mode === 'draw-to-image' ? 'active' : ''
547
+ }`}
548
+ role="menuitem">
549
+ Draw-to-Image
550
+ </div>
551
+ </div>
552
+ )}
553
+ </div>
554
+ </div>
555
+ <div className="content">
556
+ <form onSubmit={handleSubmit} className="form-content">
557
+ {mode === 'image-to-image' && (
558
+ <div className="form-group">
559
+ <label>Source Image(s)</label>
560
+ <input
561
+ ref={fileInputRef}
562
+ id="file-upload"
563
+ type="file"
564
+ onChange={handleFileChange}
565
+ accept="image/*"
566
+ className="hidden"
567
+ multiple
568
+ />
569
+ <div
570
+ className="uploader"
571
+ onDrop={handleDrop}
572
+ onDragOver={handleDragOver}
573
+ onDragEnter={handleDragEnter}
574
+ onDragLeave={handleDragLeave}>
575
+ {sourceImages.length > 0 ? (
576
+ <div className="image-grid">
577
+ {sourceImages.map((image, index) => (
578
+ <div
579
+ key={index}
580
+ className="relative group aspect-square">
581
+ <img src={image} alt={`Source ${index + 1}`} />
582
+ <button
583
+ type="button"
584
+ onClick={() => removeImage(index)}
585
+ className="image-remove-button"
586
+ aria-label={`Remove image ${index + 1}`}>
587
+ <X className="w-4 h-4" />
588
+ </button>
589
+ </div>
590
+ ))}
591
+ <button
592
+ type="button"
593
+ onClick={() => fileInputRef.current?.click()}
594
+ className="uploader-add-button">
595
+ + Add
596
+ </button>
597
+ </div>
598
+ ) : (
599
+ <button
600
+ type="button"
601
+ onClick={() => fileInputRef.current?.click()}
602
+ className="uploader-placeholder">
603
+ <ImageUp className="w-8 h-8" />
604
+ <span>Click or Drag & Drop</span>
605
+ </button>
606
+ )}
607
+ </div>
608
+ </div>
609
+ )}
610
+
611
+ {mode === 'draw-to-image' && (
612
+ <div className="form-group">
613
+ <label>Canvas</label>
614
+ <div className="canvas-container">
615
+ <canvas
616
+ ref={canvasRef}
617
+ width={960}
618
+ height={540}
619
+ onMouseDown={startDrawing}
620
+ onMouseMove={draw}
621
+ onMouseUp={stopDrawing}
622
+ onMouseLeave={stopDrawing}
623
+ onTouchStart={startDrawing}
624
+ onTouchMove={draw}
625
+ onTouchEnd={stopDrawing}
626
+ />
627
+ <div className="canvas-controls">
628
+ <button type="button" onClick={handleUndo} disabled={historyIndex <= 0} aria-label="Undo">
629
+ <Undo2 className="w-5 h-5" />
630
+ </button>
631
+ <button type="button" onClick={handleRedo} disabled={historyIndex >= canvasHistory.length - 1} aria-label="Redo">
632
+ <Redo2 className="w-5 h-5" />
633
+ </button>
634
+ </div>
635
+ </div>
636
+ </div>
637
+ )}
638
+
639
+
640
+ <div className="form-group">
641
+ <div className="label-with-info">
642
+ <label htmlFor="prompt">Prompt</label>
643
+ {mode === 'text-to-image' && (
644
+ <div className="info-tooltip-container">
645
+ <Info className="w-4 h-4 info-icon" />
646
+ <span className="info-tooltip">
647
+ The model used for this mode is <code>imagen-4.0-generate-001</code>.
648
+ </span>
649
+ </div>
650
+ )}
651
+ {(mode === 'image-to-image' || mode === 'draw-to-image') && (
652
+ <div className="info-tooltip-container">
653
+ <Info className="w-4 h-4 info-icon" />
654
+ <span className="info-tooltip">
655
+ The model used for image editing is <code>gemini-2.5-flash-image</code>.
656
+ </span>
657
+ </div>
658
+ )}
659
+ </div>
660
+ <textarea
661
+ id="prompt"
662
+ value={prompt}
663
+ onChange={(e) => setPrompt(e.target.value)}
664
+ placeholder={
665
+ mode === 'image-to-image'
666
+ ? 'Describe how to edit the image(s)...'
667
+ : mode === 'draw-to-image'
668
+ ? 'Describe the image you want to create from your drawing...'
669
+ : 'A photorealistic cat astronaut on Mars...'
670
+ }
671
+ className="input"
672
+ required
673
+ />
674
+ </div>
675
+
676
+ <div className="form-group">
677
+ <button
678
+ type="button"
679
+ onClick={() => setShowAdvanced(!showAdvanced)}
680
+ className="advanced-toggle">
681
+ <span>Advanced Settings</span>
682
+ <ChevronDown
683
+ className={`w-5 h-5 transition-transform ${
684
+ showAdvanced ? 'transform rotate-180' : ''
685
+ }`}
686
+ />
687
+ </button>
688
+ {showAdvanced && (
689
+ <div className="advanced-panel">
690
+ {mode === 'text-to-image' && (
691
+ <>
692
+ <div className="form-group">
693
+ <div className="label-with-value">
694
+ <label htmlFor="number-of-images">
695
+ Number of Images
696
+ </label>
697
+ <span>{numberOfImages}</span>
698
+ </div>
699
+ <input
700
+ id="number-of-images"
701
+ type="range"
702
+ value={numberOfImages}
703
+ onChange={(e) =>
704
+ setNumberOfImages(parseInt(e.target.value, 10))
705
+ }
706
+ min="1"
707
+ max="4"
708
+ step="1"
709
+ className="slider"
710
+ />
711
+ </div>
712
+ <div className="form-group">
713
+ <label htmlFor="aspect-ratio">Aspect Ratio</label>
714
+ <select
715
+ id="aspect-ratio"
716
+ value={aspectRatio}
717
+ onChange={(e) => setAspectRatio(e.target.value)}
718
+ className="input">
719
+ {aspectRatios.map((ar) => (
720
+ <option key={ar} value={ar}>
721
+ {ar}
722
+ </option>
723
+ ))}
724
+ </select>
725
+ </div>
726
+ </>
727
+ )}
728
+ <div className="form-group">
729
+ <label htmlFor="download-type">Download Format</label>
730
+ <select
731
+ id="download-type"
732
+ value={downloadType}
733
+ onChange={(e) =>
734
+ setDownloadType(e.target.value as 'png' | 'jpeg')
735
+ }
736
+ className="input">
737
+ <option value="png">PNG</option>
738
+ <option value="jpeg">JPEG</option>
739
+ </select>
740
+ </div>
741
+ </div>
742
+ )}
743
+ </div>
744
+
745
+ <div className="form-actions">
746
+ <button
747
+ type="submit"
748
+ disabled={isLoading}
749
+ className="button primary">
750
+ {isLoading ? (
751
+ <>
752
+ <LoaderCircle className="w-5 h-5 animate-spin" />
753
+ <span>
754
+ {mode === 'image-to-image'
755
+ ? 'Editing...'
756
+ : 'Generating...'}
757
+ </span>
758
+ </>
759
+ ) : (
760
+ <>
761
+ {mode === 'text-to-image' && 'Generate Image'}
762
+ {mode === 'image-to-image' && 'Edit Image'}
763
+ {mode === 'draw-to-image' && 'Generate from Drawing'}
764
+ </>
765
+ )}
766
+ </button>
767
+ <button
768
+ type="button"
769
+ onClick={handleClear}
770
+ className="button secondary"
771
+ aria-label="Clear inputs">
772
+ <Trash2 className="w-5 h-5" />
773
+ </button>
774
+ </div>
775
+ </form>
776
+ </div>
777
+ </div>
778
+
779
+ {/* Result Card */}
780
+ <div className="card">
781
+ <div className="head">
782
+ <span>RESULT</span>
783
+ </div>
784
+ <div className="content">
785
+ <div className="result-area">
786
+ {isLoading ? (
787
+ <div className="result-placeholder">
788
+ <LoaderCircle className="w-10 h-10 animate-spin" />
789
+ <p>
790
+ {mode === 'image-to-image' || mode === 'draw-to-image'
791
+ ? 'Processing your image...'
792
+ : `Generating ${numberOfImages} image(s)...`}
793
+ </p>
794
+ </div>
795
+ ) : resultImages.length > 0 ? (
796
+ <div className="showcase-container">
797
+ <div className="main-image-wrapper group">
798
+ <img
799
+ src={resultImages[selectedImageIndex]}
800
+ alt={`Generated result ${selectedImageIndex + 1}`}
801
+ />
802
+ <button
803
+ onClick={() =>
804
+ handleDownload(resultImages[selectedImageIndex])
805
+ }
806
+ className="download-button"
807
+ aria-label="Download image">
808
+ <Download className="w-5 h-5" />
809
+ </button>
810
+ </div>
811
+ {resultImages.length > 1 && (
812
+ <div className="thumbnail-container">
813
+ {resultImages.map((image, index) => (
814
+ <img
815
+ key={index}
816
+ src={image}
817
+ alt={`Thumbnail ${index + 1}`}
818
+ className={`thumbnail-image ${
819
+ index === selectedImageIndex ? 'active' : ''
820
+ }`}
821
+ onClick={() => setSelectedImageIndex(index)}
822
+ />
823
+ ))}
824
+ </div>
825
+ )}
826
+ </div>
827
+ ) : (
828
+ <div className="result-placeholder">
829
+ {mode === 'draw-to-image' ? <Paintbrush className="w-10 h-10" /> : <Sparkles className="w-10 h-10" />}
830
+ <h3>
831
+ {mode === 'draw-to-image'
832
+ ? "Start drawing and see your ideas come to life"
833
+ : "Your result will appear here"
834
+ }
835
+ </h3>
836
+ </div>
837
+ )}
838
+ </div>
839
+ </div>
840
+ </div>
841
+ </div>
842
+ </main>
843
+
844
+ {errorMessage && (
845
+ <div className="modal-backdrop">
846
+ <div className="card modal-card">
847
+ <div className="head">
848
+ <span>REQUEST FAILED</span>
849
+ <button
850
+ onClick={() => setErrorMessage('')}
851
+ className="modal-close-button">
852
+ <X className="w-5 h-5" />
853
+ </button>
854
+ </div>
855
+ <div className="content">
856
+ <p>{errorMessage}</p>
857
+ </div>
858
+ </div>
859
+ </div>
860
+ )}
861
+ </div>
862
+ );
863
+ }
index.css ADDED
@@ -0,0 +1,630 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* @import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@600;700;900&display=swap'); */
2
+
3
+ :root {
4
+ --bg-main: #ffd6e7;
5
+ --bg-card: #ff66a3;
6
+ --bg-header: #ffffff;
7
+ --bg-result: #ffffff;
8
+ --bg-dropdown-active: #ff66a3;
9
+ --bg-advanced: rgba(255, 255, 255, 0.3);
10
+ --bg-uploader: rgba(255, 255, 255, 0.4);
11
+ --bg-button-primary: #4ade80;
12
+ --bg-button-primary-hover: #1ac2ff;
13
+ --bg-button-secondary: #fde047;
14
+ --bg-button-secondary-hover: #f97316;
15
+
16
+ --color-border: #000000;
17
+ --color-text: #000000;
18
+ --color-text-button: #000000;
19
+ }
20
+
21
+ [data-theme="dark"] {
22
+ --bg-main: #2c132c;
23
+ --bg-card: #592659;
24
+ --bg-header: #1a1a1a;
25
+ --bg-result: #2a2a2a;
26
+ --bg-dropdown-active: #7f397f;
27
+ --bg-advanced: rgba(0, 0, 0, 0.3);
28
+ --bg-uploader: rgba(0, 0, 0, 0.4);
29
+
30
+ --color-text: #f0f0f0;
31
+ }
32
+
33
+ body {
34
+ font-family: 'Montserrat', sans-serif;
35
+ margin: 0;
36
+ padding: 0;
37
+ color: var(--color-text);
38
+ background-color: var(--bg-main);
39
+ overflow-x: hidden;
40
+ transition: background-color 0.3s ease;
41
+ }
42
+
43
+ .app-container {
44
+ min-height: 100vh;
45
+ display: flex;
46
+ flex-direction: column;
47
+ }
48
+
49
+ .app-header {
50
+ background: var(--bg-header);
51
+ padding: 2rem 4rem;
52
+ border-bottom: 3px solid var(--color-border);
53
+ color: var(--color-text);
54
+ display: flex;
55
+ justify-content: space-between;
56
+ align-items: center;
57
+ transition: background-color 0.3s ease, color 0.3s ease;
58
+ }
59
+
60
+ .app-header h1 {
61
+ font-size: 2.5rem;
62
+ font-weight: 900;
63
+ margin: 0 0 0.5rem 0;
64
+ }
65
+
66
+ .app-header p {
67
+ font-size: 1rem;
68
+ font-weight: 600;
69
+ margin: 0;
70
+ }
71
+
72
+ .app-main {
73
+ flex-grow: 1;
74
+ background: var(--bg-main);
75
+ padding: 2rem;
76
+ transition: background-color 0.3s ease;
77
+ }
78
+
79
+ .main-grid {
80
+ display: grid;
81
+ grid-template-columns: 1fr;
82
+ gap: 4rem 2rem;
83
+ }
84
+
85
+ @media (min-width: 1024px) {
86
+ .main-grid {
87
+ grid-template-columns: 1fr 1fr;
88
+ gap: 2rem 4rem;
89
+ }
90
+ }
91
+
92
+ /* Card Styles */
93
+ .card {
94
+ font-family: 'Montserrat', sans-serif;
95
+ translate: -6px -6px;
96
+ background: var(--bg-card);
97
+ border: 3px solid var(--color-border);
98
+ box-shadow: 12px 12px 0 var(--color-border);
99
+ transition: all 0.2s ease;
100
+ width: 100%;
101
+ }
102
+
103
+ .card:hover {
104
+ translate: -3px -3px;
105
+ box-shadow: 9px 9px 0 var(--color-border);
106
+ }
107
+
108
+ .head {
109
+ font-size: 14px;
110
+ font-weight: 900;
111
+ width: 100%;
112
+ background: var(--bg-header);
113
+ padding: 8px 12px;
114
+ color: var(--color-text);
115
+ border-bottom: 3px solid var(--color-border);
116
+ display: flex;
117
+ justify-content: space-between;
118
+ align-items: center;
119
+ }
120
+
121
+ .content {
122
+ padding: 1.5rem;
123
+ font-size: 14px;
124
+ font-weight: 600;
125
+ color: var(--color-text);
126
+ }
127
+
128
+ /* Button Styles */
129
+ .button {
130
+ display: inline-flex;
131
+ align-items: center;
132
+ gap: 0.5rem;
133
+ padding: 8px 16px;
134
+ border: 3px solid var(--color-border);
135
+ box-shadow: 4px 4px 0 var(--color-border);
136
+ font-weight: 900;
137
+ transition: all 0.15s ease;
138
+ cursor: pointer;
139
+ font-size: 14px;
140
+ color: var(--color-text-button);
141
+ }
142
+ .button.primary {
143
+ background: var(--bg-button-primary);
144
+ }
145
+ .button.primary:hover {
146
+ background: var(--bg-button-primary-hover);
147
+ }
148
+ .button.secondary {
149
+ background: var(--bg-button-secondary);
150
+ }
151
+ .button.secondary:hover {
152
+ background: var(--bg-button-secondary-hover);
153
+ }
154
+ .theme-toggle {
155
+ padding: 8px;
156
+ background: var(--bg-button-secondary);
157
+ }
158
+ .theme-toggle:hover {
159
+ background: var(--bg-button-secondary-hover);
160
+ }
161
+
162
+ .button:hover {
163
+ translate: 2px 2px;
164
+ box-shadow: 2px 2px 0 var(--color-border);
165
+ }
166
+ .button:active {
167
+ translate: 4px 4px;
168
+ box-shadow: 0 0 0 var(--color-border);
169
+ }
170
+ .button:disabled {
171
+ background: #d1d5db;
172
+ color: #6b7280;
173
+ cursor: not-allowed;
174
+ translate: 0 0;
175
+ box-shadow: 4px 4px 0 var(--color-border);
176
+ }
177
+
178
+ /* Form Styles */
179
+ .form-content {
180
+ display: flex;
181
+ flex-direction: column;
182
+ gap: 1.5rem;
183
+ }
184
+ .form-group {
185
+ display: flex;
186
+ flex-direction: column;
187
+ gap: 0.5rem;
188
+ }
189
+ .form-group label {
190
+ font-weight: 900;
191
+ }
192
+ .form-actions {
193
+ display: flex;
194
+ gap: 1rem;
195
+ align-items: center;
196
+ margin-top: 1rem;
197
+ }
198
+ .form-actions > .button.primary {
199
+ flex-grow: 1;
200
+ }
201
+
202
+ .label-with-value {
203
+ display: flex;
204
+ justify-content: space-between;
205
+ align-items: center;
206
+ }
207
+
208
+ .slider {
209
+ -webkit-appearance: none;
210
+ appearance: none;
211
+ width: 100%;
212
+ height: 12px;
213
+ background: var(--bg-uploader);
214
+ outline: none;
215
+ border: 3px solid var(--color-border);
216
+ padding: 0;
217
+ cursor: pointer;
218
+ }
219
+
220
+ .slider::-webkit-slider-thumb {
221
+ -webkit-appearance: none;
222
+ appearance: none;
223
+ width: 24px;
224
+ height: 24px;
225
+ background: var(--bg-button-secondary);
226
+ cursor: pointer;
227
+ border: 3px solid var(--color-border);
228
+ margin-top: -9px; /* Vertically center thumb */
229
+ }
230
+
231
+ .slider::-moz-range-thumb {
232
+ width: 18px; /* 24px - (2 * 3px border) */
233
+ height: 18px;
234
+ background: var(--bg-button-secondary);
235
+ cursor: pointer;
236
+ border: 3px solid var(--color-border);
237
+ border-radius: 0;
238
+ }
239
+
240
+ .input, textarea.input, select.input {
241
+ width: 100%;
242
+ padding: 10px;
243
+ border: 3px solid var(--color-border);
244
+ background: var(--bg-result);
245
+ color: var(--color-text);
246
+ font-weight: 600;
247
+ font-family: 'Montserrat', sans-serif;
248
+ font-size: 14px;
249
+ box-sizing: border-box;
250
+ border-radius: 0;
251
+ }
252
+ textarea.input {
253
+ resize: vertical;
254
+ min-height: 120px;
255
+ }
256
+ select.input {
257
+ -webkit-appearance: none;
258
+ -moz-appearance: none;
259
+ appearance: none;
260
+ background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
261
+ background-repeat: no-repeat;
262
+ background-position: right 1rem center;
263
+ background-size: 1em;
264
+ }
265
+
266
+ .advanced-toggle {
267
+ display: flex;
268
+ justify-content: space-between;
269
+ width: 100%;
270
+ background: none;
271
+ border: none;
272
+ font-weight: 900;
273
+ cursor: pointer;
274
+ padding: 0;
275
+ color: var(--color-text);
276
+ }
277
+ .advanced-panel {
278
+ background: var(--bg-advanced);
279
+ padding: 1rem;
280
+ border: 3px solid var(--color-border);
281
+ display: flex;
282
+ flex-direction: column;
283
+ gap: 1rem;
284
+ }
285
+
286
+ /* Dropdown */
287
+ .relative { position: relative; }
288
+ .absolute { position: absolute; }
289
+ .dropdown-panel {
290
+ position: absolute;
291
+ background: var(--bg-result);
292
+ border: 3px solid var(--color-border);
293
+ margin-top: 8px;
294
+ box-shadow: 4px 4px 0 var(--color-border);
295
+ z-index: 10;
296
+ width: 100%;
297
+ }
298
+ .dropdown-item {
299
+ padding: 8px 12px;
300
+ font-weight: 700;
301
+ cursor: pointer;
302
+ color: var(--color-text);
303
+ }
304
+ .dropdown-item:hover, .dropdown-item.active {
305
+ background: var(--bg-dropdown-active);
306
+ }
307
+ [data-theme="dark"] .dropdown-item.active,
308
+ [data-theme="dark"] .dropdown-item:hover {
309
+ color: #ffffff;
310
+ }
311
+ [data-theme="dark"] .head .button {
312
+ color: var(--color-text);
313
+ }
314
+
315
+
316
+ /* Uploader */
317
+ .uploader {
318
+ border: 3px dashed var(--color-border);
319
+ background: var(--bg-uploader);
320
+ padding: 1rem;
321
+ cursor: pointer;
322
+ min-height: 100px;
323
+ transition: background-color 0.2s;
324
+ color: var(--color-text);
325
+ }
326
+ .uploader.dragover {
327
+ background: rgba(74, 222, 128, 0.5);
328
+ }
329
+ .uploader-placeholder {
330
+ display: flex;
331
+ flex-direction: column;
332
+ align-items: center;
333
+ justify-content: center;
334
+ width: 100%;
335
+ height: 100%;
336
+ gap: 0.5rem;
337
+ font-weight: 900;
338
+ background: none;
339
+ border: none;
340
+ cursor: pointer;
341
+ color: inherit;
342
+ }
343
+ .image-grid {
344
+ display: grid;
345
+ grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
346
+ gap: 1rem;
347
+ }
348
+ .image-grid .aspect-square {
349
+ aspect-ratio: 1/1;
350
+ position: relative;
351
+ }
352
+ .image-grid img {
353
+ width: 100%;
354
+ height: 100%;
355
+ object-fit: cover;
356
+ border: 2px solid var(--color-border);
357
+ }
358
+ .image-remove-button {
359
+ position: absolute;
360
+ top: 4px;
361
+ right: 4px;
362
+ background: rgba(0,0,0,0.7);
363
+ color: white;
364
+ border: none;
365
+ border-radius: 99px;
366
+ padding: 4px;
367
+ cursor: pointer;
368
+ opacity: 0;
369
+ transition: opacity 0.2s;
370
+ }
371
+ .image-grid .group:hover .image-remove-button {
372
+ opacity: 1;
373
+ }
374
+
375
+ .uploader-add-button {
376
+ border: 3px dashed var(--color-border);
377
+ background: none;
378
+ display: flex;
379
+ align-items: center;
380
+ justify-content: center;
381
+ font-weight: 900;
382
+ font-size: 20px;
383
+ cursor: pointer;
384
+ aspect-ratio: 1/1;
385
+ color: inherit;
386
+ }
387
+
388
+ /* Canvas Styles */
389
+ .canvas-container {
390
+ position: relative;
391
+ width: 100%;
392
+ aspect-ratio: 16/9;
393
+ border: 3px solid var(--color-border);
394
+ background: white;
395
+ }
396
+ .canvas-container canvas {
397
+ position: absolute;
398
+ top: 0;
399
+ left: 0;
400
+ width: 100%;
401
+ height: 100%;
402
+ cursor: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="%23fde047" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>') 12 12, crosshair;
403
+ touch-action: none;
404
+ }
405
+ .canvas-controls {
406
+ position: absolute;
407
+ top: 8px;
408
+ right: 8px;
409
+ display: flex;
410
+ gap: 0.5rem;
411
+ }
412
+ .canvas-controls button {
413
+ background-color: var(--bg-header);
414
+ border: 3px solid var(--color-border);
415
+ box-shadow: 3px 3px 0 var(--color-border);
416
+ padding: 0.5rem;
417
+ cursor: pointer;
418
+ display: flex;
419
+ align-items: center;
420
+ justify-content: center;
421
+ transition: all 0.15s ease;
422
+ }
423
+
424
+ .canvas-controls button:hover {
425
+ translate: 1px 1px;
426
+ box-shadow: 2px 2px 0 var(--color-border);
427
+ }
428
+ .canvas-controls button:active {
429
+ translate: 3px 3px;
430
+ box-shadow: 0 0 0 var(--color-border);
431
+ }
432
+
433
+ .canvas-controls button:disabled {
434
+ opacity: 0.5;
435
+ cursor: not-allowed;
436
+ background-color: #ccc;
437
+ translate: 0;
438
+ box-shadow: 3px 3px 0 var(--color-border);
439
+ }
440
+
441
+
442
+ /* Result Area */
443
+ .result-area {
444
+ width: 100%;
445
+ height: 60vh;
446
+ min-height: 400px;
447
+ background: var(--bg-result);
448
+ border: 3px solid var(--color-border);
449
+ display: flex;
450
+ align-items: center;
451
+ justify-content: center;
452
+ padding: 1rem;
453
+ position: relative;
454
+ overflow: hidden;
455
+ }
456
+ .result-area img {
457
+ max-width: 100%;
458
+ max-height: 100%;
459
+ object-fit: contain;
460
+ }
461
+ .result-placeholder {
462
+ text-align: center;
463
+ display: flex;
464
+ flex-direction: column;
465
+ gap: 1rem;
466
+ align-items: center;
467
+ color: var(--color-text);
468
+ }
469
+ .result-placeholder h3 {
470
+ font-weight: 900;
471
+ }
472
+ .download-button {
473
+ position: absolute;
474
+ top: 8px;
475
+ right: 8px;
476
+ background: rgba(0,0,0,0.7);
477
+ color: white;
478
+ border-radius: 99px;
479
+ padding: 8px;
480
+ cursor: pointer;
481
+ opacity: 0;
482
+ transition: opacity 0.2s;
483
+ border: none;
484
+ display: flex;
485
+ align-items: center;
486
+ justify-content: center;
487
+ }
488
+ .main-image-wrapper.group:hover .download-button {
489
+ opacity: 1;
490
+ }
491
+
492
+ /* Modal */
493
+ .modal-backdrop {
494
+ position: fixed;
495
+ inset: 0;
496
+ background: rgba(0,0,0,0.6);
497
+ display: flex;
498
+ align-items: center;
499
+ justify-content: center;
500
+ z-index: 50;
501
+ backdrop-filter: blur(4px);
502
+ padding: 1rem;
503
+ }
504
+ .modal-card {
505
+ max-width: 500px;
506
+ width: 100%;
507
+ margin-bottom: 0; /* Override card margin */
508
+ }
509
+ .modal-close-button {
510
+ background: none;
511
+ border: none;
512
+ cursor: pointer;
513
+ color: inherit;
514
+ }
515
+
516
+ /* Showcase for multiple images */
517
+ .showcase-container {
518
+ display: flex;
519
+ flex-direction: column;
520
+ width: 100%;
521
+ height: 100%;
522
+ gap: 1rem;
523
+ }
524
+
525
+ .main-image-wrapper {
526
+ flex-grow: 1;
527
+ position: relative;
528
+ display: flex;
529
+ justify-content: center;
530
+ align-items: center;
531
+ overflow: hidden;
532
+ min-height: 0;
533
+ }
534
+
535
+ .thumbnail-container {
536
+ display: flex;
537
+ justify-content: center;
538
+ align-items: center;
539
+ gap: 1rem;
540
+ flex-shrink: 0;
541
+ height: 80px;
542
+ padding: 0 1rem;
543
+ }
544
+
545
+ .thumbnail-image {
546
+ height: 100%;
547
+ aspect-ratio: 1/1;
548
+ object-fit: cover;
549
+ border: 3px solid var(--color-border);
550
+ cursor: pointer;
551
+ opacity: 0.6;
552
+ transition: all 0.2s ease;
553
+ box-shadow: 4px 4px 0 var(--color-border);
554
+ }
555
+
556
+ .thumbnail-image.active,
557
+ .thumbnail-image:hover {
558
+ opacity: 1;
559
+ border-color: var(--bg-button-primary-hover);
560
+ transform: translateY(-4px);
561
+ box-shadow: 6px 6px 0 var(--color-border);
562
+ }
563
+
564
+ /* Lucide icon helpers */
565
+ .w-4 { width: 1rem; }
566
+ .h-4 { height: 1rem; }
567
+ .w-5 { width: 1.25rem; }
568
+ .h-5 { height: 1.25rem; }
569
+ .w-6 { width: 1.5rem; }
570
+ .h-6 { height: 1.5rem; }
571
+ .w-8 { width: 2rem; }
572
+ .h-8 { height: 2rem; }
573
+ .w-10 { width: 2.5rem; }
574
+ .h-10 { height: 2.5rem; }
575
+ .-mr-1 { margin-right: -0.25rem; }
576
+ .animate-spin { animation: spin 1s linear infinite; }
577
+ @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
578
+ .transform { transform: T; } /* Placeholder */
579
+ .rotate-180 { transform: rotate(180deg); }
580
+ .hidden { display: none; }
581
+
582
+ /* Info Tooltip Styles */
583
+ .label-with-info {
584
+ display: flex;
585
+ align-items: center;
586
+ gap: 0.5rem;
587
+ }
588
+
589
+ .info-tooltip-container {
590
+ position: relative;
591
+ display: flex;
592
+ align-items: center;
593
+ cursor: help;
594
+ }
595
+
596
+ .info-icon {
597
+ opacity: 0.7;
598
+ }
599
+
600
+ .info-tooltip {
601
+ position: absolute;
602
+ bottom: 125%;
603
+ left: 50%;
604
+ transform: translateX(-50%);
605
+ background-color: var(--bg-header);
606
+ color: var(--color-text);
607
+ border: 3px solid var(--color-border);
608
+ padding: 0.5rem 0.75rem;
609
+ font-weight: 600;
610
+ font-size: 12px;
611
+ width: max-content;
612
+ max-width: 250px;
613
+ text-align: center;
614
+ z-index: 10;
615
+ opacity: 0;
616
+ visibility: hidden;
617
+ transition: opacity 0.2s ease, visibility 0.2s ease;
618
+ box-shadow: 4px 4px 0 var(--color-border);
619
+ }
620
+
621
+ .info-tooltip code {
622
+ background-color: var(--bg-advanced);
623
+ padding: 2px 4px;
624
+ font-weight: 700;
625
+ }
626
+
627
+ .info-tooltip-container:hover .info-tooltip {
628
+ opacity: 1;
629
+ visibility: visible;
630
+ }
index.html ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script type="importmap">
2
+ {
3
+ "imports": {
4
+ "@google/genai": "https://esm.sh/@google/genai@^0.7.0",
5
+ "react": "https://esm.sh/react@^19.0.0",
6
+ "react/": "https://esm.sh/react@^19.0.0/",
7
+ "react-dom/": "https://esm.sh/react-dom@^19.0.0/",
8
+ "lucide-react": "https://esm.sh/lucide-react@^0.487.0",
9
+ "@tailwindcss/browser": "https://esm.sh/@tailwindcss/browser@^4.1.2"
10
+ }
11
+ }
12
+ </script>
13
+ <link rel="preconnect" href="https://fonts.googleapis.com">
14
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
15
+ <link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@600;700;900&display=swap" rel="stylesheet">
16
+ <div id="root"></div><link rel="stylesheet" href="/index.css">
17
+ <script type="module" src="/index.tsx"></script>
index.tsx ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+ import '@tailwindcss/browser';
6
+
7
+ import ReactDOM from 'react-dom/client';
8
+ import Home from './Home';
9
+
10
+ const root = ReactDOM.createRoot(document.getElementById('root'));
11
+ root.render(<Home />);
metadata.json ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ {
2
+ "name": "Gemini-Image-Studio",
3
+ "description": "A state-of-the-art tool to generate new images from text or edit existing ones, powered by Gemini.",
4
+ "requestFramePermissions": []
5
+ }
package.json ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "gemini-image-studio",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "@google/genai": "^0.7.0",
13
+ "react": "^19.0.0",
14
+ "react-dom": "^19.0.0",
15
+ "lucide-react": "^0.487.0",
16
+ "@tailwindcss/browser": "^4.1.2"
17
+ },
18
+ "devDependencies": {
19
+ "@types/node": "^22.14.0",
20
+ "@vitejs/plugin-react": "^5.0.0",
21
+ "typescript": "~5.8.2",
22
+ "vite": "^6.2.0"
23
+ }
24
+ }
tsconfig.json ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "experimentalDecorators": true,
5
+ "useDefineForClassFields": false,
6
+ "module": "ESNext",
7
+ "lib": [
8
+ "ES2022",
9
+ "DOM",
10
+ "DOM.Iterable"
11
+ ],
12
+ "skipLibCheck": true,
13
+ "types": [
14
+ "node"
15
+ ],
16
+ "moduleResolution": "bundler",
17
+ "isolatedModules": true,
18
+ "moduleDetection": "force",
19
+ "allowJs": true,
20
+ "jsx": "react-jsx",
21
+ "paths": {
22
+ "@/*": [
23
+ "./*"
24
+ ]
25
+ },
26
+ "allowImportingTsExtensions": true,
27
+ "noEmit": true
28
+ }
29
+ }
vite.config.ts ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import path from 'path';
2
+ import { defineConfig, loadEnv } from 'vite';
3
+ import react from '@vitejs/plugin-react';
4
+
5
+ export default defineConfig(({ mode }) => {
6
+ const env = loadEnv(mode, '.', '');
7
+ return {
8
+ server: {
9
+ port: 3000,
10
+ host: '0.0.0.0',
11
+ },
12
+ plugins: [react()],
13
+ define: {
14
+ 'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY),
15
+ 'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY)
16
+ },
17
+ resolve: {
18
+ alias: {
19
+ '@': path.resolve(__dirname, '.'),
20
+ }
21
+ }
22
+ };
23
+ });