diff --git a/apps/daemon/src/prompts/system.ts b/apps/daemon/src/prompts/system.ts index 643f5e38a..a14148471 100644 --- a/apps/daemon/src/prompts/system.ts +++ b/apps/daemon/src/prompts/system.ts @@ -600,6 +600,14 @@ function derivePreflight(skillBody: string): string { if (/references\/themes\.md/.test(skillBody)) refs.push('`references/themes.md`'); if (/references\/components\.md/.test(skillBody)) refs.push('`references/components.md`'); if (/references\/checklist\.md/.test(skillBody)) refs.push('`references/checklist.md`'); + // The hyperframes skill ships an html-in-canvas reference next to the + // VFX catalog blocks. The chat handler at server.ts:4138 routes through + // this composer (not the contracts copy), so the case must live here + // too — otherwise live agent runs miss the preflight directive even + // when the skill body explicitly lists the file. + if (/references\/html-in-canvas\.md|html-in-canvas\.md/.test(skillBody)) { + refs.push('`references/html-in-canvas.md`'); + } if (refs.length === 0) return ''; return ` **Pre-flight (do this before any other tool):** Read ${refs.join(', ')} via the path written in the skill-root preamble. The seed template defines the class system you'll paste into; the layouts file is the only acceptable source of section/screen/slide skeletons; the checklist is your P0/P1/P2 gate before emitting \`\`. Skipping this step is the #1 reason output regresses to generic AI-slop.`; } diff --git a/apps/daemon/tests/prompts/system.test.ts b/apps/daemon/tests/prompts/system.test.ts index 4d281bcb0..66ff6030a 100644 --- a/apps/daemon/tests/prompts/system.test.ts +++ b/apps/daemon/tests/prompts/system.test.ts @@ -25,6 +25,19 @@ const liveArtifactSkillBody = [ liveArtifactSkillMarkdown.replace(/^---[\s\S]*?---\n\n/, '').trim(), ].join('\n'); +const hyperframesRoot = path.join(repoRoot, 'skills/hyperframes'); +const hyperframesSkillPath = path.join(repoRoot, 'skills/hyperframes/SKILL.md'); +const hyperframesSkillMarkdown = readFileSync(hyperframesSkillPath, 'utf8'); +const hyperframesSkillBody = [ + `> **Skill root (absolute):** \`${hyperframesRoot}\``, + '>', + '> This skill ships side files alongside `SKILL.md`. Resolve references', + '> like `references/html-in-canvas.md` against the skill root above.', + '', + '', + hyperframesSkillMarkdown.replace(/^---[\s\S]*?---\n\n/, '').trim(), +].join('\n'); + describe('composeSystemPrompt', () => { it('injects live-artifact skill guidance and metadata intent', () => { const prompt = composeSystemPrompt({ @@ -55,6 +68,27 @@ describe('composeSystemPrompt', () => { expect(prompt).toContain('The first output should be a live artifact/dashboard/report'); }); + // The daemon composer (this file) is what apps/daemon/src/server.ts wires + // into live chat runs. The contracts copy at packages/contracts/src/prompts + // /system.ts exists for non-daemon contexts and was updated in the + // hyperframes PR; without this test the two copies drift silently and the + // main HyperFrames flow misses its preflight directive in production. + it('injects the html-in-canvas preflight for the hyperframes skill', () => { + const prompt = composeSystemPrompt({ + skillName: 'hyperframes', + skillMode: 'video', + skillBody: hyperframesSkillBody, + metadata: { + kind: 'video', + videoModel: 'hyperframes-html', + } as any, + }); + + expect(prompt).toContain('## Active skill — hyperframes'); + expect(prompt).toContain('**Pre-flight (do this before any other tool):**'); + expect(prompt).toContain('`references/html-in-canvas.md`'); + }); + describe('artifact handoff no-emit clauses (#1143)', () => { it('drops the absolute "non-negotiable" framing in favor of conditional language', () => { const prompt = composeSystemPrompt({}); diff --git a/apps/web/src/components/ChatPane.tsx b/apps/web/src/components/ChatPane.tsx index edd79318a..1dee095ee 100644 --- a/apps/web/src/components/ChatPane.tsx +++ b/apps/web/src/components/ChatPane.tsx @@ -21,7 +21,24 @@ type TranslateFn = (key: keyof Dict, vars?: Record) => // Each prompt is intentionally dense — it should showcase ambitious // layout, typographic, and information-design moves rather than a // generic landing page. -const EXAMPLE_PROMPT_KEYS: Array<{ +// +// Starter sets are picked per project kind (and per video model) so a +// fresh seedance video, a hyperframes html-in-canvas video, an image +// project and an audio project each see relevant prompts instead of the +// generic prototype trio. The default (prototype/deck/template/other/ +// live-artifact) set stays i18n-translated via existing chat.example* +// keys so the user-facing copy keeps its localizations. The new media +// sets are inline English literals — they are technical agent prompts +// that work well across locales without translation, and going through +// i18n for each of them would balloon every Dict entry by 12+ keys. +type StarterPrompt = { + icon: string; + title: string; + tag: string; + prompt: string; +}; + +const DEFAULT_STARTER_KEYS: Array<{ icon: string; titleKey: keyof Dict; tagKey: keyof Dict; @@ -47,6 +64,135 @@ const EXAMPLE_PROMPT_KEYS: Array<{ }, ]; +const IMAGE_STARTERS: StarterPrompt[] = [ + { + icon: '◯', + title: 'Editorial portrait', + tag: 'Portrait', + prompt: + 'A close-up editorial portrait of a young creative director in their late 20s, soft natural light through tall studio windows, warm neutral palette (cream, taupe, soft black), shot at 85mm f/1.8 with shallow depth of field, sharp gaze straight to camera, subtle film grain, no makeup look.', + }, + { + icon: '▭', + title: 'Product hero', + tag: 'E-commerce', + prompt: + 'A premium product hero shot of a single matte ceramic coffee mug on a warm cream paper backdrop. Hard rim light from the upper-left, gentle elongated shadow stretching to the lower-right, faint steam rising from the cup. Square crop, centered composition, room above for headline copy, no props or hands in frame.', + }, + { + icon: '◐', + title: 'Flat illustration', + tag: 'Illustration', + prompt: + 'A flat vector illustration of a cozy reading nook by a rainy window — geometric shapes, restrained 5-color palette (cream, terracotta, deep teal, burnt sienna, soft black), thin 1.5px line accents, no gradients, no textures, soft drop shadows only on the foreground armchair.', + }, +]; + +// Pure-video / cinematic-shot starters for seedance, sora, kling, veo, +// grok-imagine and similar text-to-video models. Each prompt is one +// shot, restrained motion, and a clear visual concept the model can +// nail in 5-10 seconds. +const VIDEO_SEEDANCE_STARTERS: StarterPrompt[] = [ + { + icon: '◉', + title: 'Product reveal', + tag: 'Cinematic', + prompt: + 'A 5-second product reveal: a minimal high-end skincare bottle on a clean cream stone surface, soft side light from camera-left, slow camera push-in, subtle depth-of-field shift from the cap to the label, restrained motion, no text overlays, no people in frame.', + }, + { + icon: '▣', + title: 'Lantern close-up', + tag: 'Mood', + prompt: + 'A 6-second cinematic close-up of a young woman holding a glowing paper lantern in a misty pine forest at golden hour. Shallow depth of field on her eyes, gentle dolly-in, ambient particles drifting through the warm shaft of light, no dialogue, ambient forest sound only.', + }, + { + icon: '⌘', + title: 'Neon street drift', + tag: 'Action', + prompt: + 'A 5-second street-racing tracking shot at night in a neon-lit cyberpunk Hong Kong alley. Low-angle camera following a matte-black sports car drifting around a tight corner, motion blur on the wheels, lens flares from oncoming neon signs, rain-slick asphalt reflecting the lights, no on-screen text.', + }, +]; + +// HyperFrames HTML-in-canvas starters — these target the +// hyperframes-html video model where the renderer captures live DOM +// into a WebGL texture and runs shader effects on top. References: +// https://www.remotion.dev/docs/html-in-canvas (concept), the seven +// vfx-* catalog blocks shipped via `npx hyperframes add vfx-*`, and +// skills/hyperframes/references/html-in-canvas.md. +const VIDEO_HYPERFRAMES_STARTERS: StarterPrompt[] = [ + { + icon: '◉', + title: 'Magnifying glass reveal', + tag: 'HTML-in-canvas', + prompt: + 'Make a 5-second composition with a single line of bold display text on a clean canvas. Animate a round magnifying glass that travels left to right across the line, with subtle glass refraction warping the letters underneath as it passes. Use HyperFrames html-in-canvas — capture the text DOM and run the lens shader on top via a vfx-liquid-glass-style pass. Pure CSS for the text; the glass is a WebGL layer.', + }, + { + icon: '▦', + title: 'CRT terminal scene', + tag: 'Vintage VFX', + prompt: + "Make a CRT-screen composition: dark canvas, monospace terminal text typing `npx hyperframes init my-video`, then `claude` invoked with the prompt 'Add a CRT effect using HTML-in-canvas'. Apply a subtle convex-curvature shader, scanlines, slight chromatic aberration, and a soft phosphor glow on top of the live DOM via html-in-canvas. The terminal text stays as real CSS so it's pixel-sharp before the shader pass.", + }, + { + icon: '◈', + title: 'Glitch breakdown', + tag: 'Glitch', + prompt: + 'Build a 6-second composition that displays a hero headline and a one-line subhead on a dark canvas, then breaks into a hard digital glitch — RGB channel split, horizontal displacement bands, brief frame-stutter, and a final clean reset. Capture the live DOM via html-in-canvas and run the glitch pass on top, so the type is real CSS underneath the shader.', + }, +]; + +// Speech-focused audio starters — the New Project audio panel only +// surfaces the `speech` kind today (see MediaProjectOptions), so we +// match that. If/when the music + sfx tabs come back, broaden this set. +const AUDIO_STARTERS: StarterPrompt[] = [ + { + icon: '♪', + title: 'Brand voiceover', + tag: 'Speech', + prompt: + "A 30-second warm-toned narrative voiceover for a product launch video — confident but conversational, mid-tempo, with a beat of pause after the brand name. Script: 'Three years in the making. One simple promise. Meet [product name] — the way work was supposed to feel.' English, neutral North American accent.", + }, + { + icon: '♫', + title: 'Onboarding narration', + tag: 'Speech', + prompt: + "A 20-second friendly onboarding narration for a mobile app's first-launch screen. Reassuring, smiling tone, slow enough to feel attentive without sounding scripted. Script: 'Welcome to Loop. Let's set up your space — three quick questions and you're in. You can change any of this later.'", + }, + { + icon: '♬', + title: 'Story passage read', + tag: 'Speech', + prompt: + "A 45-second cinematic read of an opening passage. Low, measured delivery with breath between sentences, slightly intimate close-mic'd quality. Script: 'The city sleeps in pieces. A neon sign flickers above the ramen counter. Across the avenue, a window glows — the only one still on this side of midnight.'", + }, +]; + +function pickStarters( + metadata: ProjectMetadata | undefined, + t: TranslateFn, +): StarterPrompt[] { + const kind = metadata?.kind; + if (kind === 'image') return IMAGE_STARTERS; + if (kind === 'video') { + return metadata?.videoModel === 'hyperframes-html' + ? VIDEO_HYPERFRAMES_STARTERS + : VIDEO_SEEDANCE_STARTERS; + } + if (kind === 'audio') return AUDIO_STARTERS; + return DEFAULT_STARTER_KEYS.map((entry) => ({ + icon: entry.icon, + title: t(entry.titleKey), + tag: t(entry.tagKey), + prompt: t(entry.promptKey), + })); +} + interface Props { messages: ChatMessage[]; streaming: boolean; @@ -489,36 +635,31 @@ export function ChatPane({
- {EXAMPLE_PROMPT_KEYS.map((ex, i) => { - const title = t(ex.titleKey); - const tag = t(ex.tagKey); - const prompt = t(ex.promptKey); - return ( - - ); - })} + {ex.prompt} + + + ↵ + + + ))}
) : null} diff --git a/apps/web/src/components/DesignFilesPanel.tsx b/apps/web/src/components/DesignFilesPanel.tsx index b98f428d2..7b351701d 100644 --- a/apps/web/src/components/DesignFilesPanel.tsx +++ b/apps/web/src/components/DesignFilesPanel.tsx @@ -619,7 +619,23 @@ export function DesignFilesPanel({ ) : null} {files.length === 0 && liveArtifacts.length === 0 ? ( -
{t('designFiles.empty')}
+
+
+ + {t('designFiles.empty')} + + +
+
) : ( <> {files.length > 0 ? ( diff --git a/apps/web/src/components/NewProjectPanel.tsx b/apps/web/src/components/NewProjectPanel.tsx index b0b15ef51..2fb10c23b 100644 --- a/apps/web/src/components/NewProjectPanel.tsx +++ b/apps/web/src/components/NewProjectPanel.tsx @@ -205,6 +205,7 @@ export function NewProjectPanel({ const [imageAspect, setImageAspect] = useState('1:1'); const [imageStyle, setImageStyle] = useState(''); const [videoModel, setVideoModel] = useState(DEFAULT_VIDEO_MODEL); + const [videoModelTouched, setVideoModelTouched] = useState(false); const [videoAspect, setVideoAspect] = useState('16:9'); const [videoLength, setVideoLength] = useState(5); const [audioKind, setAudioKind] = useState('speech'); @@ -312,12 +313,76 @@ export function NewProjectPanel({ } if (tab === 'image' || tab === 'video' || tab === 'audio') { const list = skills.filter((s) => s.mode === tab || s.surface === tab); + // The HyperFrames-HTML render path lives in the `hyperframes` skill. + // When the user has chosen `hyperframes-html` (via dropdown or template), + // pin the project to that skill explicitly — otherwise this branch falls + // back to `list[0]`, which is unsorted (apps/daemon/src/skills.ts builds + // the list from `readdir()`), so the project could route through + // `video-shortform` while metadata still says `videoModel: 'hyperframes-html'`. + if (tab === 'video' && videoModel === 'hyperframes-html') { + const hyper = list.find((s) => s.id === 'hyperframes'); + if (hyper) return hyper.id; + } return list.find((s) => s.defaultFor.includes(tab))?.id ?? list[0]?.id ?? null; } return null; - }, [tab, skills]); + }, [tab, skills, videoModel]); + + // When the user picks a curated prompt template, propagate the template's + // declared `model` and `aspect` onto the actual project state. Without + // this the user picks (e.g.) a HyperFrames template but `videoModel` + // stays on the default seedance — the agent then dispatches the wrong + // model and the render path mismatches the prompt. + function handleImagePromptTemplate(pick: PromptTemplatePick | null) { + setImagePromptTemplate(pick); + const m = pick?.summary.model; + if (m && IMAGE_MODELS.some((x) => x.id === m)) setImageModel(m); + const a = pick?.summary.aspect; + if (a && (MEDIA_ASPECTS as readonly string[]).includes(a)) { + setImageAspect(a as MediaAspect); + } + } + function handleVideoPromptTemplate(pick: PromptTemplatePick | null) { + setVideoPromptTemplate(pick); + const m = pick?.summary.model; + if (m && VIDEO_MODELS.some((x) => x.id === m)) { + setVideoModel(m); + setVideoModelTouched(true); + } + const a = pick?.summary.aspect; + if (a && (MEDIA_ASPECTS as readonly string[]).includes(a)) { + setVideoAspect(a as MediaAspect); + } + } + function handleVideoModel(id: string) { + setVideoModel(id); + setVideoModelTouched(true); + } + + // The HyperFrames skill renders HTML compositions through a local + // `npx hyperframes render` path, which dispatches under the + // `hyperframes-html` model — not seedance/veo/sora. When the resolved + // skill for the video tab is hyperframes, default `videoModel` so the + // model dropdown matches the actual render path. Once the user has + // explicitly chosen a model (via the dropdown or by picking a template + // that declares a model), `videoModelTouched` latches and this effect + // becomes a no-op for the rest of the panel session — re-entering the + // Video tab no longer silently rewrites their override back to + // hyperframes-html. + useEffect(() => { + if (tab !== 'video') return; + if (skillIdForTab !== 'hyperframes') return; + if (videoModelTouched) return; + if (videoPromptTemplate) return; + if (!VIDEO_MODELS.some((m) => m.id === 'hyperframes-html')) return; + setVideoModel('hyperframes-html'); + // Intentionally leaving videoPromptTemplate / videoModel out of deps + // so this only fires when the user toggles the tab or the skill + // resolution shifts — not whenever the user changes the dropdown. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [tab, skillIdForTab, videoModelTouched]); const canCreate = !loading && (tab !== 'template' || templateId != null); @@ -548,7 +613,7 @@ export function NewProjectPanel({ surface="image" templates={promptTemplates} value={imagePromptTemplate} - onChange={setImagePromptTemplate} + onChange={handleImagePromptTemplate} /> ) : null} @@ -557,7 +622,7 @@ export function NewProjectPanel({ surface="video" templates={promptTemplates} value={videoPromptTemplate} - onChange={setVideoPromptTemplate} + onChange={handleVideoPromptTemplate} /> ) : null} @@ -618,7 +683,7 @@ export function NewProjectPanel({ videoAspect={videoAspect} videoLength={videoLength} mediaProviders={mediaProviders} - onVideoModel={setVideoModel} + onVideoModel={handleVideoModel} onVideoAspect={setVideoAspect} onVideoLength={setVideoLength} /> diff --git a/apps/web/src/i18n/content.fr.ts b/apps/web/src/i18n/content.fr.ts index b63de9ade..87768e3ce 100644 --- a/apps/web/src/i18n/content.fr.ts +++ b/apps/web/src/i18n/content.fr.ts @@ -487,9 +487,19 @@ export const FR_PROMPT_TEMPLATE_CATEGORIES: Record = { 'Short Form': 'Short form', Travel: 'Voyage', 'Live Artifact': 'Live artifact', + 'VFX / HTML-in-Canvas': 'VFX / HTML-in-Canvas', }; -export const FR_PROMPT_TEMPLATE_IDS_WITH_EN_FALLBACK = ['notion-team-dashboard-live-artifact'] as const; +export const FR_PROMPT_TEMPLATE_IDS_WITH_EN_FALLBACK = [ + 'notion-team-dashboard-live-artifact', + 'hyperframes-html-in-canvas-iphone-device', + 'hyperframes-html-in-canvas-liquid-background', + 'hyperframes-html-in-canvas-liquid-glass', + 'hyperframes-html-in-canvas-magnetic', + 'hyperframes-html-in-canvas-portal-reveal', + 'hyperframes-html-in-canvas-shatter', + 'hyperframes-html-in-canvas-text-cursor', +] as const; export const FR_PROMPT_TEMPLATE_TAGS: Record = { '3d': '3D', @@ -590,6 +600,25 @@ export const FR_PROMPT_TEMPLATE_TAGS: Record = { 'website-to-video': 'website-to-video', wuxia: 'wuxia', zhaoyun: 'Zhaoyun', + dashboard: 'dashboard', + data: 'data', + destruction: 'destruction', + displacement: 'displacement', + hero: 'hero', + 'html-in-canvas': 'HTML-in-Canvas', + iphone: 'iPhone', + keynote: 'keynote', + liquid: 'liquide', + 'liquid-glass': 'liquid glass', + macbook: 'MacBook', + magnetic: 'magnétique', + particles: 'particules', + portal: 'portail', + 'product-demo': 'démo produit', + shader: 'shader', + shatter: 'shatter', + text: 'texte', + webgl: 'WebGL', }; export const FR_PROMPT_TEMPLATE_COPY: Record>> = { diff --git a/apps/web/src/i18n/content.ru.ts b/apps/web/src/i18n/content.ru.ts index 24a7d69b3..a83b5ffcc 100644 --- a/apps/web/src/i18n/content.ru.ts +++ b/apps/web/src/i18n/content.ru.ts @@ -487,9 +487,19 @@ export const RU_PROMPT_TEMPLATE_CATEGORIES: Record = { 'Short Form': 'Короткий формат', Travel: 'Путешествия', 'Live Artifact': 'Live-артефакт', + 'VFX / HTML-in-Canvas': 'VFX / HTML-in-Canvas', }; -export const RU_PROMPT_TEMPLATE_IDS_WITH_EN_FALLBACK = ['notion-team-dashboard-live-artifact'] as const; +export const RU_PROMPT_TEMPLATE_IDS_WITH_EN_FALLBACK = [ + 'notion-team-dashboard-live-artifact', + 'hyperframes-html-in-canvas-iphone-device', + 'hyperframes-html-in-canvas-liquid-background', + 'hyperframes-html-in-canvas-liquid-glass', + 'hyperframes-html-in-canvas-magnetic', + 'hyperframes-html-in-canvas-portal-reveal', + 'hyperframes-html-in-canvas-shatter', + 'hyperframes-html-in-canvas-text-cursor', +] as const; export const RU_PROMPT_TEMPLATE_TAGS: Record = { '3d': '3D', @@ -590,6 +600,25 @@ export const RU_PROMPT_TEMPLATE_TAGS: Record = { 'website-to-video': 'Сайт-в-видео', wuxia: 'Уся', zhaoyun: 'Чжао Юнь', + dashboard: 'Дашборд', + data: 'Данные', + destruction: 'Разрушение', + displacement: 'Смещение', + hero: 'Главный герой', + 'html-in-canvas': 'HTML-в-Canvas', + iphone: 'iPhone', + keynote: 'Keynote', + liquid: 'Жидкость', + 'liquid-glass': 'Liquid Glass', + macbook: 'MacBook', + magnetic: 'Магнитный', + particles: 'Частицы', + portal: 'Портал', + 'product-demo': 'Демо продукта', + shader: 'Шейдер', + shatter: 'Раскалывание', + text: 'Текст', + webgl: 'WebGL', }; export const RU_PROMPT_TEMPLATE_COPY: Record>> = { diff --git a/apps/web/src/i18n/content.ts b/apps/web/src/i18n/content.ts index 5327a3088..bdbce69dc 100644 --- a/apps/web/src/i18n/content.ts +++ b/apps/web/src/i18n/content.ts @@ -536,9 +536,19 @@ const DE_PROMPT_TEMPLATE_CATEGORIES: Record = { 'Short Form': 'Short Form', Travel: 'Reise', 'Live Artifact': 'Live-Artefakt', + 'VFX / HTML-in-Canvas': 'VFX / HTML-in-Canvas', }; -const DE_PROMPT_TEMPLATE_IDS_WITH_EN_FALLBACK = ['notion-team-dashboard-live-artifact'] as const; +const DE_PROMPT_TEMPLATE_IDS_WITH_EN_FALLBACK = [ + 'notion-team-dashboard-live-artifact', + 'hyperframes-html-in-canvas-iphone-device', + 'hyperframes-html-in-canvas-liquid-background', + 'hyperframes-html-in-canvas-liquid-glass', + 'hyperframes-html-in-canvas-magnetic', + 'hyperframes-html-in-canvas-portal-reveal', + 'hyperframes-html-in-canvas-shatter', + 'hyperframes-html-in-canvas-text-cursor', +] as const; const DE_PROMPT_TEMPLATE_TAGS: Record = { '3d': '3D', @@ -639,6 +649,25 @@ const DE_PROMPT_TEMPLATE_TAGS: Record = { 'website-to-video': 'Website-zu-Video', wuxia: 'Wuxia', zhaoyun: 'Zhaoyun', + dashboard: 'Dashboard', + data: 'Daten', + destruction: 'Zerstörung', + displacement: 'Displacement', + hero: 'Hero', + 'html-in-canvas': 'HTML-in-Canvas', + iphone: 'iPhone', + keynote: 'Keynote', + liquid: 'Liquid', + 'liquid-glass': 'Liquid Glass', + macbook: 'MacBook', + magnetic: 'Magnetic', + particles: 'Partikel', + portal: 'Portal', + 'product-demo': 'Produkt-Demo', + shader: 'Shader', + shatter: 'Shatter', + text: 'Text', + webgl: 'WebGL', }; const DE_PROMPT_TEMPLATE_COPY: Record = { diff --git a/apps/web/src/i18n/locales/ar.ts b/apps/web/src/i18n/locales/ar.ts index 00556e32c..1d63da50f 100644 --- a/apps/web/src/i18n/locales/ar.ts +++ b/apps/web/src/i18n/locales/ar.ts @@ -629,8 +629,7 @@ export const ar: Dict = { 'designFiles.upload': 'رفع ملفات', 'designFiles.pasteText': 'لصق كملف نصي', 'designFiles.newSketch': 'رسم جديد', - 'designFiles.empty': - 'لا يوجد شيء هنا بعد. اسحب الملفات أدناه، أو أنشئ رسماً / الصق نصاً.', + 'designFiles.empty': 'ستظهر الإبداعات هنا', 'designFiles.refresh': 'تحديث', 'designFiles.delete': 'حذف', 'designFiles.deleteSelected': 'حذف {n}', diff --git a/apps/web/src/i18n/locales/de.ts b/apps/web/src/i18n/locales/de.ts index 3921c372f..221197ff4 100644 --- a/apps/web/src/i18n/locales/de.ts +++ b/apps/web/src/i18n/locales/de.ts @@ -517,8 +517,7 @@ export const de: Dict = { 'designFiles.upload': 'Dateien hochladen', 'designFiles.pasteText': 'Als Textdatei einfügen', 'designFiles.newSketch': 'Neuer Sketch', - 'designFiles.empty': - 'Noch nichts hier. Legen Sie unten Dateien ab oder erstellen Sie einen Sketch / fügen Sie Text ein.', + 'designFiles.empty': 'Kreationen erscheinen hier', 'designFiles.refresh': 'Aktualisieren', 'designFiles.delete': 'Löschen', 'designFiles.searchPlaceholder': 'Dateien suchen…', diff --git a/apps/web/src/i18n/locales/en.ts b/apps/web/src/i18n/locales/en.ts index d94634ae2..1eebb86dc 100644 --- a/apps/web/src/i18n/locales/en.ts +++ b/apps/web/src/i18n/locales/en.ts @@ -696,8 +696,7 @@ export const en: Dict = { 'designFiles.upload': 'Upload files', 'designFiles.pasteText': 'Paste as text file', 'designFiles.newSketch': 'New sketch', - 'designFiles.empty': - 'Nothing here yet. Drop files below, or create a sketch / paste text.', + 'designFiles.empty': 'Creations will appear here', 'designFiles.refresh': 'Refresh', 'designFiles.delete': 'Delete', 'designFiles.searchPlaceholder': 'Search files…', diff --git a/apps/web/src/i18n/locales/es-ES.ts b/apps/web/src/i18n/locales/es-ES.ts index ad571e919..1906e4ead 100644 --- a/apps/web/src/i18n/locales/es-ES.ts +++ b/apps/web/src/i18n/locales/es-ES.ts @@ -518,8 +518,7 @@ export const esES: Dict = { 'designFiles.upload': 'Subir archivos', 'designFiles.pasteText': 'Pegar como archivo de texto', 'designFiles.newSketch': 'Nuevo boceto', - 'designFiles.empty': - 'Aquí no hay nada todavía. Suelta archivos abajo, o crea un boceto / pega texto.', + 'designFiles.empty': 'Las creaciones aparecerán aquí', 'designFiles.refresh': 'Actualizar', 'designFiles.delete': 'Eliminar', 'designFiles.searchPlaceholder': 'Buscar archivos…', diff --git a/apps/web/src/i18n/locales/fa.ts b/apps/web/src/i18n/locales/fa.ts index f71dcd80d..49de25e08 100644 --- a/apps/web/src/i18n/locales/fa.ts +++ b/apps/web/src/i18n/locales/fa.ts @@ -642,8 +642,7 @@ export const fa: Dict = { 'designFiles.upload': 'آپلود فایل‌ها', 'designFiles.pasteText': 'چسباندن به عنوان فایل متنی', 'designFiles.newSketch': 'طرح جدید', - 'designFiles.empty': - 'هنوز هیچ چیزی اینجا نیست. فایل‌ها را رها کنید، یا یک طرح ایجاد کنید / متن بچسبانید.', + 'designFiles.empty': 'آفرینش‌ها اینجا نمایش داده می‌شوند', 'designFiles.refresh': 'بازنشانی', 'designFiles.delete': 'حذف', 'designFiles.deleteSelected': 'حذف {n}', diff --git a/apps/web/src/i18n/locales/fr.ts b/apps/web/src/i18n/locales/fr.ts index b373dc8cc..9d865f5be 100644 --- a/apps/web/src/i18n/locales/fr.ts +++ b/apps/web/src/i18n/locales/fr.ts @@ -629,8 +629,7 @@ export const fr: Dict = { 'designFiles.upload': 'Téléverser des fichiers', 'designFiles.pasteText': 'Coller comme fichier texte', 'designFiles.newSketch': 'Nouveau croquis', - 'designFiles.empty': - 'Rien ici pour l\'instant. Déposez des fichiers ci-dessous, ou créez un croquis / collez du texte.', + 'designFiles.empty': 'Les créations apparaîtront ici', 'designFiles.refresh': 'Actualiser', 'designFiles.delete': 'Supprimer', 'designFiles.searchPlaceholder': 'Rechercher des fichiers…', diff --git a/apps/web/src/i18n/locales/hu.ts b/apps/web/src/i18n/locales/hu.ts index 7a6d21c56..24188cd62 100644 --- a/apps/web/src/i18n/locales/hu.ts +++ b/apps/web/src/i18n/locales/hu.ts @@ -629,8 +629,7 @@ export const hu: Dict = { 'designFiles.upload': 'Fájlok feltöltése', 'designFiles.pasteText': 'Beillesztés szövegfájlként', 'designFiles.newSketch': 'Új vázlat', - 'designFiles.empty': - 'Még nincs itt semmi. Húzz be fájlokat, vagy hozz létre vázlatot / illessz be szöveget.', + 'designFiles.empty': 'A kreációk itt jelennek meg', 'designFiles.refresh': 'Frissítés', 'designFiles.delete': 'Törlés', 'designFiles.searchPlaceholder': 'Fájlok keresése…', diff --git a/apps/web/src/i18n/locales/ja.ts b/apps/web/src/i18n/locales/ja.ts index eda7d502c..b39278d43 100644 --- a/apps/web/src/i18n/locales/ja.ts +++ b/apps/web/src/i18n/locales/ja.ts @@ -516,8 +516,7 @@ export const ja: Dict = { 'designFiles.upload': 'ファイルをアップロード', 'designFiles.pasteText': 'テキストファイルとして貼り付け', 'designFiles.newSketch': '新しいスケッチ', - 'designFiles.empty': - 'まだ何もありません。以下にファイルをドロップするか、スケッチを作成するかテキストを貼り付けてください。', + 'designFiles.empty': 'ここに作品が表示されます', 'designFiles.refresh': '更新', 'designFiles.delete': '削除', 'designFiles.searchPlaceholder': 'ファイルを検索…', diff --git a/apps/web/src/i18n/locales/ko.ts b/apps/web/src/i18n/locales/ko.ts index ff16c472b..82d0d0193 100644 --- a/apps/web/src/i18n/locales/ko.ts +++ b/apps/web/src/i18n/locales/ko.ts @@ -629,8 +629,7 @@ export const ko: Dict = { 'designFiles.upload': '파일 업로드', 'designFiles.pasteText': '텍스트 파일로 붙여넣기', 'designFiles.newSketch': '새 스케치', - 'designFiles.empty': - '아직 파일이 없습니다. 여기에 파일을 끌어다 놓거나, 스케치를 생성하거나, 텍스트를 붙여넣으세요.', + 'designFiles.empty': '여기에 작업물이 표시됩니다', 'designFiles.refresh': '새로고침', 'designFiles.delete': '삭제', 'designFiles.searchPlaceholder': '파일 검색…', diff --git a/apps/web/src/i18n/locales/pl.ts b/apps/web/src/i18n/locales/pl.ts index bb9a86da8..6243c32ba 100644 --- a/apps/web/src/i18n/locales/pl.ts +++ b/apps/web/src/i18n/locales/pl.ts @@ -629,8 +629,7 @@ export const pl: Dict = { 'designFiles.upload': 'Prześlij pliki', 'designFiles.pasteText': 'Wklej jako plik tekstowy', 'designFiles.newSketch': 'Nowy szkic', - 'designFiles.empty': - 'Nic tu jeszcze nie ma. Przeciągnij pliki poniżej lub utwórz szkic / wklej tekst.', + 'designFiles.empty': 'Twoje kreacje pojawią się tutaj', 'designFiles.refresh': 'Odśwież', 'designFiles.delete': 'Usuń', 'designFiles.searchPlaceholder': 'Szukaj plików…', diff --git a/apps/web/src/i18n/locales/pt-BR.ts b/apps/web/src/i18n/locales/pt-BR.ts index 3389faf9e..65f31d7e8 100644 --- a/apps/web/src/i18n/locales/pt-BR.ts +++ b/apps/web/src/i18n/locales/pt-BR.ts @@ -641,8 +641,7 @@ export const ptBR: Dict = { 'designFiles.upload': 'Enviar arquivos', 'designFiles.pasteText': 'Colar como arquivo de texto', 'designFiles.newSketch': 'Novo esboço', - 'designFiles.empty': - 'Nada aqui ainda. Arraste arquivos abaixo, crie um esboço ou cole texto.', + 'designFiles.empty': 'As criações aparecerão aqui', 'designFiles.refresh': 'Atualizar', 'designFiles.delete': 'Excluir', 'designFiles.searchPlaceholder': 'Buscar arquivos…', diff --git a/apps/web/src/i18n/locales/ru.ts b/apps/web/src/i18n/locales/ru.ts index c4184d4c0..ed10500b9 100644 --- a/apps/web/src/i18n/locales/ru.ts +++ b/apps/web/src/i18n/locales/ru.ts @@ -641,8 +641,7 @@ export const ru: Dict = { 'designFiles.upload': 'Загрузить файлы', 'designFiles.pasteText': 'Вставить как текстовый файл', 'designFiles.newSketch': 'Новый эскиз', - 'designFiles.empty': - 'Здесь пока ничего нет. Перетащите файлы ниже или создайте эскиз / вставьте текст.', + 'designFiles.empty': 'Здесь появятся ваши работы', 'designFiles.refresh': 'Обновить', 'designFiles.delete': 'Удалить', 'designFiles.searchPlaceholder': 'Поиск файлов…', diff --git a/apps/web/src/i18n/locales/tr.ts b/apps/web/src/i18n/locales/tr.ts index 7953b5527..c06dd14c4 100644 --- a/apps/web/src/i18n/locales/tr.ts +++ b/apps/web/src/i18n/locales/tr.ts @@ -620,8 +620,7 @@ export const tr: Dict = { 'designFiles.upload': 'Dosyaları yükle', 'designFiles.pasteText': 'Metin dosyası olarak yapıştır', 'designFiles.newSketch': 'Yeni taslak', - 'designFiles.empty': - 'Burada henüz bir şey yok. Dosyaları aşağı sürükleyin, veya bir taslak oluşturun / metin yapıştırın.', + 'designFiles.empty': 'Eserler burada görünecek', 'designFiles.refresh': 'Yenile', 'designFiles.delete': 'Sil', 'designFiles.searchPlaceholder': 'Dosyaları ara…', diff --git a/apps/web/src/i18n/locales/uk.ts b/apps/web/src/i18n/locales/uk.ts index 4457a23e6..b0dbc453a 100644 --- a/apps/web/src/i18n/locales/uk.ts +++ b/apps/web/src/i18n/locales/uk.ts @@ -642,8 +642,7 @@ export const uk: Dict = { 'designFiles.upload': 'Завантажити файли', 'designFiles.pasteText': 'Вставити як текстовий файл', 'designFiles.newSketch': 'Новий ескіз', - 'designFiles.empty': - 'Тут нічого немає. Перенесіть файли нижче, або створіть ескіз / вставте текст.', + 'designFiles.empty': 'Тут з\'являться ваші роботи', 'designFiles.refresh': 'Оновити', 'designFiles.delete': 'Видалити', 'designFiles.searchPlaceholder': 'Пошук файлів…', diff --git a/apps/web/src/i18n/locales/zh-CN.ts b/apps/web/src/i18n/locales/zh-CN.ts index a53687137..1431fca08 100644 --- a/apps/web/src/i18n/locales/zh-CN.ts +++ b/apps/web/src/i18n/locales/zh-CN.ts @@ -687,7 +687,7 @@ export const zhCN: Dict = { 'designFiles.upload': '上传文件', 'designFiles.pasteText': '粘贴为文本文件', 'designFiles.newSketch': '新建草图', - 'designFiles.empty': '这里还没有文件。可以拖拽下方区域,或新建草图、粘贴文本。', + 'designFiles.empty': '生成的设计会出现在这里', 'designFiles.refresh': '刷新', 'designFiles.delete': '删除', 'designFiles.searchPlaceholder': '搜索文件…', diff --git a/apps/web/src/i18n/locales/zh-TW.ts b/apps/web/src/i18n/locales/zh-TW.ts index 14930ee2a..86c49ace7 100644 --- a/apps/web/src/i18n/locales/zh-TW.ts +++ b/apps/web/src/i18n/locales/zh-TW.ts @@ -680,7 +680,7 @@ export const zhTW: Dict = { 'designFiles.upload': '上傳圖片', 'designFiles.pasteText': '貼上為文字檔案', 'designFiles.newSketch': '新建草圖', - 'designFiles.empty': '這裡還沒有檔案。可以拖曳下方區域,或新建草圖、貼上文字。', + 'designFiles.empty': '生成的設計會出現在這裡', 'designFiles.refresh': '重新整理', 'designFiles.delete': '刪除', 'designFiles.searchPlaceholder': '搜尋檔案…', diff --git a/apps/web/src/index.css b/apps/web/src/index.css index 6ff65f989..756887b37 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -501,29 +501,40 @@ code { } .app-project-title { display: flex; - flex-direction: column; - gap: 1px; + flex-direction: row; + align-items: baseline; + gap: 0; min-width: 0; - max-height: 32px; + max-height: 22px; overflow: hidden; } .app-project-title .title { color: var(--text-strong); font-size: 13.5px; - line-height: 16px; + line-height: 18px; font-weight: 600; letter-spacing: -0.01em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + flex: 0 1 auto; + min-width: 0; } .app-project-title .meta { color: var(--text-muted); font-size: 11.5px; - line-height: 15px; + line-height: 18px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + flex: 0 1 auto; + min-width: 0; +} +.app-project-title .meta::before { + content: '·'; + margin: 0 8px; + color: var(--text-muted); + opacity: 0.55; } .topbar { @@ -7254,6 +7265,8 @@ button.connector-action.is-loading { min-height: 0; overflow-y: auto; padding: 12px 0 0; + display: flex; + flex-direction: column; } .df-group-toggle { margin: 0 20px 8px; @@ -7667,10 +7680,67 @@ button.connector-action.is-loading { cursor: pointer; } .df-empty { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; padding: 48px 24px; - text-align: center; color: var(--text-muted); font-size: 13px; + min-height: 320px; + background-color: var(--bg); + background-image: + linear-gradient(to right, var(--border-soft) 1px, transparent 1px), + linear-gradient(to bottom, var(--border-soft) 1px, transparent 1px); + background-size: 24px 24px; + background-position: -1px -1px; +} +.df-empty-pill { + display: inline-flex; + flex-direction: column; + align-items: center; + gap: 14px; + padding: 28px 36px; + border-radius: 24px; + border: 1px solid var(--border); + background: var(--bg-panel); + box-shadow: + 0 1px 0 rgba(15, 15, 15, 0.02), + 0 8px 24px -12px rgba(15, 15, 15, 0.08); + max-width: min(420px, 92%); + text-align: center; +} +.df-empty-title { + font-size: 16px; + color: var(--text-strong); + font-weight: 500; + letter-spacing: -0.005em; + line-height: 1.3; +} +.df-empty-cta { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 7px 14px; + border: 1px solid var(--border); + border-radius: 999px; + background: var(--bg); + color: var(--text); + font: inherit; + font-size: 12.5px; + font-weight: 500; + cursor: pointer; + flex-shrink: 0; + transition: background 120ms ease, border-color 120ms ease; +} +.df-empty-cta:hover { + background: var(--bg-subtle); + border-color: var(--border-strong); +} +.df-empty-cta:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; } .df-drop { diff --git a/apps/web/tests/components/NewProjectPanel.test.tsx b/apps/web/tests/components/NewProjectPanel.test.tsx index b81f00013..f98a1a75c 100644 --- a/apps/web/tests/components/NewProjectPanel.test.tsx +++ b/apps/web/tests/components/NewProjectPanel.test.tsx @@ -446,4 +446,76 @@ describe('NewProjectPanel design system defaults', () => { }), ); }); + + it('pins skillId to hyperframes when the video model is hyperframes-html, regardless of skill discovery order', () => { + // Reproduces PR #866 mrcfps's reported regression: when daemon `readdir()` + // returns video skills in an order that puts `video-shortform` ahead of + // `hyperframes`, the previous `list[0]?.id` fallback would route the + // HyperFrames-HTML model through `video-shortform`, dropping the + // hyperframes SKILL body and the html-in-canvas preflight. The fix forces + // the create-time skillId to `hyperframes` whenever `hyperframes-html` is + // the chosen model. + const onCreate = vi.fn(); + const videoSkills: SkillSummary[] = [ + { + id: 'video-shortform', + name: 'Video shortform', + description: 'Shortform video skill', + mode: 'video', + surface: 'video', + previewType: 'video', + designSystemRequired: false, + defaultFor: [], + triggers: [], + upstream: null, + hasBody: true, + examplePrompt: '', + aggregatesExamples: false, + }, + { + id: 'hyperframes', + name: 'HyperFrames', + description: 'HTML-in-canvas video', + mode: 'video', + surface: 'video', + previewType: 'video', + designSystemRequired: false, + defaultFor: [], + triggers: [], + upstream: null, + hasBody: true, + examplePrompt: '', + aggregatesExamples: false, + }, + ]; + + render( + , + ); + + fireEvent.click(screen.getByRole('tab', { name: 'Video' })); + fireEvent.click(screen.getByRole('button', { name: /hyperframes-html/i })); + fireEvent.change(screen.getByTestId('new-project-name'), { + target: { value: 'HyperFrames routing' }, + }); + fireEvent.click(screen.getByTestId('create-project')); + + expect(onCreate).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'HyperFrames routing', + skillId: 'hyperframes', + metadata: expect.objectContaining({ + kind: 'video', + videoModel: 'hyperframes-html', + }), + }), + ); + }); }); diff --git a/packages/contracts/src/prompts/system.ts b/packages/contracts/src/prompts/system.ts index f734d1f84..b1931567e 100644 --- a/packages/contracts/src/prompts/system.ts +++ b/packages/contracts/src/prompts/system.ts @@ -358,6 +358,9 @@ function derivePreflight(skillBody: string): string { if (/references\/refresh-contract\.md|refresh-contract\.md/.test(skillBody)) { refs.push('`references/refresh-contract.md`'); } + if (/references\/html-in-canvas\.md|html-in-canvas\.md/.test(skillBody)) { + refs.push('`references/html-in-canvas.md`'); + } if (refs.length === 0) return ''; return ` **Pre-flight (do this before any other tool):** Read ${refs.join(', ')} via the path written in the skill-root preamble. If the skill asks for daemon wrapper commands, use the runtime tool environment documented below; it provides the daemon URL and whether a run-scoped tool token is available without exposing token internals. The seed template defines the class system you'll paste into; the layouts file is the only acceptable source of section/screen/slide skeletons; the checklist and live-artifact references are your validation gate before emitting \`\` or registering a live artifact. Skipping this step is the #1 reason output regresses to generic AI-slop.`; } diff --git a/prompt-templates/video/hyperframes-html-in-canvas-iphone-device.json b/prompt-templates/video/hyperframes-html-in-canvas-iphone-device.json new file mode 100644 index 000000000..8dabd03c1 --- /dev/null +++ b/prompt-templates/video/hyperframes-html-in-canvas-iphone-device.json @@ -0,0 +1,19 @@ +{ + "id": "hyperframes-html-in-canvas-iphone-device", + "surface": "video", + "title": "HyperFrames HTML-in-Canvas: 3D iPhone + MacBook Product Demo", + "summary": "A 15-second product demo where a real GLTF iPhone 15 Pro Max and MacBook Pro float on a clean stage with the actual app UI rendering live on their screens via drawElementImage. Morphing glass lens flare + 360° turntable. Built on the vfx-iphone-device catalog block.", + "category": "VFX / HTML-in-Canvas", + "tags": ["hyperframes", "html-in-canvas", "3d", "iphone", "macbook", "product-demo"], + "model": "hyperframes-html", + "aspect": "16:9", + "prompt": "Build a 15-second HyperFrames composition (1920×1080, 30fps) titled \"device-product-demo\". Pull the catalog block first: `npx hyperframes add vfx-iphone-device`. The block ships the iPhone and MacBook GLTFs plus screen-content placeholders.\n\nVisual identity: ultra-clean studio canvas with a vertical gradient #f7f8fb → #eaecf2, single primary brand color the user supplies (default #4f46e5), muted ink #0f172a for type. Display face: \"Söhne Halbfett\" 96px for the headline; body \"Inter Tight\" 28px (for video sizing); mono \"JetBrains Mono\" 20px on UI bits. Subtle floor-shadow blob beneath each device.\n\nReplace the block's two screen placeholders with the user's actual product surfaces:\n• `mobile-screen` — a `` containing the real iOS app UI as DOM (status bar, large title, primary list, bottom tab bar). Use real semantic HTML and CSS so the captured texture is pixel-perfect.\n• `desktop-screen` — a `` containing the macOS app UI (sidebar, main pane, command palette overlay).\n\nTimeline (paused: true, registered window.__timelines.main, data-duration=15):\n\n0.0–4.5s ENTRY — both devices fly in: iPhone from camera-left (x:-600, rotateY:35°→0°, scale 0.85→1.0, ease expo.out 1.1s), MacBook from camera-right with a 0.25s stagger (x:+800, rotateY:-28°→0°, ease expo.out 1.2s). Headline \"Your work. Anywhere.\" types in via `gsap.from(\".headline\", { y: 60, opacity: 0, duration: 0.7, ease: 'power3.out' }, 1.0)`. Subhead at 1.6s.\n\n4.5–10.5s TURNTABLE — both devices rotate slowly +18° on Y in lock-step (ease sine.inOut). Re-blit `drawElementImage` every frame so the captured screen content stays sharp under the moving normal map. Add the morphing glass-lens flare overlay (block-supplied) sweeping camera-left to camera-right between 6.0s–9.5s. At 7.5s, animate one in-app interaction inside `mobile-screen` (a list item slides in, then a checkmark scales 0→1) — the texture re-capture picks this up automatically.\n\n10.5–15.0s OUTRO — devices ease back to neutral (rotateY 0°, x 0). Headline morphs to a single CTA line \"download today\" with marker-sweep highlight using `references/css-patterns.md`. Final 0.6s holds the hero frame — no opacity-to-0 exits.\n\nFeature detection: gracefully fall back to flat 2D screenshots on browsers without `drawElementImage`. The renderer enables the flag automatically; Studio preview without it must not be black.\n\nNon-negotiables: GLTF assets stay at the block's default paths (`models/iphone.glb`, `models/macbook.glb`); only swap the screen DOM content, not the model URIs. Deterministic — no Math.random / Date.now, no `repeat: -1`. Run `npx hyperframes lint` and `npx hyperframes inspect --samples 10 --at 1,3,5,7.5,10,13` before render. Output: `device-product-demo.mp4`.", + "previewImageUrl": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/vfx-iphone-device.png", + "previewVideoUrl": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/vfx-iphone-device.mp4", + "source": { + "repo": "heygen-com/hyperframes", + "license": "Apache-2.0", + "author": "HeyGen", + "url": "https://hyperframes.heygen.com/catalog/blocks/vfx-iphone-device" + } +} diff --git a/prompt-templates/video/hyperframes-html-in-canvas-liquid-background.json b/prompt-templates/video/hyperframes-html-in-canvas-liquid-background.json new file mode 100644 index 000000000..e081f70f3 --- /dev/null +++ b/prompt-templates/video/hyperframes-html-in-canvas-liquid-background.json @@ -0,0 +1,19 @@ +{ + "id": "hyperframes-html-in-canvas-liquid-background", + "surface": "video", + "title": "HyperFrames HTML-in-Canvas: Liquid Background Hero", + "summary": "A 12-second hero with HTML content floating above an organic liquid surface — vertex-displaced subdivided plane, real-time wave dynamics, captured DOM rides on top crisp and readable. Built on the vfx-liquid-background catalog block.", + "category": "VFX / HTML-in-Canvas", + "tags": ["hyperframes", "html-in-canvas", "liquid", "displacement", "hero"], + "model": "hyperframes-html", + "aspect": "16:9", + "prompt": "Build a 12-second HyperFrames composition (1920×1080, 30fps) titled \"liquid-background-hero\". Pull the catalog block first: `npx hyperframes add vfx-liquid-background`.\n\nVisual identity: oceanic canvas — base #0a1f2c flowing into accent #1cd6c4 and secondary #4a7dff for the liquid surface, off-white type #f6fbff for the floating HTML. Display face: \"PP Right Grotesk\" Compact Bold 156px; body \"PP Right Grotesk\" Regular 26px; numerals tabular. The liquid is the painting; the type is the subtitle.\n\nLayer A — `liquid` (background): the vfx-liquid-background WebGL canvas with the block's default subdivided plane, vertex displacement amplitude 0.18, wave speed 0.42, two superposed wave functions (period 1.6s and 4.2s) for organic motion. Color gradient flows base → accent → secondary across the surface, deterministic phase from `seed = mulberry32(2046)`.\n\nLayer B — `hero-content`: a `` containing centered DOM — a single hero headline (\"every drop, accounted for\"), a one-line subhead (\"realtime water analytics across 412 sensors\"), a small numeric ticker (\"412\" tabular-nums) that increments slightly over the composition. Captured every frame and composited above the liquid layer with mix-blend-mode normal at z-index 2.\n\nTimeline (paused: true, window.__timelines.main, data-duration=12):\n\n0.0–2.0s ENTRY — liquid plane starts flat (displacement amplitude 0 → 0.18 over 1.8s ease expo.out), color flow ramps from monochrome → full gradient over 1.6s. Hero content fades in at 0.6s (`gsap.from('.headline', { y: 50, opacity: 0, duration: 0.8, ease: 'power3.out' })`, subhead at 1.0s with y:30, ticker at 1.4s scale 0.9 → 1.0).\n\n2.0–9.5s FLOAT — full motion. Liquid waves continue indefinitely-but-finitely (calculate exact repeat count from data-duration / cycle so the timeline never uses `repeat: -1`). Ticker counts 412 → 437 over 7.5s ease none, tabular-nums on every digit. At 5.0s, a gentle highlight sweep crosses the hero headline (use `references/css-patterns.md` marker sweep). At 7.0s, a soft caustic flicker on the liquid layer (deterministic seeded brightness pulse).\n\n9.5–12.0s SETTLE — wave amplitude tapers 0.18 → 0.10, color flow slows. Hero content holds. Final 0.6s holds the hero frame; no opacity-to-0 exits except optionally a 0.3s liquid-only opacity fade if the user is chaining it before another scene.\n\nFeature detection: if `drawElementImage` is unavailable, layer the hero DOM directly on a static gradient backdrop (#0a1f2c → #1cd6c4 radial-gradient) without the liquid shader. Never show a black canvas.\n\nNon-negotiables: deterministic seeds, no `repeat: -1` (use Math.ceil-based finite repeats), no async timeline construction, min font 60px on headline, 24px on subhead, tabular-nums on the ticker. The liquid timeline and the hero-content timeline both register on `window.__timelines.main` — do not split into sub-compositions unless the user asks for it. Run `npx hyperframes lint` and `npx hyperframes inspect --samples 10 --at 0.5,2,4,6,8,11` before render. Output: `liquid-background-hero.mp4`.", + "previewImageUrl": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/vfx-liquid-background.png", + "previewVideoUrl": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/vfx-liquid-background.mp4", + "source": { + "repo": "heygen-com/hyperframes", + "license": "Apache-2.0", + "author": "HeyGen", + "url": "https://hyperframes.heygen.com/catalog/blocks/vfx-liquid-background" + } +} diff --git a/prompt-templates/video/hyperframes-html-in-canvas-liquid-glass.json b/prompt-templates/video/hyperframes-html-in-canvas-liquid-glass.json new file mode 100644 index 000000000..eb69c33ae --- /dev/null +++ b/prompt-templates/video/hyperframes-html-in-canvas-liquid-glass.json @@ -0,0 +1,19 @@ +{ + "id": "hyperframes-html-in-canvas-liquid-glass", + "surface": "video", + "title": "HyperFrames HTML-in-Canvas: Liquid Glass Landing Reveal", + "summary": "A 20-second voronoi liquid-glass reveal of a real product landing page — DOM is captured live via drawElementImage, shattered into refracting glass cells, then settles into a clean hero shot. Built on the vfx-liquid-glass catalog block.", + "category": "VFX / HTML-in-Canvas", + "tags": ["hyperframes", "html-in-canvas", "liquid-glass", "webgl", "product-promo"], + "model": "hyperframes-html", + "aspect": "16:9", + "prompt": "Build a 20-second HyperFrames composition (1920×1080, 30fps) titled \"liquid-glass-landing-reveal\". Pull the catalog block first: `npx hyperframes add vfx-liquid-glass` (or `npx hyperframes add html-in-canvas` to grab the full set).\n\nVisual identity: deep-space canvas #0a0d14, single warm accent #ff8a4c for refraction tint, secondary cool #6cf3c0 for chromatic edge highlights, off-white text #f5f7fa. Display face: \"Migra\" 140px for the headline; body \"Söhne\" 24px; mono \"JetBrains Mono\" 18px. Tabular-nums on any numerals.\n\nTwo nested compositions:\n\n1. `landing-source` — a `` containing the actual product landing-page DOM: a tall hero (\"Ship faster. Stay calm.\"), a 3-column feature grid with rounded cards using `backdrop-filter: blur(24px)`, a soft radial accent glow behind the headline, and a CTA pill (\"start free\"). Real CSS, real web fonts — this gets captured live by the WebGL pass.\n2. `theater` — the WebGL canvas that runs the vfx-liquid-glass shader on the captured texture.\n\nTimeline (single root composition, paused: true, registered as window.__timelines.main, data-duration=20):\n\n0.0–6.0s ENTRY — voronoi cells start fully fractured at scale 1.6, opacity 0.2, with strong chromatic aberration (0.04 offset). Easing: expo.out. Cells coalesce inward, refraction strength tapers from 1.0 → 0.18, parallax reveals the underlying landing-page shot.\n\n6.0–13.5s SETTLE — full landing page resolves crisp behind a thin glass sheen. Add a slow lateral parallax (drift the texture x by 24px over 7s, ease sine.inOut). Tween a subtle highlight sweep across the hero using the `shimmer-sweep` component at 9.0s.\n\n13.5–20.0s OUTRO — gentle re-fracture: refraction strength 0.18 → 0.42, voronoi scale 1.0 → 1.08, slight rotation 0 → -2deg, opacity hold. Final 0.6s fades the entire theater canvas to black via the root container only — never animate the source DOM out.\n\nFeature detection (mandatory): inside the theater script tag, before instantiating Three.js, check `('layoutSubtree' in document.createElement('canvas')) && typeof CanvasRenderingContext2D.prototype.drawElementImage === 'function'`. If false, hide the theater canvas and let the source DOM display directly — the rendered MP4 always has the flag enabled, but Studio preview without it must not show black.\n\nNon-negotiables: deterministic motion (seeded mulberry32 if any noise needed), no Math.random / Date.now, no `repeat: -1`, no async timeline construction. All entrances via `gsap.from()` against the hero CSS layout. Run `npx hyperframes lint` and `npx hyperframes inspect --samples 12 --at 2,4,6,9,13,17` before render. Output: `liquid-glass-landing-reveal.mp4`.", + "previewImageUrl": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/vfx-liquid-glass.png", + "previewVideoUrl": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/vfx-liquid-glass.mp4", + "source": { + "repo": "heygen-com/hyperframes", + "license": "Apache-2.0", + "author": "HeyGen", + "url": "https://hyperframes.heygen.com/catalog/blocks/vfx-liquid-glass" + } +} diff --git a/prompt-templates/video/hyperframes-html-in-canvas-magnetic.json b/prompt-templates/video/hyperframes-html-in-canvas-magnetic.json new file mode 100644 index 000000000..96100d720 --- /dev/null +++ b/prompt-templates/video/hyperframes-html-in-canvas-magnetic.json @@ -0,0 +1,19 @@ +{ + "id": "hyperframes-html-in-canvas-magnetic", + "surface": "video", + "title": "HyperFrames HTML-in-Canvas: Magnetic Field Visualisation", + "summary": "A 15-second magnetic-field particle visualisation reacting to a live DOM heatmap or chart — particles trace field lines that bend around the captured HTML, ideal for ML/data products. Built on the vfx-magnetic catalog block.", + "category": "VFX / HTML-in-Canvas", + "tags": ["hyperframes", "html-in-canvas", "magnetic", "particles", "data"], + "model": "hyperframes-html", + "aspect": "16:9", + "prompt": "Build a 15-second HyperFrames composition (1920×1080, 30fps) titled \"magnetic-field-data\". Pull the catalog block first: `npx hyperframes add vfx-magnetic`.\n\nVisual identity: graphite canvas #0a0c11, single primary particle color #ff6a00 with a cool secondary #38e6ff for opposing-pole particles, off-white text #eef2f7. Display face: \"PP Neue Machina\" 124px Plain Ultrabold; body \"PP Mondwest\" Regular 24px; mono \"Berkeley Mono\" 18px on labels. Tabular-nums on every numeral.\n\nLayer A — `field-source`: a `` containing real DOM that drives the magnetic field — a centered chart card (760×520px) with a live SVG line chart (12 data points, animated path), two KPI labels above (\"signal strength\" 0.84 and \"coverage\" 92%), and a small mono caption beneath (\"polling 412 nodes · realtime\"). The chart card acts as the magnetic source — particles repel from its bounds, attract toward dipole anchors at the chart's two endpoints.\n\nLayer B — `theater`: WebGL canvas running the vfx-magnetic shader. Particle count 1800, deterministic spawn seeded mulberry32(91347), spawn distribution rim-biased so the field lines read clearly. Two opposing dipoles at the chart's first and last data points sample the captured DOM bounds every frame so the field re-flows when the chart line animates.\n\nTimeline (paused: true, window.__timelines.main, data-duration=15):\n\n0.0–2.4s ENTRY — particles spawn invisible, opacity ramps 0 → 0.85 over 2.0s ease expo.out. Field strength 0 → 1.0 ease power3.out 1.6s. Chart card fades in at 0.4s (`gsap.from('.chart-card', { y: 60, opacity: 0, scale: 0.96, duration: 0.9, ease: 'power3.out' })`). KPI labels stagger in at 0.9s and 1.2s (200ms apart, y:30 → 0).\n\n2.4–10.0s FLOW — particles trace field lines. Chart line animates over 6.0s (`gsap.from('.chart-line', { strokeDashoffset: pathLength, duration: 6.0, ease: 'sine.inOut' })`); the captured-DOM resampling means particles bend around the moving chart line in real time. KPI numerals tick: signal strength 0.84 → 0.91, coverage 92% → 96%, both ease none. At 6.0s, brief field-strength pulse from 1.0 → 1.4 → 1.0 over 0.5s; particles speed up correspondingly.\n\n10.0–13.5s STATEMENT — headline appears (\"see the field, not the dots\") with marker-sweep highlight from `references/css-patterns.md` at 10.4s. Particles dim slightly (opacity 0.85 → 0.55) so the headline reads cleanly above them.\n\n13.5–15.0s SETTLE — field strength tapers 1.0 → 0.6, particle speed halves, ambient drift only. Final 0.6s holds the hero frame. No opacity-to-0 exits.\n\nFeature detection: if `drawElementImage` is unavailable, render the chart card DOM directly on a flat #0a0c11 background without the particle field. Studio preview must never go black.\n\nNon-negotiables: deterministic seeds, finite repeats only, no Math.random / Date.now, no `
` in headline (let it wrap with `max-width`). Particle position update runs inside the same window.__timelines.main timeline ticker — don't spin off requestAnimationFrame outside the framework's clock. Run `npx hyperframes lint` and `npx hyperframes inspect --samples 12 --at 1,2.5,4,6,8,11,13.5` before render. Output: `magnetic-field-data.mp4`.", + "previewImageUrl": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/vfx-magnetic.png", + "previewVideoUrl": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/vfx-magnetic.mp4", + "source": { + "repo": "heygen-com/hyperframes", + "license": "Apache-2.0", + "author": "HeyGen", + "url": "https://hyperframes.heygen.com/catalog/blocks/vfx-magnetic" + } +} diff --git a/prompt-templates/video/hyperframes-html-in-canvas-portal-reveal.json b/prompt-templates/video/hyperframes-html-in-canvas-portal-reveal.json new file mode 100644 index 000000000..19d7695a1 --- /dev/null +++ b/prompt-templates/video/hyperframes-html-in-canvas-portal-reveal.json @@ -0,0 +1,19 @@ +{ + "id": "hyperframes-html-in-canvas-portal-reveal", + "surface": "video", + "title": "HyperFrames HTML-in-Canvas: Portal Reveal Dashboard", + "summary": "A 10-second dimensional portal opens onto a live data dashboard — DOM captured in real time, volumetric light spill, portal-edge particles. Built on the vfx-portal catalog block. Designed for keynote-style data hero shots.", + "category": "VFX / HTML-in-Canvas", + "tags": ["hyperframes", "html-in-canvas", "portal", "dashboard", "keynote"], + "model": "hyperframes-html", + "aspect": "16:9", + "prompt": "Build a 10-second HyperFrames composition (1920×1080, 30fps) titled \"portal-reveal-dashboard\". Pull the catalog block first: `npx hyperframes add vfx-portal`.\n\nVisual identity: jet canvas #050609, single electric accent #7df9ff for portal rim and volumetric light, secondary magenta #ff4ecd as chromatic edge tint, off-white text #f5f7fa. Display face: \"PP Editorial New\" 132px italic; body \"GT America\" 24px; mono \"GT America Mono\" 18px tabular-nums on every digit.\n\nTwo nested canvases:\n• `dashboard-source` — a `` containing a real-looking analytics dashboard DOM: top KPI strip (4 stats, large numerals), a primary line chart (animated SVG path, 12 data points), a secondary bar chart, a live counter that increments 1247 → 1392 over the composition. Use actual CSS Grid for layout, `backdrop-filter` on the KPI cards, and tabular-nums on every numeral.\n• `theater` — WebGL canvas running the vfx-portal shader on that captured texture.\n\nTimeline (paused: true, window.__timelines.main, data-duration=10):\n\n0.0–2.4s OPEN — portal radius 0 → 1.0 (ease expo.out 1.4s, slight overshoot 1.05 → 1.0). Volumetric light cone intensity 0 → 1.6 → 1.0. Edge particles spawn count ramps 0 → 240 over 1.6s with deterministic seeded mulberry32(1337). Dashboard texture is captured every frame from 0.6s onward; before that the portal is opaque rim.\n\n2.4–7.6s INSPECT — portal stable at 1.0. Camera pushes in gently (z 0 → -120, ease sine.inOut 5.2s). Inside the source DOM, drive the live counter and chart path animations on the same window.__timelines.main timeline so they sync with the camera push. Add a subtle headline outside the portal (\"the numbers, in real time\") fading in at 3.2s and a single mono kicker (\"+11.6% week over week\") with the css-patterns marker sweep at 5.0s.\n\n7.6–10.0s SETTLE — portal radius 1.0 → 1.04 (gentle breathe), light cone tapers to 0.7 intensity, edge particles settle. Final 0.4s holds the hero. No opacity-to-0 exits — the portal IS the frame.\n\nFeature detection: if `drawElementImage` is unavailable, render the dashboard DOM directly without the portal, with a single text overlay \"open in HyperFrames render for the full effect\". Studio preview must never go black.\n\nNon-negotiables: deterministic seeds, no `repeat: -1`, no async timeline construction, no `
` in content text — let it wrap. Min font 60px on the headline, 36px on KPI numerals, tabular-nums everywhere. Run `npx hyperframes lint` and `npx hyperframes inspect --samples 12 --at 1,2,3,5,7,9` before render. Output: `portal-reveal-dashboard.mp4`.", + "previewImageUrl": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/vfx-portal.png", + "previewVideoUrl": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/vfx-portal.mp4", + "source": { + "repo": "heygen-com/hyperframes", + "license": "Apache-2.0", + "author": "HeyGen", + "url": "https://hyperframes.heygen.com/catalog/blocks/vfx-portal" + } +} diff --git a/prompt-templates/video/hyperframes-html-in-canvas-shatter.json b/prompt-templates/video/hyperframes-html-in-canvas-shatter.json new file mode 100644 index 000000000..71ba786a3 --- /dev/null +++ b/prompt-templates/video/hyperframes-html-in-canvas-shatter.json @@ -0,0 +1,19 @@ +{ + "id": "hyperframes-html-in-canvas-shatter", + "surface": "video", + "title": "HyperFrames HTML-in-Canvas: Glass Shatter Outro", + "summary": "A 12-second HTML shatter outro — a real product page or pricing card holds for a beat, then explodes into refracting glass fragments with depth blur and chromatic dispersion. Built on the vfx-shatter catalog block. Pairs as an end-card after a longer composition.", + "category": "VFX / HTML-in-Canvas", + "tags": ["hyperframes", "html-in-canvas", "shatter", "outro", "destruction"], + "model": "hyperframes-html", + "aspect": "16:9", + "prompt": "Build a 12-second HyperFrames composition (1920×1080, 30fps) titled \"glass-shatter-outro\". Pull the catalog block first: `npx hyperframes add vfx-shatter`.\n\nVisual identity: deep ink canvas #0c0e14, single warm accent #ff5b3a for fragment edges, secondary cool #5cd4ff for chromatic dispersion, off-white type #f5f5f3. Display face: \"PP Telegraf\" Bold 140px on the hero card; body \"PP Telegraf\" 24px. Tabular-nums on the price.\n\nThe composition shatters one piece of UI the user supplies — default: a single pricing card centered on canvas, 760×920px, glass-morphism (rgba(255,255,255,0.06) bg, `backdrop-filter: blur(28px)`, 1px rgba border, 28px radius), with: tier name (\"Pro\"), price (\"$24/mo\" tabular-nums), 4 feature bullets, primary CTA (\"choose Pro\").\n\nSource layer: a `` containing the centered pricing-card DOM on top of a soft #0c0e14 → #14161e radial-gradient backdrop.\n\nTimeline (paused: true, window.__timelines.main, data-duration=12):\n\n0.0–3.5s HOLD — card fully composed, idle state. Subtle ambient drift (translateY ±4px ease sine.inOut, finite period 3.0s, 1 repeat). Capture the source canvas at every frame so any micro-motion in the DOM (e.g., a soft accent shimmer behind the price) makes it through.\n\n3.5–4.0s WIND-UP — card scales 1.0 → 1.04 (ease power2.in 0.5s), chromatic offset on the captured texture grows from 0 → 0.012, faint vibration on the fragment field (precomputed Voronoi seed mulberry32(8743)).\n\n4.0–6.6s SHATTER — fragment spawn at t=4.0s. Each fragment gets a deterministic vector based on its centroid (outward + slight upward bias). Translate over 2.6s ease expo.out to final positions in a 2400×1400px field, rotate 0 → ±240° per fragment (signed by centroid sign), scale 1.0 → 0.92, refraction strength 0 → 0.6 → 0.3. Chromatic dispersion peaks at 4.4s (offset 0.034) then decays. Add depth blur on fragments behind the focal plane (z < -100).\n\n6.6–10.0s DRIFT — fragments slow-tumble in negative space, deterministic per-fragment angular velocity. Camera does a slow dolly z 0 → -80 ease sine.inOut. Reveal the brand wordmark behind the fragment field at 7.4s (`gsap.from('.wordmark', { y: 40, opacity: 0, duration: 0.8, ease: 'power3.out' })`) — wordmark uses 96px display size.\n\n10.0–12.0s SETTLE — fragments tumble out of frame; wordmark and a single CTA line (\"keep building\") stay centered. Final 0.6s of grain-overlay hold. No opacity-to-0 exits on the wordmark — only the fragment field exits.\n\nFeature detection: if `drawElementImage` is unavailable, hold the source DOM card without the shatter, fade to wordmark via a CSS crossfade. Never show a black canvas.\n\nNon-negotiables: deterministic Voronoi seeding, no Math.random / Date.now, all fragment animations finite-repeat, entrance animations on the wordmark, no exit animations except on the fragment field itself. Run `npx hyperframes lint` and `npx hyperframes inspect --samples 14 --at 1,3,4,4.5,5.5,7.5,9.5,11.5` before render. Output: `glass-shatter-outro.mp4`.", + "previewImageUrl": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/vfx-shatter.png", + "previewVideoUrl": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/vfx-shatter.mp4", + "source": { + "repo": "heygen-com/hyperframes", + "license": "Apache-2.0", + "author": "HeyGen", + "url": "https://hyperframes.heygen.com/catalog/blocks/vfx-shatter" + } +} diff --git a/prompt-templates/video/hyperframes-html-in-canvas-text-cursor.json b/prompt-templates/video/hyperframes-html-in-canvas-text-cursor.json new file mode 100644 index 000000000..2f40f0792 --- /dev/null +++ b/prompt-templates/video/hyperframes-html-in-canvas-text-cursor.json @@ -0,0 +1,19 @@ +{ + "id": "hyperframes-html-in-canvas-text-cursor", + "surface": "video", + "title": "HyperFrames HTML-in-Canvas: Cinematic Text Cursor Reveal", + "summary": "An 8-second dramatic text reveal with cursor glow, chromatic shadow rays, and directional lighting on a black stage. Real DOM typography under live shader post-processing. Built on the vfx-text-cursor catalog block.", + "category": "VFX / HTML-in-Canvas", + "tags": ["hyperframes", "html-in-canvas", "text", "cinematic", "shader"], + "model": "hyperframes-html", + "aspect": "16:9", + "prompt": "Build an 8-second HyperFrames composition (1920×1080, 30fps) titled \"cinematic-text-cursor\". Pull the catalog block first: `npx hyperframes add vfx-text-cursor`.\n\nVisual identity: pure black stage #000000, off-white type #f7f7f5, single chromatic shadow tint #ff4d3a (warm) bleeding into #4d8dff (cool) for spectral edges. Display face: \"PP Neue Montreal\" 220px Medium for the hero word, fallback \"NB International Pro\". Tracking -0.02em on the hero. No body type — the whole composition is one line.\n\nThe full composition is one HTML-in-Canvas reveal of a single short line the user supplies (default: \"think bigger\"). Source DOM is a `` with one centered `

` element. The shader does the cinema.\n\nTimeline (paused: true, window.__timelines.main, data-duration=8):\n\n0.0–0.8s DARK — fully black, only a single faint cursor scan-line at 50% canvas height moving left to right at 800px/s, intensity 0.3.\n\n0.8–2.4s REVEAL — cursor reaches the headline x-position. The hero word reveals letter by letter via clip-path (linear left-to-right wipe over 1.4s, ease power3.out). Each letter, the moment it appears, lights up the chromatic shadow rays radiating outward 18° spread, length 320px, intensity peaks at 0.85, decays over 0.5s. Directional key light from camera-upper-left sweeps the type with a soft glow.\n\n2.4–5.0s HOLD — type stays lit, cursor parks just to the right of the final letter and pulses (opacity 0.9 ↔ 0.5, period 0.8s, ease sine.inOut, finite repeat — calculate `Math.ceil(2.6 / 0.8) - 1` repeats). Chromatic shadow stabilises at 0.18 intensity. A subtle film-grain overlay (use `grain-overlay` component at 6%) holds across the scene.\n\n5.0–8.0s OUTRO — cursor accelerates rightward off-canvas (x position +1200 over 1.0s ease expo.in). Chromatic rays intensify briefly to 1.0, then everything dims to black via the root container's opacity over 1.6s. The hero word stays in CSS position — only the root container fades. Final 0.4s pure black hold.\n\nFeature detection: if `drawElementImage` is unavailable, render the source DOM directly as a normal CSS-only kinetic-type composition with class-based reveal — the text itself is the fallback. Never show a black canvas with no text.\n\nNon-negotiables: one font, one expressive line, deterministic motion. Min hero size 200px. No `repeat: -1` (the cursor pulse uses a finite count). No animation conflict — the cursor glow shader and the GSAP letter reveal animate different properties on different elements. Run `npx hyperframes lint` and `npx hyperframes inspect --samples 8 --at 0.5,1.6,2.2,4.0,5.5,7.0` before render. Output: `cinematic-text-cursor.mp4`.", + "previewImageUrl": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/vfx-text-cursor.png", + "previewVideoUrl": "https://static.heygen.ai/hyperframes-oss/docs/images/catalog/blocks/vfx-text-cursor.mp4", + "source": { + "repo": "heygen-com/hyperframes", + "license": "Apache-2.0", + "author": "HeyGen", + "url": "https://hyperframes.heygen.com/catalog/blocks/vfx-text-cursor" + } +} diff --git a/scripts/seed-test-projects.ts b/scripts/seed-test-projects.ts index 9133f427e..ddfdecb9e 100644 --- a/scripts/seed-test-projects.ts +++ b/scripts/seed-test-projects.ts @@ -207,7 +207,11 @@ async function discoverDaemonUrlFromToolsDev(): Promise { return await new Promise((resolve) => { let child; try { - child = spawn('pnpm', ['exec', 'tools-dev', 'status', '--json'], { + // `--silent` suppresses pnpm's own warnings (notably "Unsupported + // engine" when the local node version doesn't match the repo's + // engines.node). Without it, those warnings land on stdout under a + // nested pnpm context and break the JSON parse below. + child = spawn('pnpm', ['--silent', 'exec', 'tools-dev', 'status', '--json'], { cwd: REPO_ROOT, stdio: ['ignore', 'pipe', 'pipe'], }); @@ -222,20 +226,34 @@ async function discoverDaemonUrlFromToolsDev(): Promise { child.stderr?.resume(); child.on('error', () => resolve(null)); child.on('exit', () => { - try { - const parsed = JSON.parse(stdout) as { - apps?: { daemon?: { url?: string | null } }; - url?: string | null; - }; - const url = parsed?.apps?.daemon?.url ?? parsed?.url ?? null; - resolve(typeof url === 'string' && url.length > 0 ? url : null); - } catch { - resolve(null); - } + const url = extractDaemonUrlFromStatusOutput(stdout); + resolve(url); }); }); } +// Robust against any leading non-JSON noise pnpm or a wrapper might still +// print on stdout (engine warnings, recursive run banners, deprecation +// notices). Find the first `{` and try to parse the JSON object that +// starts there; if that fails, walk forward to the next `{`. This keeps +// discovery working even if a future pnpm version regresses around +// `--silent`. +function extractDaemonUrlFromStatusOutput(stdout: string): string | null { + for (let i = stdout.indexOf('{'); i !== -1; i = stdout.indexOf('{', i + 1)) { + try { + const parsed = JSON.parse(stdout.slice(i)) as { + apps?: { daemon?: { url?: string | null } }; + url?: string | null; + }; + const url = parsed?.apps?.daemon?.url ?? parsed?.url ?? null; + if (typeof url === 'string' && url.length > 0) return url; + } catch { + // try the next `{` + } + } + return null; +} + async function resolveDaemonUrl(args: Args): Promise { if (args.daemonUrl) return args.daemonUrl; if (process.env.OD_DAEMON_URL) return process.env.OD_DAEMON_URL;