"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, Zap, Brain, Settings, Settings2, Video } 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 [isGeneratingVideo, setIsGeneratingVideo] = useState(false); const [uploadingRefs, setUploadingRefs] = useState>({}); const [errorNotification, setErrorNotification] = useState<{ message: string; type: 'error' | 'warning' } | null>(null); const textareaRef = useRef(null); // 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 === 'grok') { // Grok API res = await fetch('/api/grok/generate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt: finalPrompt, apiKey: settings.grokApiKey, cookies: settings.grokCookies, imageCount: settings.imageCount }) }); } else if (provider === 'meta') { // Meta AI via Python service (metaai-api) // Meta AI always generates 4 images, hardcode this res = await fetch('/api/meta-crawl', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt: finalPrompt, cookies: settings.metaCookies, num_images: 4 // Meta AI always returns 4 images }) }); } 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: img.prompt, aspectRatio: img.aspectRatio || settings.aspectRatio, createdAt: Date.now(), provider: provider as 'whisk' | 'grok' | '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); } }; // Handle video generation (Meta AI only) // If a subject reference is set, it will use image-to-video // Otherwise, it will use text-to-video const handleGenerateVideo = async () => { let finalPrompt = prompt.trim(); if (!finalPrompt || isGeneratingVideo || settings.provider !== 'meta') return; setIsGeneratingVideo(true); setIsGenerating(true); try { // Check if we have a subject reference for image-to-video const subjectRefs = references.subject || []; const imageBase64 = subjectRefs.length > 0 ? subjectRefs[0].thumbnail : undefined; const mode = imageBase64 ? 'image-to-video' : 'text-to-video'; console.log(`[PromptHero] Starting Meta AI ${mode}...`); const res = await fetch('/api/meta/video', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt: finalPrompt, cookies: settings.metaCookies, imageBase64: imageBase64 }) }); 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.videos && data.videos.length > 0) { // Add videos to store for (const video of data.videos) { useStore.getState().addVideo({ id: crypto.randomUUID(), url: video.url, prompt: video.prompt || finalPrompt, thumbnail: imageBase64, // Store the source image as thumbnail createdAt: Date.now() }); } // Show success notification setErrorNotification({ message: `🎬 Success! Generated ${data.videos.length} video(s) via ${mode}. Check the gallery.`, type: 'warning' // Using warning for visibility (amber color) }); setTimeout(() => setErrorNotification(null), 5000); } else { throw new Error('No videos were generated'); } } catch (e: any) { console.error('[Video Gen]', e); const errorMessage = e.message || ''; if (errorMessage.includes('401') || errorMessage.includes('cookies')) { setShowCookieExpired(true); } setErrorNotification({ message: `🎬 Video Error: ${errorMessage}`, type: 'error' }); setTimeout(() => setErrorNotification(null), 8000); } finally { setIsGeneratingVideo(false); setIsGenerating(false); } }; 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) => { if (!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) return; 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) { // Add to array (supports multiple refs per category) addReference(category, { id: data.id, thumbnail: base64 }); // Add to history const newItem = { id: data.id, url: base64, // For local display history we use base64. Ideally we'd valid URL but this works for session. category: category, originalName: file.name }; // exist check? const exists = history.find(h => h.id === data.id); if (!exists) { setHistory([newItem, ...history]); } } else { console.error("Upload failed details:", JSON.stringify(data)); alert(`Upload failed: ${data.error}\n\nDetails: ${JSON.stringify(data) || 'Check console'}`); } 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 === 'grok' ? ( ) : settings.provider === 'meta' ? ( ) : ( )}

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

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