prithivMLmods commited on
Commit
5960f04
·
verified ·
1 Parent(s): e58754e

update app

Browse files
Files changed (1) hide show
  1. Home.tsx +285 -318
Home.tsx CHANGED
@@ -3,8 +3,9 @@
3
  * SPDX-License-Identifier: Apache-2.0
4
  */
5
  /* tslint:disable */
 
6
  import {
7
- ChevronDown,
8
  Library,
9
  LoaderCircle,
10
  Paintbrush,
@@ -18,110 +19,78 @@ import {
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,7 +104,6 @@ export default function Home() {
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,7 +119,7 @@ export default function Home() {
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,8 +158,8 @@ export default function Home() {
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,9 +173,9 @@ 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();
@@ -217,17 +185,24 @@ export default function Home() {
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,7 +223,6 @@ export default function Home() {
248
  setGeneratedImage(null);
249
  setMultiImages([]);
250
  backgroundImageRef.current = null;
251
- setPrompt('');
252
  };
253
 
254
  const processFiles = (files: FileList | null) => {
@@ -258,16 +232,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 +245,16 @@ 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,128 +281,145 @@ 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
 
@@ -443,32 +427,20 @@ export default function Home() {
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,15 +448,15 @@ 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 +471,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 +480,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
@@ -624,12 +557,36 @@ export default function Home() {
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,7 +645,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
  />
@@ -720,9 +677,8 @@ export default function Home() {
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,7 +697,6 @@ export default function Home() {
741
  )}
742
  </div>
743
 
744
- {/* Input form */}
745
  <form onSubmit={handleSubmit} className="w-full">
746
  <div className="relative">
747
  <input
@@ -751,11 +706,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,62 +730,76 @@ 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
  )}
835
  </div>
836
  </>
837
  );
838
- }
 
3
  * SPDX-License-Identifier: Apache-2.0
4
  */
5
  /* tslint:disable */
6
+ import {Content, GoogleGenAI, Modality} from '@google/genai';
7
  import {
8
+ Eraser,
9
  Library,
10
  LoaderCircle,
11
  Paintbrush,
 
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
  // 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
 
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
  };
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
  };
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
  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
  setGeneratedImage(null);
224
  setMultiImages([]);
225
  backgroundImageRef.current = null;
 
226
  };
227
 
228
  const processFiles = (files: FileList | null) => {
 
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
  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
  );
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
  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
  }, [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
  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
  </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
  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
  {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
  )}
678
  </div>
679
  ) : (
 
680
  <div
681
+ className={`${baseDisplayClass} border-2 ${
682
  generatedImage ? 'border-black' : 'border-gray-400'
683
  }`}>
684
  {generatedImage ? (
 
697
  )}
698
  </div>
699
 
 
700
  <form onSubmit={handleSubmit} className="w-full">
701
  <div className="relative">
702
  <input
 
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
  </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
  )}
802
  </div>
803
  </>
804
  );
805
+ }