import { useEffect, useRef, useState } from "react"; import { X, Sparkles, Upload, Wand2, Loader2, Film, FolderOpen, Trash2, Mic, Type, Clapperboard, ClipboardPaste } from "lucide-react"; import { useStore, type ProjectManifest } from "../store"; import { scriptToWords } from "../lib/script"; import { ModelSwitcher } from "./ModelSwitcher"; import { aiHeaders } from "../lib/aiKeys"; const LENGTHS = ["~30 sec", "~1 minute", "~2 minutes"]; const TONES = ["Friendly", "Authoritative", "Cinematic", "Energetic"]; type Recent = { id: string; name: string; source: string; updatedAt?: string; words: number; assets: number }; const SOURCE_META: Record = { script: { icon: Type, label: "AI script" }, import: { icon: Mic, label: "Imported" }, episode: { icon: Clapperboard, label: "Demo" }, }; export function NewProjectModal({ open, onClose }: { open: boolean; onClose: () => void }) { const loadProject = useStore((s) => s.loadProject); const setView = useStore((s) => s.setView); const model = useStore((s) => s.model); const currentId = useStore((s) => s.projectId); const [mode, setMode] = useState<"script" | "paste" | "import" | "recent">("script"); const [title, setTitle] = useState(""); // every project needs a title const [pasted, setPasted] = useState(""); const [prompt, setPrompt] = useState(""); const [length, setLength] = useState(LENGTHS[1]); const [tone, setTone] = useState(TONES[0]); const [script, setScript] = useState(""); const [gen, setGen] = useState(false); const [autoBuild, setAutoBuild] = useState(true); // kick off the AI build right after creating (the <60s "wow") const [importing, setImporting] = useState(""); const [recents, setRecents] = useState([]); const inputRef = useRef(null); const refreshRecents = () => fetch("/api/projects").then((r) => r.json()).then((d) => setRecents(d.projects || [])).catch(() => {}); useEffect(() => { if (open) refreshRecents(); }, [open]); if (!open) return null; async function createAndLoad(body: Record, buildPrompt?: string) { const res = await fetch("/api/projects", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }); const m: ProjectManifest = await res.json(); if (!res.ok || !m.id) return; // don't enter the editor with a broken (id-less) project loadProject(m); // hand the brief straight to the agent so the build starts on first load — no second intake form if (buildPrompt) useStore.getState().requestAgent(buildPrompt); onClose(); } async function generate() { if (!title.trim() && prompt.trim()) setTitle(prompt.trim().split(/\s+/).slice(0, 7).join(" ")); // suggest a title from the brief setGen(true); try { const res = await fetch("/api/script", { method: "POST", headers: { "Content-Type": "application/json", ...aiHeaders() }, body: JSON.stringify({ prompt, model, length, tone }) }); const d = await res.json(); setScript(d.script || "⚠️ " + (d.error || "no script returned")); } catch (e) { setScript("⚠️ " + e); } setGen(false); } function useScript() { if (!title.trim() || !script.trim() || script.startsWith("⚠️")) return; const words = scriptToWords(script).map((w) => ({ text: w.text, startMs: w.startMs, endMs: w.endMs })); const build = autoBuild ? `Build this video from the script. Length: ${length}. Tone: ${tone}. Propose a section outline with treatments, then build every section in order — pick a design style and add an art-directed motion-graphics scene for each, placed on the beat it illustrates. Keep going until every section is done, then give a one-line wrap-up.` : undefined; createAndLoad({ name: title.trim(), words, audioSrc: "", source: "script" }, build); } function usePasted() { if (!title.trim() || !pasted.trim()) return; const words = scriptToWords(pasted).map((w) => ({ text: w.text, startMs: w.startMs, endMs: w.endMs })); const build = autoBuild ? `Build this video from the script. Propose a section outline with treatments, then build every section in order with art-directed motion-graphics scenes placed on the beat they illustrate. Keep going until every section is done, then give a one-line wrap-up.` : undefined; createAndLoad({ name: title.trim(), words, audioSrc: "", source: "script" }, build); } async function importFile(files: FileList | null) { const f = files?.[0]; if (!f) return; let createdId: string | null = null; setImporting("Creating project…"); try { const proj: ProjectManifest = await fetch("/api/projects", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: title.trim() || f.name.replace(/\.[^.]+$/, ""), source: "import" }) }).then((r) => r.json()); createdId = proj.id; setImporting("Uploading & transcribing (Whisper)…"); const res = await fetch(`/api/projects/${proj.id}/import?name=${encodeURIComponent(f.name)}`, { method: "POST", headers: { "Content-Type": f.type || "application/octet-stream", ...aiHeaders() }, body: f }); const m = await res.json(); if (!res.ok) throw new Error(m.error || "import failed"); loadProject(m); onClose(); } catch (e) { if (createdId) fetch(`/api/projects/${createdId}`, { method: "DELETE" }).catch(() => {}); // don't leave an empty project behind setImporting("⚠️ " + (e instanceof Error ? e.message : String(e))); } } async function openRecent(id: string) { const res = await fetch(`/api/projects/${id}`); if (!res.ok) return; loadProject(await res.json()); onClose(); } async function deleteRecent(id: string) { await fetch(`/api/projects/${id}`, { method: "DELETE" }).catch(() => {}); if (localStorage.getItem("ed.lastProject") === id) localStorage.removeItem("ed.lastProject"); setRecents((rs) => rs.filter((p) => p.id !== id)); if (id === currentId) { // deleted the project that's open → fall back to the demo, or home if that fails const m = await fetch("/api/projects/demo", { method: "POST" }).then((r) => r.json()).catch(() => null); if (m && m.id) loadProject(m); else setView("home"); } } const estSec = Math.round((scriptToWords(script).at(-1)?.endMs || 0) / 1000); const wordCount = script.trim() ? script.trim().split(/\s+/).filter(Boolean).length : 0; return (
e.stopPropagation()}>
New Project
setMode("script")} icon={} label="Write with AI" /> setMode("paste")} icon={} label="Paste script" /> setMode("import")} icon={} label="Import A/V" /> setMode("recent")} icon={} label={`Recent${recents.length ? ` (${recents.length})` : ""}`} />
{mode !== "recent" && (
Project title *
setTitle(e.target.value)} maxLength={80} placeholder="e.g. Launch teaser — Spring 2026" className="w-full rounded-md border border-border bg-surface px-3 py-2.5 text-[14px] font-medium outline-none focus:border-primary" />
)} {mode === "script" ? (
What's your video about?