Trudy commited on
Commit
aba9d46
·
1 Parent(s): 55d712f

fixing the debug modal + image upload

Browse files
components/CanvasContainer.js CHANGED
@@ -69,6 +69,7 @@ const CanvasContainer = () => {
69
  const [showErrorModal, setShowErrorModal] = useState(false);
70
  const [errorMessage, setErrorMessage] = useState("");
71
  const [customApiKey, setCustomApiKey] = useState("");
 
72
  const [styleMode, setStyleMode] = useState("material");
73
  const [strokeCount, setStrokeCount] = useState(0);
74
  const strokeTimeoutRef = useRef(null);
@@ -108,8 +109,37 @@ const CanvasContainer = () => {
108
  // Validate the API key silently
109
  validateApiKey(savedApiKey);
110
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
  }, []);
112
 
 
 
 
 
 
 
 
 
 
 
 
113
  // Add a function to validate the API key
114
  const validateApiKey = async (apiKey) => {
115
  if (!apiKey) return;
@@ -1608,6 +1638,8 @@ const CanvasContainer = () => {
1608
  customApiKey={customApiKey}
1609
  setCustomApiKey={setCustomApiKey}
1610
  handleApiKeySubmit={handleApiKeySubmit}
 
 
1611
  />
1612
 
1613
  <ApiKeyModal
 
69
  const [showErrorModal, setShowErrorModal] = useState(false);
70
  const [errorMessage, setErrorMessage] = useState("");
71
  const [customApiKey, setCustomApiKey] = useState("");
72
+ const [debugMode, setDebugMode] = useState(false);
73
  const [styleMode, setStyleMode] = useState("material");
74
  const [strokeCount, setStrokeCount] = useState(0);
75
  const strokeTimeoutRef = useRef(null);
 
109
  // Validate the API key silently
110
  validateApiKey(savedApiKey);
111
  }
112
+
113
+ // Check if debug mode is enabled in localStorage or URL
114
+ const debugParam = new URLSearchParams(window.location.search).get('debug');
115
+ // Only look at localStorage if debug parameter is not explicitly set to false
116
+ const savedDebug = debugParam !== "false" && localStorage.getItem("debugMode") === "true";
117
+
118
+ if (debugParam === "true" || savedDebug) {
119
+ // Set debug mode to true AND show error modal
120
+ setDebugMode(true);
121
+ setShowErrorModal(true);
122
+ } else {
123
+ // Ensure debug mode is OFF by default
124
+ setDebugMode(false);
125
+ // Also clean up any stale localStorage value
126
+ if (localStorage.getItem("debugMode") === "true") {
127
+ localStorage.setItem("debugMode", "false");
128
+ }
129
+ }
130
  }, []);
131
 
132
+ // Add effect to save debug mode to localStorage
133
+ useEffect(() => {
134
+ // Always save the current state to localStorage
135
+ localStorage.setItem("debugMode", debugMode.toString());
136
+
137
+ // ONLY auto-show error modal when debug mode is enabled
138
+ if (debugMode === true) {
139
+ setShowErrorModal(true);
140
+ }
141
+ }, [debugMode]);
142
+
143
  // Add a function to validate the API key
