Trudy commited on
Commit
899a91f
·
1 Parent(s): aba9d46

error handling for image upload

Browse files
Files changed (2) hide show
  1. components/Canvas.js +113 -40
  2. pages/api/convert-to-doodle.js +14 -11
components/Canvas.js CHANGED
@@ -1,4 +1,4 @@
1
- import { useRef, useEffect, useState, forwardRef, useImperativeHandle, useCallback } from 'react';
2
  import {
3
  getCoordinates,
4
  drawBezierCurve,
@@ -7,7 +7,7 @@ import {
7
  isNearHandle,
8
  updateHandle
9
  } from './utils/canvasUtils';
10
- import { PencilLine, Upload, ImagePlus, LoaderCircle, Brush } from 'lucide-react';
11
  import ToolBar from './ToolBar';
12
  import StyleSelector from './StyleSelector';
13
 
@@ -51,6 +51,7 @@ const Canvas = forwardRef(({
51
  const [shapeStartPos, setShapeStartPos] = useState(null);
52
  const [previewCanvas, setPreviewCanvas] = useState(null);
53
  const [isDoodleConverting, setIsDoodleConverting] = useState(false);
 
54
  const [uploadedImages, setUploadedImages] = useState([]);
55
  const [draggingImage, setDraggingImage] = useState(null);
56
  const [resizingImage, setResizingImage] = useState(null);
@@ -155,6 +156,11 @@ const Canvas = forwardRef(({
155
  // Create a stable ref for handleFileChange to avoid dependency cycles
156
  const handleFileChangeRef = useRef(null);
157
 
 
 
 
 
 
158
  // Update handleFileChange function
159
  const handleFileChange = useCallback(async (event) => {
160
  const file = event.target.files?.[0];
@@ -168,6 +174,9 @@ const Canvas = forwardRef(({
168
  onDrawingChange(true);
169
  }
170
 
 
 
 
171
  // Show loading state
172
  setIsDoodleConverting(true);
173
 
@@ -189,49 +198,94 @@ const Canvas = forwardRef(({
189
  }),
190
  });
191
 
 
192
  const data = await response.json();
193
 
194
- if (data.success && data.imageData) {
195
- const img = new Image();
196
- img.onload = () => {
197
- const ctx = canvasRef.current.getContext('2d');
198
-
199
- // Clear canvas
200
- ctx.fillStyle = '#FFFFFF';
201
- ctx.fillRect(0, 0, canvasRef.current.width, canvasRef.current.height);
202
-
203
- // Calculate dimensions
204
- const scale = Math.min(
205
- canvasRef.current.width / img.width,
206
- canvasRef.current.height / img.height
207
- );
208
- const x = (canvasRef.current.width - img.width * scale) / 2;
209
- const y = (canvasRef.current.height - img.height * scale) / 2;
210
-
211
- // Draw doodle
212
- ctx.drawImage(img, x, y, img.width * scale, img.height * scale);
213
-
214
- // Save canvas state
215
- saveCanvasState();
216
-
217
- // Hide loading state
218
- setIsDoodleConverting(false);
219
-
220
- // Ensure placeholder is hidden
221
- if (typeof onDrawingChange === 'function') {
222
- onDrawingChange(true);
223
- }
224
-
225
- // Automatically trigger generation
226
- handleGenerationRef.current();
227
- };
228
 
229
- img.src = `data:image/png;base64,${data.imageData}`;
230
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
231
  } catch (error) {
232
  console.error('Error processing image:', error);
 
 
 
 
 
 
 
 
 
 
233
  setIsDoodleConverting(false);
234
- alert('Error processing image. Please try a different image or a smaller file size.');
235
 
236
  // Restore previous tool even if there's an error
237
  setCurrentTool(previousTool);
@@ -239,7 +293,7 @@ const Canvas = forwardRef(({
239
  };
240
 
241
  reader.readAsDataURL(file);
242
- }, [canvasRef, currentTool, onDrawingChange, saveCanvasState, setCurrentTool, setIsDoodleConverting]);
243
 
244
  // Keep the ref updated
245
  useEffect(() => {
@@ -1044,7 +1098,7 @@ const Canvas = forwardRef(({
1044
  </button>
1045
 
1046
  {/* Doodle conversion loading overlay */}
1047
- {isDoodleConverting && (
1048
  <div className="absolute inset-0 flex flex-col items-center justify-center bg-gray-400/80 rounded-xl z-50">
1049
  <div className="bg-white shadow-lg rounded-xl p-6 flex flex-col items-center">
1050
  <LoaderCircle className="w-12 h-12 text-gray-700 animate-spin mb-4" />
@@ -1054,6 +1108,25 @@ const Canvas = forwardRef(({
1054
  </div>
1055
  )}
1056
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1057
  {/* Sending back to doodle loading overlay */}
1058
  {isSendingToDoodle && (
1059
  <div className="absolute inset-0 flex flex-col items-center justify-center bg-gray-400/80 rounded-xl z-50">
 
1
+ import { useRef, useEffect, useState, forwardRef, useImperativeHandle, useCallback } from 'react';
2
  import {
3
  getCoordinates,
4
  drawBezierCurve,
 
7
  isNearHandle,
8
  updateHandle
9
  } from './utils/canvasUtils';
10
+ import { PencilLine, Upload, ImagePlus, LoaderCircle, Brush, AlertCircle } from 'lucide-react';
11
  import ToolBar from './ToolBar';
12
  import StyleSelector from './StyleSelector';
13
 
 
51
  const [shapeStartPos, setShapeStartPos] = useState(null);
52
  const [previewCanvas, setPreviewCanvas] = useState(null);
53
  const [isDoodleConverting, setIsDoodleConverting] = useState(false);
54
+ const [doodleError, setDoodleError] = useState(null);
55
  const [uploadedImages, setUploadedImages] = useState([]);
56
  const [draggingImage, setDraggingImage] = useState(null);
57
  const [resizingImage, setResizingImage] = useState(null);
 
156
  // Create a stable ref for handleFileChange to avoid dependency cycles
157
  const handleFileChangeRef = useRef(null);
158
 
159
+ // Add clearDoodleError function
160
+ const clearDoodleError = useCallback(() => {
161
+ setDoodleError(null);
162
+ }, []);
163
+
164
  // Update handleFileChange function
165
  const handleFileChange = useCallback(async (event) => {
166
  const file = event.target.files?.[0];
 
174
  onDrawingChange(true);
175
  }
176
 
177
+ // Clear previous errors
178
+ setDoodleError(null);
179
+
180
  // Show loading state
181
  setIsDoodleConverting(true);
182
 
 
198
  }),
199
  });
200
 
201
+ // Get response data
202
  const data = await response.json();
203
 
204
+ // Check for API errors (non-200 status)
205
+ if (!response.ok) {
206
+ let errorMessage = data.error || `Server error (${response.status})`;
207
+
208
+ // Check if the response contains details about retry attempts
209
+ if (data.retries !== undefined) {
210
+ errorMessage += `. Failed after ${data.retries + 1} attempts.`;
211
+ }
212
+
213
+ // Check for specific error types from the server
214
+ if (errorMessage.includes('overloaded') || errorMessage.includes('503')) {
215
+ errorMessage = "The model is overloaded. Please try again later.";
216
+ } else if (errorMessage.includes('quota') || errorMessage.includes('API key')) {
217
+ errorMessage = "API quota exceeded or invalid API key.";
218
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
219
 
220
+ throw new Error(errorMessage);
221
  }
222
+
223
+ // Check for API response with success: false
224
+ if (!data.success) {
225
+ let errorMessage = data.error || "Failed to convert image to doodle";
226
+
227
+ // Check if the response contains details about retry attempts
228
+ if (data.retries !== undefined) {
229
+ errorMessage += `. Failed after ${data.retries + 1} attempts.`;
230
+ }
231
+
232
+ throw new Error(errorMessage);
233
+ }
234
+
235
+ // Check if we have image data
236
+ if (!data.imageData) {
237
+ throw new Error("No image data received from the server");
238
+ }
239
+
240
+ // Process successful response
241
+ const img = new Image();
242
+ img.onload = () => {
243
+ const ctx = canvasRef.current.getContext('2d');
244
+
245
+ // Clear canvas
246
+ ctx.fillStyle = '#FFFFFF';
247
+ ctx.fillRect(0, 0, canvasRef.current.width, canvasRef.current.height);
248
+
249
+ // Calculate dimensions
250
+ const scale = Math.min(
251
+ canvasRef.current.width / img.width,
252
+ canvasRef.current.height / img.height
253
+ );
254
+ const x = (canvasRef.current.width - img.width * scale) / 2;
255
+ const y = (canvasRef.current.height - img.height * scale) / 2;
256
+
257
+ // Draw doodle
258
+ ctx.drawImage(img, x, y, img.width * scale, img.height * scale);
259
+
260
+ // Save canvas state
261
+ saveCanvasState();
262
+
263
+ // Hide loading state
264
+ setIsDoodleConverting(false);
265
+
266
+ // Ensure placeholder is hidden
267
+ if (typeof onDrawingChange === 'function') {
268
+ onDrawingChange(true);
269
+ }
270
+
271
+ // Automatically trigger generation
272
+ handleGenerationRef.current();
273
+ };
274
+
275
+ img.src = `data:image/png;base64,${data.imageData}`;
276
  } catch (error) {
277
  console.error('Error processing image:', error);
278
+
279
+ // Set error state with message
280
+ setDoodleError(error.message || "Failed to convert image. Please try again.");
281
+
282
+ // Schedule error message to disappear after 5 seconds (was 3 seconds)
283
+ setTimeout(() => {
284
+ setDoodleError(null);
285
+ }, 5000);
286
+
287
+ // Hide loading state
288
  setIsDoodleConverting(false);
 
289
 
290
  // Restore previous tool even if there's an error
291
  setCurrentTool(previousTool);
 
293
  };
294
 
295
  reader.readAsDataURL(file);
296
+ }, [canvasRef, currentTool, onDrawingChange, saveCanvasState, setCurrentTool]);
297
 
298
  // Keep the ref updated
299
  useEffect(() => {
 
1098
  </button>
1099
 
1100
  {/* Doodle conversion loading overlay */}
1101
+ {isDoodleConverting && !doodleError && (
1102
  <div className="absolute inset-0 flex flex-col items-center justify-center bg-gray-400/80 rounded-xl z-50">
1103
  <div className="bg-white shadow-lg rounded-xl p-6 flex flex-col items-center">
1104
  <LoaderCircle className="w-12 h-12 text-gray-700 animate-spin mb-4" />
 
1108
  </div>
1109
  )}
1110
 
1111
+ {/* Updated doodle conversion error overlay with dismiss button */}
1112
+ {doodleError && (
1113
+ <div className="absolute inset-0 flex flex-col items-center justify-center bg-gray-400/80 rounded-xl z-50">
1114
+ <div className="bg-white shadow-lg rounded-xl p-6 flex flex-col items-center max-w-md">
1115
+ <AlertCircle className="w-12 h-12 text-red-500 mb-4" />
1116
+ <p className="text-gray-900 font-medium text-lg">Failed to Convert Image</p>
1117
+ <p className="text-gray-700 text-center mt-2">{doodleError}</p>
1118
+ <p className="text-gray-500 text-sm mt-4">Try a different image or try again later</p>
1119
+ <button
1120
+ type="button"
1121
+ className="mt-4 px-4 py-2 bg-gray-200 text-gray-800 rounded-md hover:bg-gray-300 transition-colors"
1122
+ onClick={clearDoodleError}
1123
+ >
1124
+ Dismiss
1125
+ </button>
1126
+ </div>
1127
+ </div>
1128
+ )}
1129
+
1130
  {/* Sending back to doodle loading overlay */}
1131
  {isSendingToDoodle && (
1132
  <div className="absolute inset-0 flex flex-col items-center justify-center bg-gray-400/80 rounded-xl z-50">
pages/api/convert-to-doodle.js CHANGED
@@ -1,4 +1,4 @@
1
- import { GoogleGenerativeAI, HarmCategory, HarmBlockThreshold } from "@google/generative-ai";
2
  import { NextResponse } from 'next/server';
3
 
4
  // Configuration for the API route
@@ -35,7 +35,8 @@ export default async function handler(req, res) {
35
  // Set up the API key
36
  const apiKey = customApiKey || process.env.GEMINI_API_KEY;
37
  if (!apiKey) {
38
- return res.status(500).json({ error: 'API key is required' });
 
39
  }
40
 
41
  // Retry loop for handling transient errors
@@ -44,6 +45,8 @@ export default async function handler(req, res) {
44
  console.log(`Initializing Gemini AI for doodle conversion (attempt ${retryCount + 1}/${MAX_RETRIES + 1})`);
45
  // Initialize the Gemini API
46
  const genAI = new GoogleGenerativeAI(apiKey);
 
 
47
  const model = genAI.getGenerativeModel({
48
  model: "gemini-2.0-flash-exp-image-generation",
49
  generationConfig: {
@@ -71,6 +74,7 @@ Requirements:
71
  * Text should remain readable in the final doodle, and true to the original :))`;
72
 
73
  // Prepare the generation content
 
74
  const generationContent = [
75
  {
76
  inlineData: {
@@ -84,8 +88,9 @@ Requirements:
84
  // Generate content
85
  console.log(`Calling Gemini API for doodle conversion (attempt ${retryCount + 1}/${MAX_RETRIES + 1})...`);
86
  const result = await model.generateContent(generationContent);
 
 
87
  const response = await result.response;
88
- console.log('Gemini API response received for doodle conversion');
89
 
90
  // Process the response to extract image data
91
  let convertedImageData = null;
@@ -97,14 +102,14 @@ Requirements:
97
  for (const part of response.candidates[0].content.parts) {
98
  if (part.inlineData) {
99
  convertedImageData = part.inlineData.data;
100
- console.log('Found image data in doodle conversion response');
101
  break;
102
  }
103
  }
104
 
105
  if (!convertedImageData) {
106
  console.error('No image data in response parts:', response.candidates[0].content.parts);
107
- throw new Error('No image data received from the API');
108
  }
109
 
110
  // Return the converted image data
@@ -128,7 +133,7 @@ Requirements:
128
 
129
  // Check if we should retry
130
  if (retryCount < MAX_RETRIES && isRetryableError) {
131
- console.log(`Retryable error encountered in doodle conversion (${retryCount + 1}/${MAX_RETRIES}):`, attemptError.message);
132
  retryCount++;
133
  // Wait before retrying
134
  await wait(RETRY_DELAY * retryCount);
@@ -140,7 +145,7 @@ Requirements:
140
  }
141
  }
142
  } catch (error) {
143
- console.error("Error during doodle conversion:", error);
144
 
145
  // Check for specific error types
146
  if (error.message?.includes('quota') || error.message?.includes('Resource has been exhausted')) {
@@ -161,10 +166,8 @@ Requirements:
161
  });
162
  }
163
 
164
- const errorMessage = error.message || "An unknown error occurred during conversion.";
165
- return res.status(500).json({
166
- success: false,
167
- error: errorMessage,
168
  details: error.stack,
169
  retries: retryCount
170
  });
 
1
+ import { GoogleGenerativeAI } from "@google/generative-ai";
2
  import { NextResponse } from 'next/server';
3
 
4
  // Configuration for the API route
 
35
  // Set up the API key
36
  const apiKey = customApiKey || process.env.GEMINI_API_KEY;
37
  if (!apiKey) {
38
+ console.error('Missing Gemini API key');
39
+ return res.status(500).json({ error: 'API key is not configured' });
40
  }
41
 
42
  // Retry loop for handling transient errors
 
45
  console.log(`Initializing Gemini AI for doodle conversion (attempt ${retryCount + 1}/${MAX_RETRIES + 1})`);
46
  // Initialize the Gemini API
47
  const genAI = new GoogleGenerativeAI(apiKey);
48
+
49
+ console.log('Configuring Gemini model');
50
  const model = genAI.getGenerativeModel({
51
  model: "gemini-2.0-flash-exp-image-generation",
52
  generationConfig: {
 
74
  * Text should remain readable in the final doodle, and true to the original :))`;
75
 
76
  // Prepare the generation content
77
+ console.log('Including image data in generation request');
78
  const generationContent = [
79
  {
80
  inlineData: {
 
88
  // Generate content
89
  console.log(`Calling Gemini API for doodle conversion (attempt ${retryCount + 1}/${MAX_RETRIES + 1})...`);
90
  const result = await model.generateContent(generationContent);
91
+ console.log('Gemini API response received');
92
+
93
  const response = await result.response;
 
94
 
95
  // Process the response to extract image data
96
  let convertedImageData = null;
 
102
  for (const part of response.candidates[0].content.parts) {
103
  if (part.inlineData) {
104
  convertedImageData = part.inlineData.data;
105
+ console.log('Found image data in response');
106
  break;
107
  }
108
  }
109
 
110
  if (!convertedImageData) {
111
  console.error('No image data in response parts:', response.candidates[0].content.parts);
112
+ throw new Error('No image data found in response parts');
113
  }
114
 
115
  // Return the converted image data
 
133
 
134
  // Check if we should retry
135
  if (retryCount < MAX_RETRIES && isRetryableError) {
136
+ console.log(`Retryable error encountered (${retryCount + 1}/${MAX_RETRIES}):`, attemptError.message);
137
  retryCount++;
138
  // Wait before retrying
139
  await wait(RETRY_DELAY * retryCount);
 
145
  }
146
  }
147
  } catch (error) {
148
+ console.error('Error in /api/convert-to-doodle:', error);
149
 
150
  // Check for specific error types
151
  if (error.message?.includes('quota') || error.message?.includes('Resource has been exhausted')) {
 
166
  });
167
  }
168
 
169
+ return res.status(500).json({
170
+ error: error.message || 'An error occurred during conversion.',
 
 
171
  details: error.stack,
172
  retries: retryCount
173
  });