prithivMLmods commited on
Commit
cf045cb
·
verified ·
1 Parent(s): ec594a9

update app

Browse files
Files changed (1) hide show
  1. Home.tsx +318 -285
Home.tsx CHANGED
@@ -2,10 +2,9 @@
2
  * @license
3
  * SPDX-License-Identifier: Apache-2.0
4
  */
5
- /* tlint:disable */
6
- import {Content, GoogleGenAI, Modality} from '@google/genai';
7
  import {
8
- Eraser,
9
  Library,
10
  LoaderCircle,
11
  Paintbrush,
@@ -19,78 +18,110 @@ import {
19
  } from 'lucide-react';
20
  import {useEffect, useRef, useState} from 'react';
21
 
 
22
  function parseError(error: string) {
23
- const regex = /{"error":(.*)}/gm;
24
- const m = regex.exec(error);
25
  try {
26
- const e = m[1];
27
- const err = JSON.parse(e);
28
- return err.message || error;
29
  } catch (e) {
30
- return error;
 
 
 
 
 
 
 
 
 
31
  }
32
  }
33
 
34
  export default function Home() {
35
- const canvasRef = useRef(null);
36
- const fileInputRef = useRef(null);
37
- const backgroundImageRef = useRef(null);
 
38
  const [isDrawing, setIsDrawing] = useState(false);
39
  const [prompt, setPrompt] = useState('');
40
  const [generatedImage, setGeneratedImage] = useState<string | null>(null);
41
- const [multiImages, setMultiImages] = useState<string[]>([]);
 
 
42
  const [isLoading, setIsLoading] = useState(false);
43
  const [showErrorModal, setShowErrorModal] = useState(false);
44
  const [errorMessage, setErrorMessage] = useState('');
45
  const [mode, setMode] = useState<
46
  'canvas' | 'editor' | 'imageGen' | 'multi-img-edit'
47
  >('editor');
48
-
49
  const [apiKey, setApiKey] = useState('');
50
  const [showApiKeyModal, setShowApiKeyModal] = useState(false);
51
- const [tempApiKey, setTempApiKey] = useState('');
52
- const submissionRef = useRef<(() => Promise<void>) | null>(null);
53
 
54
  // State for canvas history
55
  const [history, setHistory] = useState<string[]>([]);
56
  const [historyIndex, setHistoryIndex] = useState(-1);
57
- const [drawingTool, setDrawingTool] = useState<'pen' | 'eraser'>('pen');
58
 
59
  // When switching to canvas mode, initialize it and its history
60
  useEffect(() => {
61
  if (mode === 'canvas' && canvasRef.current) {
62
  const canvas = canvasRef.current;
63
- // Draw current background if it exists
64
- if (backgroundImageRef.current) {
65
- drawImageToCanvas();
 
 
 
 
 
 
 
 
 
 
 
 
66
  } else {
67
- initializeCanvas();
 
 
 
68
  }
69
- // Set this as the initial state for this canvas session
70
- const dataUrl = canvas.toDataURL();
71
- setHistory([dataUrl]);
72
- setHistoryIndex(0);
73
- setDrawingTool('pen'); // Reset tool
74
  }
75
- }, [mode]);
76
 
77
  // Load background image when generatedImage changes
78
  useEffect(() => {
79
- if (generatedImage) {
80
  const img = new window.Image();
81
  img.onload = () => {
82
  backgroundImageRef.current = img;
83
- if (mode === 'canvas' && canvasRef.current) {
84
- drawImageToCanvas();
85
- // This new image becomes a new state in history
86
- saveCanvasState();
87
  }
88
  };
89
  img.src = generatedImage;
90
- } else {
91
- backgroundImageRef.current = null;
92
  }
93
- }, [generatedImage]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
 
95
  // Initialize canvas with white background
96
  const initializeCanvas = () => {
@@ -104,6 +135,7 @@ export default function Home() {
104
  // Draw the background image to the canvas
105
  const drawImageToCanvas = () => {
106
  if (!canvasRef.current || !backgroundImageRef.current) return;
 
107
  const canvas = canvasRef.current;
108
  const ctx = canvas.getContext('2d');
109
  ctx.fillStyle = '#FFFFFF';
@@ -119,7 +151,7 @@ export default function Home() {
119
 
120
  // Canvas history functions
121
  const saveCanvasState = () => {
122
- if (!canvasRef.current || mode !== 'canvas') return;
123
  const canvas = canvasRef.current;
124
  const dataUrl = canvas.toDataURL();
125
  const newHistory = history.slice(0, historyIndex + 1);
@@ -158,8 +190,8 @@ export default function Home() {
158
  };
159
 
160
  // Get the correct coordinates based on canvas scaling
161
- const getCoordinates = (e) => {
162
- const canvas = canvasRef.current;
163
  const rect = canvas.getBoundingClientRect();
164
  const scaleX = canvas.width / rect.width;
165
  const scaleY = canvas.height / rect.height;
@@ -173,9 +205,9 @@ export default function Home() {
173
  };
174
  };
175
 
176
- const startDrawing = (e) => {
177
- const canvas = canvasRef.current;
178
- const ctx = canvas.getContext('2d');
179
  const {x, y} = getCoordinates(e);
180
  if (e.type === 'touchstart') {
181
  e.preventDefault();
@@ -185,24 +217,17 @@ export default function Home() {
185
  setIsDrawing(true);
186
  };
187
 
188
- const draw = (e) => {
189
  if (!isDrawing) return;
190
  if (e.type === 'touchmove') {
191
  e.preventDefault();
192
  }
193
- const canvas = canvasRef.current;
194
- const ctx = canvas.getContext('2d');
195
  const {x, y} = getCoordinates(e);
196
-
197
  ctx.lineCap = 'round';
198
- if (drawingTool === 'pen') {
199
- ctx.strokeStyle = '#000000';
200
- ctx.lineWidth = 5;
201
- } else {
202
- // Eraser
203
- ctx.strokeStyle = '#FFFFFF';
204
- ctx.lineWidth = 20;
205
- }
206
  ctx.lineTo(x, y);
207
  ctx.stroke();
208
  };
@@ -223,6 +248,7 @@ export default function Home() {
223
  setGeneratedImage(null);
224
  setMultiImages([]);
225
  backgroundImageRef.current = null;
 
226
  };
227
 
228
  const processFiles = (files: FileList | null) => {
@@ -232,11 +258,16 @@ export default function Home() {
232
  );
233
  if (fileArray.length === 0) return;
234
 
 
 
 
 
235
  if (mode === 'multi-img-edit') {
236
  const readers = fileArray.map((file) => {
237
- return new Promise<string>((resolve, reject) => {
238
  const reader = new FileReader();
239
- reader.onload = () => resolve(reader.result as string);
 
240
  reader.onerror = reject;
241
  reader.readAsDataURL(file);
242
  });
@@ -245,16 +276,18 @@ export default function Home() {
245
  setMultiImages((prev) => [...prev, ...newImages]);
246
  });
247
  } else {
 
248
  const reader = new FileReader();
249
  reader.onload = () => {
250
  setGeneratedImage(reader.result as string);
251
  };
252
- reader.readAsDataURL(fileArray[0]);
253
  }
254
  };
255
 
256
  const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
257
  processFiles(e.target.files);
 
258
  };
259
 
260
  const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
@@ -281,145 +314,128 @@ export default function Home() {
281
  );
282
  };
283
 
284
- const handleApiKeySubmit = async (e: React.FormEvent) => {
285
- e.preventDefault();
286
- if (tempApiKey) {
287
- setErrorMessage('');
288
- setApiKey(tempApiKey);
289
- setShowApiKeyModal(false);
290
- setTempApiKey('');
291
- if (submissionRef.current) {
292
- await submissionRef.current();
293
- submissionRef.current = null;
294
- }
295
- }
296
- };
297
-
298
- const handleSubmit = async (e) => {
299
  e.preventDefault();
300
 
301
- if (mode === 'editor' && !generatedImage) {
302
- setErrorMessage('Please upload an image to edit.');
303
- setShowErrorModal(true);
304
  return;
305
  }
306
 
307
- if (mode === 'multi-img-edit' && multiImages.length === 0) {
308
- setErrorMessage('Please upload at least one image to edit.');
309
- setShowErrorModal(true);
310
- return;
311
- }
312
 
313
- const submitAction = async () => {
314
- setIsLoading(true);
315
- try {
316
- const ai = new GoogleGenAI(apiKey);
317
-
318
- if (mode === 'imageGen') {
319
- const response = await ai.models.generateImages({
320
- model: 'imagen-4.0-generate-001',
321
- prompt: prompt,
322
- config: {
323
- numberOfImages: 1,
324
- },
325
- });
326
 
327
- const base64ImageBytes: string =
328
- response.generatedImages[0].image.imageBytes;
329
- const imageUrl = `data:image/png;base64,${base64ImageBytes}`;
330
- setGeneratedImage(imageUrl);
331
- } else {
332
- const parts: any[] = [];
333
- if (mode === 'canvas') {
334
- if (!canvasRef.current) return;
335
- const canvas = canvasRef.current;
336
- const tempCanvas = document.createElement('canvas');
337
- tempCanvas.width = canvas.width;
338
- tempCanvas.height = canvas.height;
339
- const tempCtx = tempCanvas.getContext('2d');
340
- tempCtx.fillStyle = '#FFFFFF';
341
- tempCtx.fillRect(0, 0, tempCanvas.width, tempCanvas.height);
342
- tempCtx.drawImage(canvas, 0, 0);
343
- const imageB64 = tempCanvas.toDataURL('image/png').split(',')[1];
344
- parts.push({inlineData: {data: imageB64, mimeType: 'image/png'}});
345
- } else if (mode === 'editor') {
346
- const imageB64 = generatedImage.split(',')[1];
347
- parts.push({inlineData: {data: imageB64, mimeType: 'image/png'}});
348
- } else if (mode === 'multi-img-edit') {
349
- multiImages.forEach((img) => {
350
- parts.push({
351
- inlineData: {data: img.split(',')[1], mimeType: 'image/png'},
352
- });
353
- });
354
- }
355
 
356
- parts.push({text: prompt});
357
- const contents: Content[] = [{role: 'USER', parts}];
358
- const response = await ai.models.generateContent({
359
- model: 'gemini-2.5-flash-image-preview',
360
- contents,
361
- config: {
362
- responseModalities: [Modality.TEXT, Modality.IMAGE],
363
- },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
364
  });
 
 
365
 
366
- const data = {
367
- success: true,
368
- message: '',
369
- imageData: null,
370
- error: undefined,
371
- };
372
- for (const part of response.candidates[0].content.parts) {
373
- if (part.text) {
374
- data.message = part.text;
375
- } else if (part.inlineData) {
376
- data.imageData = part.inlineData.data;
377
- }
378
- }
379
 
380
- if (data.imageData) {
381
- const imageUrl = `data:image/png;base64,${data.imageData}`;
382
- if (mode === 'multi-img-edit') {
383
- setGeneratedImage(imageUrl);
384
- setMultiImages([]);
385
- setMode('editor');
386
- } else {
387
- setGeneratedImage(imageUrl);
388
- }
389
- } else {
390
- setErrorMessage(
391
- data.message || 'Failed to generate image. Please try again.',
392
- );
393
- setShowErrorModal(true);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
394
  }
395
  }
396
- } catch (error) {
397
- console.error('Error submitting:', error);
398
- const parsedError = parseError(error.message);
399
- if (
400
- parsedError &&
401
- (parsedError.includes('API_KEY_INVALID') ||
402
- parsedError.includes('API key not valid'))
403
- ) {
404
- setErrorMessage(
405
- 'Your API key is not valid. Please enter a valid key to continue.',
406
- );
407
- setApiKey('');
408
- setShowApiKeyModal(true);
409
  } else {
410
- setErrorMessage(parsedError || 'An unexpected error occurred.');
411
- setShowErrorModal(true);
412
  }
413
- } finally {
414
- setIsLoading(false);
 
 
 
415
  }
416
- };
417
-
418
- if (!apiKey) {
419
- submissionRef.current = submitAction;
420
- setShowApiKeyModal(true);
421
- } else {
422
- await submitAction();
423
  }
424
  };
425
 
@@ -427,20 +443,32 @@ export default function Home() {
427
  setShowErrorModal(false);
428
  };
429
 
 
 
 
 
 
 
 
 
 
430
  useEffect(() => {
431
  const canvas = canvasRef.current;
432
  if (!canvas) return;
433
- const preventTouchDefault = (e) => {
 
434
  if (isDrawing) {
435
  e.preventDefault();
436
  }
437
  };
 
438
  canvas.addEventListener('touchstart', preventTouchDefault, {
439
  passive: false,
440
  });
441
  canvas.addEventListener('touchmove', preventTouchDefault, {
442
  passive: false,
443
  });
 
444
  return () => {
445
  canvas.removeEventListener('touchstart', preventTouchDefault);
446
  canvas.removeEventListener('touchmove', preventTouchDefault);
@@ -448,15 +476,15 @@ export default function Home() {
448
  }, [isDrawing]);
449
 
450
  const baseDisplayClass =
451
- 'w-full sm:h-[60vh] h-[30vh] min-h-[320px] bg-white/90 touch-none flex items-center justify-center p-4 transition-colors';
452
 
453
  return (
454
  <>
455
- <div className="min-h-screen notebook-paper-bg text-gray-900 flex flex-col justify-start items-center">
456
  <main className="container mx-auto px-3 sm:px-6 py-5 sm:py-10 pb-32 max-w-5xl w-full">
457
  <div className="flex flex-col sm:flex-row sm:justify-between sm:items-end mb-2 sm:mb-6 gap-2">
458
  <div>
459
- <h1 className="text-2xl sm:text-3xl font-bold mb-0 leading-tight font-mega">
460
  Nano Banana AIO
461
  </h1>
462
  <p className="text-sm sm:text-base text-gray-500 mt-1">
@@ -471,7 +499,7 @@ export default function Home() {
471
  by{' '}
472
  <a
473
  className="underline"
474
- href="https://www.linkedin.com/in/prithiv-sakthi/"
475
  target="_blank"
476
  rel="noopener noreferrer">
477
  prithivsakthi-ur
@@ -480,46 +508,85 @@ export default function Home() {
480
  </div>
481
 
482
  <menu className="flex items-center bg-gray-300 rounded-full p-2 shadow-sm self-start sm:self-auto">
483
- <div className="flex items-center bg-gray-200/80 rounded-full p-1 mr-2">
484
- <button
485
- onClick={() => setMode('editor')}
486
- className={`px-3 py-1.5 rounded-full text-sm font-medium flex items-center gap-2 transition-colors ${
487
- mode === 'editor'
488
- ? 'bg-white shadow'
489
- : 'text-gray-600 hover:bg-gray-300/50'
490
- }`}
491
- aria-pressed={mode === 'editor'}>
492
- <PictureInPicture className="w-4 h-4" /> Editor
493
- </button>
494
- <button
495
- onClick={() => setMode('multi-img-edit')}
496
- className={`px-3 py-1.5 rounded-full text-sm font-medium flex items-center gap-2 transition-colors ${
497
- mode === 'multi-img-edit'
498
- ? 'bg-white shadow'
499
- : 'text-gray-600 hover:bg-gray-300/50'
500
- }`}
501
- aria-pressed={mode === 'multi-img-edit'}>
502
- <Library className="w-4 h-4" /> Multi-Image
503
- </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
504
  <button
505
  onClick={() => setMode('canvas')}
506
- className={`px-3 py-1.5 rounded-full text-sm font-medium flex items-center gap-2 transition-colors ${
507
  mode === 'canvas'
508
  ? 'bg-white shadow'
509
  : 'text-gray-600 hover:bg-gray-300/50'
510
  }`}
511
  aria-pressed={mode === 'canvas'}>
512
- <Paintbrush className="w-4 h-4" /> Canvas
 
513
  </button>
514
  <button
515
  onClick={() => setMode('imageGen')}
516
- className={`px-3 py-1.5 rounded-full text-sm font-medium flex items-center gap-2 transition-colors ${
517
  mode === 'imageGen'
518
  ? 'bg-white shadow'
519
  : 'text-gray-600 hover:bg-gray-300/50'
520
  }`}
521
  aria-pressed={mode === 'imageGen'}>
522
- <Sparkles className="w-4 h-4" /> Image Gen
 
523
  </button>
524
  </div>
525
  <button
@@ -557,36 +624,12 @@ export default function Home() {
557
  onTouchStart={startDrawing}
558
  onTouchMove={draw}
559
  onTouchEnd={stopDrawing}
560
- className="border-2 border-black w-full sm:h-[60vh] h-[30vh] min-h-[320px] bg-white/90 touch-none"
561
  style={{
562
  cursor:
563
  "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",
564
  }}
565
  />
566
- <div className="absolute top-2 left-2 flex gap-2">
567
- <button
568
- onClick={() => setDrawingTool('pen')}
569
- className={`p-2 rounded-md shadow transition-colors ${
570
- drawingTool === 'pen'
571
- ? 'bg-blue-200'
572
- : 'bg-white hover:bg-gray-100'
573
- }`}
574
- aria-label="Pen"
575
- aria-pressed={drawingTool === 'pen'}>
576
- <Paintbrush className="w-5 h-5" />
577
- </button>
578
- <button
579
- onClick={() => setDrawingTool('eraser')}
580
- className={`p-2 rounded-md shadow transition-colors ${
581
- drawingTool === 'eraser'
582
- ? 'bg-blue-200'
583
- : 'bg-white hover:bg-gray-100'
584
- }`}
585
- aria-label="Eraser"
586
- aria-pressed={drawingTool === 'eraser'}>
587
- <Eraser className="w-5 h-5" />
588
- </button>
589
- </div>
590
  <div className="absolute top-2 right-2 flex gap-2">
591
  <button
592
  onClick={handleUndo}
@@ -645,7 +688,7 @@ export default function Home() {
645
  {multiImages.map((image, index) => (
646
  <div key={index} className="relative group aspect-square">
647
  <img
648
- src={image}
649
  alt={`upload preview ${index + 1}`}
650
  className="w-full h-full object-cover rounded-md"
651
  />
@@ -677,8 +720,9 @@ export default function Home() {
677
  )}
678
  </div>
679
  ) : (
 
680
  <div
681
- className={`${baseDisplayClass} border-2 ${
682
  generatedImage ? 'border-black' : 'border-gray-400'
683
  }`}>
684
  {generatedImage ? (
@@ -697,6 +741,7 @@ export default function Home() {
697
  )}
698
  </div>
699
 
 
700
  <form onSubmit={handleSubmit} className="w-full">
701
  <div className="relative">
702
  <input
@@ -706,9 +751,11 @@ export default function Home() {
706
  placeholder={
707
  mode === 'imageGen'
708
  ? 'Describe the image you want to create...'
 
 
709
  : 'Add your change...'
710
  }
711
- 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"
712
  required
713
  />
714
  <button
@@ -730,72 +777,58 @@ export default function Home() {
730
  </div>
731
  </form>
732
  </main>
733
- {showApiKeyModal && (
 
734
  <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
735
  <div className="bg-white rounded-lg shadow-xl max-w-md w-full p-6">
736
- <form onSubmit={handleApiKeySubmit}>
737
- <div className="flex justify-between items-start mb-4">
738
- <h3 className="text-xl font-bold text-gray-700">
739
- Add Gemini API Key
740
- </h3>
741
- <button
742
- type="button"
743
- onClick={() => {
744
- setShowApiKeyModal(false);
745
- setErrorMessage('');
746
- submissionRef.current = null;
747
- }}
748
- className="text-gray-400 hover:text-gray-500"
749
- aria-label="Close">
750
- <X className="w-5 h-5" />
751
- </button>
752
- </div>
753
- <p className="text-gray-600 mb-4 text-sm">
754
- Add the API key to process the request.{' '}
755
- <strong className="text-gray-800">
756
- The API key will be removed if the app page is refreshed or
757
- closed.
758
- </strong>
759
- </p>
760
- {errorMessage && (
761
- <p className="text-red-500 text-sm mb-2 font-medium">
762
- {errorMessage}
763
- </p>
764
- )}
765
- <input
766
- type="password"
767
- value={tempApiKey}
768
- onChange={(e) => setTempApiKey(e.target.value)}
769
- placeholder="Enter your Gemini API Key"
770
- 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"
771
- required
772
- aria-label="Gemini API Key"
773
- />
774
  <button
775
- type="submit"
776
- className="w-full p-2 bg-black text-white rounded hover:bg-gray-800 transition-colors">
777
- Submit and Process
778
  </button>
779
- </form>
 
 
 
780
  </div>
781
  </div>
782
  )}
783
- {showErrorModal && (
 
784
  <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
785
  <div className="bg-white rounded-lg shadow-xl max-w-md w-full p-6">
786
  <div className="flex justify-between items-start mb-4">
787
  <h3 className="text-xl font-bold text-gray-700">
788
- Failed to generate
789
  </h3>
790
  <button
791
- onClick={closeErrorModal}
792
  className="text-gray-400 hover:text-gray-500">
793
  <X className="w-5 h-5" />
794
  </button>
795
  </div>
796
- <p className="font-medium text-gray-600">
797
- {parseError(errorMessage)}
 
798
  </p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
799
  </div>
800
  </div>
801
  )}
 
2
  * @license
3
  * SPDX-License-Identifier: Apache-2.0
4
  */
5
+ /* tslint:disable */
 
6
  import {
7
+ ChevronDown,
8
  Library,
9
  LoaderCircle,
10
  Paintbrush,
 
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 = () => {
 
135
  // Draw the background image to the canvas
136
  const drawImageToCanvas = () => {
137
  if (!canvasRef.current || !backgroundImageRef.current) return;
138
+
139
  const canvas = canvasRef.current;
140
  const ctx = canvas.getContext('2d');
141
  ctx.fillStyle = '#FFFFFF';
 
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);
 
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;
 
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();
 
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';
 
 
 
 
 
 
 
231
  ctx.lineTo(x, y);
232
  ctx.stroke();
233
  };
 
248
  setGeneratedImage(null);
249
  setMultiImages([]);
250
  backgroundImageRef.current = null;
251
+ setPrompt('');
252
  };
253
 
254
  const processFiles = (files: FileList | null) => {
 
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
  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
  );
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
 
 
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
  }
463
  };
464
+
465
  canvas.addEventListener('touchstart', preventTouchDefault, {
466
  passive: false,
467
  });
468
  canvas.addEventListener('touchmove', preventTouchDefault, {
469
  passive: false,
470
  });
471
+
472
  return () => {
473
  canvas.removeEventListener('touchstart', preventTouchDefault);
474
  canvas.removeEventListener('touchmove', preventTouchDefault);
 
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
  by{' '}
500
  <a
501
  className="underline"
502
+ href="https://huggingface.co/prithivMLmods"
503
  target="_blank"
504
  rel="noopener noreferrer">
505
  prithivsakthi-ur
 
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
 
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}
 
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
  />
 
720
  )}
721
  </div>
722
  ) : (
723
+ // Image Gen mode display
724
  <div
725
+ className={`relative ${baseDisplayClass} border-2 ${
726
  generatedImage ? 'border-black' : 'border-gray-400'
727
  }`}>
728
  {generatedImage ? (
 
741
  )}
742
  </div>
743
 
744
+ {/* Input form */}
745
  <form onSubmit={handleSubmit} className="w-full">
746
  <div className="relative">
747
  <input
 
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
  </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
  )}