144
  const validateApiKey = async (apiKey) => {
145
  if (!apiKey) return;
 
1638
  customApiKey={customApiKey}
1639
  setCustomApiKey={setCustomApiKey}
1640
  handleApiKeySubmit={handleApiKeySubmit}
1641
+ debugMode={debugMode}
1642
+ setDebugMode={setDebugMode}
1643
  />
1644
 
1645
  <ApiKeyModal
components/ErrorModal.js CHANGED
@@ -1,4 +1,4 @@
1
- import { X } from 'lucide-react';
2
  import { useState, useEffect } from 'react';
3
 
4
  const ErrorModal = ({
@@ -6,7 +6,10 @@ const ErrorModal = ({
6
  closeErrorModal,
7
  customApiKey,
8
  setCustomApiKey,
9
- handleApiKeySubmit
 
 
 
10
  }) => {
11
  const [localApiKey, setLocalApiKey] = useState(customApiKey);
12
 
@@ -29,29 +32,69 @@ const ErrorModal = ({
29
  closeErrorModal();
30
  }
31
  }}
 
 
 
 
 
32
  >
33
- <div className="bg-white p-6 rounded-xl shadow-medium max-w-md w-full mx-4 my-8">
 
 
 
 
34
  <div className="flex flex-col gap-4">
35
  <div className="flex items-center justify-between">
36
- <h2 className="text-xl font-medium text-gray-800">API Quota Exceeded</h2>
37
- <button
38
- type="button"
39
- onClick={closeErrorModal}
40
- className="text-gray-500 hover:text-gray-700"
41
- aria-label="Close"
42
- >
43
- <X className="w-5 h-5" />
44
- </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  </div>
46
 
47
  <div className="text-gray-600">
48
  <p className="mb-2">
49
- You've exceeded your API quota. You can:
50
  </p>
51
- <ul className="list-disc ml-5 mb-4 space-y-1 text-gray-600">
52
- <li>Wait for your quota to reset</li>
53
- <li>Use your own API key from <a href="https://ai.google.dev/" target="_blank" rel="noopener noreferrer" className="text-blue-500 hover:underline">Google AI Studio</a></li>
54
- </ul>
55
  </div>
56
 
57
  <form onSubmit={handleSubmit} className="space-y-3">
@@ -70,7 +113,6 @@ const ErrorModal = ({
70
  onChange={(e) => setLocalApiKey(e.target.value)}
71
  className="w-full px-3 py-2 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
72
  />
73
- <p className="text-xs text-gray-500 mt-1">Your API key will be saved locally and never sent to our servers.</p>
74
  </div>
75
  <div className="flex justify-end gap-3 pt-2">
76
  <button
@@ -90,7 +132,7 @@ const ErrorModal = ({
90
  </div>
91
  </form>
92
  </div>
93
- </div>
94
  </div>
95
  )}
96
  </>
 
1
+ import { X, Bug } from 'lucide-react';
2
  import { useState, useEffect } from 'react';
3
 
4
  const ErrorModal = ({
 
6
  closeErrorModal,
7
  customApiKey,
8
  setCustomApiKey,
9
+ handleApiKeySubmit,
10
+ // Add debug toggle prop with default value
11
+ debugMode = false,
12
+ setDebugMode = () => {}
13
  }) => {
14
  const [localApiKey, setLocalApiKey] = useState(customApiKey);
15
 
 
32
  closeErrorModal();
33
  }
34
  }}
35
+ onKeyDown={(e) => {
36
+ if (e.key === 'Escape') {
37
+ closeErrorModal();
38
+ }
39
+ }}
40
  >
41
+ <dialog
42
+ open
43
+ className="bg-white p-6 rounded-xl shadow-medium max-w-md w-full mx-4 my-8 relative"
44
+ aria-labelledby="error-modal-title"
45
+ >
46
  <div className="flex flex-col gap-4">
47
  <div className="flex items-center justify-between">
48
+ <h2 id="error-modal-title" className="text-xl font-medium text-gray-800">This space is super popular</h2>
49
+ <div className="flex items-center gap-2">
50
+ {/* Debug toggle button - only visible in development */}
51
+ {process.env.NODE_ENV === 'development' && (
52
+ <div className="relative group">
53
+ <button
54
+ type="button"
55
+ onClick={() => setDebugMode(!debugMode)}
56
+ className={`flex items-center justify-center p-1.5 rounded transition-colors ${
57
+ debugMode
58
+ ? 'bg-blue-500 text-white hover:bg-blue-600'
59
+ : 'bg-gray-100 text-gray-500 hover:bg-gray-200'
60
+ }`}
61
+ aria-label={debugMode ? "Debug mode on" : "Debug mode off"}
62
+ >
63
+ <Bug className="w-4 h-4" />
64
+ <span className="ml-1 text-xs font-medium">{debugMode ? "ON" : "OFF"}</span>
65
+ </button>
66
+
67
+ {/* Tooltip */}
68
+ <div className="absolute left-1/2 bottom-full mb-2 -translate-x-1/2 hidden group-hover:block w-48 p-2 bg-gray-800 text-xs text-white rounded shadow-lg z-10">
69
+ <p className="text-center">
70
+ Debug Mode: {debugMode ? "ON" : "OFF"}
71
+ </p>
72
+ <p className="mt-1">
73
+ When enabled, this modal will automatically appear on page load for development purposes.
74
+ </p>
75
+ <div className="w-2 h-2 bg-gray-800 absolute left-1/2 -bottom-1 -ml-1 rotate-45" />
76
+ </div>
77
+ </div>
78
+ )}
79
+ <button
80
+ type="button"
81
+ onClick={closeErrorModal}
82
+ className="text-gray-500 hover:text-gray-700"
83
+ aria-label="Close"
84
+ >
85
+ <X className="w-5 h-5" />
86
+ </button>
87
+ </div>
88
  </div>
89
 
90
  <div className="text-gray-600">
91
  <p className="mb-2">
92
+ Our free API key is currently at capacity. To continue:
93
  </p>
94
+ <ol className="list-decimal pl-5 space-y-2 mb-0">
95
+ <li>Get your own API key at <a href="https://ai.dev/app/apikey" target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:text-blue-800 underline">ai.dev/app/apikey</a></li>
96
+ <li>Enter it below</li>
97
+ </ol>
98
  </div>
99
 
100
  <form onSubmit={handleSubmit} className="space-y-3">
 
113
  onChange={(e) => setLocalApiKey(e.target.value)}
114
  className="w-full px-3 py-2 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
115
  />
 
116
  </div>
117
  <div className="flex justify-end gap-3 pt-2">
118
  <button
 
132
  </div>
133
  </form>
134
  </div>
135
+ </dialog>
136
  </div>
137
  )}
138
  </>
pages/api/convert-to-doodle.js CHANGED
@@ -16,6 +16,14 @@ export default async function handler(req, res) {
16
  return res.status(405).json({ error: 'Method not allowed' });
17
  }
18
 
 
 
 
 
 
 
 
 
19
  try {
20
  const { imageData, customApiKey } = req.body;
21
 
@@ -30,21 +38,25 @@ export default async function handler(req, res) {
30
  return res.status(500).json({ error: 'API key is required' });
31
  }
32
 
33
- // Initialize the Gemini API
34
- const genAI = new GoogleGenerativeAI(apiKey);
35
- const model = genAI.getGenerativeModel({
36
- model: "gemini-2.0-flash-exp-image-generation",
37
- generationConfig: {
38
- temperature: 1,
39
- topP: 0.95,
40
- topK: 40,
41
- maxOutputTokens: 8192,
42
- responseModalities: ["image", "text"]
43
- }
44
- });
 
 
 
 
45
 
46
- // Create the prompt for doodle conversion
47
- const prompt = `Could you please convert this image into a black and white doodle.
48
  Requirements:
49
  - Use ONLY pure black lines on a pure white background
50
  - No gray tones or shading
@@ -58,43 +70,103 @@ Requirements:
58
  * Do not simplify or omit any text elements
59
  * Text should remain readable in the final doodle, and true to the original :))`;
60
 
61
- // Prepare the generation content
62
- const generationContent = [
63
- {
64
- inlineData: {
65
- data: imageData,
66
- mimeType: "image/png"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  }
68
- },
69
- { text: prompt }
70
- ];
71
 
72
- // Generate content
73
- const result = await model.generateContent(generationContent);
74
- const response = await result.response;
75
-
76
- // Process the response to extract image data
77
- let convertedImageData = null;
78
- for (const part of response.candidates[0].content.parts) {
79
- if (part.inlineData) {
80
- convertedImageData = part.inlineData.data;
81
- break;
82
- }
83
- }
84
 
85
- if (!convertedImageData) {
86
- throw new Error('No image data received from the API');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  }
88
-
89
- // Return the converted image data
90
- return res.status(200).json({
91
- success: true,
92
- imageData: convertedImageData
93
- });
94
  } catch (error) {
95
  console.error("Error during doodle conversion:", error);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  const errorMessage = error.message || "An unknown error occurred during conversion.";
97
- // Ensure JSON response even on error
98
- return res.status(500).json({ success: false, error: errorMessage });
 
 
 
 
99
  }
100
  }
 
