suisuyy commited on
Commit
02ce812
·
1 Parent(s): 0817dc1

Rename project back to xpaintai in package.json and metadata.json; add QuestionMarkIcon component; enhance AI features with ask functionality in useAiFeatures hook and App component

Browse files
App.tsx CHANGED
@@ -75,10 +75,14 @@ const App: React.FC = () => {
75
  isGeneratingAiImage,
76
  sharedImageUrlForAi,
77
  aiEditError,
 
 
78
  handleMagicUpload,
79
  handleGenerateAiImage,
 
80
  handleCancelAiEdit,
81
  setAiPrompt,
 
82
  } = useAiFeatures({
83
  currentDataURL,
84
  showToast,
@@ -90,6 +94,14 @@ const App: React.FC = () => {
90
  currentCanvasWidth: canvasWidth, // Pass current canvas dimensions
91
  currentCanvasHeight: canvasHeight, // Pass current canvas dimensions
92
  });
 
 
 
 
 
 
 
 
93
 
94
  // Simple UI Toggles
95
  const handleToggleSettingsPanel = () => setShowSettingsPanel(prev => !prev);
@@ -170,11 +182,14 @@ const App: React.FC = () => {
170
  isOpen={showAiEditModal}
171
  onClose={handleCancelAiEdit}
172
  onGenerate={handleGenerateAiImage}
 
173
  imageUrl={sharedImageUrlForAi}
174
  prompt={aiPrompt}
175
- onPromptChange={setAiPrompt}
176
  isLoading={isGeneratingAiImage}
 
177
  error={aiEditError}
 
178
  />
179
  )}
180
 
@@ -218,7 +233,7 @@ const App: React.FC = () => {
218
  />
219
  )}
220
 
221
- <div className="fixed top-4 right-4 z-[1200] space-y-2">
222
  {toasts.map(toast => (
223
  <div
224
  key={toast.id}
@@ -238,4 +253,4 @@ const App: React.FC = () => {
238
  );
239
  };
240
 
241
- export default App;
 
75
  isGeneratingAiImage,
76
  sharedImageUrlForAi,
77
  aiEditError,
78
+ isAskingAi,
79
+ askUrl,
80
  handleMagicUpload,
81
  handleGenerateAiImage,
82
+ handleAskAi,
83
  handleCancelAiEdit,
84
  setAiPrompt,
85
+ clearAskUrl,
86
  } = useAiFeatures({
87
  currentDataURL,
88
  showToast,
 
94
  currentCanvasWidth: canvasWidth, // Pass current canvas dimensions
95
  currentCanvasHeight: canvasHeight, // Pass current canvas dimensions
96
  });
97
+
98
+ const handlePromptChange = (newPrompt: string) => {
99
+ setAiPrompt(newPrompt);
100
+ // Clear the previous "Ask" response when the user types a new prompt
101
+ if (askUrl !== null) {
102
+ clearAskUrl();
103
+ }
104
+ };
105
 
106
  // Simple UI Toggles
107
  const handleToggleSettingsPanel = () => setShowSettingsPanel(prev => !prev);
 
182
  isOpen={showAiEditModal}
183
  onClose={handleCancelAiEdit}
184
  onGenerate={handleGenerateAiImage}
185
+ onAsk={handleAskAi}
186
  imageUrl={sharedImageUrlForAi}
187
  prompt={aiPrompt}
188
+ onPromptChange={handlePromptChange}
189
  isLoading={isGeneratingAiImage}
190
+ isAsking={isAskingAi}
191
  error={aiEditError}
192
+ askUrl={askUrl}
193
  />
194
  )}
195
 
 
233
  />
234
  )}
235
 
