import { useCallback, useEffect, useState } from 'react' import { api } from './api' import { loadAllBundles } from './persist' import { ErrorBox } from './hooks' import Stepper from './components/Stepper' import ReelBar from './components/ReelBar' import CloneBar from './components/CloneBar' import SetupStep from './steps/SetupStep' import ReferencesStep from './steps/ReferencesStep' import TopicsStep from './steps/TopicsStep' import ScriptStep from './steps/ScriptStep' import MediaStep from './steps/MediaStep' import EffectsStep from './steps/EffectsStep' import RenderStep from './steps/RenderStep' import ReviewStep from './steps/ReviewStep' export const STEPS = ['Setup', 'References', 'Topics', 'Script', 'Media', 'Effects', 'Render', 'Review'] const REEL_STEP_START = 3 // steps >= this operate on the selected reel export default function App() { const [projects, setProjects] = useState(null) const [projectId, setProjectId] = useState(null) const [data, setData] = useState(null) // project-level: {project, style, topics, reels} const [reelId, setReelId] = useState(null) const [reelData, setReelData] = useState(null) // {reel, script, voice, media, renders, clone} const [step, setStep] = useState(0) const [error, setError] = useState('') // Replay a browser-cached project onto the server (returns true if it did). // Used when the server (ephemeral hosting) has wiped a project we still hold. const restoreFromCache = useCallback(async (pid) => { const bundle = (await loadAllBundles()).find((b) => b.project_id === pid) if (!bundle) return false try { await api.post('/api/projects/import', bundle); return true } catch { return false } }, []) const loadProjects = useCallback(async () => { try { let serverList = await api.get('/api/projects') const bundles = await loadAllBundles() const serverIds = new Set(serverList.map((p) => p.id)) const missing = bundles.filter((b) => b.project_id && !serverIds.has(b.project_id)) if (missing.length) { // Server lost these (restart/wipe) but the browser still has them. await Promise.all(missing.map((b) => api.post('/api/projects/import', b).catch(() => {}))) serverList = await api.get('/api/projects') } setProjects(serverList) } catch (e) { setError(e.message) } }, []) const refreshProject = useCallback(async () => { if (!projectId) return null try { const d = await api.get(`/api/projects/${projectId}`) setData(d) return d } catch (e) { if (await restoreFromCache(projectId)) { try { const d = await api.get(`/api/projects/${projectId}`); setData(d); return d } catch { /* fall through */ } } setError(e.message) return null } }, [projectId, restoreFromCache]) const refreshReel = useCallback(async () => { if (!projectId || !reelId) { setReelData(null); return null } try { const d = await api.get(`/api/projects/${projectId}/reels/${reelId}`) setReelData(d) return d } catch (e) { if (await restoreFromCache(projectId)) { try { const d = await api.get(`/api/projects/${projectId}/reels/${reelId}`); setReelData(d); return d } catch { /* fall through */ } } setError(e.message) return null } }, [projectId, reelId, restoreFromCache]) useEffect(() => { loadProjects() }, [loadProjects]) useEffect(() => { setData(null); setReelId(null); setReelData(null); refreshProject() }, [projectId, refreshProject]) useEffect(() => { refreshReel() }, [reelId, refreshReel]) const selectReel = (id) => { setReelId(id); setStep(REEL_STEP_START) } const newReel = async () => { const reel = await api.post(`/api/projects/${projectId}/reels`, {}).catch((e) => { setError(e.message) }) if (reel) { await refreshProject(); selectReel(reel.id) } } const deleteReel = async (id) => { if (!window.confirm('Delete this reel and all its media?')) return await api.del(`/api/projects/${projectId}/reels/${id}`).catch((e) => setError(e.message)) if (id === reelId) { setReelId(null); setStep(2) } await refreshProject() } const canEnter = (i) => { if (!data) return i === 0 if (i < REEL_STEP_START) return true if (!reelId) return false if (i === 3) return true // Script (offers generate) if (i === 4) return !!reelData?.script // Media if (i === 5) return !!reelData?.voice && !!reelData?.media // Effects if (i === 6) return !!reelData?.voice && !!reelData?.media // Render if (i === 7) return !!(reelData?.renders?.results?.length) // Review return false } if (!projectId) { return ( { setProjectId(id); setStep(1) }} onCreate={(project) => { setProjectId(project.id); setStep(1); loadProjects() }} onDelete={async (id) => { if (!window.confirm('Delete this project and all its reels?')) return await api.del(`/api/projects/${id}`).catch((e) => setError(e.message)) loadProjects() }} /> ) } // Props shared with every step. const shared = { projectId, data, refreshProject, reelId, reelData, refreshReel, selectReel, setReelId, setStep, } return (

Reel Studio v2

{data &&
{data.project.name}
} {data && ( )}
{ setError(''); refreshProject() }} /> {!data && !error &&

Loading project…

} {data && ( <> { await refreshProject(); selectReel(reel.id) }} /> {step === 0 && } {step === 1 && } {step === 2 && } {step === 3 && } {step === 4 && } {step === 5 && } {step === 6 && } {step === 7 && } )}
) } function Home({ projects, error, onOpen, onCreate, onDelete }) { const [creating, setCreating] = useState(false) return (

Reel Studio v2

AI short-form video pipeline — pick a project or start a new one.

{creating ? ( setCreating(false)} /> ) : ( <>
{projects === null &&

Loading…

} {projects?.length === 0 &&

No projects yet.

} {projects?.map((p) => (
onOpen(p.id)}>
{p.name}
{p.niche.topic} · {p.format.replace('_', ' ')}
))} )}
) }