import { useCallback, useEffect, useState } from "react"; import { Link, useNavigate } from "react-router-dom"; import { videoTemplatesApi } from "../api/endpoints"; import type { ExtractedTheme, TemplateGeneration, TemplatesResponse } from "../api/types"; import { ConfirmButton, EmptyState, errorMessage, formatDate, Loading, PageHeader, StatusPill, Tabs } from "../components/ui"; import { usePoll } from "../hooks/usePoll"; /** The workspace's own custom templates. */ export default function VideoTemplates() { const [data, setData] = useState(null); const [error, setError] = useState(null); const load = useCallback(() => videoTemplatesApi.list().then(setData).catch((e) => setError(errorMessage(e))), []); useEffect(() => { load(); }, [load]); const slots = data?.limits.templates; return (
Back to videos Create a template {slots && (

{slots.used} of {slots.limit} custom templates made. Deleting one does not free its place.

)} {error &&

{error}

} {!data && !error && } {data?.templates.length === 0 && ( Create a template} /> )} {data && data.templates.length > 0 && (
{data.templates.map((t) => (
{t.name}

{t.created_at && `Made ${formatDate(t.created_at)}`}

Edit videoTemplatesApi.remove(t.id).then(load).catch((e) => setError(errorMessage(e)))}> Delete
))}
)}
); } type From = "url" | "doc" | "prompt"; /** Extract a theme, name it, create the template (uses a slot), then generate its scenes. */ export function VideoTemplateNew() { const navigate = useNavigate(); const [from, setFrom] = useState("url"); const [url, setUrl] = useState(""); const [prompt, setPrompt] = useState(""); const [file, setFile] = useState(null); const [name, setName] = useState(""); const [theme, setTheme] = useState(null); const [made, setMade] = useState(null); const [busy, setBusy] = useState(null); const [error, setError] = useState(null); const gen = usePoll(made ? () => videoTemplatesApi.generationStatus(made) : null, { intervalMs: 4000, key: made, until: (s) => s.ready || s.status === "error", }); async function run(label: string, fn: () => Promise) { setBusy(label); setError(null); try { await fn(); } catch (e) { setError(errorMessage(e)); } finally { setBusy(null); } } const extract = () => run("extract", async () => { const t = from === "url" ? await videoTemplatesApi.extractFromUrl(url.trim()) : from === "doc" ? await videoTemplatesApi.extractFromDoc(file!, name) : await videoTemplatesApi.extractFromPrompt(prompt.trim(), name || undefined); if (!t.extractable || !t.theme) throw new Error(t.reason || "No theme could be made from that. Try another source."); setTheme(t); if (!name && t.template_name) setName(t.template_name); }); const create = () => run("create", async () => { const t = await videoTemplatesApi.create({ name: name.trim(), theme: theme!.theme!, source_url: from === "url" ? url.trim() : undefined, logo_urls: theme!.logo_urls, og_image: theme!.og_image || undefined, screenshot_url: theme!.screenshot_url || undefined }); await videoTemplatesApi.generate(t.id); setMade(t.id); }); const colors = (theme?.theme?.colors ?? {}) as Record; const g = gen.data; return (
Back to templates
{!made && ( <> tabs={[{ id: "url", label: "From a website" }, { id: "doc", label: "From a document" }, { id: "prompt", label: "From a description" }]} value={from} onChange={(f) => { setFrom(f); setTheme(null); }} /> {from === "url" && ( )} {from === "doc" && ( )} {from === "prompt" && (