import React, { useState, useEffect, useRef } from 'react'; import { Send, Copy, RotateCcw, Sparkles, Terminal, Film, CheckCircle, Clock, Layers, User, Mic, Clapperboard, FileText, Check, FileJson } from 'lucide-react'; // --- Types --- type Message = { id: string; sender: 'bot' | 'user'; text: string; isError?: boolean; }; type Answers = { title: string; duration: string; promptsPerScene: string; characterLock: string; voPerspective: string; voType: string; storyStructure: string; voDensity: string; }; // JSON Structure for API Response type StoryData = { title: string; production_settings: { duration: string; estimated_scenes: string; prompts_per_scene: string; vo_perspective: string; vo_style: string; vo_density: string; }; story_beat: string; vo_script: string; scenes: { number: number; name: string; purpose: string; vo_lines: string; prompts: string[]; }[]; }; // --- Question Flow Definitions --- const QUESTIONS = [ { id: 'title', text: "Cerita apa yang mau dibuat? Masukkan judul cerita.", icon: }, { id: 'duration', text: "Mau video durasi berapa menit? (contoh: 3 / 6 / 8 / 10 / 12)", icon: }, { id: 'promptsPerScene', text: "Mau berapa prompt Veo 3 per scene? (contoh: 3 / 4 / 5)", icon: }, { id: 'characterLock', text: "Karakter utama siapa/apa? Tempelkan CHARACTER LOCK lengkap (atau deskripsi detail karakter + pakaian).", icon: , validate: (input: string) => input.length > 20 }, { id: 'voPerspective', text: "VO mau seperti apa? (ketik pilihan: 'third person narrator', 'no VO', 'other')", icon: }, { id: 'voType', text: "Tipe VO apa? (ketik pilihan: 'National Geographic documentary (American)', 'BBC documentary', 'YouTube casual documentary', 'other')", icon: }, { id: 'storyStructure', text: "Alur cerita mau seperti apa? Pilih salah satu:\nA) Hook dengan adegan inti (ekstrim/bahaya) lalu diikuti cerita\nB) Storyline urut seperti biasa (chronological)\nC) Yang lain (aku buatin)", icon: }, { id: 'voDensity', text: "VO ngomongnya gimana?\nA) Massive (sering ngomong)\nB) Singkat & padat tapi penonton paham", icon: }, ]; // --- API Helper --- const generateStoryWithGemini = async (answers: Answers, apiKey: string): Promise => { const systemPrompt = ` You are "VEO STORY WIZARD" — an interactive prompt generator for Veo 3. GOAL: Generate a complete video package based on the user's inputs. Return ONLY valid JSON. USER INPUTS: 1. Title: ${answers.title} 2. Duration: ${answers.duration} minutes 3. Prompts per scene: ${answers.promptsPerScene} 4. CHARACTER LOCK BLOCK: """${answers.characterLock}""" 5. VO Perspective: ${answers.voPerspective} 6. VO Type: ${answers.voType} 7. Story Structure Choice: ${answers.storyStructure} 8. VO Density: ${answers.voDensity} HARD CONSTRAINTS (MUST ENFORCE): - Only ONE on-screen character: the main character defined in the CHARACTER LOCK. No other humans. - No first-person voiceover. No "I / my / me" from the character. - No direct-to-camera behavior. Character never looks at lens. - Documentary/observational realism. Avoid "AI look". - Prompts must be in English. VO must be American English (NatGeo style). CRITICAL - CONTINUITY & FLOW RULES: 1. **Seamless Transitions:** Ensure strict narrative flow. Scene 2 must logically follow Scene 1 in terms of geography, time of day, and action. If Scene 1 ends with the character walking towards a cabin, Scene 2 must start near or entering that cabin. Do not make random jumps. 2. **Prop Consistency:** If a prop is introduced (e.g., "wooden sled", "red lantern"), reuse the EXACT same phrasing in subsequent scenes where it appears. 3. **Environmental Continuity:** Weather and lighting must transition logically (e.g., from afternoon sun to sunset to twilight). OUTPUT FORMAT (JSON ONLY): { "title": "String", "production_settings": { "duration": "String", "estimated_scenes": "String", "prompts_per_scene": "String", "vo_perspective": "String", "vo_style": "String", "vo_density": "String" }, "story_beat": "String (Explain structure)", "vo_script": "String (Full script with timecodes using \\n for line breaks)", "scenes": [ { "number": Number, "name": "String", "purpose": "String", "vo_lines": "String (Specific VO lines for this scene)", "prompts": ["String (Full Prompt 1)", "String (Full Prompt 2)"] } ] } IMPORTANT PROMPT FORMAT: - In the "prompts" array, each string MUST be the COMPLETE prompt: Start verbatim with the CHARACTER LOCK BLOCK, then a blank line, then "SCENE: [Description]". - Generate ${answers.promptsPerScene} variations per scene. - Describe the scene action as a continuation of the previous scene's event. `; try { const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-09-2025:generateContent?key=${apiKey}`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ contents: [{ parts: [{ text: systemPrompt }] }], generationConfig: { response_mime_type: "application/json" } }) }); const data = await response.json(); if (data.error) throw new Error(data.error.message); const text = data.candidates[0].content.parts[0].text; return JSON.parse(text); } catch (error) { throw error; } }; export default function VeoStoryWizard() { const [apiKey, setApiKey] = useState(''); const [hasKey, setHasKey] = useState(false); const [step, setStep] = useState(0); const [messages, setMessages] = useState([]); const [inputText, setInputText] = useState(''); const [answers, setAnswers] = useState({ title: '', duration: '', promptsPerScene: '', characterLock: '', voPerspective: '', voType: '', storyStructure: '', voDensity: '', }); const [isGenerating, setIsGenerating] = useState(false); const [finalResult, setFinalResult] = useState(null); const [copiedId, setCopiedId] = useState(null); const messagesEndRef = useRef(null); // Initialize chat useEffect(() => { if (hasKey && messages.length === 0) { setMessages([{ id: 'init', sender: 'bot', text: QUESTIONS[0].text }]); } }, [hasKey]); // Auto-scroll useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages, isGenerating, finalResult]); const handleSendMessage = async () => { if (!inputText.trim()) return; const currentQ = QUESTIONS[step]; const userMsg: Message = { id: Date.now().toString(), sender: 'user', text: inputText }; setMessages(prev => [...prev, userMsg]); if (currentQ.validate && !currentQ.validate(inputText)) { setMessages(prev => [...prev, { id: Date.now().toString() + 'err', sender: 'bot', text: "Input terlalu pendek atau tidak lengkap. Mohon masukkan deskripsi yang lebih detail.", isError: true }]); setInputText(''); return; } const newAnswers = { ...answers, [currentQ.id]: inputText }; setAnswers(newAnswers); setInputText(''); if (step < QUESTIONS.length - 1) { const nextStep = step + 1; setStep(nextStep); setTimeout(() => { setMessages(prev => [...prev, { id: Date.now().toString(), sender: 'bot', text: QUESTIONS[nextStep].text }]); }, 500); } else { setIsGenerating(true); try { const result = await generateStoryWithGemini(newAnswers, apiKey); setFinalResult(result); } catch (error: any) { setMessages(prev => [...prev, { id: Date.now().toString(), sender: 'bot', text: `Error generating story: ${error.message || 'Unknown error'}. Please check your API key.`, isError: true }]); } finally { setIsGenerating(false); } } }; // Robust Copy Function using document.execCommand for iframe compatibility const copyText = (text: string, id: string) => { try { // Create a temporary textarea element const textArea = document.createElement("textarea"); textArea.value = text; // Ensure it's not visible but part of DOM textArea.style.position = "fixed"; textArea.style.left = "-9999px"; textArea.style.top = "0"; document.body.appendChild(textArea); // Select and copy textArea.focus(); textArea.select(); const successful = document.execCommand('copy'); document.body.removeChild(textArea); if (successful) { setCopiedId(id); setTimeout(() => setCopiedId(null), 2000); } else { console.error('Copy command failed'); } } catch (err) { console.error('Fallback copy failed', err); } }; const copyFullScript = () => { if (!finalResult) return; let fullText = `# ${finalResult.title}\n\n## Story Beat\n${finalResult.story_beat}\n\n## VO Script\n${finalResult.vo_script}\n\n## Scenes\n`; finalResult.scenes.forEach(scene => { fullText += `\n### Scene ${scene.number}: ${scene.name}\nVO: ${scene.vo_lines}\n`; scene.prompts.forEach((p, i) => { fullText += `\nPrompt ${i+1}:\n${p}\n`; }); }); copyText(fullText, 'full-script'); }; const resetWizard = () => { setStep(0); setMessages([{ id: 'init', sender: 'bot', text: QUESTIONS[0].text }]); setAnswers({ title: '', duration: '', promptsPerScene: '', characterLock: '', voPerspective: '', voType: '', storyStructure: '', voDensity: '', }); setFinalResult(null); }; // --- Render Functions --- if (!hasKey) { return (
{/* Decorative Background Elements */}

