| import React, { useEffect, useRef } from 'react'; | |
| import { MagicSparkleIcon, CloseIcon, QuestionMarkIcon } from './icons'; | |
| interface AiEditModalProps { | |
| isOpen: boolean; | |
| onClose: () => void; | |
| onGenerate: () => void; | |
| onAsk: () => void; | |
| imageUrl: string; | |
| prompt: string; | |
| onPromptChange: (newPrompt: string) => void; | |
| isLoading: boolean; | |
| isAsking: boolean; | |
| error: string | null; | |
| askUrl: string | null; | |
| } | |
| const AiEditModal: React.FC<AiEditModalProps> = ({ | |
| isOpen, | |
| onClose, | |
| onGenerate, | |
| onAsk, | |
| imageUrl, | |
| prompt, | |
| onPromptChange, | |
| isLoading, | |
| isAsking, | |
| error, | |
| askUrl, | |
| }) => { | |
| const iframeContainerRef = useRef<HTMLDivElement>(null); | |
| useEffect(() => { | |
| if (askUrl && iframeContainerRef.current) { | |
| iframeContainerRef.current.scrollIntoView({ behavior: 'smooth', block: 'start' }); | |
| } | |
| }, [askUrl]); | |
| if (!isOpen) return null; | |
| const isAnyLoading = isLoading || isAsking; | |
| return ( | |
| <div | |
| className="fixed inset-0 bg-black bg-opacity-60 backdrop-blur-sm flex justify-center items-start z-[1000] p-4 pt-16" | |
| aria-modal="true" | |
| role="dialog" | |
| aria-labelledby="ai-edit-modal-title" | |
| > | |
| <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"> | |
| <div className="flex justify-between items-center mb-4"> | |
| <h2 id="ai-edit-modal-title" className="text-xl font-semibold text-slate-700">Edit or Ask about Image with AI</h2> | |
| <button | |
| onClick={onClose} | |
| className="text-slate-400 hover:text-slate-600 transition-colors" | |
| aria-label="Close AI edit modal" | |
| disabled={isAnyLoading} | |
| > | |
| <CloseIcon className="w-6 h-6" /> | |
| </button> | |
| </div> | |
| <div className="mb-4"> | |
| <img | |
| src={imageUrl} | |
| alt="Uploaded image preview" | |
| className="max-w-full w-auto h-32 rounded border border-slate-300 object-contain mx-auto" | |
| /> | |
| </div> | |
| <div className="mb-4"> | |
| <label htmlFor="aiPrompt" className="block text-sm font-medium text-slate-700 mb-1"> | |
| Describe what you want to change, or ask a question: | |
| </label> | |
| <textarea | |
| id="aiPrompt" | |
| value={prompt} | |
| onChange={(e) => onPromptChange(e.target.value)} | |
| placeholder="e.g., 'make the cat wear a party hat' or 'what is in this image?'" | |
| 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-[200px]" | |
| disabled={isAnyLoading} | |
| aria-describedby={error ? "ai-edit-error" : undefined} | |
| /> | |
| </div> | |
| {error && ( | |
| <div id="ai-edit-error" className="mb-4 p-3 bg-red-100 border border-red-300 text-red-700 rounded-md text-sm" role="alert"> | |
| <p className="font-semibold">Error:</p> | |
| <p>{error}</p> | |
| </div> | |
| )} | |
| <div className="flex justify-between items-center gap-3"> | |
| <button | |
| onClick={onAsk} | |
| disabled={isAnyLoading || !prompt.trim()} | |
| 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" | |
| > | |
| {isAsking ? ( | |
| <> | |
| <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"> | |
| <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle> | |
| <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> | |
| </svg> | |
| Asking... | |
| </> | |
| ) : ( | |
| <> | |
| <QuestionMarkIcon className="w-4 h-4 mr-1" /> | |
| Ask | |
| </> | |
| )} | |
| </button> | |
| <button | |
| onClick={onGenerate} | |
| disabled={isAnyLoading || !prompt.trim()} | |
| 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" | |
| > | |
| {isLoading ? ( | |
| <> | |
| <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"> | |
| <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle> | |
| <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> | |
| </svg> | |
| Generating... | |
| </> | |
| ) : ( | |
| <> | |
| <MagicSparkleIcon className="w-4 h-4 mr-1" /> | |
| Generate Image | |
| </> | |
| )} | |
| </button> | |
| </div> | |
| {askUrl && ( | |
| <div ref={iframeContainerRef} className="mt-4 pt-2"> | |
| <iframe | |
| src={askUrl} | |
| title="AI Response" | |
| className="w-full h-[70vh] border-0 rounded-md bg-slate-50" | |
| /> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| export default AiEditModal; | |