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 +18 -3
- components/AiEditModal.tsx +57 -19
- components/icons.tsx +7 -1
- hooks/useAiFeatures.ts +46 -5
- hooks/useCanvasFileUtils.ts +6 -15
- metadata.json +1 -1
- package.json +1 -1
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={
|
| 176 |
isLoading={isGeneratingAiImage}
|
|
|
|
| 177 |
error={aiEditError}
|
|
|
|
| 178 |
/>
|
| 179 |
)}
|
| 180 |
|
|
@@ -218,7 +233,7 @@ const App: React.FC = () => {
|
|
| 218 |
/>
|
| 219 |
)}
|
| 220 |
|
| 221 |
-
<div className="fixed
|
| 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-
|
| 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={
|
| 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
|
| 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'
|
| 65 |
-
|
| 66 |
-
|
| 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
|
| 80 |
-
|
| 81 |
-
onClick={
|
| 82 |
-
disabled={
|
| 83 |
-
className="px-4 py-2 bg-
|
| 84 |
>
|
| 85 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
</button>
|
| 87 |
<button
|
| 88 |
onClick={onGenerate}
|
| 89 |
-
disabled={
|
| 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
|
| 97 |
|
| 98 |
const handleExportImage = useCallback(() => {
|
| 99 |
if (currentDataURL) {
|
|
@@ -110,18 +109,10 @@ export const useCanvasFileUtils = ({
|
|
| 110 |
}, [currentDataURL, showToast]);
|
| 111 |
|
| 112 |
const handleClearCanvas = useCallback(() => {
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 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": "
|
| 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": "
|
| 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",
|