);
});
function CategoryPill({
label,
category,
active,
onClick,
}: {
label: string;
category?: BlockCategory;
active: boolean;
onClick: () => void;
}) {
const colors = category ? getCategoryColors(category) : null;
return (
);
}
interface CompositionContext {
currentTime: number;
activeCompPath: string | null;
elements: Array<{
id: string;
start: number;
duration: number;
track: number;
label?: string;
compositionSrc?: string;
}>;
compositionDimensions?: { width: number; height: number };
}
function formatCompositionContext(ctx: CompositionContext): string {
const lines: string[] = [
`Playback time: ${formatTime(ctx.currentTime)}`,
`Active composition: ${ctx.activeCompPath || "index.html"}`,
];
if (ctx.compositionDimensions) {
lines.push(
`Dimensions: ${ctx.compositionDimensions.width}x${ctx.compositionDimensions.height}`,
);
}
const visibleNow = ctx.elements.filter(
(el) => ctx.currentTime >= el.start && ctx.currentTime < el.start + el.duration,
);
if (visibleNow.length > 0) {
lines.push(
"",
`Elements visible at ${formatTime(ctx.currentTime)}:`,
...visibleNow.map(
(el) =>
`- ${el.label || el.id} (track ${el.track}, ${formatTime(el.start)}–${formatTime(el.start + el.duration)}${el.compositionSrc ? `, src: ${el.compositionSrc}` : ""})`,
),
);
}
const maxZ = ctx.elements.length > 0 ? Math.max(...ctx.elements.map((_, i) => i + 1)) : 0;
lines.push("", `Highest track index: ${maxZ}`);
return lines.join("\n");
}
function buildAgentPrompt(
title: string,
name: string,
description: string,
category: BlockCategory,
blockType: string,
context: CompositionContext,
): string {
const isComponent = blockType === "hyperframes:component";
const kind = isComponent ? "component" : "block";
const compositionInfo = formatCompositionContext(context);
const categoryPrompts: Record = {
captions: [
`Using /hyperframes, add the "${title}" caption style (registry: ${name}) to my composition.`,
`${description}`,
`Transcribe the audio with /hyperframes-media, then wire the transcript into this caption component. Match the font colors and animation timing to my composition's design tokens. Place it as an overlay above the main content with the highest z-index.`,
].join("\n\n"),
vfx: [
`Using /hyperframes, add the "${title}" VFX (registry: ${name}) as a full-screen overlay on my composition.`,
`${description}`,
`This is a WebGL effect that requires chrome://flags/#html-in-canvas. Layer it on top of all content, adjust the shader uniforms and color palette to complement my scene, and set the duration to match the composition length.`,
].join("\n\n"),
transitions: [
`Using /hyperframes, add the "${title}" transition (registry: ${name}) between my scenes.`,
`${description}`,
`Place this transition at the cut point between the current scene and the next. Set the duration to 0.5–1s, position it at the scene boundary on the timeline, and make sure the z-index is above both scenes. Adjust colors to match my palette.`,
].join("\n\n"),
effects: [
`Using /hyperframes, add the "${title}" effect (registry: ${name}) as an overlay on my composition.`,
`${description}`,
`Layer this on top of the current content. Adjust the opacity, colors, and animation timing to enhance the scene without overwhelming the main content.`,
].join("\n\n"),
social: [
`Using /hyperframes, add the "${title}" template (registry: ${name}) to my composition.`,
`${description}`,
`Replace the placeholder text, handle, and avatar with my actual content. Match the typography and colors to my brand. Adjust timing so the elements animate in sync with the voiceover.`,
].join("\n\n"),
data: [
`Using /hyperframes, add the "${title}" visualization (registry: ${name}) to my composition.`,
`${description}`,
`Replace the placeholder data with my actual values and labels. Adjust the color scale, animation stagger timing, and typography to match my composition's design system. Size it to fit the current viewport.`,
].join("\n\n"),
scenes: [
`Using /hyperframes, add the "${title}" scene (registry: ${name}) to my composition.`,
`${description}`,
`Replace all placeholder text, images, and content with my actual material. Match fonts, colors, and layout to my existing design tokens. Set the timeline position and duration to fit the narrative flow.`,
].join("\n\n"),
};
const instruction =
categoryPrompts[category] ??
[
`Using /hyperframes, add the "${title}" ${kind} (registry: ${name}) to my composition.`,
`${description}`,
`Customize it to match my composition's design and timeline.`,
].join("\n\n");
return [instruction, "", "## Current composition state", "", compositionInfo].join("\n");
}
function BlockCard({
name,
title,
description,
blockType,
duration,
category,
tags,
posterUrl,
videoUrl,
onAdd,
onShowPrompt,
onPreview,
}: {
name: string;
title: string;
description: string;
blockType: string;
duration?: number;
category: BlockCategory;
tags?: string[];
posterUrl?: string;
videoUrl?: string;
onAdd?: () => void;
onShowPrompt?: (info: { title: string; prompt: string }) => void;
onPreview?: (preview: BlockPreviewInfo | null) => void;
}) {
const [hovered, setHovered] = useState(false);
const [adding, setAdding] = useState(false);
const hoverTimer = useRef | null>(null);
const colors = getCategoryColors(category);
const needsWebGL = tags?.includes("html-in-canvas") || tags?.includes("webgl");
const handleEnter = useCallback(() => {
hoverTimer.current = setTimeout(() => {
setHovered(true);
onPreview?.({ videoUrl, posterUrl, title });
}, 300);
}, [onPreview, videoUrl, posterUrl, title]);
const handleLeave = useCallback(() => {
if (hoverTimer.current) {
clearTimeout(hoverTimer.current);
hoverTimer.current = null;
}
setHovered(false);
onPreview?.(null);
}, [onPreview]);
useEffect(() => {
return () => {
if (hoverTimer.current) clearTimeout(hoverTimer.current);
};
}, []);
const handleAdd = useCallback(
(e: React.MouseEvent) => {
e.stopPropagation();
if (adding || !onAdd) return;
setAdding(true);
onAdd();
setTimeout(() => setAdding(false), 1000);
},
[onAdd, adding],
);
const { activeCompPath, compositionDimensions } = useStudioShellContext();
const handleShowPrompt = useCallback(
(e: React.MouseEvent) => {
e.stopPropagation();
const state = usePlayerStore.getState();
const context: CompositionContext = {
currentTime: state.currentTime,
activeCompPath,
elements: state.elements.map((el) => ({
id: el.id,
start: el.start,
duration: el.duration,
track: el.track,
label: el.label,
compositionSrc: el.compositionSrc,
})),
compositionDimensions: compositionDimensions ?? undefined,
};
const prompt = buildAgentPrompt(title, name, description, category, blockType, context);
onShowPrompt?.({ title, prompt });
},
[
title,
name,
description,
category,
blockType,
activeCompPath,
compositionDimensions,
onShowPrompt,
],
);
return (