prithivMLmods commited on
Commit
3fc0272
·
verified ·
1 Parent(s): 786a8fb
Files changed (1) hide show
  1. Home.tsx +286 -398
Home.tsx CHANGED
@@ -1,133 +1,79 @@
1
  /**
2
  * @license
3
  * SPDX-License-Identifier: Apache-2.0
4
- */
5
  /* tslint:disable */
 
6
  import {
7
- ChevronDown,
8
  Library,
9
  LoaderCircle,
10
  Paintbrush,
11
  PictureInPicture,
12
- Redo2,
13
  SendHorizontal,
14
  Sparkles,
15
  Trash2,
16
- Undo2,
17
  X,
18
  } from 'lucide-react';
19
  import {useEffect, useRef, useState} from 'react';
20
 
21
- // This function remains useful for parsing potential error messages
22
  function parseError(error: string) {
 
 
23
  try {
24
- // Attempt to parse the error as a JSON object which the proxy might send
25
- const errObj = JSON.parse(error);
26
- return errObj.message || error;
27
  } catch (e) {
28
- // If it's not JSON, return the original error string
29
- const regex = /{"error":(.*)}/gm;
30
- const m = regex.exec(error);
31
- try {
32
- const e = m[1];
33
- const err = JSON.parse(e);
34
- return err.message || error;
35
- } catch (e) {
36
- return error;
37
- }
38
  }
39
  }
40
 
