feat(studio): include composition context in catalog agent prompts

The "Ask agent" button now copies the current composition state along
with the category-specific prompt: playback time, active composition
path, dimensions, and all elements visible at the current time with
their track, timing, and source paths. Gives the agent full context
to place and customize the block correctly.
This commit is contained in:
Miguel Ángel
2026-05-21 19:23:08 -04:00
parent 2614071618
commit fb48b8d9b4
@@ -5,6 +5,9 @@ import {
getCategoryColors,
type BlockCategory,
} from "../../utils/blockCategories";
import { usePlayerStore } from "../../player";
import { formatTime } from "../../player/lib/time";
import { useStudioContext } from "../../contexts/StudioContext";
export interface BlockPreviewInfo {
videoUrl?: string;
posterUrl?: string;
@@ -153,15 +156,59 @@ function CategoryPill({
);
}
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<string, string> = {
captions: [
@@ -201,14 +248,15 @@ function buildAgentPrompt(
].join("\n\n"),
};
return (
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")
);
].join("\n\n");
return [instruction, "", "## Current composition state", "", compositionInfo].join("\n");
}
function BlockCard({
@@ -262,15 +310,31 @@ function BlockCard({
};
}, []);
const { activeCompPath, compositionDimensions } = useStudioContext();
const handleCopyPrompt = useCallback(
(e: React.MouseEvent) => {
e.stopPropagation();
const prompt = buildAgentPrompt(title, name, description, category, blockType);
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);
navigator.clipboard.writeText(prompt);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
},
[title, name, description, category, blockType],
[title, name, description, category, blockType, activeCompPath, compositionDimensions],
);
return (