File size: 7,205 Bytes
fe029e8 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 | import { Plus, Sparkles, UserCircle, Clapperboard } from "lucide-react";
import type { ProjectModeData } from "../../types";
interface ProjectSidebarProps {
data: ProjectModeData;
}
export function ProjectSidebar({ data }: ProjectSidebarProps) {
const { project, scenes, shots, selectedSceneId, phase, onSelectScene, onAddScene } = data;
if (scenes.length === 0) {
return <OutlineSidebar data={data} />;
}
return (
<div className="flex-1 min-h-0 flex flex-col">
<div className="px-4 py-3 border-b border-gray-800/40 flex-shrink-0">
<p className="text-[10px] uppercase tracking-wider text-gray-500 font-medium">Description</p>
<p className="text-xs text-gray-300 mt-1 leading-relaxed line-clamp-3">
{project.description || <span className="italic text-gray-600">No description yet.</span>}
</p>
</div>
<div className="px-4 py-2.5 flex items-center justify-between flex-shrink-0">
<span className="text-[11px] uppercase tracking-wider text-gray-500 font-medium">Scenes</span>
<button
onClick={onAddScene}
title="Add scene"
className="inline-flex items-center gap-1 rounded-lg border border-gray-800 hover:border-gray-700 hover:bg-gray-900/60 px-2 py-1 text-[10px] text-gray-400 hover:text-gray-200 transition"
>
<Plus className="w-3 h-3" /> Add scene
</button>
</div>
<div className="flex-1 min-h-0 overflow-y-auto px-2 pb-3 space-y-1">
{scenes.map((scene) => {
const sceneShots = shots.filter((s) => s.scene_id === scene.id);
const generated = sceneShots.filter((s) => s.image_file).length;
const active = scene.id === selectedSceneId;
return (
<button
key={scene.id}
onClick={() => onSelectScene(scene.id)}
className={`w-full text-left rounded-xl px-3 py-2.5 transition group ${active ? "bg-rose-600/10 ring-1 ring-rose-500/30" : "hover:bg-gray-900/50"}`}
>
<div className="flex items-center justify-between gap-2 mb-1">
<div className="flex items-center gap-2 min-w-0">
<span className={`text-[10px] font-mono ${active ? "text-rose-300" : "text-gray-500"}`}>S{scene.scene_number}</span>
<span className={`text-xs font-medium truncate ${active ? "text-gray-100" : "text-gray-300"}`}>
{scene.heading || "Untitled scene"}
</span>
</div>
</div>
{scene.summary && (
<p className="text-[11px] text-gray-500 leading-relaxed line-clamp-2">{scene.summary}</p>
)}
<div className="flex items-center gap-2 mt-2 text-[10px] text-gray-600">
<span>{sceneShots.length} shot{sceneShots.length === 1 ? "" : "s"}</span>
{generated > 0 && (
<span className={phase === "remix" ? "text-violet-400/70" : "text-emerald-400/70"}>
· {generated} rendered
</span>
)}
</div>
</button>
);
})}
</div>
</div>
);
}
function OutlineSidebar({ data }: ProjectSidebarProps) {
const { project, onAddScene } = data;
return (
<div className="flex-1 min-h-0 overflow-y-auto p-4 space-y-4">
<section className="rounded-2xl border border-rose-500/20 bg-gradient-to-b from-rose-950/15 to-gray-900/20 p-4 space-y-2.5">
<div className="flex items-center gap-2">
<Sparkles className="w-3.5 h-3.5 text-rose-300" />
<span className="text-[11px] uppercase tracking-wider text-rose-200 font-semibold">Start the outline</span>
</div>
<p className="text-xs text-gray-300 leading-relaxed">
Pitch your project to your agent and it'll draft scenes and shots here. No scenes yet — once they appear, this sidebar becomes a scene switcher.
</p>
<div className="rounded-xl border border-gray-800/60 bg-black/30 p-3 space-y-1.5">
<p className="text-[10px] uppercase tracking-wider text-gray-500">Try saying</p>
<p className="text-[11px] text-gray-300 italic leading-relaxed">"Make me a 30s cyberpunk teaser, my character walking through neon rain."</p>
<p className="text-[11px] text-gray-300 italic leading-relaxed">"Put me in an Iron Man movie. Workshop, suit assembly, rooftop in the rain."</p>
</div>
</section>
<section className="space-y-2.5">
<p className="text-[10px] uppercase tracking-wider text-gray-500 font-medium">Project</p>
<div className="rounded-xl border border-gray-800/60 bg-gray-900/30 p-3 space-y-2">
<div>
<p className="text-[10px] uppercase tracking-wider text-gray-600">Title</p>
<p className="text-sm text-gray-100 mt-0.5">{project.title}</p>
</div>
{project.description && (
<div>
<p className="text-[10px] uppercase tracking-wider text-gray-600">Description</p>
<p className="text-xs text-gray-400 mt-0.5 leading-relaxed">{project.description}</p>
</div>
)}
<div className="grid grid-cols-2 gap-2 pt-1">
<div>
<p className="text-[10px] uppercase tracking-wider text-gray-600">Aspect</p>
<p className="text-xs text-gray-300 mt-0.5 font-mono">{project.aspect_ratio}</p>
</div>
<div>
<p className="text-[10px] uppercase tracking-wider text-gray-600">Duration</p>
<p className="text-xs text-gray-300 mt-0.5">{project.duration_seconds ?? "—"}s</p>
</div>
</div>
{project.characters.length > 0 && (
<div className="pt-1">
<p className="text-[10px] uppercase tracking-wider text-gray-600 mb-1.5">Cast</p>
<div className="flex flex-wrap gap-1">
{project.characters.map((id) => (
<span key={id} className="inline-flex items-center gap-1 rounded-md border border-gray-800 bg-gray-900/40 px-2 py-0.5 text-[11px] text-gray-300 font-mono">
<UserCircle className="w-3 h-3 text-gray-500" /> {id}
</span>
))}
</div>
</div>
)}
</div>
</section>
<button
onClick={onAddScene}
className="w-full inline-flex items-center justify-center gap-2 rounded-xl border border-gray-700 bg-gray-900/40 hover:bg-gray-900 hover:border-gray-600 px-3 py-2.5 text-xs text-gray-300 transition"
>
<Plus className="w-3.5 h-3.5" /> Add scene manually
</button>
<div className="rounded-xl border border-gray-800/40 bg-gray-900/20 p-3 flex items-start gap-2.5">
<Clapperboard className="w-3.5 h-3.5 text-gray-500 mt-0.5 flex-shrink-0" />
<p className="text-[11px] text-gray-500 leading-relaxed">
Scenes hold the story beats. Shots inside scenes get rendered into images, then animated. Outline first; only generate after the structure is approved.
</p>
</div>
</div>
);
}
|