mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
feat(studio): storyboard empty state with agent copy-prompt and skeleton preview (#2091)
The storyboard view's empty state was a single "no storyboard yet" line. Add a copy-to-clipboard prompt box (a ready-to-paste handoff prompt carrying the canonical STORYBOARD.md frontmatter + per-frame format) so users can hand it to their coding agent, plus a faded skeleton of the contact-sheet grid so landing on an empty board previews what a filled one looks like instead of a dead end.
This commit is contained in:
@@ -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: <the one-line takeaway of the video>
|
||||
arc: <the narrative shape, e.g. Problem → Solution>
|
||||
audience: <who it's for>
|
||||
---
|
||||
|
||||
## Frame 1 — <title>
|
||||
- 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 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>
|
||||
<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 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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user