"use client"; import React, { useRef, useState, useEffect } from "react"; import { useStore, ReferenceCategory } from "@/lib/store"; import { cn } from "@/lib/utils"; import { Sparkles, Maximize2, X, Hash, AlertTriangle, Upload, Brain, Settings, Settings2 } from "lucide-react"; const IMAGE_COUNTS = [1, 2, 4]; export function PromptHero() { const { prompt, setPrompt, addToGallery, settings, setSettings, references, setReference, addReference, removeReference, clearReferences, setSelectionMode, setCurrentView, history, setHistory, setIsGenerating, // Get global setter setShowCookieExpired } = useStore(); const [isGenerating, setLocalIsGenerating] = useState(false); const { addVideo } = useStore(); const [uploadingRefs, setUploadingRefs] = useState>({}); const [errorNotification, setErrorNotification] = useState<{ message: string; type: 'error' | 'warning' } | null>(null); const textareaRef = useRef(null); // CLEANUP: Remove corrupted localStorage keys that crash browser extensions useEffect(() => { try { if (typeof window !== 'undefined') { for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key) { const val = localStorage.getItem(key); // Clean up "undefined" string values which cause JSON.parse errors in extensions if (val === "undefined" || val === "null") { console.warn(`[Cleanup] Removing corrupted localStorage key: ${key}`); localStorage.removeItem(key); } } } } } catch (e) { console.error("Storage cleanup failed", e); } }, []); // Auto-enable Precise mode when references are added useEffect(() => { const hasReferences = Object.values(references).some(refs => refs && refs.length > 0); if (hasReferences && !settings.preciseMode) { setSettings({ preciseMode: true }); } }, [references, settings.preciseMode, setSettings]); // File input refs for each reference category const fileInputRefs = { subject: useRef(null), scene: useRef(null), style: useRef(null), video: useRef(null), }; // Auto-resize textarea useEffect(() => { if (textareaRef.current) { textareaRef.current.style.height = 'auto'; textareaRef.current.style.height = Math.min(textareaRef.current.scrollHeight, 200) + 'px'; } }, [prompt]); const handleGenerate = async () => { let finalPrompt = prompt.trim(); if (!finalPrompt || isGenerating) return; // Try to parse JSON if it looks like it if (finalPrompt.startsWith('{') && finalPrompt.endsWith('}')) { try { const json = JSON.parse(finalPrompt); if (json.prompt || json.text || json.positive) { finalPrompt = json.prompt || json.text || json.positive; setPrompt(finalPrompt); } } catch (e) { // Ignore parse errors } } setIsGenerating(true); setLocalIsGenerating(true); // Keep local state for button UI try { // Route to the selected provider const provider = settings.provider || 'whisk'; let res: Response; if (provider === 'meta') { // Image Generation Path (Meta AI) // Video is now handled by handleGenerateVideo // Prepend aspect ratio for better adherence let metaPrompt = finalPrompt; if (settings.aspectRatio === '16:9') { metaPrompt = "wide 16:9 landscape image of " + finalPrompt; } else if (settings.aspectRatio === '9:16') { metaPrompt = "tall 9:16 portrait image of " + finalPrompt; } // Merge cookies safely let mergedCookies = settings.metaCookies; try { const safeParse = (str: string) => { if (!str || str === "undefined" || str === "null") return []; try { return JSON.parse(str); } catch { return []; } }; const m = safeParse(settings.metaCookies); const f = safeParse(settings.facebookCookies); if (Array.isArray(m) || Array.isArray(f)) { mergedCookies = [...(Array.isArray(m) ? m : []), ...(Array.isArray(f) ? f : [])]; } } catch (e) { console.error("Cookie merge failed", e); } // Meta AI always generates 4 images, hardcode this // Extract subject reference if available (for Image-to-Image) const subjectRef = references.subject?.[0]; const imageUrl = subjectRef ? subjectRef.thumbnail : undefined; // Use full data URI from thumbnail property res = await fetch('/api/meta/generate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt: metaPrompt, cookies: typeof mergedCookies === 'string' ? mergedCookies : JSON.stringify(mergedCookies), imageCount: 4, // Meta AI always returns 4 images useMetaFreeWrapper: settings.useMetaFreeWrapper, metaFreeWrapperUrl: settings.metaFreeWrapperUrl }) }); } else { // Default: Whisk (Google ImageFX) const refsForApi = { subject: references.subject?.map(r => r.id) || [], scene: references.scene?.map(r => r.id) || [], style: references.style?.map(r => r.id) || [], }; res = await fetch('/api/generate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt: finalPrompt, aspectRatio: settings.aspectRatio, preciseMode: settings.preciseMode, imageCount: settings.imageCount, cookies: settings.whiskCookies, refs: refsForApi }) }); } const responseText = await res.text(); let data; try { data = JSON.parse(responseText); } catch (e) { console.error("API Error (Non-JSON response):", responseText.substring(0, 500)); throw new Error(`Server Error: ${res.status} ${res.statusText}`); } if (data.error) throw new Error(data.error); if (data.images) { // Add images one by one with createdAt for (const img of data.images) { await addToGallery({ data: img.data || img.url, // Use URL as fallback (Meta AI returns URLs) prompt: finalPrompt, // Use original user prompt to avoid showing engineered prompts aspectRatio: img.aspectRatio || settings.aspectRatio, createdAt: Date.now(), provider: provider as 'whisk' | 'meta' }); } } } catch (e: any) { console.error(e); const errorMessage = e.message || ''; // Check for various Google safety policy errors if (errorMessage.includes('PROMINENT_PEOPLE_FILTER_FAILED') || errorMessage.includes('prominent_people')) { setErrorNotification({ message: '🚫 Content Policy: The reference image contains a recognizable person. Google blocks generating images of real/famous people. Try using a different reference image without identifiable faces.', type: 'warning' }); } else if (errorMessage.includes("Oops! I can't generate that image") || errorMessage.includes("Can I help you imagine something else")) { setErrorNotification({ message: '🛡️ Meta AI Safety: The prompt was rejected by Meta AI safety filters. Please try a different prompt.', type: 'warning' }); } else if (errorMessage.includes('Safety Filter') || errorMessage.includes('SAFETY_FILTER') || errorMessage.includes('content_policy')) { setErrorNotification({ message: '⚠️ Content Moderation: Your prompt or image was flagged by Google\'s safety filters. Try using different wording or a safer subject.', type: 'warning' }); } else if (errorMessage.includes('NSFW') || errorMessage.includes('nsfw_filter')) { setErrorNotification({ message: '🔞 Content Policy: The request was blocked for NSFW content. Please use appropriate prompts and images.', type: 'warning' }); } else if (errorMessage.includes('CHILD_SAFETY') || errorMessage.includes('child_safety')) { setErrorNotification({ message: '⛔ Content Policy: Request blocked for child safety concerns.', type: 'error' }); } else if (errorMessage.includes('RATE_LIMIT') || errorMessage.includes('429') || errorMessage.includes('quota')) { setErrorNotification({ message: '⏳ Rate Limited: Too many requests. Please wait a moment and try again.', type: 'warning' }); } else if (errorMessage.includes('401') || errorMessage.includes('Unauthorized') || errorMessage.includes('cookies not found') || errorMessage.includes('Auth failed')) { // Trigger the new popup setShowCookieExpired(true); // Also show a simplified toast as backup setErrorNotification({ message: '🔐 Authentication Error: Cookies Refreshed Required', type: 'error' }); } else { setErrorNotification({ message: errorMessage || 'Generation failed. Please try again.', type: 'error' }); } // Auto-dismiss after 8 seconds setTimeout(() => setErrorNotification(null), 8000); } finally { setIsGenerating(false); setLocalIsGenerating(false); } }; // Note: Meta AI Video generation was removed - use Whisk for video generation from the gallery lightbox const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { e.preventDefault(); handleGenerate(); } }; const handlePaste = (e: React.ClipboardEvent) => { try { const text = e.clipboardData.getData('text'); if (text.trim().startsWith('{')) { const json = JSON.parse(text); const cleanPrompt = json.prompt || json.text || json.positive || json.caption; if (cleanPrompt && typeof cleanPrompt === 'string') { e.preventDefault(); setPrompt(cleanPrompt); } } } catch (e) { // Not JSON } }; const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); }; const handleDrop = async (e: React.DragEvent, category: ReferenceCategory) => { e.preventDefault(); e.stopPropagation(); if (e.dataTransfer.files && e.dataTransfer.files.length > 0) { const file = e.dataTransfer.files[0]; if (!file.type.startsWith('image/')) return; await uploadReference(file, category); } }; const uploadReference = async (file: File, category: ReferenceCategory) => { // Enforce Whisk cookies ONLY if using Whisk provider if ((!settings.provider || settings.provider === 'whisk') && !settings.whiskCookies) { alert("Please set your Whisk Cookies in Settings first!"); return; } setUploadingRefs(prev => ({ ...prev, [category]: true })); try { const reader = new FileReader(); reader.onload = async (e) => { const base64 = e.target?.result as string; if (!base64) { setUploadingRefs(prev => ({ ...prev, [category]: false })); return; } let refId = ''; // If Whisk, upload to backend to get ID if (!settings.provider || settings.provider === 'whisk') { try { const res = await fetch('/api/references/upload', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ imageBase64: base64, mimeType: file.type, category: category, cookies: settings.whiskCookies }) }); const data = await res.json(); if (data.id) { refId = data.id; } else { console.error("Upload failed details:", JSON.stringify(data)); alert(`Upload failed: ${data.error}\n\nDetails: ${JSON.stringify(data) || 'Check console'}`); } } catch (err) { console.error("API Upload Error", err); alert("API Upload failed"); } } else { // For Meta/Grok, just use local generated ID refId = 'loc-' + Date.now() + Math.random().toString(36).substr(2, 5); } if (refId) { // Add to array (supports multiple refs per category) // Note: Store uses 'thumbnail' property for the image data addReference(category, { id: refId, thumbnail: base64 }); // Add to history const newItem = { id: refId, url: base64, category: category, originalName: file.name }; const exists = history.find(h => h.id === refId); if (!exists) { setHistory([newItem, ...history].slice(0, 50)); } } setUploadingRefs(prev => ({ ...prev, [category]: false })); }; reader.readAsDataURL(file); } catch (error) { console.error(error); setUploadingRefs(prev => ({ ...prev, [category]: false })); } }; // Handle file input change for click-to-upload (supports multiple files) const handleFileInputChange = (e: React.ChangeEvent, category: ReferenceCategory) => { const files = e.target.files; if (files && files.length > 0) { // Upload each selected file Array.from(files).forEach(file => { if (file.type.startsWith('image/')) { uploadReference(file, category); } }); } // Reset input value so same file can be selected again e.target.value = ''; }; // Open file picker for a category const openFilePicker = (category: ReferenceCategory) => { const inputRef = fileInputRefs[category]; if (inputRef.current) { inputRef.current.click(); } }; const toggleReference = (category: ReferenceCategory) => { // Always open file picker to add more references // Users can remove individual refs or clear all via the X button openFilePicker(category); }; const nextAspectRatio = () => { const ratios = ['1:1', '16:9', '9:16', '4:3', '3:4']; const currentIdx = ratios.indexOf(settings.aspectRatio); setSettings({ aspectRatio: ratios[(currentIdx + 1) % ratios.length] }); }; const cycleImageCount = () => { const currentIdx = IMAGE_COUNTS.indexOf(settings.imageCount); setSettings({ imageCount: IMAGE_COUNTS[(currentIdx + 1) % IMAGE_COUNTS.length] }); }; // Whisk-style gradient button helper const GradientButton = ({ onClick, disabled, children, className }: any) => ( ); return (
{/* Error/Warning Notification Toast */} {errorNotification && (

{errorNotification.type === 'warning' ? '⚠️ Content Moderation' : '❌ Generation Error'}

{errorNotification.message}

)}
{/* Header / Title + Provider Toggle */}
{settings.provider === 'meta' ? ( ) : ( )}

Create by {settings.provider === 'meta' ? 'Meta AI' : 'Whisk'}

{/* Provider Toggle */}
{/* Input Area */}