mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
feat(studio): render with preview variables + template handoff + docs
Sixth PR of the template-variables Studio stack — closing the loop from
preview to render to developer handoff.
- renders started from the Renders tab now carry the active preview
variable overrides (StartRenderOptions.variables → POST /render →
RenderConfig.variables), so "render" produces exactly what the user is
previewing.
- Variables panel "Use this template" footer: copy the effective values
(defaults merged with overrides) as JSON, or as a ready-to-run
`npx hyperframes render <comp> --variables '<json>'` command.
- gitignore: negate the renders/ output rule for the tracked
src/components/renders/ source dir — without it, pre-commit's format
re-stage (`git add {staged_files}`) hard-fails on any change to those
files.
- docs: the Studio panel docs/concepts/variables.mdx described was
aspirational — replace with the real Variables-in-Studio section
(declare/edit, render-truthful preview, render-with-values, handoff,
usage badges); document the new SDK variable APIs in
docs/sdk/reference/composition.mdx (declaration ops, read APIs,
setPreviewVariables).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,7 @@ import { SlideshowPanel } from "./panels/SlideshowPanel";
|
||||
import type { SceneInfo } from "./panels/SlideshowPanel";
|
||||
import { VariablesPanel } from "./panels/VariablesPanel";
|
||||
import { PanelTabButton } from "./PanelTabButton";
|
||||
import { usePreviewVariablesStore } from "../hooks/previewVariablesStore";
|
||||
import type { RenderJob } from "./renders/useRenderQueue";
|
||||
import type { BlockParam } from "@hyperframes/core/registry";
|
||||
import type { IframeWindow } from "../player/lib/playbackTypes";
|
||||
@@ -421,6 +422,9 @@ export function StudioRightPanel({
|
||||
format,
|
||||
resolution,
|
||||
composition,
|
||||
// Render what the user is previewing: active variable overrides
|
||||
// from the Variables panel ride along (undefined = defaults).
|
||||
variables: usePreviewVariablesStore.getState().values ?? undefined,
|
||||
});
|
||||
}}
|
||||
compositionDimensions={compositionDimensions}
|
||||
|
||||
@@ -17,8 +17,14 @@ import {
|
||||
EMPTY_DRAFT,
|
||||
} from "./VariablesDeclarationForm";
|
||||
import { PreviewValueControl } from "./VariablesValueControls";
|
||||
import { copyTextToClipboard } from "../../utils/clipboard";
|
||||
import { isScalarVariableValue as isScalar } from "@hyperframes/core/variables";
|
||||
|
||||
/** POSIX single-quote escaping so the copied command survives quotes in values. */
|
||||
function shellSingleQuote(value: string): string {
|
||||
return `'${value.replace(/'/g, `'\\''`)}'`;
|
||||
}
|
||||
|
||||
interface VariablesPanelProps {
|
||||
sdkSession: Composition | null;
|
||||
reloadPreview: () => void;
|
||||
@@ -209,6 +215,45 @@ function PreviewModeHeader({
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Developer/agent handoff: copy the effective values as JSON or as a
|
||||
* ready-to-run render command mirroring exactly what the preview shows.
|
||||
*/
|
||||
function HandoffFooter({
|
||||
effectiveValues,
|
||||
compPath,
|
||||
onCopy,
|
||||
}: {
|
||||
effectiveValues: Record<string, unknown>;
|
||||
compPath: string;
|
||||
onCopy: (text: string, what: string) => void;
|
||||
}) {
|
||||
const json = JSON.stringify(effectiveValues);
|
||||
const command = `npx hyperframes render ${shellSingleQuote(compPath)} --variables ${shellSingleQuote(json)}`;
|
||||
return (
|
||||
<div className="space-y-1.5 rounded-lg border border-neutral-800/70 bg-neutral-900/40 p-2">
|
||||
<p className="text-[9px] font-medium uppercase tracking-wider text-neutral-500">
|
||||
Use this template
|
||||
</p>
|
||||
<code className="block truncate font-mono text-[9px] text-neutral-500" title={command}>
|
||||
{command}
|
||||
</code>
|
||||
<div className="flex items-center gap-2">
|
||||
<RowAction
|
||||
label="Copy render command"
|
||||
title="CLI command rendering exactly what the preview shows"
|
||||
onClick={() => onCopy(command, "Render command")}
|
||||
/>
|
||||
<RowAction
|
||||
label="Copy values JSON"
|
||||
title="Effective values (defaults merged with preview overrides)"
|
||||
onClick={() => onCopy(json, "Values JSON")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const EMPTY_STATE = (
|
||||
<p className="text-[10px] leading-relaxed text-neutral-500">
|
||||
No variables declared. Variables make parts of this composition dynamic — declare them here (or
|
||||
@@ -269,6 +314,24 @@ export const VariablesPanel = memo(function VariablesPanel({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[sdkSession, previewValues, refreshKey, revision],
|
||||
);
|
||||
const effectiveValues = useMemo(
|
||||
() => sdkSession?.getVariableValues(previewValues ?? undefined) ?? {},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[sdkSession, previewValues, refreshKey, revision],
|
||||
);
|
||||
|
||||
const copyToClipboard = useCallback(
|
||||
(text: string, what: string) => {
|
||||
// Shared helper carries the execCommand fallback Safari needs.
|
||||
void copyTextToClipboard(text).then((ok) =>
|
||||
showToast(
|
||||
ok ? `${what} copied` : `Couldn't copy ${what.toLowerCase()}`,
|
||||
ok ? "info" : "error",
|
||||
),
|
||||
);
|
||||
},
|
||||
[showToast],
|
||||
);
|
||||
|
||||
const dropPreviewOverride = useCallback(
|
||||
(id: string) => {
|
||||
@@ -424,6 +487,13 @@ export const VariablesPanel = memo(function VariablesPanel({
|
||||
Scripts access variables dynamically — usage info may be incomplete.
|
||||
</p>
|
||||
)}
|
||||
{declarations.length > 0 && (
|
||||
<HandoffFooter
|
||||
effectiveValues={effectiveValues}
|
||||
compPath={activeCompPath ?? "index.html"}
|
||||
onCopy={copyToClipboard}
|
||||
/>
|
||||
)}
|
||||
{addOpen ? (
|
||||
<DeclarationForm
|
||||
initial={EMPTY_DRAFT}
|
||||
|
||||
@@ -34,6 +34,12 @@ export interface StartRenderOptions {
|
||||
resolution?: ResolutionPreset | "auto";
|
||||
/** Render a specific composition file instead of index.html. */
|
||||
composition?: string;
|
||||
/**
|
||||
* Composition-variable overrides ({variableId: value}), forwarded to the
|
||||
* render route and injected as window.__hfVariables — the same channel
|
||||
* `hyperframes render --variables` uses.
|
||||
*/
|
||||
variables?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
// "Hide" (formerly "Clear") is a view operation, not a delete: hidden ids are
|
||||
@@ -126,7 +132,9 @@ export function useRenderQueue(projectId: string | null) {
|
||||
}, [loadRenders]);
|
||||
|
||||
// Start a render and track progress via SSE
|
||||
// Pre-existing branchy fetch/poll flow — the variables passthrough added one branch.
|
||||
const startRender = useCallback(
|
||||
// fallow-ignore-next-line complexity
|
||||
async (opts: StartRenderOptions = {}) => {
|
||||
if (!projectId) return;
|
||||
|
||||
@@ -154,6 +162,7 @@ export function useRenderQueue(projectId: string | null) {
|
||||
format: string;
|
||||
resolution?: string;
|
||||
composition?: string;
|
||||
variables?: Record<string, unknown>;
|
||||
telemetryDistinctId: string;
|
||||
} = {
|
||||
fps,
|
||||
@@ -166,6 +175,9 @@ export function useRenderQueue(projectId: string | null) {
|
||||
};
|
||||
if (resolution && resolution !== "auto") body.resolution = resolution;
|
||||
if (composition) body.composition = composition;
|
||||
if (opts.variables && Object.keys(opts.variables).length > 0) {
|
||||
body.variables = opts.variables;
|
||||
}
|
||||
let res: Response;
|
||||
try {
|
||||
res = await fetch(`/api/projects/${projectId}/render`, {
|
||||
|
||||
Reference in New Issue
Block a user