VEO STORY WIZARD

Interactive Cinematic Prompt Generator

setApiKey(e.target.value)} placeholder="AIzaSy..." className="w-full bg-black/40 border border-slate-700 rounded-xl p-3.5 text-white focus:ring-2 focus:ring-emerald-500/50 focus:border-emerald-500 focus:outline-none transition-all placeholder:text-slate-700" />

Requires Google Gemini API Key. Get it here.

); } return (
{/* Background */}
{/* Header */}

VEO STORY WIZARD

{!finalResult && (
Step {step + 1} of {QUESTIONS.length}
)}
{finalResult && ( )}
{/* Main Content */}
{/* Progress Bar (Visible only during chat) */} {!finalResult && (
)} {/* Chat Area */} {!finalResult && (
{messages.map((msg, idx) => (
{msg.sender === 'user' ? : (QUESTIONS[idx] ? QUESTIONS[idx].icon : )}
{msg.text}
))} {isGenerating && (
Crafting story package...
)}
)} {/* Final Result Area */} {finalResult && (
{/* Title & Stats */}

{finalResult.title}

Duration
{finalResult.production_settings.duration}
Scenes
{finalResult.production_settings.estimated_scenes}
Style
{finalResult.production_settings.vo_style}
Density
{finalResult.production_settings.vo_density}
{/* Story & VO */}

Story Beat

{finalResult.story_beat}

VO Script Preview

{finalResult.vo_script}
{/* SCENES ITERATION */}

Scene Breakdown & Prompts

{finalResult.scenes.map((scene, index) => (
{/* Scene Header */}
SCENE {scene.number}

{scene.name}

{scene.purpose}

"{scene.vo_lines}"

{/* Prompts List */}
{scene.prompts.map((prompt, pIndex) => (
Prompt Option {pIndex + 1}

{prompt}

))}
))}
)}
{/* Input Area (Only visible during chat) */} {!finalResult && (
setInputText(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && !isGenerating && handleSendMessage()} placeholder="Type your answer here..." disabled={isGenerating} autoFocus className="w-full bg-slate-950/50 text-white border border-slate-700/50 rounded-2xl pl-6 pr-16 py-4 focus:ring-2 focus:ring-emerald-500/50 focus:border-emerald-500/50 focus:outline-none shadow-xl transition-all disabled:opacity-50 placeholder:text-slate-600" />
)}
); }