import { useState, useEffect, useCallback } from "react"; import { Menu, Sparkles, UserCircle } from "lucide-react"; import { StudioView } from "./components/GalleryView"; import { CharacterProfileView } from "./components/CharacterProfileView"; import { ProjectsView } from "./components/ProjectsView"; import { ProjectDetailView } from "./components/ProjectDetailView"; import { AppSidebar } from "./components/sidebar/AppSidebar"; import type { SidebarTab } from "./components/sidebar/AppSidebar"; import type { JobItem, LoraCheckpoint, LoraTrainingStatus, MediaItem, Project, Scene, Shot, ProjectPhase } from "./types"; async function fetchJson(url: string, timeoutMs = 5000): Promise { const controller = new AbortController(); const id = window.setTimeout(() => controller.abort(), timeoutMs); try { const response = await fetch(url, { signal: controller.signal }); if (!response.ok) throw new Error(`${url} returned ${response.status}`); return await response.json(); } finally { window.clearTimeout(id); } } export default function App() { const [items, setItems] = useState([]); const [jobs, setJobs] = useState([]); const [training, setTraining] = useState(null); const [checkpoints, setCheckpoints] = useState([]); const [loading, setLoading] = useState(true); const [hasLoadedOnce, setHasLoadedOnce] = useState(false); const [error, setError] = useState(null); const [selected, setSelected] = useState(null); const [sidebarOpen, setSidebarOpen] = useState(true); const [activeSidebarTab, setActiveSidebarTab] = useState("generate"); const [selectedCharacterId, setSelectedCharacterId] = useState(null); const [mainView, setMainView] = useState<"studio" | "character" | "projects" | "project-detail">("studio"); const [selectedProjectId, setSelectedProjectId] = useState(null); const [projectData, setProjectData] = useState<{ project: Project; scenes: Scene[]; shots: Shot[] } | null>(null); const [selectedSceneId, setSelectedSceneId] = useState(null); const [selectedShotId, setSelectedShotId] = useState(null); const loadProject = useCallback(async () => { if (!selectedProjectId) { setProjectData(null); return; } try { const response = await fetch(`/api/projects/${selectedProjectId}`); if (!response.ok) throw new Error(`Project fetch failed: ${response.status}`); const data = await response.json(); const scenes: Scene[] = (data.scenes || []).slice().sort((a: Scene, b: Scene) => a.scene_number - b.scene_number); const shots: Shot[] = (data.shots || []).slice().sort((a: Shot, b: Shot) => a.shot_number - b.shot_number); setProjectData({ project: data.project, scenes, shots }); setSelectedSceneId((current) => { if (current && scenes.some((scene) => scene.id === current)) return current; return scenes.length > 0 ? scenes[0].id : null; }); } catch (e) { console.error("Failed to load project", e); } }, [selectedProjectId]); useEffect(() => { loadProject(); }, [loadProject]); const phase: ProjectPhase = projectData?.shots.some((shot) => shot.image_file) ? "remix" : "outline"; const addScene = useCallback(async () => { if (!selectedProjectId || !projectData) return; const next = projectData.scenes.length > 0 ? Math.max(...projectData.scenes.map((scene) => scene.scene_number)) + 1 : 1; try { const response = await fetch(`/api/projects/${selectedProjectId}/scenes`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ scene_number: next, heading: `SCENE ${next}` }), }); if (!response.ok) throw new Error(`Add scene failed: ${response.status}`); const created = await response.json(); await loadProject(); setSelectedSceneId(created.id); setSelectedShotId(null); } catch (e) { console.error("Failed to add scene", e); } }, [selectedProjectId, projectData, loadProject]); const load = useCallback(async () => { if (!hasLoadedOnce) setLoading(true); try { setError(null); const listing = await fetchJson<{ images?: MediaItem[] }>("/api/listing", 8000); setItems(listing.images || []); setHasLoadedOnce(true); } catch (e) { console.error("Failed to load gallery", e); setError(e instanceof Error ? e.message : "Failed to load gallery"); } finally { setLoading(false); } const [jobsResult, trainingResult, checkpointsResult] = await Promise.allSettled([ fetchJson<{ jobs?: JobItem[] }>("/api/jobs", 3500), fetchJson("/api/lora-training/status", 3500), fetchJson<{ checkpoints?: LoraCheckpoint[] }>("/api/lora-training/checkpoints", 3500), ]); if (jobsResult.status === "fulfilled") setJobs(jobsResult.value.jobs || []); if (trainingResult.status === "fulfilled") setTraining(trainingResult.value.ok ? trainingResult.value : null); if (checkpointsResult.status === "fulfilled") setCheckpoints(checkpointsResult.value.checkpoints || []); }, [hasLoadedOnce]); useEffect(() => { load(); const id = window.setInterval(load, 5000); return () => window.clearInterval(id); }, [load]); const deleteItem = useCallback(async (item: MediaItem) => { const filename = item.filename || item.url.replace(/^\/media\//, ""); const response = await fetch("/api/delete", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ files: [filename] }), }); if (!response.ok) { const data = await response.json().catch(() => ({})); throw new Error(data?.detail || `Failed to delete ${filename}`); } setItems((current) => current.filter((candidate) => (candidate.filename || candidate.url) !== (item.filename || item.url))); if (selected === item.url) setSelected(null); }, [selected]); const hasContent = jobs.length > 0 || items.length > 0; const videoCount = items.filter((item) => item.type === "video" || item.url.endsWith(".mp4") || item.url.endsWith(".webm")).length; const imageCount = items.length - videoCount; return (
{!sidebarOpen && ( )}
{jobs.length > 0 && ( {jobs.length} generating )} {items.length} media {imageCount} images {videoCount} videos

Demo workspace

This hackathon build uses a sample owner dataset to demonstrate character LoRA training, generation, and media management. Authentication is intentionally mocked for the demo.

{sidebarOpen && ( setSidebarOpen(false)} checkpoints={checkpoints} onQueued={load} onSelectCharacter={(id) => { setSelectedCharacterId(id); setMainView("character"); }} projectMode={mainView === "project-detail" && projectData ? { project: projectData.project, scenes: projectData.scenes, shots: projectData.shots, selectedSceneId, selectedShotId, phase, onSelectScene: (id) => { setSelectedSceneId(id); setSelectedShotId(null); }, onSelectShot: setSelectedShotId, onBack: () => { setSelectedShotId(null); setMainView("projects"); }, onRefresh: loadProject, onAddScene: addScene, } : undefined} /> )}
{mainView === "character" && selectedCharacterId && ( setActiveSidebarTab("generate")} /> )} {mainView === "projects" && ( { setSelectedProjectId(id); setMainView("project-detail"); setActiveSidebarTab("projects"); setSidebarOpen(true); }} /> )} {mainView === "project-detail" && projectData && ( { setSelectedSceneId(id); setSelectedShotId(null); }} onSelectShot={setSelectedShotId} onRefresh={loadProject} onBack={() => { setSelectedShotId(null); setMainView("projects"); }} /> )} {mainView === "studio" && ( setMainView("projects")} /> )}
{selected && (
setSelected(null)} >
e.stopPropagation()}> {selected.endsWith(".mp4") || selected.endsWith(".webm") ? (
)}
); }