236
+ <div className="fixed bottom-4 right-4 z-[1200] flex flex-col-reverse gap-2">
237
  {toasts.map(toast => (
238
  <div
239
  key={toast.id}
 
253
  );
254
  };
255
 
256
+ export default App;
components/AiEditModal.tsx CHANGED
@@ -1,44 +1,60 @@
1
- import React from 'react';
2
- import { MagicSparkleIcon, CloseIcon } from './icons';
3
 
4
  interface AiEditModalProps {
5
  isOpen: boolean;
6
  onClose: () => void;
7
  onGenerate: () => void;
 
8
  imageUrl: string;
9
  prompt: string;
10
  onPromptChange: (newPrompt: string) => void;
11
  isLoading: boolean;
 
12
  error: string | null;
 
13
  }
14
 
15
  const AiEditModal: React.FC<AiEditModalProps> = ({
16
  isOpen,
17
  onClose,
18
  onGenerate,
 
19
  imageUrl,
20
  prompt,
21
  onPromptChange,
22
  isLoading,
 
23
  error,
 
24
  }) => {
 
 
 
 
 
 
 
 
25
  if (!isOpen) return null;
 
 
26
 
27
  return (
28
  <div
29
- className="fixed inset-0 bg-black bg-opacity-60 backdrop-blur-sm flex justify-center items-center z-[1000] p-4"
30
  aria-modal="true"
31
  role="dialog"
32
  aria-labelledby="ai-edit-modal-title"
33
  >
34
- <div className="bg-white rounded-lg shadow-2xl p-6 w-full max-w-lg transform transition-all">
35
  <div className="flex justify-between items-center mb-4">
36
- <h2 id="ai-edit-modal-title" className="text-xl font-semibold text-slate-700">Edit Image with AI</h2>
37
  <button
38
  onClick={onClose}
39
  className="text-slate-400 hover:text-slate-600 transition-colors"
40
  aria-label="Close AI edit modal"
41
- disabled={isLoading}
42
  >
43
  <CloseIcon className="w-6 h-6" />
44
  </button>
@@ -49,22 +65,21 @@ const AiEditModal: React.FC<AiEditModalProps> = ({
49
  <img
50
  src={imageUrl}
51
  alt="Uploaded image preview"
52
- className="max-w-full h-auto max-h-48 rounded border border-slate-300 object-contain mx-auto"
53
  />
54
  </div>
55
 
56
  <div className="mb-4">
57
  <label htmlFor="aiPrompt" className="block text-sm font-medium text-slate-700 mb-1">
58
- Describe what you want to change:
59
  </label>
60
  <textarea
61
  id="aiPrompt"
62
  value={prompt}
63
  onChange={(e) => onPromptChange(e.target.value)}
64
- placeholder="e.g., 'make the cat wear a party hat', 'change background to a beach'"
65
- rows={3}
66
- className="w-full p-2 border border-slate-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-shadow text-sm"
67
- disabled={isLoading}
68
  aria-describedby={error ? "ai-edit-error" : undefined}
69
  />
70
  </div>
@@ -76,17 +91,30 @@ const AiEditModal: React.FC<AiEditModalProps> = ({
76
  </div>
77
  )}
78
 
79
- <div className="flex flex-col sm:flex-row justify-end gap-3">
80
- <button
81
- onClick={onClose}
82
- disabled={isLoading}
83
- className="px-4 py-2 bg-slate-200 text-slate-700 rounded-md hover:bg-slate-300 transition-colors disabled:opacity-50 disabled:cursor-not-allowed text-sm font-medium"
84
  >
85
- Cancel
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  </button>
87
  <button
88
  onClick={onGenerate}
89
- disabled={isLoading || !prompt.trim()}
90
  className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors disabled:opacity-50 disabled:cursor-not-allowed text-sm font-medium flex items-center justify-center gap-2"
91
  >
92
  {isLoading ? (
@@ -105,6 +133,16 @@ const AiEditModal: React.FC<AiEditModalProps> = ({
105
  )}
106
  </button>
107
  </div>
 
 
 
 
 
 
 
 
 
 
108
  </div>
109
  </div>
110
  );
 
1
+ import React, { useEffect, useRef } from 'react';
2
+ import { MagicSparkleIcon, CloseIcon, QuestionMarkIcon } from './icons';
3
 
4
  interface AiEditModalProps {
5
  isOpen: boolean;
6
  onClose: () => void;
7
  onGenerate: () => void;
8
+ onAsk: () => void;
9
  imageUrl: string;
10
  prompt: string;
11
  onPromptChange: (newPrompt: string) => void;
12
  isLoading: boolean;
13
+ isAsking: boolean;
14
  error: string | null;
15
+ askUrl: string | null;
16
  }
17
 
18
  const AiEditModal: React.FC<AiEditModalProps> = ({
19
  isOpen,
20
  onClose,
21
  onGenerate,
22
+ onAsk,
23
  imageUrl,
24
  prompt,
25
  onPromptChange,
26
  isLoading,
27
+ isAsking,
28
  error,
29
+ askUrl,
30
  }) => {
31
+ const iframeContainerRef = useRef<HTMLDivElement>(null);
32
+
33
+ useEffect(() => {
34
+ if (askUrl && iframeContainerRef.current) {
35
+ iframeContainerRef.current.scrollIntoView({ behavior: 'smooth', block: 'start' });
36
+ }
37
+ }, [askUrl]);
38
+
39
  if (!isOpen) return null;
40
+
41
+ const isAnyLoading = isLoading || isAsking;
42
 
43
  return (
44
  <div
45
+ className="fixed inset-0 bg-black bg-opacity-60 backdrop-blur-sm flex justify-center items-start z-[1000] p-4 pt-16"
46
  aria-modal="true"
47
  role="dialog"
48
  aria-labelledby="ai-edit-modal-title"
49
  >
50
+ <div className="bg-white rounded-lg shadow-2xl p-6 w-full max-w-lg transform transition-all max-h-[calc(100vh-5rem)] overflow-y-auto">
51
  <div className="flex justify-between items-center mb-4">
52
+ <h2 id="ai-edit-modal-title" className="text-xl font-semibold text-slate-700">Edit or Ask about Image with AI</h2>
53
  <button
54
  onClick={onClose}
55
  className="text-slate-400 hover:text-slate-600 transition-colors"
56
  aria-label="Close AI edit modal"
57
+ disabled={isAnyLoading}
58
  >
59
  <CloseIcon className="w-6 h-6" />
60
  </button>
 
65
  <img
66
  src={imageUrl}
67
  alt="Uploaded image preview"
68
+ className="max-w-full w-auto h-48 rounded border border-slate-300 object-contain mx-auto"
69
  />
70
  </div>
71
 
72
  <div className="mb-4">
73
  <label htmlFor="aiPrompt" className="block text-sm font-medium text-slate-700 mb-1">
74
+ Describe what you want to change, or ask a question:
75
  </label>
76
  <textarea
77
  id="aiPrompt"
78
  value={prompt}
79
  onChange={(e) => onPromptChange(e.target.value)}
80
+ placeholder="e.g., 'make the cat wear a party hat' or 'what is in this image?'"
81
+ className="w-full p-2 border border-slate-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-shadow text-xl h-[40vh]"
82
+ disabled={isAnyLoading}
 
83
  aria-describedby={error ? "ai-edit-error" : undefined}
84
  />
85
  </div>
 
91
  </div>
92
  )}
93
 
94
+ <div className="flex justify-between items-center gap-3">
95
+ <button
96
+ onClick={onAsk}
97
+ disabled={isAnyLoading || !prompt.trim()}
98
+ className="px-4 py-2 bg-sky-600 text-white rounded-md hover:bg-sky-700 focus:ring-2 focus:ring-sky-500 focus:ring-offset-2 transition-colors disabled:opacity-50 disabled:cursor-not-allowed text-sm font-medium flex items-center justify-center gap-2"
99
  >
100
+ {isAsking ? (
101
+ <>
102
+ <svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
103
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
104
+ <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
105
+ </svg>
106
+ Asking...
107
+ </>
108
+ ) : (
109
+ <>
110
+ <QuestionMarkIcon className="w-4 h-4 mr-1" />
111
+ Ask
112
+ </>
113
+ )}
114
  </button>
115
  <button
116
  onClick={onGenerate}
117
+ disabled={isAnyLoading || !prompt.trim()}
118
  className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors disabled:opacity-50 disabled:cursor-not-allowed text-sm font-medium flex items-center justify-center gap-2"
119
  >
120
  {isLoading ? (
 
133
  )}
134
  </button>
135
  </div>
136
+
137
+ {askUrl && (
138
+ <div ref={iframeContainerRef} className="mt-4 pt-2">
139
+ <iframe
140
+ src={askUrl}
141
+ title="AI Response"
142
+ className="w-full h-[70vh] border-0 rounded-md bg-slate-50"
143
+ />
144
+ </div>
145
+ )}
146
  </div>
147
  </div>
148
  );
components/icons.tsx CHANGED
@@ -71,4 +71,10 @@ export const FullscreenExitIcon: React.FC<{className?: string}> = ({className})
71
  <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className || "w-6 h-6"}>
72
  <path strokeLinecap="round" strokeLinejoin="round" d="M9 9V4.5M9 9H4.5M9 9 3.75 3.75M9 15v4.5M9 15H4.5M9 15l-5.25 5.25M15 9h4.5M15 9V4.5M15 9l5.25-5.25M15 15h4.5M15 15v4.5M15 15l5.25 5.25" />
73
  </svg>
74
- );
 
 
 
 
 
 
 
71
  <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className || "w-6 h-6"}>
72
  <path strokeLinecap="round" strokeLinejoin="round" d="M9 9V4.5M9 9H4.5M9 9 3.75 3.75M9 15v4.5M9 15H4.5M9 15l-5.25 5.25M15 9h4.5M15 9V4.5M15 9l5.25-5.25M15 15h4.5M15 15v4.5M15 15l5.25 5.25" />
73
  </svg>
74
+ );
75
+
76
+ export const QuestionMarkIcon: React.FC<{className?: string}> = ({className}) => (
77
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className || "w-6 h-6"}>
78
+ <path strokeLinecap="round" strokeLinejoin="round" d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 5.25h.008v.008H12v-.008Z" />
79
+ </svg>
80
+ );
hooks/useAiFeatures.ts CHANGED
@@ -4,6 +4,7 @@ import { dataURLtoBlob, calculateSHA256, copyToClipboard } from '../utils/canvas
4
  import { CanvasHistoryHook } from './useCanvasHistory';
