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

api recycling with refresh timeouts (fix3) (#6)

Browse files

- api recycling with refresh timeouts (fix3) (911eec7e32093e71ab0e42be7277825d2fa0d151)

Files changed (1) hide show
  1. Home.tsx +349 -135
Home.tsx CHANGED
@@ -4,23 +4,28 @@
4
  */
5
  /* tslint:disable */
6
  import {
 
7
  Library,
8
  LoaderCircle,
9
  Paintbrush,
10
  PictureInPicture,
 
11
  SendHorizontal,
12
  Sparkles,
13
  Trash2,
 
14
  X,
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 {
@@ -37,18 +42,54 @@ 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('');
49
  const [mode, setMode] = useState<
50
  'canvas' | 'editor' | 'imageGen' | 'multi-img-edit'
51
  >('editor');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
 
53
  // Load background image when generatedImage changes
54
  useEffect(() => {
@@ -57,17 +98,30 @@ export default function Home() {
57
  img.onload = () => {
58
  backgroundImageRef.current = img;
59
  drawImageToCanvas();
 
 
 
 
60
  };
61
  img.src = generatedImage;
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 = () => {
@@ -81,6 +135,7 @@ export default function Home() {
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';
@@ -94,6 +149,46 @@ export default function Home() {
94
  );
95
  };
96
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  // Get the correct coordinates based on canvas scaling
98
  const getCoordinates = (e: any) => {
99
  const canvas = canvasRef.current!;
@@ -111,10 +206,12 @@ export default function Home() {
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);
@@ -122,7 +219,9 @@ export default function Home() {
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);
@@ -134,15 +233,17 @@ export default function Home() {
134
  };
135
 
136
  const stopDrawing = () => {
 
137
  setIsDrawing(false);
 
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([]);
@@ -209,105 +310,121 @@ export default function Home() {
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);
219
- return;
220
- }
221
-
222
- if (mode === 'multi-img-edit' && multiImages.length === 0) {
223
- setErrorMessage('Please upload at least one image to edit.');
224
- 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);
@@ -322,6 +439,15 @@ export default function Home() {
322
  setShowErrorModal(false);
323
  };
324
 
 
 
 
 
 
 
 
 
 
325
  useEffect(() => {
326
  const canvas = canvasRef.current;
327
  if (!canvas) return;
@@ -379,28 +505,63 @@ export default function Home() {
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 ${
@@ -447,23 +608,41 @@ export default function Home() {
447
  multiple={mode === 'multi-img-edit'}
448
  />
449
  {mode === 'canvas' ? (
450
- <canvas
451
- ref={canvasRef}
452
- width={960}
453
- height={540}
454
- onMouseDown={startDrawing}
455
- onMouseMove={draw}
456
- onMouseUp={stopDrawing}
457
- onMouseLeave={stopDrawing}
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",
465
- }}
466
- />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
467
  ) : mode === 'editor' ? (
468
  <div
469
  className={`${baseDisplayClass} ${
@@ -614,6 +793,41 @@ export default function Home() {
614
  </div>
615
  </div>
616
  )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
617
  </div>
618
  </>
619
  );
 
4
  */
5
  /* tslint:disable */
6
  import {
7
+ ChevronDown,
8
  Library,
9
  LoaderCircle,
10
  Paintbrush,
11
  PictureInPicture,
12
+ Redo2,
13
  SendHorizontal,
14
  Sparkles,
15
  Trash2,
16
+ Undo2,
17
  X,
18
  } from 'lucide-react';
19
  import {useEffect, useRef, useState} from 'react';
20
 
21
+ // This function remains useful for parsing potential error messages
22
  function parseError(error: string) {
23
  try {
24
+ // Attempt to parse the error as a JSON object which the proxy might send
25
  const errObj = JSON.parse(error);
26
  return errObj.message || error;
27
  } catch (e) {
28
+ // If it's not JSON, return the original error string
29
  const regex = /{"error":(.*)}/gm;
30
  const m = regex.exec(error);
31
  try {
 
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(() => {
 
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';
 
149
  );
150
  };
151
 
152
+ // Canvas history functions
153
+ const saveCanvasState = () => {
154
+ if (!canvasRef.current) return;
155
+ const canvas = canvasRef.current;
156
+ const dataUrl = canvas.toDataURL();
157
+ const newHistory = history.slice(0, historyIndex + 1);
158
+ newHistory.push(dataUrl);
159
+ setHistory(newHistory);
160
+ setHistoryIndex(newHistory.length - 1);
161
+ };
162
+
163
+ const restoreCanvasState = (index: number) => {
164
+ if (!canvasRef.current || !history[index]) return;
165
+ const canvas = canvasRef.current;
166
+ const ctx = canvas.getContext('2d');
167
+ const dataUrl = history[index];
168
+ const img = new window.Image();
169
+ img.onload = () => {
170
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
171
+ ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
172
+ };
173
+ img.src = dataUrl;
174
+ };
175
+
176
+ const handleUndo = () => {
177
+ if (historyIndex > 0) {
178
+ const newIndex = historyIndex - 1;
179
+ setHistoryIndex(newIndex);
180
+ restoreCanvasState(newIndex);
181
+ }
182
+ };
183
+
184
+ const handleRedo = () => {
185
+ if (historyIndex < history.length - 1) {
186
+ const newIndex = historyIndex + 1;
187
+ setHistoryIndex(newIndex);
188
+ restoreCanvasState(newIndex);
189
+ }
190
+ };
191
+
192
  // Get the correct coordinates based on canvas scaling
193
  const getCoordinates = (e: any) => {
194
  const canvas = canvasRef.current!;
 
206
  };
207
 
208
  const startDrawing = (e: any) => {
 
209
  const canvas = canvasRef.current!;
210
  const ctx = canvas.getContext('2d')!;
211
  const {x, y} = getCoordinates(e);
212
+ if (e.type === 'touchstart') {
213
+ e.preventDefault();
214
+ }
215
  ctx.beginPath();
216
  ctx.moveTo(x, y);
217
  setIsDrawing(true);
 
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);
 
233
  };
234
 
235
  const stopDrawing = () => {
236
+ if (!isDrawing) return;
237
  setIsDrawing(false);
238
+ saveCanvasState();
239
  };
240
 
241
  const handleClear = () => {
242
  if (mode === 'canvas' && canvasRef.current) {
243
+ initializeCanvas();
244
+ const dataUrl = canvasRef.current.toDataURL();
245
+ setHistory([dataUrl]);
246
+ setHistoryIndex(0);
247
  }
248
  setGeneratedImage(null);
249
  setMultiImages([]);
 
310
  );
311
  };
312
 
313
+ // *** MODIFIED FUNCTION ***
314
  const handleSubmit = async (e: React.FormEvent) => {
315
  e.preventDefault();
316
 
317
+ if (!apiKey) {
318
+ setShowApiKeyModal(true);
 
 
 
 
 
 
 
319
  return;
320
  }
321
 
322
  setIsLoading(true);
323
 
324
  try {
325
+ if (mode === 'editor' && !generatedImage) {
326
+ setErrorMessage('Please upload an image to edit.');
327
+ setShowErrorModal(true);
328
+ return;
329
+ }
 
 
 
 
 
330
 
331
+ if (mode === 'multi-img-edit' && multiImages.length === 0) {
332
+ setErrorMessage('Please upload at least one image to edit.');
333
+ setShowErrorModal(true);
334
+ return;
335
+ }
 
 
 
 
 
 
 
336
 
337
+ const parts: any[] = [];
338
+
339
+ // This logic for building the 'parts' array is correct.
340
+ if (mode === 'imageGen') {
341
+ const tempCanvas = document.createElement('canvas');
342
+ tempCanvas.width = 960;
343
+ tempCanvas.height = 540;
344
+ const tempCtx = tempCanvas.getContext('2d')!;
345
+ tempCtx.fillStyle = '#FFFFFF';
346
+ tempCtx.fillRect(0, 0, tempCanvas.width, tempCanvas.height);
347
+ tempCtx.fillStyle = '#FEFEFE';
348
+ tempCtx.fillRect(0, 0, 1, 1);
349
+ const imageB64 = tempCanvas.toDataURL('image/png').split(',')[1];
350
+ parts.push({inlineData: {data: imageB64, mimeType: 'image/png'}});
351
+ } else if (mode === 'canvas') {
352
+ if (!canvasRef.current) return;
353
+ const canvas = canvasRef.current;
354
+ const imageB64 = canvas.toDataURL('image/png').split(',')[1];
355
+ parts.push({inlineData: {data: imageB64, mimeType: 'image/png'}});
356
+ } else if (mode === 'editor' && generatedImage) {
357
+ const mimeType = generatedImage.substring(
358
+ generatedImage.indexOf(':') + 1,
359
+ generatedImage.indexOf(';'),
360
+ );
361
+ const imageB64 = generatedImage.split(',')[1];
362
+ parts.push({inlineData: {data: imageB64, mimeType}});
363
+ } else if (mode === 'multi-img-edit') {
364
+ multiImages.forEach((img) => {
365
+ parts.push({
366
+ inlineData: {data: img.url.split(',')[1], mimeType: img.type},
367
  });
 
 
 
 
 
 
 
 
 
368
  });
369
+ }
370
 
371
+ parts.push({text: prompt});
372
+
373
+ // Construct the request body for the Gemini REST API
374
+ const requestBody = {
375
+ contents: [{role: 'USER', parts}],
376
+ };
377
+
378
+ // Define the proxy endpoint
379
+ const proxyUrl = `/api-proxy/v1beta/models/gemini-pro-vision:generateContent?key=${apiKey}`;
380
+
381
+ // Use fetch to send the request to your proxy server
382
+ const response = await fetch(proxyUrl, {
383
+ method: 'POST',
384
+ headers: {
385
+ 'Content-Type': 'application/json',
386
+ },
387
+ body: JSON.stringify(requestBody),
388
+ });
389
+
390
+ if (!response.ok) {
391
+ const errorData = await response.json();
392
+ throw new Error(
393
+ errorData.error?.message || `HTTP error! status: ${response.status}`,
394
+ );
395
+ }
396
+
397
+ const responseData = await response.json();
398
+
399
+ // Process the response
400
+ const result = {message: '', imageData: null};
401
+
402
+ if (responseData.candidates && responseData.candidates.length > 0) {
403
+ for (const part of responseData.candidates[0].content.parts) {
404
+ if (part.text) {
405
+ result.message = part.text;
406
+ } else if (part.inlineData) {
407
+ result.imageData = part.inlineData.data;
408
+ }
409
  }
410
+ } else {
411
+ throw new Error('Invalid response structure from API.');
412
+ }
413
 
414
+ if (result.imageData) {
415
+ const imageUrl = `data:image/png;base64,${result.imageData}`;
416
+ if (mode === 'multi-img-edit') {
417
+ setGeneratedImage(imageUrl);
418
+ setMultiImages([]);
419
+ setMode('editor');
 
 
 
420
  } else {
421
+ setGeneratedImage(imageUrl);
 
422
  }
423
+ } else {
424
+ setErrorMessage(
425
+ result.message || 'Failed to generate image. Please try again.',
426
+ );
427
+ setShowErrorModal(true);
428
  }
429
  } catch (error: any) {
430
  console.error('Error submitting:', error);
 
439
  setShowErrorModal(false);
440
  };
441
 
442
+ const handleApiKeySubmit = (e: React.FormEvent) => {
443
+ e.preventDefault();
444
+ const newApiKey = (e.target as any).apiKey.value;
445
+ if (newApiKey) {
446
+ setApiKey(newApiKey);
447
+ setShowApiKeyModal(false);
448
+ }
449
+ };
450
+
451
  useEffect(() => {
452
  const canvas = canvasRef.current;
453
  if (!canvas) return;
 
505
 
506
  <menu className="flex items-center bg-gray-300 rounded-full p-2 shadow-sm self-start sm:self-auto">
507
  <div className="flex flex-wrap justify-center items-center bg-gray-200/80 rounded-full p-1 mr-2">
508
+ <div className="relative" ref={dropdownRef}>
509
+ <button
510
+ onClick={() => setIsDropdownOpen(!isDropdownOpen)}
511
+ className={`p-2 sm:px-3 sm:py-1.5 rounded-full text-sm font-medium flex items-center gap-2 transition-colors ${
512
+ mode === 'editor' || mode === 'multi-img-edit'
513
+ ? 'bg-white shadow'
514
+ : 'text-gray-600 hover:bg-gray-300/50'
515
+ }`}
516
+ aria-haspopup="true"
517
+ aria-expanded={isDropdownOpen}>
518
+ {mode === 'multi-img-edit' ? (
519
+ <>
520
+ <Library className="w-4 h-4" />
521
+ <span className="hidden sm:inline">Multi-Image</span>
522
+ </>
523
+ ) : (
524
+ <>
525
+ <PictureInPicture className="w-4 h-4" />
526
+ <span className="hidden sm:inline">Editor</span>
527
+ </>
528
+ )}
529
+ <ChevronDown className="w-4 h-4 opacity-70" />
530
+ </button>
531
+ {isDropdownOpen && (
532
+ <div className="absolute top-full mt-2 w-48 bg-white rounded-lg shadow-xl z-10 border border-gray-200 py-1">
533
+ <button
534
+ onClick={() => {
535
+ setMode('editor');
536
+ setIsDropdownOpen(false);
537
+ }}
538
+ className={`w-full text-left px-3 py-2 text-sm flex items-center gap-3 transition-colors ${
539
+ mode === 'editor'
540
+ ? 'bg-gray-100 text-gray-900'
541
+ : 'text-gray-700 hover:bg-gray-50'
542
+ }`}
543
+ aria-pressed={mode === 'editor'}>
544
+ <PictureInPicture className="w-4 h-4" />
545
+ <span>Single Image Edit</span>
546
+ </button>
547
+ <button
548
+ onClick={() => {
549
+ setMode('multi-img-edit');
550
+ setIsDropdownOpen(false);
551
+ }}
552
+ className={`w-full text-left px-3 py-2 text-sm flex items-center gap-3 transition-colors ${
553
+ mode === 'multi-img-edit'
554
+ ? 'bg-gray-100 text-gray-900'
555
+ : 'text-gray-700 hover:bg-gray-50'
556
+ }`}
557
+ aria-pressed={mode === 'multi-img-edit'}>
558
+ <Library className="w-4 h-4" />
559
+ <span>Multi-Image Edit</span>
560
+ </button>
561
+ </div>
562
+ )}
563
+ </div>
564
+
565
  <button
566
  onClick={() => setMode('canvas')}
567
  className={`p-2 sm:px-3 sm:py-1.5 rounded-full text-sm font-medium flex items-center gap-2 transition-colors ${
 
608
  multiple={mode === 'multi-img-edit'}
609
  />
610
  {mode === 'canvas' ? (
611
+ <div className="relative w-full">
612
+ <canvas
613
+ ref={canvasRef}
614
+ width={960}
615
+ height={540}
616
+ onMouseDown={startDrawing}
617
+ onMouseMove={draw}
618
+ onMouseUp={stopDrawing}
619
+ onMouseLeave={stopDrawing}
620
+ onTouchStart={startDrawing}
621
+ onTouchMove={draw}
622
+ onTouchEnd={stopDrawing}
623
+ className="border-2 border-black w-full sm:h-[60vh] h-[40vh] min-h-[320px] bg-white/90 touch-none"
624
+ style={{
625
+ cursor:
626
+ "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",
627
+ }}
628
+ />
629
+ <div className="absolute top-2 right-2 flex gap-2">
630
+ <button
631
+ onClick={handleUndo}
632
+ disabled={historyIndex <= 0}
633
+ className="p-2 bg-white rounded-md shadow disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-100 transition-colors"
634
+ aria-label="Undo">
635
+ <Undo2 className="w-5 h-5" />
636
+ </button>
637
+ <button
638
+ onClick={handleRedo}
639
+ disabled={historyIndex >= history.length - 1}
640
+ className="p-2 bg-white rounded-md shadow disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-100 transition-colors"
641
+ aria-label="Redo">
642
+ <Redo2 className="w-5 h-5" />
643
+ </button>
644
+ </div>
645
+ </div>
646
  ) : mode === 'editor' ? (
647
  <div
648
  className={`${baseDisplayClass} ${
 
793
  </div>
794
  </div>
795
  )}
796
+ {/* API Key Modal */}
797
+ {showApiKeyModal && (
798
+ <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
799
+ <div className="bg-white rounded-lg shadow-xl max-w-md w-full p-6">
800
+ <div className="flex justify-between items-start mb-4">
801
+ <h3 className="text-xl font-bold text-gray-700">
802
+ Add Gemini API Key
803
+ </h3>
804
+ <button
805
+ onClick={() => setShowApiKeyModal(false)}
806
+ className="text-gray-400 hover:text-gray-500">
807
+ <X className="w-5 h-5" />
808
+ </button>
809
+ </div>
810
+ <p className="text-gray-600 mb-4">
811
+ Add the API key to process the request. The API key will be
812
+ removed if the app page is refreshed or closed.
813
+ </p>
814
+ <form onSubmit={handleApiKeySubmit}>
815
+ <input
816
+ type="password"
817
+ name="apiKey"
818
+ className="w-full p-2 border-2 border-gray-300 rounded-md mb-4"
819
+ placeholder="Enter your Gemini API Key"
820
+ required
821
+ />
822
+ <button
823
+ type="submit"
824
+ className="w-full bg-black text-white p-2 rounded-md hover:bg-gray-800 transition-colors">
825
+ Submit
826
+ </button>
827
+ </form>
828
+ </div>
829
+ </div>
830
+ )}
831
  </div>
832
  </>
833
  );