diff --git a/packages/studio/src/components/storyboard/StoryboardView.tsx b/packages/studio/src/components/storyboard/StoryboardView.tsx index 3965cd6e5e..56310c426e 100644 --- a/packages/studio/src/components/storyboard/StoryboardView.tsx +++ b/packages/studio/src/components/storyboard/StoryboardView.tsx @@ -1,5 +1,7 @@ -import type { ReactNode } from "react"; +import { useState, type ReactNode } from "react"; +import { Copy, Check } from "@phosphor-icons/react"; import { useStoryboard } from "../../hooks/useStoryboard"; +import { copyTextToClipboard } from "../../utils/clipboard"; import { Button } from "../ui/Button"; import { StoryboardLoaded } from "./StoryboardLoaded"; @@ -70,15 +72,91 @@ function Message({ children, tone = "muted" }: { children: ReactNode; tone?: "mu ); } +function handoffPrompt(path: string): string { + return `Create a \`${path}\` at the project root to plan this video frame by frame. + +Use this format: + +--- +format: 1920x1080 +message: +arc: +audience: +--- + +## Frame 1 — +- duration: 5s +- transition_in: crossfade +- status: planned +- src: compositions/frames/01-<slug>.html + +<A sentence or two: what's on screen and what the narration says.> + +Add one \`## Frame N\` section per beat. Keep the arc tight.`; +} + function EmptyState({ path }: { path: string }) { + const [copied, setCopied] = useState(false); + const prompt = handoffPrompt(path); + + const onCopy = async () => { + if (await copyTextToClipboard(prompt)) { + setCopied(true); + window.setTimeout(() => setCopied(false), 2000); + } + }; + + return ( + <div> + <div className="rounded-lg border border-dashed border-neutral-800 px-6 py-10 text-center"> + <h2 className="text-base font-semibold text-neutral-300">No storyboard yet</h2> + <p className="mx-auto mt-2 max-w-md text-sm text-neutral-500"> + Add a <code className="rounded bg-neutral-900 px-1 py-0.5 text-neutral-400">{path}</code>{" "} + at the project root to plan this video frame by frame. Hand this prompt to your coding + agent to scaffold it. + </p> + + <div className="mx-auto mt-6 max-w-2xl overflow-hidden rounded-lg border border-neutral-800 bg-neutral-900 text-left"> + <div className="flex items-center justify-between border-b border-neutral-800 px-3 py-2"> + <span className="font-mono text-xs text-neutral-500">Prompt for your agent</span> + <Button + size="sm" + variant="secondary" + onClick={onCopy} + icon={copied ? <Check size={14} /> : <Copy size={14} />} + > + {copied ? "Copied" : "Copy prompt"} + </Button> + </div> + <pre className="max-h-64 overflow-auto px-4 py-3 font-mono text-xs leading-relaxed text-neutral-400 whitespace-pre-wrap"> + {prompt} + </pre> + </div> + </div> + + <SkeletonPreview /> + </div> + ); +} + +/** Faded placeholder of a filled board so landing here isn't a dead end — + * it previews the contact-sheet layout {@link StoryboardGrid} renders. */ +function SkeletonPreview() { return ( - <div className="rounded-lg border border-dashed border-neutral-800 px-6 py-16 text-center"> - <h2 className="text-base font-semibold text-neutral-300">No storyboard yet</h2> - <p className="mx-auto mt-2 max-w-md text-sm text-neutral-500"> - Add a <code className="rounded bg-neutral-900 px-1 py-0.5 text-neutral-400">{path}</code> at - the project root to plan this video frame by frame. Your agent can create and iterate on it - for you. - </p> + <div aria-hidden="true" className="mt-10 select-none opacity-40"> + <div className="mb-4 text-center text-xs uppercase tracking-wide text-neutral-600"> + Preview + </div> + <div className="grid gap-x-6 gap-y-8 [grid-template-columns:repeat(auto-fill,minmax(300px,1fr))]"> + {[0, 1, 2].map((i) => ( + <div key={i} className="rounded-lg border border-neutral-800 bg-neutral-900/40 p-3"> + <div className="aspect-video w-full rounded bg-neutral-800/60" /> + <div className="mt-3 h-3 w-2/3 rounded bg-neutral-800/60" /> + <div className="mt-2 h-2.5 w-full rounded bg-neutral-800/40" /> + <div className="mt-1.5 h-2.5 w-4/5 rounded bg-neutral-800/40" /> + </div> + ))} + </div> </div> ); }