5
 
6
  const SHARE_API_URL = 'https://sharefile.suisuy.eu.org';
 
7
  const MAX_UPLOAD_SIZE_BYTES = 50 * 1024 * 1024; // 50MB
8
 
9
  export type AiImageQuality = 'low' | 'medium' | 'hd';
@@ -16,10 +17,14 @@ interface AiFeaturesHook {
16
  isGeneratingAiImage: boolean;
17
  sharedImageUrlForAi: string | null;
18
  aiEditError: string | null;
 
 
19
  handleMagicUpload: () => Promise<void>;
20
  handleGenerateAiImage: () => Promise<void>;
 
21
  handleCancelAiEdit: () => void;
22
  setAiPrompt: React.Dispatch<React.SetStateAction<string>>;
 
23
  }
24
 
25
  interface UseAiFeaturesProps {
@@ -51,6 +56,12 @@ export const useAiFeatures = ({
51
  const [isGeneratingAiImage, setIsGeneratingAiImage] = useState<boolean>(false);
52
  const [sharedImageUrlForAi, setSharedImageUrlForAi] = useState<string | null>(null);
53
  const [aiEditError, setAiEditError] = useState<string | null>(null);
 
 
 
 
 
 
54
 
55
  const loadAiImageOntoCanvas = useCallback((aiImageDataUrl: string) => {
56
  const img = new Image();
@@ -153,7 +164,8 @@ export const useAiFeatures = ({
153
  setSharedImageUrlForAi(finalShareUrl);
154
  setShowAiEditModal(true);
155
  setAiPrompt('');
156
- setAiEditError(null);
 
157
 
158
  } catch (error: any) {
159
  console.error('Magic upload error:', error);
@@ -161,7 +173,7 @@ export const useAiFeatures = ({
161
  } finally {
162
  setIsMagicUploading(false);
163
  }
164
- }, [isMagicUploading, currentDataURL, showToast]);
165
 
166
  const handleGenerateAiImage = useCallback(async () => {
167
  if (!aiPrompt.trim() || !sharedImageUrlForAi) {
@@ -170,6 +182,7 @@ export const useAiFeatures = ({
170
  }
171
  setIsGeneratingAiImage(true);
172
  setAiEditError(null);
 
173
  showToast('Generating AI image...', 'info');
174
 
175
  const encodedPrompt = encodeURIComponent(aiPrompt);
@@ -231,7 +244,30 @@ export const useAiFeatures = ({
231
  setAiEditError(error.message || 'An unknown error occurred during AI image generation.');
232
  setIsGeneratingAiImage(false);
233
  }
234
- }, [aiPrompt, sharedImageUrlForAi, showToast, loadAiImageOntoCanvas, aiImageQuality, aiApiEndpoint, aiDimensionsMode, currentCanvasWidth, currentCanvasHeight]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
235
 
236
 
237
  const handleCancelAiEdit = () => {
@@ -245,7 +281,8 @@ export const useAiFeatures = ({
245
  }
246
  setAiPrompt('');
247
  setAiEditError(null);
248
- setSharedImageUrlForAi(null);
 
249
  };
250
 
251
  return {
@@ -255,9 +292,13 @@ export const useAiFeatures = ({
255
  isGeneratingAiImage,
256
  sharedImageUrlForAi,
257
  aiEditError,
 
 
258
  handleMagicUpload,
259
  handleGenerateAiImage,
 
260
  handleCancelAiEdit,
261
  setAiPrompt,
 
262
  };
263
- };
 
4
  import { CanvasHistoryHook } from './useCanvasHistory';
5
 
6
  const SHARE_API_URL = 'https://sharefile.suisuy.eu.org';
7
+ const ASK_API_URL = 'https://getai.deno.dev/';
8
  const MAX_UPLOAD_SIZE_BYTES = 50 * 1024 * 1024; // 50MB
9
 
10
  export type AiImageQuality = 'low' | 'medium' | 'hd';
 
17
  isGeneratingAiImage: boolean;
18
  sharedImageUrlForAi: string | null;
19
  aiEditError: string | null;
20
+ isAskingAi: boolean;
21
+ askUrl: string | null;
22
  handleMagicUpload: () => Promise<void>;
23
  handleGenerateAiImage: () => Promise<void>;
24
+ handleAskAi: () => Promise<void>;
25
  handleCancelAiEdit: () => void;
26
  setAiPrompt: React.Dispatch<React.SetStateAction<string>>;
27
+ clearAskUrl: () => void;
28
  }
29
 
30
  interface UseAiFeaturesProps {
 
56
  const [isGeneratingAiImage, setIsGeneratingAiImage] = useState<boolean>(false);
57
  const [sharedImageUrlForAi, setSharedImageUrlForAi] = useState<string | null>(null);
58
  const [aiEditError, setAiEditError] = useState<string | null>(null);
59
+ const [isAskingAi, setIsAskingAi] = useState<boolean>(false);
60
+ const [askUrl, setAskUrl] = useState<string | null>(null);
61
+
62
+ const clearAskUrl = useCallback(() => {
63
+ setAskUrl(null);
64
+ }, []);
65
 
66
  const loadAiImageOntoCanvas = useCallback((aiImageDataUrl: string) => {
67
  const img = new Image();
 
164
  setSharedImageUrlForAi(finalShareUrl);
165
  setShowAiEditModal(true);
166
  setAiPrompt('');
167
+ setAiEditError(null);
168
+ clearAskUrl();
169
 
170
  } catch (error: any) {
171
  console.error('Magic upload error:', error);
 
173
  } finally {
174
  setIsMagicUploading(false);
175
  }
176
+ }, [isMagicUploading, currentDataURL, showToast, clearAskUrl]);
177
 
178
  const handleGenerateAiImage = useCallback(async () => {
179
  if (!aiPrompt.trim() || !sharedImageUrlForAi) {
 
182
  }
183
  setIsGeneratingAiImage(true);
184
  setAiEditError(null);
185
+ clearAskUrl();
186
  showToast('Generating AI image...', 'info');
187
 
188
  const encodedPrompt = encodeURIComponent(aiPrompt);
 
244
  setAiEditError(error.message || 'An unknown error occurred during AI image generation.');
245
  setIsGeneratingAiImage(false);
246
  }
247
+ }, [aiPrompt, sharedImageUrlForAi, showToast, loadAiImageOntoCanvas, aiImageQuality, aiApiEndpoint, aiDimensionsMode, currentCanvasWidth, currentCanvasHeight, clearAskUrl]);
248
+
249
+ const handleAskAi = useCallback(async () => {
250
+ if (!aiPrompt.trim() || !sharedImageUrlForAi) {
251
+ setAiEditError('Please enter a question about the image.');
252
+ return;
253
+ }
254
+ setIsAskingAi(true);
255
+ setAiEditError(null);
256
+ setAskUrl(null); // Clear previous URL first
257
+
258
+ try {
259
+ const encodedPrompt = encodeURIComponent(aiPrompt);
260
+ const encodedImageUrl = encodeURIComponent(sharedImageUrlForAi);
261
+ const finalAskUrl = `${ASK_API_URL}?q=${encodedPrompt}&image=${encodedImageUrl}`;
262
+ setAskUrl(finalAskUrl);
263
+
264
+ } catch (error: any) {
265
+ console.error('AI ask error:', error);
266
+ setAiEditError(error.message || 'An unknown error occurred while asking AI.');
267
+ } finally {
268
+ setIsAskingAi(false);
269
+ }
270
+ }, [aiPrompt, sharedImageUrlForAi]);
271
 
272
 
273
  const handleCancelAiEdit = () => {
 
281
  }
282
  setAiPrompt('');
283
  setAiEditError(null);
284
+ setSharedImageUrlForAi(null);
285
+ clearAskUrl();
286
  };
287
 
288
  return {
 
292
  isGeneratingAiImage,
293
  sharedImageUrlForAi,
294
  aiEditError,
295
+ isAskingAi,
296
+ askUrl,
297
  handleMagicUpload,
298
  handleGenerateAiImage,
299
+ handleAskAi,
300
  handleCancelAiEdit,
301
  setAiPrompt,
302
+ clearAskUrl,
303
  };
304
+ };
hooks/useCanvasFileUtils.ts CHANGED
@@ -81,7 +81,6 @@ export const useCanvasFileUtils = ({
81
  const compositeDataURL = tempCanvas.toDataURL('image/png');
82
 
83
  updateCanvasState(compositeDataURL, finalCanvasWidth, finalCanvasHeight);
84
- setZoomLevel(0.5);
85
  showToast('Image loaded successfully.', 'success');
86
  }
87
  };
@@ -93,7 +92,7 @@ export const useCanvasFileUtils = ({
93
  reader.readAsDataURL(file);
94
  if(event.target) event.target.value = '';
95
  }
96
- }, [canvasWidth, canvasHeight, updateCanvasState, showToast, setZoomLevel]);
97
 
98
  const handleExportImage = useCallback(() => {
99
  if (currentDataURL) {
@@ -110,18 +109,10 @@ export const useCanvasFileUtils = ({
110
  }, [currentDataURL, showToast]);
111
 
112
  const handleClearCanvas = useCallback(() => {
113
- requestConfirmation(
114
- "Clear Canvas",
115
- "Are you sure you want to clear the entire canvas? This action cannot be undone from history.",
116
- () => {
117
- const blankCanvas = getBlankCanvasDataURL(canvasWidth, canvasHeight);
118
- updateCanvasState(blankCanvas, canvasWidth, canvasHeight);
119
- setZoomLevel(0.5);
120
- showToast("Canvas cleared.", "info");
121
- },
122
- { isDestructive: true }
123
- );
124
- }, [requestConfirmation, canvasWidth, canvasHeight, updateCanvasState, setZoomLevel, showToast]);
125
 
126
  const handleCanvasSizeChange = useCallback((newWidth: number, newHeight: number) => {
127
  if (newWidth === canvasWidth && newHeight === canvasHeight) {
@@ -141,4 +132,4 @@ export const useCanvasFileUtils = ({
141
  }, [canvasWidth, canvasHeight, requestConfirmation, updateCanvasState, setZoomLevel, showToast]);
142
 
143
  return { handleLoadImageFile, handleExportImage, handleClearCanvas, handleCanvasSizeChange };
144
- };
 
81
  const compositeDataURL = tempCanvas.toDataURL('image/png');
82
 
83
  updateCanvasState(compositeDataURL, finalCanvasWidth, finalCanvasHeight);
 
84
  showToast('Image loaded successfully.', 'success');
85
  }
86
  };
 
92
  reader.readAsDataURL(file);
93
  if(event.target) event.target.value = '';
94
  }
95
+ }, [canvasWidth, canvasHeight, updateCanvasState, showToast]);
96
 
97
  const handleExportImage = useCallback(() => {
98
  if (currentDataURL) {
 
109
  }, [currentDataURL, showToast]);
110
 
111
  const handleClearCanvas = useCallback(() => {
112
+ const blankCanvas = getBlankCanvasDataURL(canvasWidth, canvasHeight);
113
+ updateCanvasState(blankCanvas, canvasWidth, canvasHeight);
114
+ showToast("Canvas cleared.", "info");
115
+ }, [canvasWidth, canvasHeight, updateCanvasState, showToast]);
 
 
 
 
 
 
 
 
116
 
117
  const handleCanvasSizeChange = useCallback((newWidth: number, newHeight: number) => {
118
  if (newWidth === canvasWidth && newHeight === canvasHeight) {
 
132
  }, [canvasWidth, canvasHeight, requestConfirmation, updateCanvasState, setZoomLevel, showToast]);
133
 
134
  return { handleLoadImageFile, handleExportImage, handleClearCanvas, handleCanvasSizeChange };
135
+ };
metadata.json CHANGED
@@ -1,5 +1,5 @@
1
  {
2
- "name": "paintai",
3
  "description": "A simple web-based paint application with features like pen customization, undo/redo, image loading/exporting, and autosave to IndexedDB.",
4
  "requestFramePermissions": [],
5
  "prompt": ""
 
1
  {
2
+ "name": "xpaintai",
3
  "description": "A simple web-based paint application with features like pen customization, undo/redo, image loading/exporting, and autosave to IndexedDB.",
4
  "requestFramePermissions": [],
5
  "prompt": ""
package.json CHANGED
@@ -1,5 +1,5 @@
1
  {
2
- "name": "paintai",
3
  "private": true,
4
  "version": "0.0.0",
5
  "type": "module",
 
1
  {
2
+ "name": "xpaintai",
3
  "private": true,
4
  "version": "0.0.0",
5
  "type": "module",