prithivMLmods commited on
Commit
d77b02a
·
verified ·
1 Parent(s): 3fc0272

api recycling with refresh timeouts (fix2) (#5)

Browse files

- api recycling with refresh timeouts (fix2) (ec3ea359b5501f062afc0d951d7b03305ec80bad)

Files changed (1) hide show
  1. Home.tsx +144 -250
Home.tsx CHANGED
@@ -1,9 +1,8 @@
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,
@@ -16,26 +15,34 @@ import {
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('');
@@ -43,15 +50,9 @@ export default function Home() {
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;
@@ -61,19 +62,18 @@ export default function Home() {
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
  };
@@ -81,15 +81,10 @@ export default function Home() {
81
  // Draw the background image to the canvas
82
  const drawImageToCanvas = () => {
83
  if (!canvasRef.current || !backgroundImageRef.current) return;
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,
@@ -100,15 +95,11 @@ export default function Home() {
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,34 +110,22 @@ export default function Home() {
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';
@@ -159,17 +138,16 @@ export default function Home() {
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) => {
@@ -181,9 +159,10 @@ export default function Home() {
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,17 +171,18 @@ export default function Home() {
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,24 +209,10 @@ export default function Home() {
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);
@@ -259,135 +225,108 @@ export default function Home() {
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,7 +339,6 @@ export default function Home() {
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,16 +346,15 @@ export default function Home() {
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,7 +369,7 @@ export default function Home() {
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,46 +378,50 @@ export default function Home() {
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,7 +436,6 @@ export default function Home() {
495
  </menu>
496
  </div>
497
 
498
- {/* Main display section */}
499
  <div className="w-full mb-6">
500
  <input
501
  ref={fileInputRef}
@@ -518,7 +458,7 @@ export default function Home() {
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",
@@ -565,7 +505,7 @@ export default function Home() {
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,7 +539,7 @@ export default function Home() {
599
  ) : (
600
  // Image Gen mode display
601
  <div
602
- className={`${baseDisplayClass} border-2 ${
603
  generatedImage ? 'border-black' : 'border-gray-400'
604
  }`}>
605
  {generatedImage ? (
@@ -628,9 +568,11 @@ export default function Home() {
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,54 +594,6 @@ export default function Home() {
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">
 
1
  /**
2
  * @license
3
  * SPDX-License-Identifier: Apache-2.0
4
+ */
5
  /* tslint:disable */
 
6
  import {
7
  Library,
8
  LoaderCircle,
 
15
  } from 'lucide-react';
16
  import {useEffect, useRef, useState} from 'react';
17
 
18
+ // This function remains useful for parsing potential error messages from the proxy
19
  function parseError(error: string) {
 
 
20
  try {
21
+ const errObj = JSON.parse(error);
22
+ return errObj.message || error;
 
23
  } catch (e) {
24
+ const regex = /{"error":(.*)}/gm;
25
+ const m = regex.exec(error);
26
+ try {
27
+ const e = m[1];
28
+ const err = JSON.parse(e);
29
+ return err.message || error;
30
+ } catch (e) {
31
+ return error;
32
+ }
33
  }
34
  }
35
 
36
  export default function Home() {
37
+ const canvasRef = useRef<HTMLCanvasElement>(null);
38
+ const fileInputRef = useRef<HTMLInputElement>(null);
39
+ const backgroundImageRef = useRef<HTMLImageElement | null>(null);
40
  const [isDrawing, setIsDrawing] = useState(false);
41
  const [prompt, setPrompt] = useState('');
42
  const [generatedImage, setGeneratedImage] = useState<string | null>(null);
43
+ const [multiImages, setMultiImages] = useState<{url: string; type: string}[]>(
44
+ [],
45
+ );
46
  const [isLoading, setIsLoading] = useState(false);
47
  const [showErrorModal, setShowErrorModal] = useState(false);
48
  const [errorMessage, setErrorMessage] = useState('');
 
50
  'canvas' | 'editor' | 'imageGen' | 'multi-img-edit'
51
  >('editor');
52
 
 
 
 
 
 
53
  // Load background image when generatedImage changes
54
  useEffect(() => {
55
  if (generatedImage && canvasRef.current) {
 
56
  const img = new window.Image();
57
  img.onload = () => {
58
  backgroundImageRef.current = img;
 
62
  }
63
  }, [generatedImage]);
64
 
65
+ // Initialize canvas when component mounts or mode changes to canvas
66
  useEffect(() => {
67
+ if (mode === 'canvas' && canvasRef.current) {
68
  initializeCanvas();
69
  }
70
+ }, [mode]);
71
 
72
  // Initialize canvas with white background
73
  const initializeCanvas = () => {
74
  const canvas = canvasRef.current;
75
+ if (!canvas) return;
76
  const ctx = canvas.getContext('2d');
 
 
77
  ctx.fillStyle = '#FFFFFF';
78
  ctx.fillRect(0, 0, canvas.width, canvas.height);
79
  };
 
81
  // Draw the background image to the canvas
82
  const drawImageToCanvas = () => {
83
  if (!canvasRef.current || !backgroundImageRef.current) return;
 
84
  const canvas = canvasRef.current;
85
  const ctx = canvas.getContext('2d');
 
 
86
  ctx.fillStyle = '#FFFFFF';
87
  ctx.fillRect(0, 0, canvas.width, canvas.height);
 
 
88
  ctx.drawImage(
89
  backgroundImageRef.current,
90
  0,
 
95
  };
96
 
97
  // Get the correct coordinates based on canvas scaling
98
+ const getCoordinates = (e: any) => {
99
+ const canvas = canvasRef.current!;
100
  const rect = canvas.getBoundingClientRect();
 
 
101
  const scaleX = canvas.width / rect.width;
102
  const scaleY = canvas.height / rect.height;
 
 
103
  return {
104
  x:
105
  (e.nativeEvent.offsetX ||
 
110
  };
111
  };
112
 
113
+ const startDrawing = (e: any) => {
114
+ if (e.type === 'touchstart') e.preventDefault();
115
+ const canvas = canvasRef.current!;
116
+ const ctx = canvas.getContext('2d')!;
117
  const {x, y} = getCoordinates(e);
 
 
 
 
 
 
 
118
  ctx.beginPath();
119
  ctx.moveTo(x, y);
120
  setIsDrawing(true);
121
  };
122
 
123
+ const draw = (e: any) => {
124
  if (!isDrawing) return;
125
+ if (e.type === 'touchmove') e.preventDefault();
126
+ const canvas = canvasRef.current!;
127
+ const ctx = canvas.getContext('2d')!;
 
 
 
 
 
128
  const {x, y} = getCoordinates(e);
 
129
  ctx.lineWidth = 5;
130
  ctx.lineCap = 'round';
131
  ctx.strokeStyle = '#000000';
 
138
  };
139
 
140
  const handleClear = () => {
 
141
  if (mode === 'canvas' && canvasRef.current) {
142
  const canvas = canvasRef.current;
143
  const ctx = canvas.getContext('2d');
144
  ctx.fillStyle = '#FFFFFF';
145
  ctx.fillRect(0, 0, canvas.width, canvas.height);
146
  }
 
147
  setGeneratedImage(null);
148
  setMultiImages([]);
149
  backgroundImageRef.current = null;
150
+ setPrompt('');
151
  };
152
 
153
  const processFiles = (files: FileList | null) => {
 
159
 
160
  if (mode === 'multi-img-edit') {
161
  const readers = fileArray.map((file) => {
162
+ return new Promise<{url: string; type: string}>((resolve, reject) => {
163
  const reader = new FileReader();
164
+ reader.onload = () =>
165
+ resolve({url: reader.result as string, type: file.type});
166
  reader.onerror = reject;
167
  reader.readAsDataURL(file);
168
  });
 
171
  setMultiImages((prev) => [...prev, ...newImages]);
172
  });
173
  } else {
174
+ const file = fileArray[0];
175
  const reader = new FileReader();
176
  reader.onload = () => {
177
  setGeneratedImage(reader.result as string);
178
  };
179
+ reader.readAsDataURL(file);
180
  }
181
  };
182
 
183
  const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
184
  processFiles(e.target.files);
185
+ e.target.value = '';
186
  };
187
 
188
  const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
 
209
  );
210
  };
211
 
212
+ // *** COMPLETELY REWRITTEN AND FIXED FUNCTION ***
213
+ const handleSubmit = async (e: React.FormEvent) => {
 
 
 
 
 
 
 
 
 
 
 
 
 
214
  e.preventDefault();
215
 
 
216
  if (mode === 'editor' && !generatedImage) {
217
  setErrorMessage('Please upload an image to edit.');
218
  setShowErrorModal(true);
 
225
  return;
226
  }
227
 
228
+ setIsLoading(true);
229
+
230
+ try {
231
+ let response;
232
+ if (mode === 'imageGen') {
233
+ // Handle Image Generation using the 'imagen' model via the proxy
234
+ const proxyUrl = '/api-proxy/v1beta/models/imagen-4.0-generate-001:generateImages';
235
+ const requestBody = { prompt };
236
+ response = await fetch(proxyUrl, {
237
+ method: 'POST',
238
+ headers: {'Content-Type': 'application/json'},
239
+ body: JSON.stringify(requestBody),
240
+ });
241
 
242
+ if (!response.ok) {
243
+ const errorData = await response.json();
244
+ throw new Error(errorData.error?.message || `HTTP error! status: ${response.status}`);
245
+ }
246
+ const responseData = await response.json();
247
+ if (responseData.generatedImages && responseData.generatedImages.length > 0) {
248
+ const base64ImageBytes = responseData.generatedImages[0].image.imageBytes;
249
+ const imageUrl = `data:image/png;base64,${base64ImageBytes}`;
250
+ setGeneratedImage(imageUrl);
251
  } else {
252
+ throw new Error('Image generation failed to return an image.');
253
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
254
 
255
+ } else {
256
+ // Handle all other modes (edit, canvas, multi-image) via the proxy
257
+ const parts: any[] = [];
258
+ if (mode === 'canvas') {
259
+ if (!canvasRef.current) return;
260
+ const imageB64 = canvasRef.current.toDataURL('image/png').split(',')[1];
261
+ parts.push({inlineData: {data: imageB64, mimeType: 'image/png'}});
262
+ } else if (mode === 'editor' && generatedImage) {
263
+ const mimeType = generatedImage.substring(generatedImage.indexOf(':') + 1, generatedImage.indexOf(';'));
264
+ const imageB64 = generatedImage.split(',')[1];
265
+ parts.push({inlineData: {data: imageB64, mimeType}});
266
+ } else if (mode === 'multi-img-edit') {
267
+ multiImages.forEach((img) => {
268
+ parts.push({
269
+ inlineData: {data: img.url.split(',')[1], mimeType: img.type},
270
+ });
271
  });
272
+ }
273
+ parts.push({text: prompt});
274
+
275
+ const proxyUrl = '/api-proxy/v1beta/models/gemini-2.5-flash-image-preview:generateContent';
276
+ const requestBody = { contents: [{role: 'USER', parts}] };
277
+ response = await fetch(proxyUrl, {
278
+ method: 'POST',
279
+ headers: {'Content-Type': 'application/json'},
280
+ body: JSON.stringify(requestBody),
281
+ });
282
 
283
+ if (!response.ok) {
284
+ const errorData = await response.json();
285
+ throw new Error(errorData.error?.message || `HTTP error! status: ${response.status}`);
286
+ }
287
+ const responseData = await response.json();
288
+ const result = { message: '', imageData: null };
289
+ if (responseData.candidates && responseData.candidates.length > 0) {
290
+ for (const part of responseData.candidates[0].content.parts) {
291
+ if (part.text) result.message = part.text;
292
+ else if (part.inlineData) result.imageData = part.inlineData.data;
 
293
  }
294
+ } else {
295
+ throw new Error('Invalid response structure from API.');
296
+ }
297
 
298
+ if (result.imageData) {
299
+ const imageUrl = `data:image/png;base64,${result.imageData}`;
300
  if (mode === 'multi-img-edit') {
301
+ setGeneratedImage(imageUrl);
302
+ setMultiImages([]);
303
+ setMode('editor');
304
  } else {
305
+ setGeneratedImage(imageUrl);
306
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
307
  } else {
308
+ setErrorMessage(result.message || 'Failed to generate image. Please try again.');
309
+ setShowErrorModal(true);
310
  }
 
 
311
  }
312
+ } catch (error: any) {
313
+ console.error('Error submitting:', error);
314
+ setErrorMessage(error.message || 'An unexpected error occurred.');
315
+ setShowErrorModal(true);
316
+ } finally {
317
+ setIsLoading(false);
 
318
  }
319
  };
320
 
 
321
  const closeErrorModal = () => {
322
  setShowErrorModal(false);
323
  };
324
 
 
325
  useEffect(() => {
326
  const canvas = canvasRef.current;
327
  if (!canvas) return;
328
 
329
+ const preventTouchDefault = (e: TouchEvent) => {
 
330
  if (isDrawing) {
331
  e.preventDefault();
332
  }
 
339
  passive: false,
340
  });
341
 
 
342
  return () => {
343
  canvas.removeEventListener('touchstart', preventTouchDefault);
344
  canvas.removeEventListener('touchmove', preventTouchDefault);
 
346
  }, [isDrawing]);
347
 
348
  const baseDisplayClass =
349
+ 'w-full sm:h-[60vh] h-[40vh] min-h-[320px] bg-white/90 touch-none flex items-center justify-center p-4 transition-colors';
350
 
351
  return (
352
  <>
353
+ <div className="min-h-screen text-gray-900 flex flex-col justify-start items-center">
354
  <main className="container mx-auto px-3 sm:px-6 py-5 sm:py-10 pb-32 max-w-5xl w-full">
 
355
  <div className="flex flex-col sm:flex-row sm:justify-between sm:items-end mb-2 sm:mb-6 gap-2">
356
  <div>
357
+ <h1 className="text-2xl sm:text-3xl font-bold mb-0 leading-tight">
358
  Nano Banana AIO
359
  </h1>
360
  <p className="text-sm sm:text-base text-gray-500 mt-1">
 
369
  by{' '}
370
  <a
371
  className="underline"
372
+ href="https://huggingface.co/prithivMLmods"
373
  target="_blank"
374
  rel="noopener noreferrer">
375
  prithivsakthi-ur
 
378
  </div>
379
 
380
  <menu className="flex items-center bg-gray-300 rounded-full p-2 shadow-sm self-start sm:self-auto">
381
+ <div className="flex flex-wrap justify-center items-center bg-gray-200/80 rounded-full p-1 mr-2">
382
  <button
383
  onClick={() => setMode('editor')}
384
+ className={`p-2 sm:px-3 sm:py-1.5 rounded-full text-sm font-medium flex items-center gap-2 transition-colors ${
385
  mode === 'editor'
386
  ? 'bg-white shadow'
387
  : 'text-gray-600 hover:bg-gray-300/50'
388
  }`}
389
  aria-pressed={mode === 'editor'}>
390
+ <PictureInPicture className="w-4 h-4" />
391
+ <span className="hidden sm:inline">Editor</span>
392
  </button>
393
+ <button
394
  onClick={() => setMode('multi-img-edit')}
395
+ className={`p-2 sm:px-3 sm:py-1.5 rounded-full text-sm font-medium flex items-center gap-2 transition-colors ${
396
  mode === 'multi-img-edit'
397
  ? 'bg-white shadow'
398
  : 'text-gray-600 hover:bg-gray-300/50'
399
  }`}
400
  aria-pressed={mode === 'multi-img-edit'}>
401
+ <Library className="w-4 h-4" />
402
+ <span className="hidden sm:inline">Multi-Image</span>
403
  </button>
404
  <button
405
  onClick={() => setMode('canvas')}
406
+ className={`p-2 sm:px-3 sm:py-1.5 rounded-full text-sm font-medium flex items-center gap-2 transition-colors ${
407
  mode === 'canvas'
408
  ? 'bg-white shadow'
409
  : 'text-gray-600 hover:bg-gray-300/50'
410
  }`}
411
  aria-pressed={mode === 'canvas'}>
412
+ <Paintbrush className="w-4 h-4" />
413
+ <span className="hidden sm:inline">Canvas</span>
414
  </button>
415
  <button
416
  onClick={() => setMode('imageGen')}
417
+ className={`p-2 sm:px-3 sm:py-1.5 rounded-full text-sm font-medium flex items-center gap-2 transition-colors ${
418
  mode === 'imageGen'
419
  ? 'bg-white shadow'
420
  : 'text-gray-600 hover:bg-gray-300/50'
421
  }`}
422
  aria-pressed={mode === 'imageGen'}>
423
+ <Sparkles className="w-4 h-4" />
424
+ <span className="hidden sm:inline">Image Gen</span>
425
  </button>
426
  </div>
427
  <button
 
436
  </menu>
437
  </div>
438
 
 
439
  <div className="w-full mb-6">
440
  <input
441
  ref={fileInputRef}
 
458
  onTouchStart={startDrawing}
459
  onTouchMove={draw}
460
  onTouchEnd={stopDrawing}
461
+ className="border-2 border-black w-full sm:h-[60vh] h-[40vh] min-h-[320px] bg-white/90 touch-none"
462
  style={{
463
  cursor:
464
  "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",
 
505
  {multiImages.map((image, index) => (
506
  <div key={index} className="relative group aspect-square">
507
  <img
508
+ src={image.url}
509
  alt={`upload preview ${index + 1}`}
510
  className="w-full h-full object-cover rounded-md"
511
  />
 
539
  ) : (
540
  // Image Gen mode display
541
  <div
542
+ className={`relative ${baseDisplayClass} border-2 ${
543
  generatedImage ? 'border-black' : 'border-gray-400'
544
  }`}>
545
  {generatedImage ? (
 
568
  placeholder={
569
  mode === 'imageGen'
570
  ? 'Describe the image you want to create...'
571
+ : mode === 'multi-img-edit'
572
+ ? 'Describe how to edit or combine the images...'
573
  : 'Add your change...'
574
  }
575
+ 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"
576
  required
577
  />
578
  <button
 
594
  </div>
595
  </form>
596
  </main>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
597
  {/* Error Modal */}
598
  {showErrorModal && (
599
  <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">