nemoflix-amd-video / studio /src /components /sidebar /ProjectSidebar.tsx
ortegarod's picture
Update Docker Studio UI for projects workflow
fe029e8
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>
);
}