16
  return res.status(405).json({ error: 'Method not allowed' });
17
  }
18
 
19
+ // Add retry configuration
20
+ const MAX_RETRIES = 2;
21
+ const RETRY_DELAY = 1000; // 1 second
22
+ let retryCount = 0;
23
+
24
+ // Function to wait for a delay
25
+ const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms));
26
+
27
  try {
28
  const { imageData, customApiKey } = req.body;
29
 
 
38
  return res.status(500).json({ error: 'API key is required' });
39
  }
40
 
41
+ // Retry loop for handling transient errors
42
+ while (true) {
43
+ try {
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: {
50
+ temperature: 1,
51
+ topP: 0.95,
52
+ topK: 40,
53
+ maxOutputTokens: 8192,
54
+ responseModalities: ["image", "text"]
55
+ }
56
+ });
57
 
58
+ // Create the prompt for doodle conversion
59
+ const prompt = `Could you please convert this image into a black and white doodle.
60
  Requirements:
61
  - Use ONLY pure black lines on a pure white background
62
  - No gray tones or shading
 
70
  * Do not simplify or omit any text elements
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: {
77
+ data: imageData,
78
+ mimeType: "image/png"
79
+ }
80
+ },
81
+ { text: prompt }
82
+ ];
83
+
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;
92
+ if (!response?.candidates?.[0]?.content?.parts) {
93
+ console.error('Invalid response structure:', response);
94
+ throw new Error('Invalid response structure from Gemini API');
95
+ }
96
+
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
111
+ console.log('Successfully generated doodle');
112
+ return res.status(200).json({
113
+ success: true,
114
+ imageData: convertedImageData
115
+ });
116
+
117
+ } catch (attemptError) {
118
+ // Only retry certain types of errors that might be transient
119
+ const isRetryableError =
120
+ attemptError.message?.includes('timeout') ||
121
+ attemptError.message?.includes('network') ||
122
+ attemptError.message?.includes('ECONNRESET') ||
123
+ attemptError.message?.includes('rate limit') ||
124
+ attemptError.message?.includes('503') ||
125
+ attemptError.message?.includes('500') ||
126
+ attemptError.message?.includes('overloaded') ||
127
+ attemptError.message?.includes('connection');
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);
135
+ continue;
136
+ }
137
+
138
+ // If we've exhausted retries or it's not a retryable error, rethrow
139
+ throw attemptError;
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')) {
147
+ return res.status(429).json({
148
+ error: 'API quota exceeded. Please try again later or use your own API key.'
149
+ });
150
+ }
151
+
152
+ if (error.message?.includes('API key')) {
153
+ return res.status(500).json({
154
+ error: 'Invalid or missing API key. Please check your configuration.'
155
+ });
156
+ }
157
+
158
+ if (error.message?.includes('safety') || error.message?.includes('blocked') || error.message?.includes('harmful')) {
159
+ return res.status(400).json({
160
+ error: 'Content was blocked due to safety filters. Try a different prompt.'
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
+ });
171
  }
172
  }