41
  export default function Home() {
42
- const canvasRef = useRef<HTMLCanvasElement>(null);
43
- const fileInputRef = useRef<HTMLInputElement>(null);
44
- const backgroundImageRef = useRef<HTMLImageElement | null>(null);
45
- const dropdownRef = useRef<HTMLDivElement>(null);
46
  const [isDrawing, setIsDrawing] = useState(false);
47
  const [prompt, setPrompt] = useState('');
48
  const [generatedImage, setGeneratedImage] = useState<string | null>(null);
49
- const [multiImages, setMultiImages] = useState<
50
- {url: string; type: string}[]
51
- >([]);
52
  const [isLoading, setIsLoading] = useState(false);
53
  const [showErrorModal, setShowErrorModal] = useState(false);
54
  const [errorMessage, setErrorMessage] = useState('');
55
  const [mode, setMode] = useState<
56
  'canvas' | 'editor' | 'imageGen' | 'multi-img-edit'
57
  >('editor');
58
- const [isDropdownOpen, setIsDropdownOpen] = useState(false);
59
  const [apiKey, setApiKey] = useState('');
60
  const [showApiKeyModal, setShowApiKeyModal] = useState(false);
61
-
62
- // State for canvas history
63
- const [history, setHistory] = useState<string[]>([]);
64
- const [historyIndex, setHistoryIndex] = useState(-1);
65
-
66
- // When switching to canvas mode, initialize it and its history
67
- useEffect(() => {
68
- if (mode === 'canvas' && canvasRef.current) {
69
- const canvas = canvasRef.current;
70
- const ctx = canvas.getContext('2d');
71
- ctx.fillStyle = '#FFFFFF';
72
- ctx.fillRect(0, 0, canvas.width, canvas.height);
73
-
74
- // If an image already exists from another mode, draw it.
75
- if (generatedImage) {
76
- const img = new window.Image();
77
- img.onload = () => {
78
- ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
79
- // Save this as the initial state for this session
80
- const dataUrl = canvas.toDataURL();
81
- setHistory([dataUrl]);
82
- setHistoryIndex(0);
83
- };
84
- img.src = generatedImage;
85
- } else {
86
- // Otherwise, save the blank state as initial
87
- const dataUrl = canvas.toDataURL();
88
- setHistory([dataUrl]);
89
- setHistoryIndex(0);
90
- }
91
- }
92
- }, [mode, generatedImage]);
93
 
94
  // Load background image when generatedImage changes
95
  useEffect(() => {
96
  if (generatedImage && canvasRef.current) {
 
97
  const img = new window.Image();
98
  img.onload = () => {
99
  backgroundImageRef.current = img;
100
  drawImageToCanvas();
101
- if (mode === 'canvas') {
102
- // A small timeout to let the draw happen before saving
103
- setTimeout(saveCanvasState, 50);
104
- }
105
  };
106
  img.src = generatedImage;
107
  }
108
- }, [generatedImage, mode]);
109
 
110
- // Handle clicks outside the dropdown to close it
111
  useEffect(() => {
112
- function handleClickOutside(event: MouseEvent) {
113
- if (
114
- dropdownRef.current &&
115
- !dropdownRef.current.contains(event.target as Node)
116
- ) {
117
- setIsDropdownOpen(false);
118
- }
119
  }
120
- document.addEventListener('mousedown', handleClickOutside);
121
- return () => {
122
- document.removeEventListener('mousedown', handleClickOutside);
123
- };
124
- }, [dropdownRef]);
125
 
126
  // Initialize canvas with white background
127
  const initializeCanvas = () => {
128
  const canvas = canvasRef.current;
129
- if (!canvas) return;
130
  const ctx = canvas.getContext('2d');
 
 
131
  ctx.fillStyle = '#FFFFFF';
132
  ctx.fillRect(0, 0, canvas.width, canvas.height);
133
  };
@@ -138,8 +84,12 @@ export default function Home() {
138
 
139
  const canvas = canvasRef.current;
140
  const ctx = canvas.getContext('2d');
 
 
141
  ctx.fillStyle = '#FFFFFF';
142
  ctx.fillRect(0, 0, canvas.width, canvas.height);
 
 
143
  ctx.drawImage(
144
  backgroundImageRef.current,
145
  0,
@@ -149,52 +99,16 @@ export default function Home() {
149
  );
150
  };
151
 
152
- // Canvas history functions
153
- const saveCanvasState = () => {
154
- if (!canvasRef.current) return;
155
- const canvas = canvasRef.current;
156
- const dataUrl = canvas.toDataURL();
157
- const newHistory = history.slice(0, historyIndex + 1);
158
- newHistory.push(dataUrl);
159
- setHistory(newHistory);
160
- setHistoryIndex(newHistory.length - 1);
161
- };
162
-
163
- const restoreCanvasState = (index: number) => {
164
- if (!canvasRef.current || !history[index]) return;
165
- const canvas = canvasRef.current;
166
- const ctx = canvas.getContext('2d');
167
- const dataUrl = history[index];
168
- const img = new window.Image();
169
- img.onload = () => {
170
- ctx.clearRect(0, 0, canvas.width, canvas.height);
171
- ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
172
- };
173
- img.src = dataUrl;
174
- };
175
-
176
- const handleUndo = () => {
177
- if (historyIndex > 0) {
178
- const newIndex = historyIndex - 1;
179
- setHistoryIndex(newIndex);
180
- restoreCanvasState(newIndex);
181
- }
182
- };
183
-
184
- const handleRedo = () => {
185
- if (historyIndex < history.length - 1) {
186
- const newIndex = historyIndex + 1;
187
- setHistoryIndex(newIndex);
188
- restoreCanvasState(newIndex);
189
- }
190
- };
191
-
192
  // Get the correct coordinates based on canvas scaling
193
- const getCoordinates = (e: any) => {
194
- const canvas = canvasRef.current!;
195
  const rect = canvas.getBoundingClientRect();
 
 
196
  const scaleX = canvas.width / rect.width;
197
  const scaleY = canvas.height / rect.height;
 
 
198
  return {
199
  x:
200
  (e.nativeEvent.offsetX ||
@@ -205,26 +119,34 @@ export default function Home() {
205
  };
206
  };
207
 
208
- const startDrawing = (e: any) => {
209
- const canvas = canvasRef.current!;
210
- const ctx = canvas.getContext('2d')!;
211
  const {x, y} = getCoordinates(e);
 
 
212
  if (e.type === 'touchstart') {
213
  e.preventDefault();
214
  }
 
 
215
  ctx.beginPath();
216
  ctx.moveTo(x, y);
217
  setIsDrawing(true);
218
  };
219
 
220
- const draw = (e: any) => {
221
  if (!isDrawing) return;
 
 
222
  if (e.type === 'touchmove') {
223
  e.preventDefault();
224
  }
225
- const canvas = canvasRef.current!;
226
- const ctx = canvas.getContext('2d')!;
 
227
  const {x, y} = getCoordinates(e);
 
228
  ctx.lineWidth = 5;
229
  ctx.lineCap = 'round';
230
  ctx.strokeStyle = '#000000';
@@ -233,22 +155,21 @@ export default function Home() {
233
  };
234
 
235
  const stopDrawing = () => {
236
- if (!isDrawing) return;
237
  setIsDrawing(false);
238
- saveCanvasState();
239
  };
240
 
241
  const handleClear = () => {
 
242
  if (mode === 'canvas' && canvasRef.current) {
243
- initializeCanvas();
244
- const dataUrl = canvasRef.current.toDataURL();
245
- setHistory([dataUrl]);
246
- setHistoryIndex(0);
247
  }
 
248
  setGeneratedImage(null);
249
  setMultiImages([]);
250
  backgroundImageRef.current = null;
251
- setPrompt('');
252
  };
253
 
254
  const processFiles = (files: FileList | null) => {
@@ -258,16 +179,11 @@ export default function Home() {
258
  );
259
  if (fileArray.length === 0) return;
260
 
261
- if (!apiKey) {
262
- setShowApiKeyModal(true);
263
- }
264
-
265
  if (mode === 'multi-img-edit') {
266
  const readers = fileArray.map((file) => {
267
- return new Promise<{url: string; type: string}>((resolve, reject) => {
268
  const reader = new FileReader();
269
- reader.onload = () =>
270
- resolve({url: reader.result as string, type: file.type});
271
  reader.onerror = reject;
272
  reader.readAsDataURL(file);
273
  });
@@ -276,18 +192,17 @@ export default function Home() {
276
  setMultiImages((prev) => [...prev, ...newImages]);
277
  });
278
  } else {
279
- const file = fileArray[0];
280
  const reader = new FileReader();
281
  reader.onload = () => {
282
  setGeneratedImage(reader.result as string);
283
  };
284
- reader.readAsDataURL(file);
285
  }
286
  };
287
 
288
  const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
289
  processFiles(e.target.files);
290
- e.target.value = '';
291
  };
292
 
293
  const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
@@ -314,149 +229,165 @@ export default function Home() {
314
  );
315
  };
316
 
317
- // *** MODIFIED FUNCTION ***
318
- const handleSubmit = async (e: React.FormEvent) => {
319
  e.preventDefault();
320
-
321
- if (!apiKey) {
322
- setShowApiKeyModal(true);
323
- return;
 
 
 
 
 
324
  }
 
325
 
326
- setIsLoading(true);
 
327
 
328
- try {
329
- if (mode === 'editor' && !generatedImage) {
330
- setErrorMessage('Please upload an image to edit.');
331
- setShowErrorModal(true);
332
- return;
333
- }
334
 
335
- if (mode === 'multi-img-edit' && multiImages.length === 0) {
336
- setErrorMessage('Please upload at least one image to edit.');
337
- setShowErrorModal(true);
338
- return;
339
- }
340
 
341
- const parts: any[] = [];
342
-
343
- // This logic for building the 'parts' array is correct.
344
- if (mode === 'imageGen') {
345
- const tempCanvas = document.createElement('canvas');
346
- tempCanvas.width = 960;
347
- tempCanvas.height = 540;
348
- const tempCtx = tempCanvas.getContext('2d')!;
349
- tempCtx.fillStyle = '#FFFFFF';
350
- tempCtx.fillRect(0, 0, tempCanvas.width, tempCanvas.height);
351
- tempCtx.fillStyle = '#FEFEFE';
352
- tempCtx.fillRect(0, 0, 1, 1);
353
- const imageB64 = tempCanvas.toDataURL('image/png').split(',')[1];
354
- parts.push({inlineData: {data: imageB64, mimeType: 'image/png'}});
355
- } else if (mode === 'canvas') {
356
- if (!canvasRef.current) return;
357
- const canvas = canvasRef.current;
358
- const imageB64 = canvas.toDataURL('image/png').split(',')[1];
359
- parts.push({inlineData: {data: imageB64, mimeType: 'image/png'}});
360
- } else if (mode === 'editor' && generatedImage) {
361
- const mimeType = generatedImage.substring(
362
- generatedImage.indexOf(':') + 1,
363
- generatedImage.indexOf(';'),
364
- );
365
- const imageB64 = generatedImage.split(',')[1];
366
- parts.push({inlineData: {data: imageB64, mimeType}});
367
- } else if (mode === 'multi-img-edit') {
368
- multiImages.forEach((img) => {
369
- parts.push({
370
- inlineData: {data: img.url.split(',')[1], mimeType: img.type},
371
  });
372
- });
373
- }
374
-
375
- parts.push({text: prompt});
376
 
377
- // Construct the request body for the Gemini REST API
378
- const requestBody = {
379
- contents: [{role: 'USER', parts}],
380
- };
381
-
382
- // Define the proxy endpoint
383
- const proxyUrl = `/api-proxy/v1beta/models/gemini-2.5-flash-image-preview:generateContent?key=${apiKey}`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
384
 
385
- // Use fetch to send the request to your proxy server
386
- const response = await fetch(proxyUrl, {
387
- method: 'POST',
388
- headers: {
389
- 'Content-Type': 'application/json',
390
- },
391
- body: JSON.stringify(requestBody),
392
- });
393
 
394
- if (!response.ok) {
395
- const errorData = await response.json();
396
- throw new Error(
397
- errorData.error?.message || `HTTP error! status: ${response.status}`,
398
- );
399
- }
400
 
401
- const responseData = await response.json();
 
 
 
 
 
 
402
 
403
- // Process the response
404
- const result = {message: '', imageData: null};
 
 
 
 
 
 
 
 
 
 
 
405
 
406
- if (responseData.candidates && responseData.candidates.length > 0) {
407
- for (const part of responseData.candidates[0].content.parts) {
408
- if (part.text) {
409
- result.message = part.text;
410
- } else if (part.inlineData) {
411
- result.imageData = part.inlineData.data;
 
 
 
 
 
 
 
 
412
  }
413
  }
414
- } else {
415
- throw new Error('Invalid response structure from API.');
416
- }
417
-
418
- if (result.imageData) {
419
- const imageUrl = `data:image/png;base64,${result.imageData}`;
420
- if (mode === 'multi-img-edit') {
421
- setGeneratedImage(imageUrl);
422
- setMultiImages([]);
423
- setMode('editor');
 
 
 
 
424
  } else {
425
- setGeneratedImage(imageUrl);
 
426
  }
427
- } else {
428
- setErrorMessage(
429
- result.message || 'Failed to generate image. Please try again.',
430
- );
431
- setShowErrorModal(true);
432
  }
433
- } catch (error: any) {
434
- console.error('Error submitting:', error);
435
- setErrorMessage(error.message || 'An unexpected error occurred.');
436
- setShowErrorModal(true);
437
- } finally {
438
- setIsLoading(false);
 
439
  }
440
  };
441
 
 
442
  const closeErrorModal = () => {
443
  setShowErrorModal(false);
444
  };
445
 
446
- const handleApiKeySubmit = (e: React.FormEvent) => {
447
- e.preventDefault();
448
- const newApiKey = (e.target as any).apiKey.value;
449
- if (newApiKey) {
450
- setApiKey(newApiKey);
451
- setShowApiKeyModal(false);
452
- }
453
- };
454
-
455
  useEffect(() => {
456
  const canvas = canvasRef.current;
457
  if (!canvas) return;
458
 
459
- const preventTouchDefault = (e: TouchEvent) => {
 
460
  if (isDrawing) {
461
  e.preventDefault();
462
  }
@@ -469,6 +400,7 @@ export default function Home() {
469
  passive: false,
470
  });
471
 
 
472
  return () => {
473
  canvas.removeEventListener('touchstart', preventTouchDefault);
474
  canvas.removeEventListener('touchmove', preventTouchDefault);
@@ -476,15 +408,16 @@ export default function Home() {
476
  }, [isDrawing]);
477
 
478
  const baseDisplayClass =
479
- 'w-full sm:h-[60vh] h-[40vh] min-h-[320px] bg-white/90 touch-none flex items-center justify-center p-4 transition-colors';
480
 
481
  return (
482
  <>
483
- <div className="min-h-screen text-gray-900 flex flex-col justify-start items-center">
484
  <main className="container mx-auto px-3 sm:px-6 py-5 sm:py-10 pb-32 max-w-5xl w-full">
 
485
  <div className="flex flex-col sm:flex-row sm:justify-between sm:items-end mb-2 sm:mb-6 gap-2">
486
  <div>
487
- <h1 className="text-2xl sm:text-3xl font-bold mb-0 leading-tight">
488
  Nano Banana AIO
489
  </h1>
490
  <p className="text-sm sm:text-base text-gray-500 mt-1">
@@ -499,7 +432,7 @@ export default function Home() {
499
  by{' '}
500
  <a
501
  className="underline"
502
- href="https://huggingface.co/prithivMLmods"
503
  target="_blank"
504
  rel="noopener noreferrer">
505
  prithivsakthi-ur
@@ -508,85 +441,46 @@ export default function Home() {
508
  </div>
509
 
510
  <menu className="flex items-center bg-gray-300 rounded-full p-2 shadow-sm self-start sm:self-auto">
511
- <div className="flex flex-wrap justify-center items-center bg-gray-200/80 rounded-full p-1 mr-2">
512
- <div className="relative" ref={dropdownRef}>
513
- <button
514
- onClick={() => setIsDropdownOpen(!isDropdownOpen)}
515
- className={`p-2 sm:px-3 sm:py-1.5 rounded-full text-sm font-medium flex items-center gap-2 transition-colors ${
516
- mode === 'editor' || mode === 'multi-img-edit'
517
- ? 'bg-white shadow'
518
- : 'text-gray-600 hover:bg-gray-300/50'
519
- }`}
520
- aria-haspopup="true"
521
- aria-expanded={isDropdownOpen}>
522
- {mode === 'multi-img-edit' ? (
523
- <>
524
- <Library className="w-4 h-4" />
525
- <span className="hidden sm:inline">Multi-Image</span>
526
- </>
527
- ) : (
528
- <>
529
- <PictureInPicture className="w-4 h-4" />
530
- <span className="hidden sm:inline">Editor</span>
531
- </>
532
- )}
533
- <ChevronDown className="w-4 h-4 opacity-70" />
534
- </button>
535
- {isDropdownOpen && (
536
- <div className="absolute top-full mt-2 w-48 bg-white rounded-lg shadow-xl z-10 border border-gray-200 py-1">
537
- <button
538
- onClick={() => {
539
- setMode('editor');
540
- setIsDropdownOpen(false);
541
- }}
542
- className={`w-full text-left px-3 py-2 text-sm flex items-center gap-3 transition-colors ${
543
- mode === 'editor'
544
- ? 'bg-gray-100 text-gray-900'
545
- : 'text-gray-700 hover:bg-gray-50'
546
- }`}
547
- aria-pressed={mode === 'editor'}>
548
- <PictureInPicture className="w-4 h-4" />
549
- <span>Single Image Edit</span>
550
- </button>
551
- <button
552
- onClick={() => {
553
- setMode('multi-img-edit');
554
- setIsDropdownOpen(false);
555
- }}
556
- className={`w-full text-left px-3 py-2 text-sm flex items-center gap-3 transition-colors ${
557
- mode === 'multi-img-edit'
558
- ? 'bg-gray-100 text-gray-900'
559
- : 'text-gray-700 hover:bg-gray-50'
560
- }`}
561
- aria-pressed={mode === 'multi-img-edit'}>
562
- <Library className="w-4 h-4" />
563
- <span>Multi-Image Edit</span>
564
- </button>
565
- </div>
566
- )}
567
- </div>
568
-
569
  <button
570
  onClick={() => setMode('canvas')}
571
- className={`p-2 sm:px-3 sm:py-1.5 rounded-full text-sm font-medium flex items-center gap-2 transition-colors ${
572
  mode === 'canvas'
573
  ? 'bg-white shadow'
574
  : 'text-gray-600 hover:bg-gray-300/50'
575
  }`}
576
  aria-pressed={mode === 'canvas'}>
577
- <Paintbrush className="w-4 h-4" />
578
- <span className="hidden sm:inline">Canvas</span>
579
  </button>
580
  <button
581
  onClick={() => setMode('imageGen')}
582
- className={`p-2 sm:px-3 sm:py-1.5 rounded-full text-sm font-medium flex items-center gap-2 transition-colors ${
583
  mode === 'imageGen'
584
  ? 'bg-white shadow'
585
  : 'text-gray-600 hover:bg-gray-300/50'
586
  }`}
587
  aria-pressed={mode === 'imageGen'}>
588
- <Sparkles className="w-4 h-4" />
589
- <span className="hidden sm:inline">Image Gen</span>
590
  </button>
591
  </div>
592
  <button
@@ -601,6 +495,7 @@ export default function Home() {
601
  </menu>
602
  </div>
603
 
 
604
  <div className="w-full mb-6">
605
  <input
606
  ref={fileInputRef}
@@ -612,41 +507,23 @@ export default function Home() {
612
  multiple={mode === 'multi-img-edit'}
613
  />
614
  {mode === 'canvas' ? (
615
- <div className="relative w-full">
616
- <canvas
617
- ref={canvasRef}
618
- width={960}
619
- height={540}
620
- onMouseDown={startDrawing}
621
- onMouseMove={draw}
622
- onMouseUp={stopDrawing}
623
- onMouseLeave={stopDrawing}
624
- onTouchStart={startDrawing}
625
- onTouchMove={draw}
626
- onTouchEnd={stopDrawing}
627
- className="border-2 border-black w-full sm:h-[60vh] h-[40vh] min-h-[320px] bg-white/90 touch-none"
628
- style={{
629
- cursor:
630
- "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=\"%23FF0000\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 5v14M5 12h14\"/></svg>') 12 12, crosshair",
631
- }}
632
- />
633
- <div className="absolute top-2 right-2 flex gap-2">
634
- <button
635
- onClick={handleUndo}
636
- disabled={historyIndex <= 0}
637
- className="p-2 bg-white rounded-md shadow disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-100 transition-colors"
638
- aria-label="Undo">
639
- <Undo2 className="w-5 h-5" />
640
- </button>
641
- <button
642
- onClick={handleRedo}
643
- disabled={historyIndex >= history.length - 1}
644
- className="p-2 bg-white rounded-md shadow disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-100 transition-colors"
645
- aria-label="Redo">
646
- <Redo2 className="w-5 h-5" />
647
- </button>
648
- </div>
649
- </div>
650
  ) : mode === 'editor' ? (
651
  <div
652
  className={`${baseDisplayClass} ${
@@ -688,7 +565,7 @@ export default function Home() {
688
  {multiImages.map((image, index) => (
689
  <div key={index} className="relative group aspect-square">
690
  <img
691
- src={image.url}
692
  alt={`upload preview ${index + 1}`}
693
  className="w-full h-full object-cover rounded-md"
694
  />
@@ -722,7 +599,7 @@ export default function Home() {
722
  ) : (
723
  // Image Gen mode display
724
  <div
725
- className={`relative ${baseDisplayClass} border-2 ${
726
  generatedImage ? 'border-black' : 'border-gray-400'
727
  }`}>
728
  {generatedImage ? (
@@ -751,11 +628,9 @@ export default function Home() {
751
  placeholder={
752
  mode === 'imageGen'
753
  ? 'Describe the image you want to create...'
754
- : mode === 'multi-img-edit'
755
- ? 'Describe how to edit or combine the images...'
756
  : 'Add your change...'
757
  }
758
- className="w-full p-3 sm:p-4 pr-12 sm:pr-14 text-sm sm:text-base border-2 border-black bg-white text-gray-800 shadow-sm focus:ring-2 focus:ring-gray-200 focus:outline-none transition-all"
759
  required
760
  />
761
  <button
@@ -777,58 +652,71 @@ export default function Home() {
777
  </div>
778
  </form>
779
  </main>
780
- {/* Error Modal */}
781
- {showErrorModal && (
782
  <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
783
  <div className="bg-white rounded-lg shadow-xl max-w-md w-full p-6">
784
- <div className="flex justify-between items-start mb-4">
785
- <h3 className="text-xl font-bold text-gray-700">
786
- Failed to generate
787
- </h3>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
788
  <button
789
- onClick={closeErrorModal}
790
- className="text-gray-400 hover:text-gray-500">
791
- <X className="w-5 h-5" />
792
  </button>
793
- </div>
794
- <p className="font-medium text-gray-600">
795
- {parseError(errorMessage)}
796
- </p>
797
  </div>
798
  </div>
799
  )}
800
- {/* API Key Modal */}
801
- {showApiKeyModal && (
802
  <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
803
  <div className="bg-white rounded-lg shadow-xl max-w-md w-full p-6">
804
  <div className="flex justify-between items-start mb-4">
805
  <h3 className="text-xl font-bold text-gray-700">
806
- Add Gemini API Key
807
  </h3>
808
  <button
809
- onClick={() => setShowApiKeyModal(false)}
810
  className="text-gray-400 hover:text-gray-500">
811
  <X className="w-5 h-5" />
812
  </button>
813
  </div>
814
- <p className="text-gray-600 mb-4">
815
- Add the API key to process the request. The API key will be
816
- removed if the app page is refreshed or closed.
817
  </p>
818
- <form onSubmit={handleApiKeySubmit}>
819
- <input
820
- type="password"
821
- name="apiKey"
822
- className="w-full p-2 border-2 border-gray-300 rounded-md mb-4"
823
- placeholder="Enter your Gemini API Key"
824
- required
825
- />
826
- <button
827
- type="submit"
828
- className="w-full bg-black text-white p-2 rounded-md hover:bg-gray-800 transition-colors">
829
- Submit
830
- </button>
831
- </form>
832
  </div>
833
  </div>
834
  )}
 
1
  /**
2
  * @license
3
  * SPDX-License-Identifier: Apache-2.0
4
+ */
5
  /* tslint:disable */
6
+ import {Content, GoogleGenAI, Modality} from '@google/genai';
7
  import {
 
8
  Library,
9
  LoaderCircle,
10
  Paintbrush,
11
  PictureInPicture,
 
12
  SendHorizontal,
13
  Sparkles,
14
  Trash2,
 
15
  X,
16
  } from 'lucide-react';
17
  import {useEffect, useRef, useState} from 'react';
18
 
 
19
  function parseError(error: string) {
20
+ const regex = /{"error":(.*)}/gm;
21
+ const m = regex.exec(error);
22
  try {
23
+ const e = m[1];
24
+ const err = JSON.parse(e);
25
+ return err.message || error;
26
  } catch (e) {
27
+ return error;
 
 
 
 
 
 
 
 
 
28
  }
29
  }
30
 
31
  export default function Home() {
32
+ const canvasRef = useRef(null);
33
+ const fileInputRef = useRef(null);
34
+ const backgroundImageRef = useRef(null);
 
35
  const [isDrawing, setIsDrawing] = useState(false);
36
  const [prompt, setPrompt] = useState('');
37
  const [generatedImage, setGeneratedImage] = useState<string | null>(null);
38
+ const [multiImages, setMultiImages] = useState<string[]>([]);
 
 
39
  const [isLoading, setIsLoading] = useState(false);
40
  const [showErrorModal, setShowErrorModal] = useState(false);
41
  const [errorMessage, setErrorMessage] = useState('');
42
  const [mode, setMode] = useState<
43
  'canvas' | 'editor' | 'imageGen' | 'multi-img-edit'
44
  >('editor');
45
+
46
  const [apiKey, setApiKey] = useState('');
47
  const [showApiKeyModal, setShowApiKeyModal] = useState(false);
48
+ const [tempApiKey, setTempApiKey] = useState('');
49
+ const submissionRef = useRef<(() => Promise<void>) | null>(null);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
 
51
  // Load background image when generatedImage changes
52
  useEffect(() => {
53
  if (generatedImage && canvasRef.current) {
54
+ // Use the window.Image constructor to avoid conflict with Next.js Image component
55
  const img = new window.Image();
56
  img.onload = () => {
57
  backgroundImageRef.current = img;
58
  drawImageToCanvas();
 
 
 
 
59
  };
60
  img.src = generatedImage;
61
  }
62
+ }, [generatedImage]);
63
 
64
+ // Initialize canvas with white background when component mounts
65
  useEffect(() => {
66
+ if (canvasRef.current) {
67
+ initializeCanvas();
 
 
 
 
 
68
  }
69
+ }, []);
 
 
 
 
70
 
71
  // Initialize canvas with white background
72
  const initializeCanvas = () => {
73
  const canvas = canvasRef.current;
 
74
  const ctx = canvas.getContext('2d');
75
+
76
+ // Fill canvas with white background
77
  ctx.fillStyle = '#FFFFFF';
78
  ctx.fillRect(0, 0, canvas.width, canvas.height);
79
  };
 
84
 
85
  const canvas = canvasRef.current;
86
  const ctx = canvas.getContext('2d');
87
+
88
+ // Fill with white background first
89
  ctx.fillStyle = '#FFFFFF';
90
  ctx.fillRect(0, 0, canvas.width, canvas.height);
91
+
92
+ // Draw the background image
93
  ctx.drawImage(
94
  backgroundImageRef.current,
95
  0,
 
99
  );
100
  };
101
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  // Get the correct coordinates based on canvas scaling
103
+ const getCoordinates = (e) => {
104
+ const canvas = canvasRef.current;
105
  const rect = canvas.getBoundingClientRect();
106
+
107
+ // Calculate the scaling factor between the internal canvas size and displayed size
108
  const scaleX = canvas.width / rect.width;
109
  const scaleY = canvas.height / rect.height;
110
+
111
+ // Apply the scaling to get accurate coordinates
112
  return {
113
  x:
114
  (e.nativeEvent.offsetX ||
 
119
  };
120
  };
121
 
122
+ const startDrawing = (e) => {
123
+ const canvas = canvasRef.current;
124
+ const ctx = canvas.getContext('2d');
125
  const {x, y} = getCoordinates(e);
126
+
127
+ // Prevent default behavior to avoid scrolling on touch devices
128
  if (e.type === 'touchstart') {
129
  e.preventDefault();
130
  }
131
+
132
+ // Start a new path without clearing the canvas
133
  ctx.beginPath();
134
  ctx.moveTo(x, y);
135
  setIsDrawing(true);
136
  };
137
 
138
+ const draw = (e) => {
139
  if (!isDrawing) return;
140
+
141
+ // Prevent default behavior to avoid scrolling on touch devices
142
  if (e.type === 'touchmove') {
143
  e.preventDefault();
144
  }
145
+
146
+ const canvas = canvasRef.current;
147
+ const ctx = canvas.getContext('2d');
148
  const {x, y} = getCoordinates(e);
149
+
150
  ctx.lineWidth = 5;
151
  ctx.lineCap = 'round';
152
  ctx.strokeStyle = '#000000';
 
155
  };
156
 
157
  const stopDrawing = () => {
 
158
  setIsDrawing(false);
 
159
  };
160
 
161
  const handleClear = () => {
162
+ // For canvas, we must clear the actual drawing strokes
163
  if (mode === 'canvas' && canvasRef.current) {
164
+ const canvas = canvasRef.current;
165
+ const ctx = canvas.getContext('2d');
166
+ ctx.fillStyle = '#FFFFFF';
167
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
168
  }
169
+ // For all modes, we clear the background/uploaded/generated image state
170
  setGeneratedImage(null);
171
  setMultiImages([]);
172
  backgroundImageRef.current = null;
 
173
  };
174
 
175
  const processFiles = (files: FileList | null) => {
 
179
  );
180
  if (fileArray.length === 0) return;
181
 
 
 
 
 
182
  if (mode === 'multi-img-edit') {
183
  const readers = fileArray.map((file) => {
184
+ return new Promise<string>((resolve, reject) => {
185
  const reader = new FileReader();
186
+ reader.onload = () => resolve(reader.result as string);
 
187
  reader.onerror = reject;
188
  reader.readAsDataURL(file);
189
  });
 
192
  setMultiImages((prev) => [...prev, ...newImages]);
193
  });
194
  } else {
195
+ // single file modes
196
  const reader = new FileReader();
197
  reader.onload = () => {
198
  setGeneratedImage(reader.result as string);
199
  };
200
+ reader.readAsDataURL(fileArray[0]);
201
  }
202
  };
203
 
204
  const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
205
  processFiles(e.target.files);
 
206
  };
207
 
208
  const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
 
229
  );
230
  };
231
 
232
+ const handleApiKeySubmit = async (e: React.FormEvent) => {
 
233
  e.preventDefault();
234
+ if (tempApiKey) {
235
+ setErrorMessage(''); // Clear previous error message
236
+ setApiKey(tempApiKey);
237
+ setShowApiKeyModal(false);
238
+ setTempApiKey('');
239
+ if (submissionRef.current) {
240
+ await submissionRef.current();
241
+ submissionRef.current = null;
242
+ }
243
  }
244
+ };
245
 
246
+ const handleSubmit = async (e) => {
247
+ e.preventDefault();
248
 
249
+ // Pre-submission checks that don't require an API key
250
+ if (mode === 'editor' && !generatedImage) {
251
+ setErrorMessage('Please upload an image to edit.');
252
+ setShowErrorModal(true);
253
+ return;
254
+ }
255
 
256
+ if (mode === 'multi-img-edit' && multiImages.length === 0) {
257
+ setErrorMessage('Please upload at least one image to edit.');
258
+ setShowErrorModal(true);
259
+ return;
260
+ }
261
 
262
+ const submitAction = async () => {
263
+ setIsLoading(true);
264
+ try {
265
+ const ai = new GoogleGenAI({apiKey});
266
+
267
+ if (mode === 'imageGen') {
268
+ const response = await ai.models.generateImages({
269
+ model: 'imagen-4.0-generate-001',
270
+ prompt: prompt,
271
+ config: {
272
+ numberOfImages: 1,
273
+ },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
274
  });
 
 
 
 
275
 
276
+ const base64ImageBytes: string =
277
+ response.generatedImages[0].image.imageBytes;
278
+ const imageUrl = `data:image/png;base64,${base64ImageBytes}`;
279
+ setGeneratedImage(imageUrl);
280
+ } else {
281
+ // Editor, Canvas, and Multi-Img modes
282
+ const parts: any[] = [];
283
+ if (mode === 'canvas') {
284
+ if (!canvasRef.current) return;
285
+ const canvas = canvasRef.current;
286
+ const tempCanvas = document.createElement('canvas');
287
+ tempCanvas.width = canvas.width;
288
+ tempCanvas.height = canvas.height;
289
+ const tempCtx = tempCanvas.getContext('2d');
290
+ tempCtx.fillStyle = '#FFFFFF';
291
+ tempCtx.fillRect(0, 0, tempCanvas.width, tempCanvas.height);
292
+ tempCtx.drawImage(canvas, 0, 0);
293
+ const imageB64 = tempCanvas.toDataURL('image/png').split(',')[1];
294
+ parts.push({inlineData: {data: imageB64, mimeType: 'image/png'}});
295
+ } else if (mode === 'editor') {
296
+ const imageB64 = generatedImage.split(',')[1];
297
+ parts.push({inlineData: {data: imageB64, mimeType: 'image/png'}});
298
+ } else if (mode === 'multi-img-edit') {
299
+ multiImages.forEach((img) => {
300
+ parts.push({
301
+ inlineData: {data: img.split(',')[1], mimeType: 'image/png'},
302
+ });
303
+ });
304
+ }
305
 
306
+ parts.push({text: prompt});
 
 
 
 
 
 
 
307
 
308
+ const contents: Content[] = [{role: 'USER', parts}];
 
 
 
 
 
309
 
310
+ const response = await ai.models.generateContent({
311
+ model: 'gemini-2.5-flash-image-preview',
312
+ contents,
313
+ config: {
314
+ responseModalities: [Modality.TEXT, Modality.IMAGE],
315
+ },
316
+ });
317
 
318
+ const data = {
319
+ success: true,
320
+ message: '',
321
+ imageData: null,
322
+ error: undefined,
323
+ };
324
+ for (const part of response.candidates[0].content.parts) {
325
+ if (part.text) {
326
+ data.message = part.text;
327
+ } else if (part.inlineData) {
328
+ data.imageData = part.inlineData.data;
329
+ }
330
+ }
331
 
332
+ if (data.imageData) {
333
+ const imageUrl = `data:image/png;base64,${data.imageData}`;
334
+ if (mode === 'multi-img-edit') {
335
+ setGeneratedImage(imageUrl);
336
+ setMultiImages([]);
337
+ setMode('editor');
338
+ } else {
339
+ setGeneratedImage(imageUrl);
340
+ }
341
+ } else {
342
+ setErrorMessage(
343
+ data.message || 'Failed to generate image. Please try again.',
344
+ );
345
+ setShowErrorModal(true);
346
  }
347
  }
348
+ } catch (error) {
349
+ console.error('Error submitting:', error);
350
+ const parsedError = parseError(error.message);
351
+ if (
352
+ parsedError &&
353
+ (parsedError.includes('API_KEY_INVALID') ||
354
+ parsedError.includes('API key not valid'))
355
+ ) {
356
+ setErrorMessage(
357
+ 'Your API key is not valid. Please enter a valid key to continue.',
358
+ );
359
+ setApiKey(''); // Clear the invalid key
360
+ setShowApiKeyModal(true); // Re-show the modal
361
+ // submissionRef is not cleared, so user can retry.
362
  } else {
363
+ setErrorMessage(parsedError || 'An unexpected error occurred.');
364
+ setShowErrorModal(true);
365
  }
366
+ } finally {
367
+ setIsLoading(false);
 
 
 
368
  }
369
+ };
370
+
371
+ if (!apiKey) {
372
+ submissionRef.current = submitAction;
373
+ setShowApiKeyModal(true);
374
+ } else {
375
+ await submitAction();
376
  }
377
  };
378
 
379
+ // Close the error modal
380
  const closeErrorModal = () => {
381
  setShowErrorModal(false);
382
  };
383
 
384
+ // Add touch event prevention function
 
 
 
 
 
 
 
 
385
  useEffect(() => {
386
  const canvas = canvasRef.current;
387
  if (!canvas) return;
388
 
389
+ // Function to prevent default touch behavior on canvas
390
+ const preventTouchDefault = (e) => {
391
  if (isDrawing) {
392
  e.preventDefault();
393
  }
 
400
  passive: false,
401
  });
402
 
403
+ // Remove event listener when component unmounts
404
  return () => {
405
  canvas.removeEventListener('touchstart', preventTouchDefault);
406
  canvas.removeEventListener('touchmove', preventTouchDefault);
 
408
  }, [isDrawing]);
409
 
410
  const baseDisplayClass =
411
+ 'w-full sm:h-[60vh] h-[30vh] min-h-[320px] bg-white/90 touch-none flex items-center justify-center p-4 transition-colors';
412
 
413
  return (
414
  <>
415
+ <div className="min-h-screen notebook-paper-bg text-gray-900 flex flex-col justify-start items-center">
416
  <main className="container mx-auto px-3 sm:px-6 py-5 sm:py-10 pb-32 max-w-5xl w-full">
417
+ {/* Header section with title and tools */}
418
  <div className="flex flex-col sm:flex-row sm:justify-between sm:items-end mb-2 sm:mb-6 gap-2">
419
  <div>
420
+ <h1 className="text-2xl sm:text-3xl font-bold mb-0 leading-tight font-mega">
421
  Nano Banana AIO
422
  </h1>
423
  <p className="text-sm sm:text-base text-gray-500 mt-1">
 
432
  by{' '}
433
  <a
434
  className="underline"
435
+ href="https://www.linkedin.com/in/prithiv-sakthi/"
436
  target="_blank"
437
  rel="noopener noreferrer">
438
  prithivsakthi-ur
 
441
  </div>
442
 
443
  <menu className="flex items-center bg-gray-300 rounded-full p-2 shadow-sm self-start sm:self-auto">
444
+ <div className="flex items-center bg-gray-200/80 rounded-full p-1 mr-2">
445
+ <button
446
+ onClick={() => setMode('editor')}
447
+ className={`px-3 py-1.5 rounded-full text-sm font-medium flex items-center gap-2 transition-colors ${
448
+ mode === 'editor'
449
+ ? 'bg-white shadow'
450
+ : 'text-gray-600 hover:bg-gray-300/50'
451
+ }`}
452
+ aria-pressed={mode === 'editor'}>
453
+ <PictureInPicture className="w-4 h-4" /> Editor
454
+ </button>
455
+ <button
456
+ onClick={() => setMode('multi-img-edit')}
457
+ className={`px-3 py-1.5 rounded-full text-sm font-medium flex items-center gap-2 transition-colors ${
458
+ mode === 'multi-img-edit'
459
+ ? 'bg-white shadow'
460
+ : 'text-gray-600 hover:bg-gray-300/50'
461
+ }`}
462
+ aria-pressed={mode === 'multi-img-edit'}>
463
+ <Library className="w-4 h-4" /> Multi-Image
464
+ </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
465
  <button
466
  onClick={() => setMode('canvas')}
467
+ className={`px-3 py-1.5 rounded-full text-sm font-medium flex items-center gap-2 transition-colors ${
468
  mode === 'canvas'
469
  ? 'bg-white shadow'
470
  : 'text-gray-600 hover:bg-gray-300/50'
471
  }`}
472
  aria-pressed={mode === 'canvas'}>
473
+ <Paintbrush className="w-4 h-4" /> Canvas
 
474
  </button>
475
  <button
476
  onClick={() => setMode('imageGen')}
477
+ className={`px-3 py-1.5 rounded-full text-sm font-medium flex items-center gap-2 transition-colors ${
478
  mode === 'imageGen'
479
  ? 'bg-white shadow'
480
  : 'text-gray-600 hover:bg-gray-300/50'
481
  }`}
482
  aria-pressed={mode === 'imageGen'}>
483
+ <Sparkles className="w-4 h-4" /> Image Gen
 
484
  </button>
485
  </div>
486
  <button
 
495
  </menu>
496
  </div>
497
 
498
+ {/* Main display section */}
499
  <div className="w-full mb-6">
500
  <input
501
  ref={fileInputRef}
 
507
  multiple={mode === 'multi-img-edit'}
508
  />
509
  {mode === 'canvas' ? (
510
+ <canvas
511
+ ref={canvasRef}
512
+ width={960}
513
+ height={540}
514
+ onMouseDown={startDrawing}
515
+ onMouseMove={draw}
516
+ onMouseUp={stopDrawing}
517
+ onMouseLeave={stopDrawing}
518
+ onTouchStart={startDrawing}
519
+ onTouchMove={draw}
520
+ onTouchEnd={stopDrawing}
521
+ className="border-2 border-black w-full sm:h-[60vh] h-[30vh] min-h-[320px] bg-white/90 touch-none"
522
+ style={{
523
+ cursor:
524
+ "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=\"%23FF0000\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 5v14M5 12h14\"/></svg>') 12 12, crosshair",
525
+ }}
526
+ />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
527
  ) : mode === 'editor' ? (
528
  <div
529
  className={`${baseDisplayClass} ${
 
565
  {multiImages.map((image, index) => (
566
  <div key={index} className="relative group aspect-square">
567
  <img
568
+ src={image}
569
  alt={`upload preview ${index + 1}`}
570
  className="w-full h-full object-cover rounded-md"
571
  />
 
599
  ) : (
600
  // Image Gen mode display
601
  <div
602
+ className={`${baseDisplayClass} border-2 ${
603
  generatedImage ? 'border-black' : 'border-gray-400'
604
  }`}>
605
  {generatedImage ? (
 
628
  placeholder={
629
  mode === 'imageGen'
630
  ? 'Describe the image you want to create...'
 
 
631
  : 'Add your change...'
632
  }
633
+ className="w-full p-3 sm:p-4 pr-12 sm:pr-14 text-sm sm:text-base border-2 border-black bg-white text-gray-800 shadow-sm focus:ring-2 focus:ring-gray-200 focus:outline-none transition-all font-mono"
634
  required
635
  />
636
  <button
 
652
  </div>
653
  </form>
654
  </main>
655
+ {/* API Key Modal */}
656
+ {showApiKeyModal && (
657
  <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
658
  <div className="bg-white rounded-lg shadow-xl max-w-md w-full p-6">
659
+ <form onSubmit={handleApiKeySubmit}>
660
+ <div className="flex justify-between items-start mb-4">
661
+ <h3 className="text-xl font-bold text-gray-700">
662
+ Add Gemini API Key
663
+ </h3>
664
+ <button
665
+ type="button"
666
+ onClick={() => {
667
+ setShowApiKeyModal(false);
668
+ setErrorMessage('');
669
+ submissionRef.current = null; // Cancel submission
670
+ }}
671
+ className="text-gray-400 hover:text-gray-500"
672
+ aria-label="Close">
673
+ <X className="w-5 h-5" />
674
+ </button>
675
+ </div>
676
+ <p className="text-gray-600 mb-4 text-sm">
677
+ Add the API key to process the request. The API key will be
678
+ removed if the app page is refreshed or closed.
679
+ </p>
680
+ {errorMessage && (
681
+ <p className="text-red-500 text-sm mb-2 font-medium">
682
+ {errorMessage}
683
+ </p>
684
+ )}
685
+ <input
686
+ type="password"
687
+ value={tempApiKey}
688
+ onChange={(e) => setTempApiKey(e.target.value)}
689
+ placeholder="Enter your Gemini API Key"
690
+ className="w-full p-2 mb-4 border-2 border-gray-300 rounded focus:ring-2 focus:ring-gray-400 focus:outline-none transition-all"
691
+ required
692
+ aria-label="Gemini API Key"
693
+ />
694
  <button
695
+ type="submit"
696
+ className="w-full p-2 bg-black text-white rounded hover:bg-gray-800 transition-colors">
697
+ Save & Continue
698
  </button>
699
+ </form>
 
 
 
700
  </div>
701
  </div>
702
  )}
703
+ {/* Error Modal */}
704
+ {showErrorModal && (
705
  <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
706
  <div className="bg-white rounded-lg shadow-xl max-w-md w-full p-6">
707
  <div className="flex justify-between items-start mb-4">
708
  <h3 className="text-xl font-bold text-gray-700">
709
+ Failed to generate
710
  </h3>
711
  <button
712
+ onClick={closeErrorModal}
713
  className="text-gray-400 hover:text-gray-500">
714
  <X className="w-5 h-5" />
715
  </button>
716
  </div>
717
+ <p className="font-medium text-gray-600">
718
+ {parseError(errorMessage)}
 
719
  </p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
720
  </div>
721
  </div>
722
  )}