@mulmoclaude/mulmoscript-plugin 1.0.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/paths.d.ts +2 -5
- package/dist/core/paths.d.ts.map +1 -1
- package/dist/core/plugin.d.ts.map +1 -1
- package/dist/core/validate.d.ts.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/{plugin-Bi986Wga.cjs → plugin-BLp8tSYt.cjs} +86 -46
- package/dist/plugin-BLp8tSYt.cjs.map +1 -0
- package/dist/{plugin-BQyzxkui.js → plugin-CXhOP_xU.js} +81 -47
- package/dist/plugin-CXhOP_xU.js.map +1 -0
- package/dist/server/mulmoErrorCapture.d.ts.map +1 -1
- package/dist/server/ops.d.ts.map +1 -1
- package/dist/server/support.d.ts +0 -5
- package/dist/server/support.d.ts.map +1 -1
- package/dist/server/types.d.ts +3 -7
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server.cjs +34 -58
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +33 -57
- package/dist/server.js.map +1 -1
- package/dist/style.css +36 -18
- package/dist/vue/View.vue.d.ts.map +1 -1
- package/dist/vue/components/BeatLightbox.vue.d.ts +25 -0
- package/dist/vue/components/BeatLightbox.vue.d.ts.map +1 -0
- package/dist/vue/components/CharacterStrip.vue.d.ts +29 -0
- package/dist/vue/components/CharacterStrip.vue.d.ts.map +1 -0
- package/dist/vue/components/MulmoScriptToolbar.vue.d.ts +26 -0
- package/dist/vue/components/MulmoScriptToolbar.vue.d.ts.map +1 -0
- package/dist/vue/composables/useBeatMovie.d.ts +20 -0
- package/dist/vue/composables/useBeatMovie.d.ts.map +1 -0
- package/dist/vue/composables/useCharacterImages.d.ts +31 -0
- package/dist/vue/composables/useCharacterImages.d.ts.map +1 -0
- package/dist/vue/composables/useDeckEditor.d.ts +18 -0
- package/dist/vue/composables/useDeckEditor.d.ts.map +1 -0
- package/dist/vue/composables/useMediaExport.d.ts +26 -0
- package/dist/vue/composables/useMediaExport.d.ts.map +1 -0
- package/dist/vue/helpers.d.ts +78 -0
- package/dist/vue/helpers.d.ts.map +1 -1
- package/dist/vue/support.d.ts +6 -4
- package/dist/vue/support.d.ts.map +1 -1
- package/dist/vue/transport.d.ts.map +1 -1
- package/dist/vue/viewTypes.d.ts +49 -0
- package/dist/vue/viewTypes.d.ts.map +1 -0
- package/dist/vue.cjs +1016 -715
- package/dist/vue.cjs.map +1 -1
- package/dist/vue.js +1013 -712
- package/dist/vue.js.map +1 -1
- package/package.json +11 -7
- package/dist/plugin-BQyzxkui.js.map +0 -1
- package/dist/plugin-Bi986Wga.cjs.map +0 -1
package/dist/vue.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vue.cjs","names":[],"sources":["../src/vue/support.ts","../src/vue/helpers.ts","../src/vue/transport.ts","../src/vue/hostAdapter.ts","../src/lang/de.ts","../src/lang/en.ts","../src/lang/es.ts","../src/lang/fr.ts","../src/lang/ja.ts","../src/lang/ko.ts","../src/lang/ptBR.ts","../src/lang/zh.ts","../src/lang/index.ts","../src/vue/View.vue","../src/vue/View.vue","../src/vue/Preview.vue","../src/vue/Preview.vue","../src/vue/index.ts"],"sourcesContent":["// Small host-independent utilities the View needs, ported from\n// MulmoClaude's `src/utils/errors.ts` / `src/composables/useClipboardCopy.ts`\n// so the package has no host imports.\n\nimport { ref, type Ref } from \"vue\";\n\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/** Canonical unknown-caught-value → human-readable string. Non-Error\n * objects with a `details` (gRPC convention) or `message` string field\n * have that field surfaced. */\nexport function errorMessage(err: unknown, fallback?: string): string {\n if (err instanceof Error) return err.message;\n if (err !== null && typeof err === \"object\") {\n const obj = err as { details?: unknown; message?: unknown };\n if (typeof obj.details === \"string\" && obj.details) return obj.details;\n if (typeof obj.message === \"string\" && obj.message) return obj.message;\n }\n if (fallback !== undefined) return fallback;\n return String(err);\n}\n\nexport interface UseClipboardCopyHandle {\n copied: Ref<boolean>;\n copy: (text: string) => Promise<void>;\n}\n\n/** Clipboard failures (permissions, insecure context) are swallowed on\n * purpose: the UI just leaves the \"Copied!\" hint off, which is what\n * `copied=false` already signals. */\nexport function useClipboardCopy(resetMs = 2000): UseClipboardCopyHandle {\n const copied = ref(false);\n\n async function copy(text: string): Promise<void> {\n try {\n await navigator.clipboard.writeText(text);\n copied.value = true;\n setTimeout(() => {\n copied.value = false;\n }, resetMs);\n } catch {\n // Clipboard API blocked (iframe without permissions, non-HTTPS origin) — leave `copied` false.\n }\n }\n\n return { copied, copy };\n}\n","// Pure helpers for the presentMulmoScript View. Kept separate so their\n// logic is unit-testable without mounting the Vue component. Ported from\n// the host's `src/plugins/presentMulmoScript/helpers.ts`; the SSE-stream\n// helpers did not move — per-beat generation progress now arrives on the\n// plugin pubsub channel (see `core/contract.ts`).\n\nimport { isRecord } from \"./support\";\n\n/**\n * Decide whether a beat should be rendered automatically at\n * script load time. Text-based beats (slides, charts, etc.) are\n * auto-rendered only when the script has no characters —\n * characters must be rendered first so they can be referenced by\n * any character-using beat.\n */\nexport function shouldAutoRenderBeat(beat: { image?: { type?: string } }, hasCharacters: boolean, autoRenderTypes: readonly string[]): boolean {\n if (hasCharacters) return false;\n const type = beat.image?.type;\n if (typeof type !== \"string\") return false;\n return autoRenderTypes.includes(type);\n}\n\n/**\n * Of the given character keys, return those whose image is not\n * yet loaded and is not currently rendering. Used to fetch only\n * what's missing after a movie-generation event arrives.\n */\nexport function getMissingCharacterKeys(keys: readonly string[], images: Record<string, unknown>, renderState: Record<string, string | undefined>): string[] {\n return keys.filter((charKey) => !images[charKey] && renderState[charKey] !== \"rendering\");\n}\n\n/**\n * A schema shape that exposes `safeParse` — matches Zod's API\n * without pulling the dep into this module.\n */\nexport interface SafeParseSchema {\n safeParse: (value: unknown) => { success: boolean };\n}\n\n/**\n * Validate a candidate Beat JSON string against a schema.\n * Returns false on any JSON parse error or schema mismatch.\n */\nexport function validateBeatJSON(json: string, schema: SafeParseSchema): boolean {\n let parsed: unknown;\n try {\n parsed = JSON.parse(json);\n } catch {\n return false;\n }\n return schema.safeParse(parsed).success;\n}\n\n/**\n * Stable structural equality for two MulmoScripts via JSON\n * canonicalisation. We compare the full re-serialised string\n * rather than walking keys because (a) MulmoScript is\n * deeply-nested and Object.keys-recursion would be ~50 lines, and\n * (b) `JSON.stringify` already preserves insertion order, which\n * `mulmoScriptSchema.safeParse` keeps stable across runs of the\n * same input. False positives (= \"differ\" when they don't) only\n * cost an extra `emit(\"updateResult\", ...)` which is a no-op when\n * data hasn't actually changed.\n */\nexport function isSameScript(left: unknown, right: unknown): boolean {\n return JSON.stringify(left) === JSON.stringify(right);\n}\n\n/**\n * True when a beat can have a generated video clip on disk — used to\n * decide whether to probe the beat-movie endpoint. `moviePrompt`\n * beats produce a per-beat movie file; `html_tailwind` beats with\n * `animation` set (either `true` or an options object) produce an\n * `_animated.mp4` render.\n */\nexport function beatMayHaveMovie(beat: { moviePrompt?: string; image?: { type?: string; animation?: unknown } }): boolean {\n if (beat.moviePrompt) return true;\n return beat.image?.type === \"html_tailwind\" && Boolean(beat.image.animation);\n}\n\n/** Pure check: is every beat in the script a `slide`-typed beat?\n * When true, the View mounts `@mulmocast/deck-web`'s\n * `MulmoScriptDeckEditor` instead of the per-beat list UI (#1575).\n * Empty / missing `beats[]` returns false — there's nothing to edit\n * as a deck, fall through to the existing UI which renders an empty\n * state. Mixed scripts (any non-`slide` beat) also return false; that\n * case is deferred to a future phase. */\nexport function isAllSlideDeck(script: unknown): boolean {\n if (!isRecord(script)) return false;\n const { beats } = script;\n if (!Array.isArray(beats) || beats.length === 0) return false;\n return beats.every((beat) => {\n if (!isRecord(beat)) return false;\n const { image } = beat;\n return isRecord(image) && image.type === \"slide\";\n });\n}\n","// Host-agnostic transport for the presentMulmoScript View. Every operation\n// goes through `useRuntime().dispatch({ kind, … })` and returns the same\n// `{ ok, data | error }` shape the pre-extraction `apiGet`/`apiPost`\n// helpers produced, so the View's call sites stay structurally identical.\n//\n// Dispatch responses are `{ ok: … }` envelopes (see `core/contract.ts`):\n// business failures arrive as `{ ok: false, error }` data rather than HTTP\n// errors, keeping user-facing messages free of transport prefixes. A thrown\n// dispatch (network drop, host bug) is caught and folded into the same\n// failure shape.\n\nimport { useRuntime } from \"gui-chat-protocol/vue\";\nimport type { MulmoScriptDispatchArgs, MulmoScriptDispatchResult, MulmoScriptGenerationEvent } from \"../core/contract\";\nimport { GENERATION_EVENT } from \"../core/contract\";\nimport { errorMessage, isRecord } from \"./support\";\n\nexport type TransportResult<T> = { ok: true; data: T } | { ok: false; error: string };\n\ntype ArgsFor<K extends MulmoScriptDispatchArgs[\"kind\"]> = Omit<Extract<MulmoScriptDispatchArgs, { kind: K }>, \"kind\">;\n\nconst GENERATION_EVENT_KINDS: ReadonlySet<string> = new Set([\"beatImage\", \"beatAudio\", \"characterImage\", \"movie\", \"pdf\"]);\n\nfunction parseGenerationEvent(payload: unknown): MulmoScriptGenerationEvent | null {\n if (!isRecord(payload)) return null;\n const { kind, filePath, key, done, error } = payload;\n if (typeof kind !== \"string\" || !GENERATION_EVENT_KINDS.has(kind)) return null;\n if (typeof filePath !== \"string\" || typeof key !== \"string\" || typeof done !== \"boolean\") return null;\n return {\n kind: kind as MulmoScriptGenerationEvent[\"kind\"],\n filePath,\n key,\n done,\n ...(typeof error === \"string\" ? { error } : {}),\n };\n}\n\nexport interface MulmoScriptTransport {\n call<K extends MulmoScriptDispatchArgs[\"kind\"]>(kind: K, args: ArgsFor<K>): Promise<TransportResult<MulmoScriptDispatchResult[K]>>;\n /** Subscribe to the host's generation channel, pre-filtered to one\n * script's wire path. Returns the unsubscribe function. */\n onGenerationEvent(filePath: () => string, handler: (event: MulmoScriptGenerationEvent) => void): () => void;\n}\n\nexport function useMulmoScriptTransport(): MulmoScriptTransport {\n const runtime = useRuntime();\n\n async function call<K extends MulmoScriptDispatchArgs[\"kind\"]>(kind: K, args: ArgsFor<K>): Promise<TransportResult<MulmoScriptDispatchResult[K]>> {\n let result: unknown;\n try {\n result = await runtime.dispatch({ kind, ...args });\n } catch (err) {\n return { ok: false, error: errorMessage(err) };\n }\n if (!isRecord(result) || result.ok !== true) {\n const error = isRecord(result) && typeof result.error === \"string\" ? result.error : `dispatch ${kind} returned an unexpected response`;\n return { ok: false, error };\n }\n return { ok: true, data: result as MulmoScriptDispatchResult[K] };\n }\n\n function onGenerationEvent(filePath: () => string, handler: (event: MulmoScriptGenerationEvent) => void): () => void {\n return runtime.pubsub.subscribe(GENERATION_EVENT, (payload: unknown) => {\n const event = parseGenerationEvent(payload);\n if (!event) return;\n const current = filePath();\n if (!current || event.filePath !== current) return;\n handler(event);\n });\n }\n\n return { call, onGenerationEvent };\n}\n","// Optional host-supplied capabilities that are genuinely host TRANSPORT,\n// not plugin logic — the browser-side sibling of html-plugin's host-injected\n// `previewUrl`. The generic runtime covers JSON dispatch + pubsub; what it\n// can't cover is (a) which chat session a generation should be tagged to\n// (MulmoClaude's sidebar indicator) and (b) how to fetch movie/PDF bytes,\n// which every host serves behind its own auth (MulmoClaude keeps them on\n// bearer-guarded /api routes by explicit review decision — see the\n// downloadMovie comment trail in the pre-extraction View).\n//\n// Hosts provide the adapter with Vue's provide() around the View; absent\n// capabilities degrade gracefully (no session tagging; download / clip-play\n// UI hidden).\n\nimport { inject, type InjectionKey, type Ref } from \"vue\";\n\nexport interface MulmoScriptHostAdapter {\n /** Active chat session id, forwarded on generation dispatches so the\n * host can light its per-session progress indicators. */\n chatSessionId?: Ref<string | undefined>;\n /** Authenticated media download. Exactly one of `moviePath` / `pdfPath`\n * is set — both are the wire `stories/…` paths the status/probe\n * dispatches return. Rejects on transport/HTTP failure. */\n fetchMediaBlob?: (query: { moviePath?: string; pdfPath?: string }) => Promise<Blob>;\n}\n\nexport const MULMOSCRIPT_HOST_ADAPTER_KEY: InjectionKey<MulmoScriptHostAdapter> = Symbol(\"mulmoscript-host-adapter\");\n\nconst EMPTY_ADAPTER: MulmoScriptHostAdapter = {};\n\nexport function useHostAdapter(): MulmoScriptHostAdapter {\n return inject(MULMOSCRIPT_HOST_ADAPTER_KEY, EMPTY_ADAPTER);\n}\n","import type { Messages } from \"./messages\";\n\nconst de: Messages = {\n beatCount: (count) => (count === 1 ? `${count} Beat` : `${count} Beats`),\n movie: \"Video\",\n generating: \"Wird generiert…\",\n rendering: \"Wird gerendert…\",\n saving: \"Wird gespeichert…\",\n update: \"Aktualisieren\",\n characters: \"Charaktere\",\n drop: \"Ablegen\",\n gen: \"Generieren\",\n play: \"▶ Abspielen\",\n stop: \"■ Stoppen\",\n playPresentation: \"Präsentation abspielen\",\n regenerateMovie: \"Video neu generieren\",\n movieGenerationFailed: \"Videoerstellung fehlgeschlagen\",\n pdf: \"PDF\",\n regeneratePdf: \"PDF neu generieren\",\n generatingPdf: \"PDF wird erstellt…\",\n retry: \"Erneut versuchen\",\n errPrefix: \"⚠ Fehler\",\n noBeats: \"Keine Beats im Skript gefunden\",\n editSource: \"Skript-Quelle bearbeiten\",\n applyChanges: \"Änderungen übernehmen\",\n generateAll: \"Alle generieren\",\n orDropImage: \"oder Bild ablegen\",\n generate: \"Generieren\",\n generateAudio: \"♪ Generieren\",\n saveErrorInvalidJson: (error) => `⚠ Ungültiges JSON: ${error}`,\n saveErrorSaveFailed: (error) => `⚠ Speichern fehlgeschlagen: ${error}`,\n close: \"Schließen\",\n cancel: \"Abbrechen\",\n};\n\nexport default de;\n","import type { Messages } from \"./messages\";\n\nconst en: Messages = {\n beatCount: (count) => (count === 1 ? `${count} beat` : `${count} beats`),\n movie: \"Movie\",\n generating: \"Generating…\",\n rendering: \"Rendering…\",\n saving: \"Saving…\",\n update: \"Update\",\n characters: \"Characters\",\n drop: \"Drop\",\n gen: \"Gen\",\n play: \"▶ Play\",\n stop: \"■ Stop\",\n playPresentation: \"Play presentation\",\n regenerateMovie: \"Regenerate movie\",\n movieGenerationFailed: \"Movie generation failed\",\n pdf: \"PDF\",\n regeneratePdf: \"Regenerate PDF\",\n generatingPdf: \"Generating PDF…\",\n retry: \"Retry\",\n errPrefix: \"⚠ Error\",\n noBeats: \"No beats found in script\",\n editSource: \"Edit Script Source\",\n applyChanges: \"Apply Changes\",\n generateAll: \"Generate All\",\n orDropImage: \"or drop image\",\n generate: \"Generate\",\n generateAudio: \"♪ Generate\",\n saveErrorInvalidJson: (error) => `⚠ Invalid JSON: ${error}`,\n saveErrorSaveFailed: (error) => `⚠ Save failed: ${error}`,\n close: \"Close\",\n cancel: \"Cancel\",\n};\n\nexport default en;\n","import type { Messages } from \"./messages\";\n\nconst es: Messages = {\n beatCount: (count) => (count === 1 ? `${count} beat` : `${count} beats`),\n movie: \"Vídeo\",\n generating: \"Generando…\",\n rendering: \"Renderizando…\",\n saving: \"Guardando…\",\n update: \"Actualizar\",\n characters: \"Personajes\",\n drop: \"Soltar\",\n gen: \"Generar\",\n play: \"▶ Reproducir\",\n stop: \"■ Detener\",\n playPresentation: \"Reproducir presentación\",\n regenerateMovie: \"Regenerar vídeo\",\n movieGenerationFailed: \"Error al generar el vídeo\",\n pdf: \"PDF\",\n regeneratePdf: \"Regenerar PDF\",\n generatingPdf: \"Generando PDF…\",\n retry: \"Reintentar\",\n errPrefix: \"⚠ Error\",\n noBeats: \"No se encontraron beats en el script\",\n editSource: \"Editar fuente del script\",\n applyChanges: \"Aplicar cambios\",\n generateAll: \"Generar todo\",\n orDropImage: \"o arrastra una imagen\",\n generate: \"Generar\",\n generateAudio: \"♪ Generar\",\n saveErrorInvalidJson: (error) => `⚠ JSON no válido: ${error}`,\n saveErrorSaveFailed: (error) => `⚠ Error al guardar: ${error}`,\n close: \"Cerrar\",\n cancel: \"Cancelar\",\n};\n\nexport default es;\n","import type { Messages } from \"./messages\";\n\nconst fr: Messages = {\n beatCount: (count) => (count === 1 ? `${count} beat` : `${count} beats`),\n movie: \"Film\",\n generating: \"Génération…\",\n rendering: \"Rendu…\",\n saving: \"Enregistrement…\",\n update: \"Mettre à jour\",\n characters: \"Personnages\",\n drop: \"Déposer\",\n gen: \"Générer\",\n play: \"▶ Lire\",\n stop: \"■ Arrêter\",\n playPresentation: \"Lire la présentation\",\n regenerateMovie: \"Régénérer la vidéo\",\n movieGenerationFailed: \"Échec de la génération de la vidéo\",\n pdf: \"PDF\",\n regeneratePdf: \"Régénérer le PDF\",\n generatingPdf: \"Génération du PDF…\",\n retry: \"Réessayer\",\n errPrefix: \"⚠ Erreur\",\n noBeats: \"Aucun beat trouvé dans le script\",\n editSource: \"Modifier la source du script\",\n applyChanges: \"Appliquer les modifications\",\n generateAll: \"Tout générer\",\n orDropImage: \"ou déposez une image\",\n generate: \"Générer\",\n generateAudio: \"♪ Générer\",\n saveErrorInvalidJson: (error) => `⚠ JSON invalide : ${error}`,\n saveErrorSaveFailed: (error) => `⚠ Échec de la sauvegarde : ${error}`,\n close: \"Fermer\",\n cancel: \"Annuler\",\n};\n\nexport default fr;\n","import type { Messages } from \"./messages\";\n\nconst ja: Messages = {\n beatCount: (count) => `${count} ビート`,\n movie: \"動画\",\n generating: \"生成中…\",\n rendering: \"レンダリング中…\",\n saving: \"保存中…\",\n update: \"更新\",\n characters: \"キャラクター\",\n drop: \"ドロップ\",\n gen: \"生成\",\n play: \"▶ 再生\",\n stop: \"■ 停止\",\n playPresentation: \"プレゼンテーション再生\",\n regenerateMovie: \"動画を再生成\",\n movieGenerationFailed: \"動画の生成に失敗しました\",\n pdf: \"PDF\",\n regeneratePdf: \"PDF を再生成\",\n generatingPdf: \"PDF を生成中…\",\n retry: \"再試行\",\n errPrefix: \"⚠ エラー\",\n noBeats: \"スクリプトにビートが見つかりません\",\n editSource: \"スクリプトソースを編集\",\n applyChanges: \"変更を適用\",\n generateAll: \"すべて生成\",\n orDropImage: \"画像をドロップ\",\n generate: \"生成\",\n generateAudio: \"♪ 生成\",\n saveErrorInvalidJson: (error) => `⚠ 不正な JSON: ${error}`,\n saveErrorSaveFailed: (error) => `⚠ 保存失敗: ${error}`,\n close: \"閉じる\",\n cancel: \"キャンセル\",\n};\n\nexport default ja;\n","import type { Messages } from \"./messages\";\n\nconst ko: Messages = {\n beatCount: (count) => `${count}개 비트`,\n movie: \"영상\",\n generating: \"생성 중…\",\n rendering: \"렌더링 중…\",\n saving: \"저장 중…\",\n update: \"업데이트\",\n characters: \"캐릭터\",\n drop: \"드롭\",\n gen: \"생성\",\n play: \"▶ 재생\",\n stop: \"■ 정지\",\n playPresentation: \"프레젠테이션 재생\",\n regenerateMovie: \"동영상 재생성\",\n movieGenerationFailed: \"동영상 생성에 실패했습니다\",\n pdf: \"PDF\",\n regeneratePdf: \"PDF 재생성\",\n generatingPdf: \"PDF 생성 중…\",\n retry: \"다시 시도\",\n errPrefix: \"⚠ 오류\",\n noBeats: \"스크립트에서 비트를 찾을 수 없습니다\",\n editSource: \"스크립트 원본 편집\",\n applyChanges: \"변경 사항 적용\",\n generateAll: \"전체 생성\",\n orDropImage: \"또는 이미지 드롭\",\n generate: \"생성\",\n generateAudio: \"♪ 생성\",\n saveErrorInvalidJson: (error) => `⚠ 잘못된 JSON: ${error}`,\n saveErrorSaveFailed: (error) => `⚠ 저장 실패: ${error}`,\n close: \"닫기\",\n cancel: \"취소\",\n};\n\nexport default ko;\n","import type { Messages } from \"./messages\";\n\nconst ptBR: Messages = {\n beatCount: (count) => (count === 1 ? `${count} beat` : `${count} beats`),\n movie: \"Vídeo\",\n generating: \"Gerando…\",\n rendering: \"Renderizando…\",\n saving: \"Salvando…\",\n update: \"Atualizar\",\n characters: \"Personagens\",\n drop: \"Soltar\",\n gen: \"Gerar\",\n play: \"▶ Reproduzir\",\n stop: \"■ Parar\",\n playPresentation: \"Reproduzir apresentação\",\n regenerateMovie: \"Regenerar vídeo\",\n movieGenerationFailed: \"Falha ao gerar o vídeo\",\n pdf: \"PDF\",\n regeneratePdf: \"Regenerar PDF\",\n generatingPdf: \"Gerando PDF…\",\n retry: \"Tentar novamente\",\n errPrefix: \"⚠ Erro\",\n noBeats: \"Nenhum beat encontrado no script\",\n editSource: \"Editar fonte do script\",\n applyChanges: \"Aplicar alterações\",\n generateAll: \"Gerar tudo\",\n orDropImage: \"ou solte uma imagem\",\n generate: \"Gerar\",\n generateAudio: \"♪ Gerar\",\n saveErrorInvalidJson: (error) => `⚠ JSON inválido: ${error}`,\n saveErrorSaveFailed: (error) => `⚠ Falha ao salvar: ${error}`,\n close: \"Fechar\",\n cancel: \"Cancelar\",\n};\n\nexport default ptBR;\n","import type { Messages } from \"./messages\";\n\nconst zh: Messages = {\n beatCount: (count) => `${count} 个 beat`,\n movie: \"视频\",\n generating: \"生成中…\",\n rendering: \"渲染中…\",\n saving: \"保存中…\",\n update: \"更新\",\n characters: \"角色\",\n drop: \"拖放\",\n gen: \"生成\",\n play: \"▶ 播放\",\n stop: \"■ 停止\",\n playPresentation: \"播放演示\",\n regenerateMovie: \"重新生成视频\",\n movieGenerationFailed: \"视频生成失败\",\n pdf: \"PDF\",\n regeneratePdf: \"重新生成 PDF\",\n generatingPdf: \"生成 PDF…\",\n retry: \"重试\",\n errPrefix: \"⚠ 错误\",\n noBeats: \"脚本中没有找到 beat\",\n editSource: \"编辑脚本源\",\n applyChanges: \"应用更改\",\n generateAll: \"全部生成\",\n orDropImage: \"或拖入图片\",\n generate: \"生成\",\n generateAudio: \"♪ 生成\",\n saveErrorInvalidJson: (error) => `⚠ JSON 无效: ${error}`,\n saveErrorSaveFailed: (error) => `⚠ 保存失败: ${error}`,\n close: \"关闭\",\n cancel: \"取消\",\n};\n\nexport default zh;\n","import { createUseT } from \"gui-chat-protocol/vue\";\nimport type { Messages } from \"./messages\";\nimport de from \"./de\";\nimport en from \"./en\";\nimport es from \"./es\";\nimport fr from \"./fr\";\nimport ja from \"./ja\";\nimport ko from \"./ko\";\nimport ptBR from \"./ptBR\";\nimport zh from \"./zh\";\n\nconst MESSAGES = { de, en, es, fr, ja, ko, \"pt-BR\": ptBR, zh } as const;\n\n/** Reactive message bundle for the active host locale. The plugin carries its\n * own translations (no host i18n dependency); it reads the locale off the\n * injected `BrowserPluginRuntime.locale` ref and falls back to English.\n * Same pattern as @mulmoclaude/html-plugin. */\nexport const useT = createUseT(MESSAGES);\n\nexport type { Messages };\n","<template>\n <div class=\"h-full bg-white flex flex-col overflow-hidden\">\n <!-- Header -->\n <div class=\"flex items-start justify-between px-6 py-4 border-b border-gray-100 shrink-0\">\n <div class=\"min-w-0 flex-1\">\n <h2 class=\"text-lg font-semibold text-gray-800 truncate\" data-testid=\"mulmo-script-title\">\n {{ script.title || \"Untitled Script\" }}\n </h2>\n <p v-if=\"script.description\" class=\"text-sm text-gray-500 mt-0.5 truncate\" data-testid=\"mulmo-script-description\">\n {{ script.description }}\n </p>\n <div class=\"flex items-center gap-3 mt-1 text-xs text-gray-400\">\n <span>{{ m.beatCount(beats.length) }}</span>\n <span v-if=\"script.lang\">{{ script.lang }}</span>\n <span v-if=\"filePath\" class=\"truncate\">{{ filePath }}</span>\n </div>\n </div>\n <div class=\"ml-4 shrink-0 flex items-center gap-2\">\n <!-- Play presentation: opens the lightbox at beat 0 and starts\n audio. Same gating as Download Movie — only when a movie has\n been generated, which is our proxy for \"every beat has both\n an image and audio on disk\". Green outline + green icon\n share the visual idiom with the (filled) Download button so\n both completed-artifact actions read as the same family.\n `isPlayReady` ensures we don't open the lightbox before the\n first beat's image (and audio, if it has text) finish their\n async load — moviePath can be set while loadExistingBeatImage\n is still in flight. -->\n <button\n v-if=\"moviePath && !movieGenerating\"\n class=\"h-8 w-8 flex items-center justify-center rounded border border-green-600 text-green-600 hover:bg-green-50 disabled:opacity-40 disabled:cursor-not-allowed transition-colors\"\n :disabled=\"!isPlayReady\"\n :title=\"m.playPresentation\"\n :aria-label=\"m.playPresentation\"\n @click=\"playPresentation\"\n >\n <span class=\"material-icons text-base\">play_arrow</span>\n </button>\n <!-- Download Movie: authenticated blob fetch through the host\n adapter, then a synthetic <a download> click. A plain\n <a href download> can't attach the host's auth headers, which\n would have forced an auth exemption on the media route — the\n host-injected `fetchMediaBlob` keeps the auth boundary intact\n (and hosts that don't provide it simply don't show this\n button). -->\n <button\n v-if=\"moviePath && !movieGenerating && canFetchMedia\"\n class=\"h-8 px-2.5 flex items-center gap-1 rounded bg-green-600 hover:bg-green-700 text-white text-sm disabled:opacity-60 disabled:cursor-not-allowed transition-colors\"\n :disabled=\"movieDownloading\"\n data-testid=\"mulmo-script-download-movie-button\"\n @click=\"downloadMovie\"\n >\n <span class=\"material-icons text-base\">download</span>\n <span>{{ m.movie }}</span>\n </button>\n <!-- Regenerate Movie (icon-only): collapses to a square once a\n movie exists — the adjacent Download / Play already make\n the subject clear, so the \"Movie\" label only adds noise. -->\n <button\n v-if=\"moviePath && !movieGenerating\"\n class=\"h-8 w-8 flex items-center justify-center rounded border border-gray-200 text-gray-600 hover:bg-gray-100 transition-colors\"\n :title=\"m.regenerateMovie\"\n :aria-label=\"m.regenerateMovie\"\n data-testid=\"mulmo-script-regenerate-movie-button\"\n @click=\"generateMovie\"\n >\n <span class=\"material-icons text-base\">refresh</span>\n </button>\n <!-- Generate Movie (pill): no movie yet, or one is currently\n generating. Keeps the label so first-time users know what\n they're triggering. -->\n <button\n v-else\n class=\"h-8 px-2.5 flex items-center gap-1 text-sm rounded border border-gray-200 text-gray-600 hover:bg-gray-100 disabled:opacity-40 disabled:cursor-not-allowed transition-colors\"\n :disabled=\"movieGenerating\"\n data-testid=\"mulmo-script-generate-movie-button\"\n @click=\"generateMovie\"\n >\n <svg v-if=\"movieGenerating\" class=\"animate-spin w-4 h-4 shrink-0\" viewBox=\"0 0 24 24\" fill=\"none\">\n <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\" />\n <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8v8H4z\" />\n </svg>\n <span v-if=\"movieGenerating\">{{ m.generating }}</span>\n <template v-else>\n <span class=\"material-icons text-sm\">refresh</span>\n <span>{{ m.movie }}</span>\n </template>\n </button>\n <!-- PDF (#1614): same Generate / Download / Regenerate pattern\n as the Movie cluster above, kept structurally separate so\n the two outputs can be requested independently and report\n status independently. -->\n <button\n v-if=\"pdfPath && !pdfGenerating && canFetchMedia\"\n class=\"h-8 px-2.5 flex items-center gap-1 rounded bg-red-600 hover:bg-red-700 text-white text-sm disabled:opacity-60 disabled:cursor-not-allowed transition-colors\"\n :disabled=\"pdfDownloading\"\n data-testid=\"mulmo-script-download-pdf-button\"\n @click=\"downloadPdf\"\n >\n <span class=\"material-icons text-base\">download</span>\n <span>{{ m.pdf }}</span>\n </button>\n <button\n v-if=\"pdfPath && !pdfGenerating\"\n class=\"h-8 w-8 flex items-center justify-center rounded border border-gray-200 text-gray-600 hover:bg-gray-100 transition-colors\"\n :title=\"m.regeneratePdf\"\n :aria-label=\"m.regeneratePdf\"\n data-testid=\"mulmo-script-regenerate-pdf-button\"\n @click=\"generatePdf\"\n >\n <span class=\"material-icons text-base\">refresh</span>\n </button>\n <button\n v-else\n class=\"h-8 px-2.5 flex items-center gap-1 text-sm rounded border border-gray-200 text-gray-600 hover:bg-gray-100 disabled:opacity-40 disabled:cursor-not-allowed transition-colors\"\n :disabled=\"pdfGenerating\"\n data-testid=\"mulmo-script-generate-pdf-button\"\n @click=\"generatePdf\"\n >\n <svg v-if=\"pdfGenerating\" class=\"animate-spin w-4 h-4 shrink-0\" viewBox=\"0 0 24 24\" fill=\"none\">\n <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\" />\n <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8v8H4z\" />\n </svg>\n <span v-if=\"pdfGenerating\">{{ m.generatingPdf }}</span>\n <template v-else>\n <span class=\"material-icons text-sm\">picture_as_pdf</span>\n <span>{{ m.pdf }}</span>\n </template>\n </button>\n </div>\n </div>\n\n <!--\n Inline error chip for movie-generation failures (#1197).\n Previously the catch arm of `generateMovie` raised an `alert()` —\n blocking, no retry path, and many users just dismissed the modal\n and saw a stalled spinner with no explanation. The chip stays\n visible until the next generate attempt clears it.\n -->\n <div\n v-if=\"movieError\"\n data-testid=\"mulmo-script-movie-error-chip\"\n class=\"bg-red-50 border border-red-200 text-red-800 text-xs px-3 py-2 mx-4 mt-3 mb-1 rounded flex items-start gap-2\"\n >\n <span class=\"material-icons text-base shrink-0 mt-px\">error_outline</span>\n <div class=\"flex-1 min-w-0\">\n <div class=\"font-medium\">{{ m.movieGenerationFailed }}</div>\n <div class=\"break-words whitespace-pre-wrap mt-0.5\">{{ movieError }}</div>\n </div>\n <button\n class=\"shrink-0 h-7 px-2 text-xs rounded border border-red-300 text-red-700 hover:bg-red-100 disabled:opacity-50\"\n :disabled=\"movieGenerating\"\n data-testid=\"mulmo-script-movie-retry-button\"\n @click=\"generateMovie\"\n >\n {{ m.retry }}\n </button>\n </div>\n\n <!-- Characters section -->\n <div v-if=\"characterKeys.length > 0\" class=\"border-b border-gray-100 shrink-0 px-4 py-3\">\n <div class=\"flex items-center justify-between mb-2\">\n <span class=\"text-xs font-semibold text-gray-500 uppercase tracking-wide\">{{ m.characters }}</span>\n <button\n class=\"px-2 py-0.5 text-xs rounded border border-gray-300 text-gray-500 hover:bg-gray-50 disabled:opacity-50\"\n :disabled=\"movieGenerating || anyBeatRendering || characterKeys.every((key) => charRenderState[key] === 'rendering')\"\n @click=\"generateAllCharacters\"\n >\n {{ m.generateAll }}\n </button>\n </div>\n <div class=\"flex gap-3 flex-wrap\">\n <div v-for=\"key in characterKeys\" :key=\"key\" class=\"flex flex-col items-center gap-1 w-36\">\n <!-- Character thumbnail -->\n <div\n class=\"relative w-36 h-36 rounded-lg border overflow-hidden bg-gray-50 flex items-center justify-center transition-colors\"\n :class=\"charDragOver[key] ? 'border-blue-400 bg-blue-50' : 'border-gray-200'\"\n @dragover=\"onCharDragOver($event, key)\"\n @dragleave=\"onCharDragLeave(key)\"\n @drop=\"onCharDrop($event, key)\"\n >\n <img\n v-if=\"charImages[key]\"\n :src=\"charImages[key]\"\n class=\"w-full h-full object-cover cursor-zoom-in\"\n :alt=\"key\"\n @click=\"openCharacterLightbox(key)\"\n />\n <template v-else-if=\"charRenderState[key] === 'rendering'\">\n <svg class=\"animate-spin w-4 h-4 text-green-400\" viewBox=\"0 0 24 24\" fill=\"none\">\n <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\" />\n <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8v8H4z\" />\n </svg>\n </template>\n <template v-else-if=\"charRenderState[key] === 'error'\">\n <span class=\"text-xs text-red-400 text-center px-1\">{{ charErrors[key] }}</span>\n </template>\n <template v-else>\n <span class=\"text-xs text-gray-300 text-center px-1 leading-tight\">{{ characterPrompt(key) }}</span>\n </template>\n <!-- Permanent drop hint -->\n <div v-if=\"!charDragOver[key]\" class=\"absolute bottom-0 inset-x-0 text-center text-xs text-gray-400 bg-white/70 py-0.5 pointer-events-none\">\n {{ m.orDropImage }}\n </div>\n <!-- Drop overlay -->\n <div v-if=\"charDragOver[key]\" class=\"absolute inset-0 flex items-center justify-center bg-blue-50/80 pointer-events-none\">\n <span class=\"text-xs text-blue-500 font-medium\">{{ m.drop }}</span>\n </div>\n <!-- Regenerate button -->\n <button\n v-if=\"charImages[key] && charRenderState[key] !== 'rendering'\"\n class=\"absolute top-0.5 right-0.5 px-1 py-0.5 text-xs rounded border bg-white\"\n :class=\"\n movieGenerating || anyBeatRendering ? 'border-yellow-400 text-yellow-500 cursor-not-allowed' : 'border-gray-400 text-gray-600 hover:bg-gray-50'\n \"\n :disabled=\"movieGenerating || anyBeatRendering\"\n @click.stop=\"renderCharacter(key, true)\"\n >\n <span v-if=\"movieGenerating || anyBeatRendering\" class=\"inline-block animate-spin\">↺</span>\n <span v-else>↺</span>\n </button>\n <!-- Generate button -->\n <button\n v-else-if=\"!charImages[key] && charRenderState[key] !== 'rendering'\"\n class=\"absolute top-0.5 right-0.5 px-1 py-0.5 text-xs rounded border bg-white\"\n :class=\"\n movieGenerating || anyBeatRendering ? 'border-yellow-400 text-yellow-500 cursor-not-allowed' : 'border-blue-400 text-blue-600 hover:bg-blue-50'\n \"\n :disabled=\"movieGenerating || anyBeatRendering\"\n @click.stop=\"renderCharacter(key, false)\"\n >\n <svg v-if=\"movieGenerating || anyBeatRendering\" class=\"animate-spin w-3 h-3\" viewBox=\"0 0 24 24\" fill=\"none\">\n <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\" />\n <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8v8H4z\" />\n </svg>\n <span v-else>{{ m.gen }}</span>\n </button>\n </div>\n <span class=\"text-xs text-gray-600 text-center truncate w-full\">{{ key }}</span>\n </div>\n </div>\n </div>\n\n <!-- Deck editor (#1575): every beat is a slide → mount the\n interactive deck editor from @mulmocast/deck-web. The Vue\n component is lazy-loaded via defineAsyncComponent, so users\n whose scripts aren't decks never pay the bundle cost. -->\n <div v-if=\"isDeck\" class=\"flex-1 overflow-hidden\" data-testid=\"mulmo-script-deck-editor\">\n <MulmoScriptDeckEditor :script=\"deckScriptInput\" layout=\"compact\" @update:script=\"onDeckUpdate\" />\n </div>\n\n <!-- Beat list (fallback when the script has any non-slide beat) -->\n <div v-else ref=\"beatListEl\" class=\"flex-1 overflow-y-auto p-2 space-y-1.5\">\n <div v-for=\"(beat, index) in beats\" :key=\"index\" class=\"rounded-lg border border-gray-200 overflow-hidden\">\n <!-- Beat body: thumbnail + narration side by side -->\n <div class=\"flex gap-3 items-stretch\">\n <!-- Thumbnail -->\n <div\n class=\"relative shrink-0 w-[45%] overflow-hidden bg-gray-50 transition-colors\"\n :class=\"beatDragOver[index] ? 'bg-blue-50' : ''\"\n @dragover=\"onBeatDragOver($event, index)\"\n @dragleave=\"onBeatDragLeave(index)\"\n @drop=\"onBeatDrop($event, index)\"\n >\n <!-- Inline player for the beat's generated video clip.\n Replaces the thumbnail while open; the close button\n returns to the still image. -->\n <template v-if=\"beatMovieOpen[index] && beatMovieUrls[index]\">\n <video :src=\"beatMovieUrls[index]\" class=\"w-full object-contain\" controls autoplay :data-testid=\"`mulmo-script-beat-movie-player-${index}`\" />\n <button\n class=\"absolute top-1.5 right-1.5 flex items-center justify-center w-6 h-6 rounded border border-gray-400 text-gray-600 bg-white hover:bg-gray-50\"\n :title=\"m.close\"\n :aria-label=\"m.close\"\n :data-testid=\"`mulmo-script-beat-movie-close-${index}`\"\n @click.stop=\"closeBeatMovie(index)\"\n >\n <span class=\"material-icons text-sm\">close</span>\n </button>\n </template>\n <template v-else>\n <img\n v-if=\"renderedImages[index]\"\n :src=\"renderedImages[index]\"\n class=\"w-full object-contain cursor-zoom-in\"\n :alt=\"`Beat ${index + 1}`\"\n @click=\"openLightbox(index)\"\n />\n <!-- Play overlay: shown when the beat-movie probe found a\n generated clip for this beat. Blob is fetched lazily\n on first click (host-authenticated), hence the spinner. -->\n <button\n v-if=\"renderedImages[index] && beatMovies[index] && canFetchMedia\"\n class=\"absolute inset-0 m-auto w-12 h-12 flex items-center justify-center rounded-full bg-black/50 text-white hover:bg-black/70\"\n :title=\"m.play\"\n :aria-label=\"m.play\"\n :data-testid=\"`mulmo-script-beat-movie-play-${index}`\"\n @click.stop=\"playBeatMovie(index)\"\n >\n <svg v-if=\"beatMovieLoading[index]\" class=\"animate-spin w-5 h-5\" viewBox=\"0 0 24 24\" fill=\"none\">\n <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\" />\n <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8v8H4z\" />\n </svg>\n <span v-else class=\"material-icons text-3xl\">play_arrow</span>\n </button>\n <button\n v-if=\"renderedImages[index] && renderState[index] !== 'rendering'\"\n class=\"absolute top-1.5 right-1.5 flex items-center gap-1 px-2 py-0.5 text-xs rounded border border-gray-400 text-gray-600 bg-white hover:bg-gray-50 disabled:opacity-60 disabled:cursor-not-allowed\"\n :disabled=\"movieGenerating\"\n @click.stop=\"regenerateBeat(index)\"\n >\n ↺\n </button>\n <div v-else-if=\"!renderedImages[index]\" class=\"w-full aspect-video flex flex-col items-center justify-center gap-1 p-2\">\n <template v-if=\"renderState[index] === 'rendering' || (movieGenerating && !renderedImages[index] && effectiveBeat(index).imagePrompt)\">\n <svg class=\"animate-spin w-4 h-4 text-green-400\" viewBox=\"0 0 24 24\" fill=\"none\">\n <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\" />\n <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8v8H4z\" />\n </svg>\n <span class=\"text-xs text-green-500\">{{ m.rendering }}</span>\n </template>\n <template v-else-if=\"renderState[index] === 'error'\">\n <span class=\"text-xs text-red-400 text-center\">{{ renderErrors[index] }}</span>\n </template>\n <template v-else>\n <span v-if=\"effectiveBeat(index).imagePrompt\" class=\"text-xs text-gray-400 text-center italic leading-relaxed px-1\">{{\n effectiveBeat(index).imagePrompt\n }}</span>\n <span v-else class=\"text-xs text-gray-300\">{{ beat.image?.type ?? \"—\" }}</span>\n </template>\n </div>\n </template>\n <!-- Beat drop hint / overlay -->\n <div v-if=\"beatDragOver[index]\" class=\"absolute inset-0 flex items-center justify-center bg-blue-50/80 pointer-events-none\">\n <span class=\"text-xs text-blue-500 font-medium\">{{ m.drop }}</span>\n </div>\n <div\n v-else-if=\"!renderedImages[index] && renderState[index] !== 'rendering'\"\n class=\"absolute bottom-0 inset-x-0 text-center text-xs text-gray-400 bg-white/70 py-0.5 pointer-events-none\"\n >\n {{ m.orDropImage }}\n </div>\n <!-- Generate button for any beat without a rendered image.\n renderBeat works for every beat type: imagePrompt /\n typed image beats render directly, moviePrompt beats\n get a frame extracted from the generated clip, and\n text-only beats fall back to a prompt derived from\n the narration text (mulmocast prompt.js). -->\n <button\n v-if=\"!renderedImages[index] && renderState[index] !== 'rendering' && !movieGenerating\"\n class=\"absolute top-1.5 right-1.5 flex items-center gap-1 px-2 py-0.5 text-xs rounded border border-blue-400 text-blue-600 bg-white hover:bg-blue-50\"\n @click=\"renderBeat(index)\"\n >\n {{ m.generate }}\n </button>\n </div>\n\n <!-- Narration text -->\n <div class=\"flex flex-col flex-1 min-w-0 px-2 py-1.5\">\n <span class=\"text-sm text-gray-800 leading-relaxed\">{{ effectiveBeat(index).text }}</span>\n <div class=\"flex justify-between mt-auto pt-1\">\n <!-- Audio controls -->\n <div class=\"flex items-center gap-1\">\n <template v-if=\"audioState[index] === 'generating' || (movieGenerating && !beatAudios[index] && effectiveBeat(index).text)\">\n <svg class=\"animate-spin w-3 h-3 text-green-400\" viewBox=\"0 0 24 24\" fill=\"none\">\n <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\" />\n <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8v8H4z\" />\n </svg>\n </template>\n <button\n v-else-if=\"beatAudios[index]\"\n class=\"text-xs px-2 py-0.5 rounded border\"\n :class=\"playingAudio?.index === index ? 'border-red-400 text-red-600 hover:bg-red-50' : 'border-green-400 text-green-600 hover:bg-green-50'\"\n @click=\"playAudio(index)\"\n >\n {{ playingAudio?.index === index ? m.stop : m.play }}\n </button>\n <template v-else-if=\"audioErrors[index]\">\n <span class=\"text-xs text-red-400 truncate min-w-0 max-w-[20rem]\" :title=\"audioErrors[index]\">\n {{ m.errPrefix }} {{ audioErrors[index] }}\n </span>\n <button\n v-if=\"effectiveBeat(index).text\"\n class=\"text-xs px-2 py-0.5 rounded border border-gray-300 text-gray-500 hover:bg-gray-50 disabled:opacity-50\"\n :disabled=\"movieGenerating\"\n @click=\"generateAudio(index)\"\n >\n ↺\n </button>\n </template>\n <button\n v-else-if=\"effectiveBeat(index).text\"\n class=\"text-xs px-2 py-0.5 rounded border border-gray-300 text-gray-500 hover:bg-gray-50\"\n @click=\"generateAudio(index)\"\n >\n {{ m.generateAudio }}\n </button>\n </div>\n <button\n class=\"text-gray-400 hover:text-gray-600\"\n :title=\"sourceOpen[index] ? 'Hide source' : 'Show source'\"\n :data-testid=\"`mulmo-script-beat-source-toggle-${index}`\"\n @click=\"toggleSource(index)\"\n >\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n class=\"w-3.5 h-3.5\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n >\n <polyline points=\"16 18 22 12 16 6\" />\n <polyline points=\"8 6 2 12 8 18\" />\n </svg>\n </button>\n </div>\n </div>\n </div>\n\n <!-- Source editor -->\n <div v-if=\"sourceOpen[index]\" class=\"border-t border-gray-100\">\n <textarea\n v-model=\"sourceText[index]\"\n class=\"w-full text-xs text-gray-600 bg-gray-50 p-2 font-mono resize-none\"\n :class=\"isValidBeat(index) ? 'outline-none' : 'outline outline-2 outline-red-400'\"\n rows=\"8\"\n spellcheck=\"false\"\n :data-testid=\"`mulmo-script-beat-source-textarea-${index}`\"\n />\n <div class=\"flex items-center justify-end gap-2 px-2 pb-2\">\n <span v-if=\"beatSaveErrors[index]\" class=\"text-xs text-red-600\" role=\"alert\">{{\n beatSaveErrors[index].kind === \"invalidJson\"\n ? m.saveErrorInvalidJson(beatSaveErrors[index].error)\n : m.saveErrorSaveFailed(beatSaveErrors[index].error)\n }}</span>\n <button\n class=\"px-2 py-1 text-xs rounded border\"\n :class=\"\n isValidBeat(index) && !beatSaving[index]\n ? 'border-blue-400 text-blue-600 hover:bg-blue-50 cursor-pointer'\n : 'border-gray-200 text-gray-300 cursor-not-allowed'\n \"\n :disabled=\"!isValidBeat(index) || !!beatSaving[index]\"\n :data-testid=\"`mulmo-script-beat-update-button-${index}`\"\n @click=\"updateBeat(index)\"\n >\n {{ beatSaving[index] ? m.saving : m.update }}\n </button>\n </div>\n </div>\n </div>\n\n <div v-if=\"beats.length === 0\" class=\"flex items-center justify-center h-32 text-gray-400 text-sm\">{{ m.noBeats }}</div>\n </div>\n\n <!-- Bottom bar: Edit Script Source + Copy -->\n <div class=\"bottom-bar-wrapper\">\n <details ref=\"sourceDetails\" class=\"script-source\" @toggle=\"onSourceToggle(($event.target as HTMLDetailsElement).open)\">\n <summary>{{ m.editSource }}</summary>\n <textarea\n v-model=\"editableSource\"\n class=\"script-editor\"\n :class=\"{ 'script-editor-invalid': sourceChanged && !sourceValid }\"\n spellcheck=\"false\"\n ></textarea>\n <div class=\"editor-actions\">\n <button class=\"apply-btn\" :disabled=\"!sourceChanged || !sourceValid\" @click=\"applySource\">{{ m.applyChanges }}</button>\n <button class=\"cancel-btn\" @click=\"cancelSourceEdit\">{{ m.cancel }}</button>\n </div>\n </details>\n <button v-show=\"!editing\" class=\"copy-btn\" :title=\"copied ? 'Copied!' : 'Copy'\" @click=\"copyText\">\n <span class=\"material-icons\">{{ copied ? \"check\" : \"content_copy\" }}</span>\n </button>\n </div>\n\n <!-- Lightbox -->\n <div v-if=\"lightbox\" class=\"fixed inset-0 z-50 bg-black/80 overflow-y-auto\" @click=\"closeLightbox\">\n <button class=\"fixed top-2 right-4 z-10 text-white/60 hover:text-white text-3xl leading-none\" :title=\"m.close\" @click.stop=\"closeLightbox\">✕</button>\n <div class=\"flex flex-col items-center gap-4 pt-4 pb-8\" @click.stop>\n <div class=\"flex items-center gap-4\">\n <button\n v-if=\"!lightbox.isCharacter\"\n class=\"text-white/60 hover:text-white disabled:opacity-20 text-5xl leading-none\"\n :disabled=\"!hasPrev\"\n @click=\"lightboxMove(-1)\"\n >\n ‹\n </button>\n <div class=\"flex flex-col items-center\">\n <img :src=\"lightbox.src\" class=\"max-w-[80vw] max-h-[85vh] object-contain rounded shadow-2xl\" />\n <div v-if=\"!lightbox.isCharacter && beats.length > 1\" class=\"relative w-full h-1\">\n <div class=\"flex gap-1 h-full\">\n <div\n v-for=\"i in beats.length\"\n :key=\"i - 1\"\n class=\"group flex-1 cursor-pointer relative transition-colors\"\n :class=\"\n i - 1 === lightbox.index\n ? 'bg-white/80 hover:bg-white'\n : i - 1 < lightbox.index\n ? 'bg-white/40 hover:bg-white/60'\n : 'bg-white/20 hover:bg-white/40'\n \"\n @click=\"jumpToBeat(i - 1)\"\n >\n <span class=\"absolute -inset-y-3 inset-x-0\" />\n <div\n v-if=\"beatTooltip(i - 1)\"\n class=\"absolute bottom-full mb-2 left-1/2 -translate-x-1/2 z-20 px-2 py-1 rounded bg-black/90 text-white text-xs leading-tight w-48 max-h-[53px] overflow-hidden opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity\"\n >\n {{ beatTooltip(i - 1) }}\n </div>\n </div>\n </div>\n <div\n v-if=\"playingAudio && playingAudio.index === lightbox.index\"\n class=\"absolute top-1/2 w-3.5 h-3.5 rounded-full bg-white shadow ring-2 ring-black/30 -translate-y-1/2 -translate-x-1/2 pointer-events-none\"\n :style=\"{ left: `${((lightbox.index + audioProgress) / beats.length) * 100}%` }\"\n />\n </div>\n </div>\n <button\n v-if=\"!lightbox.isCharacter\"\n class=\"text-white/60 hover:text-white disabled:opacity-20 text-5xl leading-none\"\n :disabled=\"!hasNext\"\n @click=\"lightboxMove(1)\"\n >\n ›\n </button>\n </div>\n <div v-if=\"lightbox.text || beatAudios[lightbox.index]\" class=\"relative w-screen flex justify-center px-16\">\n <p v-if=\"lightbox.text\" class=\"max-w-[80vw] text-center text-white leading-relaxed text-[clamp(0.8rem,1.76vw,1.6rem)]\">\n {{ lightbox.text }}\n </p>\n <button\n v-if=\"beatAudios[lightbox.index]\"\n class=\"absolute top-0 right-4 text-sm px-3 py-1 rounded border border-white/60 text-white/60 hover:bg-white/20\"\n @click=\"playAudio(lightbox.index)\"\n >\n {{ playingAudio?.index === lightbox.index ? m.stop : m.play }}\n </button>\n </div>\n </div>\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, defineAsyncComponent, onBeforeUnmount, onMounted, reactive, ref, watch } from \"vue\";\nimport type { ToolResultComplete } from \"gui-chat-protocol/vue\";\nimport { mulmoBeatSchema, mulmoScriptSchema } from \"@mulmocast/types\";\nimport type { SlideLayout, SlideTheme } from \"@mulmocast/deck-web\";\nimport type { MulmoScriptData } from \"../core/types\";\nimport type { MulmoScriptGenerationEvent } from \"../core/contract\";\nimport { getMissingCharacterKeys, isAllSlideDeck, isSameScript, beatMayHaveMovie, shouldAutoRenderBeat, validateBeatJSON } from \"./helpers\";\nimport { errorMessage, useClipboardCopy } from \"./support\";\nimport { useMulmoScriptTransport } from \"./transport\";\nimport { useHostAdapter } from \"./hostAdapter\";\nimport { useT } from \"../lang/index\";\n\n// Lazy-loaded so the deck editor's Vue / tailwind / SlidePreview chunk\n// stays out of the initial bundle for users whose scripts aren't decks\n// (movies, html_tailwind animations, mixed beats). `defineAsyncComponent`\n// triggers the dynamic import only when `isDeck` first flips true.\nconst MulmoScriptDeckEditor = defineAsyncComponent(() => import(\"@mulmocast/deck-web\").then((mod) => mod.MulmoScriptDeckEditor));\n\nconst api = useMulmoScriptTransport();\nconst adapter = useHostAdapter();\n// Media bytes (movie / PDF / beat clips) are served behind host auth; hosts\n// opt in by injecting `fetchMediaBlob`. Without it the download / clip-play\n// affordances are hidden (the probes still run — state stays warm for a\n// host that injects later at remount).\nconst canFetchMedia = computed(() => Boolean(adapter.fetchMediaBlob));\n\nconst m = useT();\n\ninterface Beat {\n speaker?: string;\n text?: string;\n id?: string;\n imagePrompt?: string;\n moviePrompt?: string;\n image?: { type: string; [key: string]: unknown };\n /** Beat duration in seconds. The mulmocast schema notes this is\n * \"Used only when the text is empty\" — when there's no TTS audio\n * to drive playback, the Play loop uses this as the auto-advance\n * timer (#1073). */\n duration?: number;\n}\n\ninterface ImageEntry {\n type: string;\n prompt?: string;\n [key: string]: unknown;\n}\n\ninterface MulmoScript {\n title?: string;\n description?: string;\n lang?: string;\n beats?: Beat[];\n imageParams?: {\n images?: Record<string, ImageEntry>;\n [key: string]: unknown;\n };\n [key: string]: unknown;\n}\n\nconst props = defineProps<{\n selectedResult: ToolResultComplete<MulmoScriptData>;\n}>();\nconst emit = defineEmits<{ updateResult: [result: ToolResultComplete] }>();\n\nconst data = computed(() => props.selectedResult.data);\nconst script = computed<MulmoScript>(() => data.value?.script ?? {});\nconst filePath = computed(() => data.value?.filePath ?? \"\");\nconst beats = computed<Beat[]>(() => script.value.beats ?? []);\n\n// Per-beat render state\ntype RenderState = \"idle\" | \"rendering\" | \"done\" | \"error\";\nconst renderState = reactive<Record<number, RenderState>>({});\nconst renderedImages = reactive<Record<number, string>>({});\nconst renderErrors = reactive<Record<number, string>>({});\nconst sourceOpen = reactive<Record<number, boolean>>({});\nconst sourceText = reactive<Record<number, string>>({});\n// Surface update-beat failures inline next to the Update button.\n// Cleared on next successful save or editor close. Store raw error +\n// kind tag so the template picks a localized message, instead of\n// pre-composing an English-prefixed string here.\ninterface BeatSaveError {\n kind: \"invalidJson\" | \"saveFailed\";\n error: string;\n}\nconst beatSaveErrors = reactive<Record<number, BeatSaveError>>({});\nconst beatSaving = reactive<Record<number, boolean>>({});\nconst localOverrides = reactive<Record<number, Beat>>({});\nconst movieGenerating = ref(false);\nconst movieDownloading = ref(false);\nconst moviePath = ref<string | null>(null);\n// Persists the most-recent movie-generation failure so the spinner\n// area can surface it inline with a retry button (#1197). Cleared\n// at the start of every generate / regenerate attempt.\nconst movieError = ref<string | null>(null);\n// PDF generation (#1614). Mirrors the movie triple — path / spinner /\n// downloading flag — kept independent so a PDF and a movie can be\n// generated for the same script without state collision.\nconst pdfGenerating = ref(false);\nconst pdfDownloading = ref(false);\nconst pdfPath = ref<string | null>(null);\nconst beatAudios = reactive<Record<number, string>>({});\nconst audioState = reactive<Record<number, \"generating\" | \"done\" | \"error\">>({});\nconst audioErrors = reactive<Record<number, string>>({});\n// Per-beat generated video clip (moviePrompt / animated beats).\n// `beatMovies` holds the \"stories/…\" wire path from the beat-movie\n// probe; the blob object URL is fetched lazily on first play through\n// the host adapter's authenticated `fetchMediaBlob` — a plain\n// <video src> can't attach the host's auth headers.\nconst beatMovies = reactive<Record<number, string>>({});\nconst beatMovieUrls = reactive<Record<number, string>>({});\nconst beatMovieOpen = reactive<Record<number, boolean>>({});\nconst beatMovieLoading = reactive<Record<number, boolean>>({});\nconst playingAudio = ref<{ index: number; audio: HTMLAudioElement } | null>(null);\n// Tracks the auto-advance timer running on a silent beat\n// (`beat.text === \"\"`). Beats without text generate no audio, so the\n// Play loop falls back to a `setTimeout(beat.duration)` for cues —\n// without this, Play would stall on the first silent beat (#1073).\nconst silentPlaybackTimer = ref<{ index: number; timer: ReturnType<typeof setTimeout> } | null>(null);\nconst audioProgress = ref(0);\n\n// Default duration (seconds) for a silent beat whose script doesn't\n// set `duration` either. Picked to roughly match the time it takes a\n// reader to scan a `textSlide` — long enough to read, short enough\n// not to feel stuck. The script's own `duration` always wins.\nconst SILENT_BEAT_DEFAULT_SEC = 3;\nconst MS_PER_SECOND = 1000;\nconst beatListEl = ref<HTMLElement | null>(null);\nconst lightbox = ref<{\n src: string;\n text?: string;\n index: number;\n isCharacter?: boolean;\n} | null>(null);\n// Character (imageParams.images) state\ntype CharRenderState = \"idle\" | \"rendering\" | \"done\" | \"error\";\nconst charRenderState = reactive<Record<string, CharRenderState>>({});\nconst charImages = reactive<Record<string, string>>({});\nconst charErrors = reactive<Record<string, string>>({});\nconst charDragOver = reactive<Record<string, boolean>>({});\nconst beatDragOver = reactive<Record<number, boolean>>({});\n\nconst anyBeatRendering = computed(() => Object.values(renderState).some((state) => state === \"rendering\"));\n\nconst characterKeys = computed(() => {\n const imgs = script.value.imageParams?.images ?? {};\n return Object.keys(imgs).filter((key) => imgs[key]?.type === \"imagePrompt\");\n});\n\n// Session tagging is host transport: MulmoClaude injects the active chat\n// session id so generations light its per-session sidebar indicator;\n// hosts without sessions leave the adapter empty and the field is simply\n// omitted from generation dispatches.\nconst chatSessionId = computed(() => adapter.chatSessionId?.value);\n\nfunction characterPrompt(key: string): string {\n return (script.value.imageParams?.images?.[key]?.prompt as string) ?? \"\";\n}\n\nfunction stopPlayingAudio() {\n // Single helper that clears both the audio path and the silent\n // auto-advance timer — callers (lightbox open / arrow nav / Stop\n // button) get consistent behaviour without remembering which\n // playback mode the current beat was using (#1073).\n stopAllPlayback();\n}\n\nfunction openLightbox(index: number) {\n stopPlayingAudio();\n lightbox.value = {\n src: renderedImages[index],\n text: effectiveBeat(index).text,\n index,\n };\n}\n\n// Backdrop click handler. Stops any in-flight narration so the audio\n// doesn't keep playing after the lightbox is dismissed — without this,\n// the HTMLAudioElement created by playAudio() outlives the modal and\n// the user hears disembodied narration with no UI to stop it.\nfunction closeLightbox() {\n stopPlayingAudio();\n lightbox.value = null;\n}\n\n// \"Play presentation\" toolbar action. Opens the lightbox at beat 0 and\n// kicks off its narration audio; the existing on-ended hook then chains\n// through the rest of the deck (lightboxMove(1) → playAudio if the next\n// beat has audio), so one click runs the whole presentation. Only wired\n// to the toolbar button when moviePath is set, which is our proxy for\n// \"every beat has both image and audio on disk\".\n//\n// `moviePath` arrives synchronously from movieStatus, but the per-beat\n// image and audio data URIs are populated asynchronously by\n// loadExistingBeatImage / loadExistingBeatAudio in initializeScript().\n// The Play button can therefore become visible before beat 0's assets\n// hydrate — `isPlayReady` gates the click so the lightbox never opens\n// with an undefined src or silent narration on a beat that does have\n// text.\nconst isPlayReady = computed<boolean>(() => {\n if (beats.value.length === 0) return false;\n if (!renderedImages[0]) return false;\n // Audio is only required when the beat has text (the source of TTS).\n // Beats without text are valid; they just play silently.\n if (effectiveBeat(0).text && !beatAudios[0]) return false;\n return true;\n});\n\nfunction playPresentation() {\n if (!isPlayReady.value) return;\n openLightbox(0);\n playBeat(0);\n}\n\n// Stop whichever playback handle is active. Idempotent. Called by\n// openLightbox, manual stop / pause buttons, and by `playBeat`\n// before kicking off a new beat so we never double-schedule. (#1073)\nfunction stopAllPlayback(): void {\n if (playingAudio.value) {\n playingAudio.value.audio.pause();\n playingAudio.value = null;\n audioProgress.value = 0;\n }\n if (silentPlaybackTimer.value) {\n clearTimeout(silentPlaybackTimer.value.timer);\n silentPlaybackTimer.value = null;\n }\n}\n\n// Single entry point for \"start playback at beat <index>\". Routes\n// on what the script DECLARED, not on what's currently hydrated:\n//\n// - `text` empty → silent path (`scheduleSilentAdvance`). The\n// schema says no audio is generated for empty-text beats, so\n// `duration` drives auto-advance.\n// - `text` present + audio loaded → audio path. `audio.ended`\n// chains via `advanceFromBeat`.\n// - `text` present + audio NOT loaded → stop. The Play button's\n// `isPlayReady` gate prevented this for beat 0, but mid-stream\n// a transient fetch miss must not silently skip the narration\n// by falling through to the silent timer (Codex review on\n// #1073 — gating on `beatAudios[index]` would do exactly that).\n//\n// Either path chains to the next beat via `advanceFromBeat`, so a\n// run of silent beats — or audio / silent / audio sequences —\n// plays through without manual interaction.\nfunction playBeat(index: number): void {\n stopAllPlayback();\n const hasText = Boolean(effectiveBeat(index).text);\n if (!hasText) {\n scheduleSilentAdvance(index);\n return;\n }\n if (beatAudios[index]) {\n playAudio(index);\n }\n // Text beat with no audio yet → stop. The user can re-click Play\n // once the audio finishes hydrating.\n}\n\nfunction scheduleSilentAdvance(index: number): void {\n // Defensively narrow the script-supplied duration. A bad value\n // (zero, negative, NaN, non-number) would otherwise collapse to\n // an immediate timeout and the Play loop would race through every\n // silent beat in a single tick (Codex review iter-5 on #1365).\n // Falling back to the default keeps the presentation watchable.\n const raw = effectiveBeat(index).duration;\n const seconds = typeof raw === \"number\" && Number.isFinite(raw) && raw > 0 ? raw : SILENT_BEAT_DEFAULT_SEC;\n const timer = setTimeout(() => {\n if (silentPlaybackTimer.value?.index !== index) return;\n silentPlaybackTimer.value = null;\n if (lightbox.value?.index === index) advanceFromBeat(index);\n }, seconds * MS_PER_SECOND);\n silentPlaybackTimer.value = { index, timer };\n}\n\nfunction advanceFromBeat(fromIndex: number): void {\n lightboxMove(1);\n const nextIndex = lightbox.value?.index;\n if (nextIndex === undefined || nextIndex === fromIndex) return;\n playBeat(nextIndex);\n}\n\nconst hasPrev = computed(() => {\n if (!lightbox.value) return false;\n for (let i = lightbox.value.index - 1; i >= 0; i--) {\n if (renderedImages[i]) return true;\n }\n return false;\n});\n\nconst hasNext = computed(() => {\n if (!lightbox.value) return false;\n for (let i = lightbox.value.index + 1; i < beats.value.length; i++) {\n if (renderedImages[i]) return true;\n }\n return false;\n});\n\nfunction jumpToBeat(index: number) {\n if (!lightbox.value) return;\n if (index === lightbox.value.index) return;\n if (!renderedImages[index]) return;\n // Carry the playback mode forward (audio OR silent timer) so a\n // user clicking the beat-strip thumbnail mid-playback keeps the\n // presentation rolling (#1073).\n const wasPlaying = playingAudio.value !== null || silentPlaybackTimer.value !== null;\n openLightbox(index);\n if (wasPlaying) playBeat(index);\n}\n\nfunction beatTooltip(index: number): string {\n const text = effectiveBeat(index).text ?? \"\";\n return text.length > 80 ? `${text.slice(0, 80)}…` : text;\n}\n\nfunction lightboxMove(delta: number) {\n if (!lightbox.value) return;\n const total = beats.value.length;\n // If a playback was in progress when the user clicked the arrow,\n // carry it forward to whichever beat we land on — `playBeat`\n // picks audio vs silent automatically. `openLightbox` stops the\n // current playback, so capture the flag BEFORE that and chain\n // AFTER. The on-ended / silent-advance paths already null their\n // own state before calling `lightboxMove`, so this branch won't\n // double-fire there.\n const wasPlaying = playingAudio.value !== null || silentPlaybackTimer.value !== null;\n let i = lightbox.value.index + delta;\n while (i >= 0 && i < total) {\n if (renderedImages[i]) {\n openLightbox(i);\n if (wasPlaying) playBeat(i);\n return;\n }\n i += delta;\n }\n}\nconst sourceDetails = ref<HTMLDetailsElement>();\nconst editing = ref(false);\nconst editableSource = ref(\"\");\nconst { copied, copy } = useClipboardCopy();\n\n// Beats may be edited in-place via `updateBeat()` and rendered through\n// `effectiveBeat()`, so the Copy / source-view text must read the merged\n// shape — otherwise the clipboard returns the original prop snapshot\n// until the full result is reloaded.\nconst effectiveScript = computed<MulmoScript>(() => ({\n ...script.value,\n beats: beats.value.map((beat, i) => localOverrides[i] ?? beat),\n}));\nconst scriptSourceText = computed(() => JSON.stringify(effectiveScript.value, null, 2));\n\n// #1575 — when every beat is a `slide`, swap the per-beat list UI for\n// the interactive deck editor (@mulmocast/deck-web). Mixed scripts\n// (any non-slide beat) fall back to the existing list so the user can\n// keep editing movie / textSlide / html_tailwind beats as before.\nconst isDeck = computed(() => isAllSlideDeck(effectiveScript.value));\n\n// `@mulmocast/deck-web` types its `script` prop as a *structural*\n// superset of MulmoScript (every key optional + index signature) using\n// `SlideLayout` / `SlideTheme` from `@mulmocast/deck`. Our strict\n// `MulmoScript` from `@mulmocast/types` doesn't unify with that shape\n// by name, so we re-type at the boundary. The cast is safe — any real\n// MulmoScript instance fits the structural shape. Mirrored on the way\n// out (`onDeckUpdate`).\ninterface DeckBeatShape {\n image?: {\n type?: string;\n slide?: SlideLayout;\n theme?: SlideTheme;\n [k: string]: unknown;\n };\n [k: string]: unknown;\n}\ninterface DeckScriptShape {\n beats?: DeckBeatShape[];\n presentationStyle?: { slideParams?: { theme?: SlideTheme } };\n slideParams?: { theme?: SlideTheme };\n [k: string]: unknown;\n}\nconst deckScriptInput = computed<DeckScriptShape>(() => effectiveScript.value as unknown as DeckScriptShape);\n\n// Debounce window for deck-editor → update-script. Drag a slide,\n// reorder, edit a field — each emit fires `update:script`, and we\n// only want one network round-trip per quiet stretch. 300ms is short\n// enough to feel live, long enough that typing in the Inspector\n// doesn't carpet-bomb the server.\nconst DECK_SAVE_DEBOUNCE_MS = 300;\nlet deckSaveTimer: ReturnType<typeof setTimeout> | null = null;\nlet pendingDeckScript: MulmoScript | null = null;\n\nfunction scheduleDeckSave(next: MulmoScript): void {\n pendingDeckScript = next;\n if (deckSaveTimer) clearTimeout(deckSaveTimer);\n deckSaveTimer = setTimeout(() => {\n void flushDeckSave();\n }, DECK_SAVE_DEBOUNCE_MS);\n}\n\nasync function flushDeckSave(): Promise<void> {\n deckSaveTimer = null;\n const next = pendingDeckScript;\n pendingDeckScript = null;\n if (!next || !filePath.value) return;\n const response = await api.call(\"updateScript\", {\n filePath: filePath.value,\n script: next,\n });\n if (!response.ok) {\n // Surface via console so the user can see what failed; a full\n // toast UI is P2. The deck editor still holds the latest edit\n // in its props until the next prop refresh, so the visible state\n // doesn't snap back on a transient failure.\n console.error(\"[presentMulmoScript] deck save failed:\", response.error);\n return;\n }\n // Mirror the JSON-source `applySource` flow so the parent's in-memory\n // script and our reactive beats[] stay in sync without a remount.\n emit(\"updateResult\", {\n ...props.selectedResult,\n data: { ...props.selectedResult.data, script: next },\n });\n}\n\nfunction onDeckUpdate(next: DeckScriptShape): void {\n scheduleDeckSave(next as unknown as MulmoScript);\n}\n\nonBeforeUnmount(() => {\n if (deckSaveTimer) {\n clearTimeout(deckSaveTimer);\n // Flush synchronously-scheduled work on unmount so a quick switch\n // away doesn't lose the last keystroke. Fire-and-forget — the\n // component is gone, we just want the bytes to land.\n void flushDeckSave();\n }\n // Release beat-clip blob object URLs — they outlive the component\n // otherwise (document-scoped, not GC'd with it).\n resetBeatMovies();\n unsubscribeGenerationEvents();\n});\nconst loadedSource = ref(\"\");\nconst sourceChanged = computed(() => editableSource.value !== loadedSource.value);\nconst sourceValid = computed(() => {\n try {\n const parsed = JSON.parse(editableSource.value);\n return mulmoScriptSchema.safeParse(parsed).success;\n } catch {\n return false;\n }\n});\n\nasync function onSourceToggle(open: boolean) {\n editing.value = open;\n if (open) {\n let text = scriptSourceText.value;\n // Re-read the current file from disk so beat-level edits made\n // since mount (other tabs, MCP, manual edits) surface in the\n // editor. Uses the reopen dispatch for the same reason\n // refreshScriptFromDisk does — `filePath.value` is the wire form\n // `stories/<rel>` and only the mulmoScript save/reopen op knows\n // how to map it to the on-disk path under `artifacts/stories/...`.\n if (filePath.value) {\n const response = await api.call(\"save\", { filePath: filePath.value });\n const diskScript = response.ok ? (response.data.script as MulmoScript | undefined) : undefined;\n if (diskScript) text = JSON.stringify(diskScript, null, 2);\n // fall through to in-memory script on failure\n }\n editableSource.value = text;\n loadedSource.value = text;\n }\n}\n\nfunction cancelSourceEdit() {\n if (sourceDetails.value) sourceDetails.value.open = false;\n}\n\nasync function applySource() {\n let parsed: MulmoScript;\n try {\n parsed = JSON.parse(editableSource.value);\n } catch (err) {\n alert(errorMessage(err));\n return;\n }\n const response = await api.call(\"updateScript\", {\n filePath: filePath.value,\n script: parsed,\n });\n if (!response.ok) {\n alert(response.error || \"Update failed\");\n return;\n }\n\n // Update the UI with the new script.\n // Note: the parent's handleUpdateResult uses Object.assign (in-place\n // mutation), so the watcher on props.selectedResult won't fire.\n // We emit first so the parent data is updated, then manually\n // re-initialize the view.\n emit(\"updateResult\", {\n ...props.selectedResult,\n data: { ...props.selectedResult.data, script: parsed },\n });\n\n if (sourceDetails.value) sourceDetails.value.open = false;\n await initializeScript();\n}\n\nasync function copyText() {\n await copy(scriptSourceText.value);\n}\n\nfunction effectiveBeat(index: number): Beat {\n return localOverrides[index] ?? beats.value[index] ?? {};\n}\n\nfunction toggleSource(index: number) {\n if (!sourceOpen[index]) {\n sourceText[index] = JSON.stringify(effectiveBeat(index), null, 2);\n Reflect.deleteProperty(beatSaveErrors, index);\n }\n sourceOpen[index] = !sourceOpen[index];\n}\n\nfunction isValidBeat(index: number): boolean {\n return validateBeatJSON(sourceText[index] ?? \"\", mulmoBeatSchema);\n}\n\nasync function updateBeat(index: number) {\n let beat: Beat;\n try {\n beat = JSON.parse(sourceText[index]);\n } catch (err) {\n beatSaveErrors[index] = { kind: \"invalidJson\", error: errorMessage(err) };\n return;\n }\n const prevImage = JSON.stringify(effectiveBeat(index).image);\n const prevText = effectiveBeat(index).text;\n\n const requestedFilePath = filePath.value;\n Reflect.deleteProperty(beatSaveErrors, index);\n beatSaving[index] = true;\n const response = await api.call(\"updateBeat\", {\n filePath: requestedFilePath,\n beatIndex: index,\n beat,\n });\n if (staleSince(requestedFilePath)) return;\n Reflect.deleteProperty(beatSaving, index);\n if (!response.ok) {\n beatSaveErrors[index] = { kind: \"saveFailed\", error: response.error };\n return;\n }\n\n localOverrides[index] = beat;\n sourceOpen[index] = false;\n\n if (JSON.stringify(beat.image) !== prevImage) {\n Reflect.deleteProperty(renderedImages, index);\n renderBeat(index);\n }\n\n // Audio files are content-addressed by the beat's text\n // (getBeatAudioPathOrUrl hashes text + voice), so after a text edit\n // the cached data URI belongs to the OLD narration. Drop it so the\n // \"Generate Audio\" button reappears, then re-probe — if the new text\n // matches previously generated audio (e.g. the edit was a revert),\n // the probe restores Play without a paid TTS call.\n if (beat.text !== prevText) {\n // If this beat's old narration is mid-playback, stop it first —\n // the deletes below remove the Play/Stop control from the row,\n // which would otherwise leave the stale audio playing with no\n // way to stop it (Codex review on #2143).\n if (playingAudio.value?.index === index) stopAllPlayback();\n Reflect.deleteProperty(beatAudios, index);\n Reflect.deleteProperty(audioState, index);\n Reflect.deleteProperty(audioErrors, index);\n if (beat.text) void loadExistingBeatAudio(index);\n }\n}\n\nasync function renderBeat(index: number) {\n const requestedFilePath = filePath.value;\n renderState[index] = \"rendering\";\n const response = await api.call(\"renderBeat\", {\n filePath: requestedFilePath,\n beatIndex: index,\n chatSessionId: chatSessionId.value,\n });\n if (staleSince(requestedFilePath)) return;\n if (!response.ok) {\n renderErrors[index] = response.error || \"Render failed\";\n renderState[index] = \"error\";\n return;\n }\n renderedImages[index] = response.data.image ?? \"\";\n renderState[index] = \"done\";\n refreshMissingCharacterImages();\n if (beatMayHaveMovie(effectiveBeat(index))) void loadExistingBeatMovie(index);\n}\n\nasync function regenerateBeat(index: number) {\n const requestedFilePath = filePath.value;\n Reflect.deleteProperty(renderedImages, index);\n invalidateBeatMovie(index);\n renderState[index] = \"rendering\";\n const response = await api.call(\"renderBeat\", {\n filePath: requestedFilePath,\n beatIndex: index,\n force: true,\n chatSessionId: chatSessionId.value,\n });\n if (staleSince(requestedFilePath)) return;\n if (!response.ok) {\n renderErrors[index] = response.error || \"Render failed\";\n renderState[index] = \"error\";\n return;\n }\n renderedImages[index] = response.data.image ?? \"\";\n renderState[index] = \"done\";\n if (beatMayHaveMovie(effectiveBeat(index))) void loadExistingBeatMovie(index);\n}\n\n// Stale-response guard shared by every per-beat/character loader and\n// mutator below: capture the wire path at call time and discard the\n// response when the user has navigated to a different result meanwhile —\n// otherwise late responses from script A's bulk mount-time probes would\n// write into the per-beat maps that now belong to script B.\nfunction staleSince(requestedFilePath: string): boolean {\n return filePath.value !== requestedFilePath;\n}\n\nasync function loadExistingBeatImage(index: number) {\n const requestedFilePath = filePath.value;\n const response = await api.call(\"beatImage\", { filePath: requestedFilePath, beatIndex: index });\n if (staleSince(requestedFilePath)) return;\n // silently ignore errors — image simply hasn't been generated yet\n if (response.ok && response.data.image) {\n renderedImages[index] = response.data.image;\n renderState[index] = \"done\";\n }\n}\n\nasync function loadExistingBeatAudio(index: number) {\n const requestedFilePath = filePath.value;\n const response = await api.call(\"beatAudio\", { filePath: requestedFilePath, beatIndex: index });\n if (staleSince(requestedFilePath)) return;\n // silently ignore errors\n if (response.ok && response.data.audio) {\n beatAudios[index] = response.data.audio;\n audioState[index] = \"done\";\n }\n}\n\nasync function loadExistingBeatMovie(index: number) {\n const requestedFilePath = filePath.value;\n const response = await api.call(\"beatMovie\", { filePath: requestedFilePath, beatIndex: index });\n if (staleSince(requestedFilePath)) return;\n // silently ignore errors — the clip simply hasn't been generated yet\n if (response.ok && response.data.moviePath) {\n beatMovies[index] = response.data.moviePath;\n }\n}\n\nasync function playBeatMovie(index: number) {\n const fetchMediaBlob = adapter.fetchMediaBlob;\n if (!fetchMediaBlob || !beatMovies[index] || beatMovieLoading[index]) return;\n if (beatMovieUrls[index]) {\n beatMovieOpen[index] = true;\n return;\n }\n beatMovieLoading[index] = true;\n try {\n // Re-type the .mov blob as video/mp4 — same ISO-BMFF family, and\n // <video> support for \"video/mp4\" is broader than \"video/quicktime\".\n const blob = new Blob([await fetchMediaBlob({ moviePath: beatMovies[index] })], { type: \"video/mp4\" });\n beatMovieUrls[index] = URL.createObjectURL(blob);\n beatMovieOpen[index] = true;\n } catch (err) {\n alert(errorMessage(err));\n } finally {\n Reflect.deleteProperty(beatMovieLoading, index);\n }\n}\n\nfunction closeBeatMovie(index: number) {\n Reflect.deleteProperty(beatMovieOpen, index);\n}\n\n// Drop one beat's cached clip (regenerate is about to replace it on\n// disk). Revoking the object URL frees the blob immediately.\nfunction invalidateBeatMovie(index: number): void {\n if (beatMovieUrls[index]) URL.revokeObjectURL(beatMovieUrls[index]);\n [beatMovies, beatMovieUrls, beatMovieOpen].forEach((map) => Reflect.deleteProperty(map, index));\n}\n\nfunction resetBeatMovies(): void {\n Object.values(beatMovieUrls).forEach((url) => URL.revokeObjectURL(url));\n [beatMovies, beatMovieUrls, beatMovieOpen, beatMovieLoading].forEach((map) => {\n Object.keys(map).forEach((key) => Reflect.deleteProperty(map, key));\n });\n}\n\nasync function generateAudio(index: number) {\n const requestedFilePath = filePath.value;\n audioState[index] = \"generating\";\n Reflect.deleteProperty(audioErrors, index);\n const response = await api.call(\"generateBeatAudio\", {\n filePath: requestedFilePath,\n beatIndex: index,\n chatSessionId: chatSessionId.value,\n });\n if (staleSince(requestedFilePath)) return;\n if (!response.ok) {\n audioErrors[index] = response.error || \"Audio generation failed\";\n audioState[index] = \"error\";\n return;\n }\n beatAudios[index] = response.data.audio ?? \"\";\n audioState[index] = \"done\";\n}\n\nfunction playAudio(index: number) {\n if (playingAudio.value) {\n playingAudio.value.audio.pause();\n const wasIndex = playingAudio.value.index;\n playingAudio.value = null;\n if (wasIndex === index) return;\n }\n const src = beatAudios[index];\n if (!src) return;\n const audio = new Audio(src);\n playingAudio.value = { index, audio };\n audioProgress.value = 0;\n audio.addEventListener(\"timeupdate\", () => {\n if (playingAudio.value?.index !== index) return;\n if (audio.duration > 0) audioProgress.value = audio.currentTime / audio.duration;\n });\n audio.addEventListener(\"ended\", () => {\n if (playingAudio.value?.index !== index) return;\n playingAudio.value = null;\n audioProgress.value = 0;\n if (lightbox.value?.index === index) advanceFromBeat(index);\n });\n audio.play();\n}\n\nfunction onBeatDragOver(event: DragEvent, index: number) {\n if (!event.dataTransfer?.types.includes(\"Files\")) return;\n event.preventDefault();\n beatDragOver[index] = true;\n}\n\nfunction onBeatDragLeave(index: number) {\n beatDragOver[index] = false;\n}\n\nasync function onBeatDrop(event: DragEvent, index: number) {\n event.preventDefault();\n beatDragOver[index] = false;\n const file = event.dataTransfer?.files[0];\n if (!file || !file.type.startsWith(\"image/\")) return;\n\n renderState[index] = \"rendering\";\n Reflect.deleteProperty(renderErrors, index);\n let imageData: string;\n try {\n imageData = await new Promise<string>((resolve, reject) => {\n const reader = new FileReader();\n reader.onload = () => resolve(reader.result as string);\n reader.onerror = reject;\n reader.readAsDataURL(file);\n });\n } catch (err) {\n renderErrors[index] = errorMessage(err);\n renderState[index] = \"error\";\n return;\n }\n const requestedFilePath = filePath.value;\n const response = await api.call(\"uploadBeatImage\", {\n filePath: requestedFilePath,\n beatIndex: index,\n imageData,\n });\n if (staleSince(requestedFilePath)) return;\n if (!response.ok) {\n renderErrors[index] = response.error || \"Upload failed\";\n renderState[index] = \"error\";\n return;\n }\n renderedImages[index] = response.data.image ?? \"\";\n renderState[index] = \"done\";\n}\n\nfunction onCharDragOver(event: DragEvent, key: string) {\n if (!event.dataTransfer?.types.includes(\"Files\")) return;\n event.preventDefault();\n charDragOver[key] = true;\n}\n\nfunction onCharDragLeave(key: string) {\n charDragOver[key] = false;\n}\n\nasync function onCharDrop(event: DragEvent, key: string) {\n event.preventDefault();\n charDragOver[key] = false;\n const file = event.dataTransfer?.files[0];\n if (!file || !file.type.startsWith(\"image/\")) return;\n\n charRenderState[key] = \"rendering\";\n Reflect.deleteProperty(charErrors, key);\n let imageData: string;\n try {\n imageData = await new Promise<string>((resolve, reject) => {\n const reader = new FileReader();\n reader.onload = () => resolve(reader.result as string);\n reader.onerror = reject;\n reader.readAsDataURL(file);\n });\n } catch (err) {\n charErrors[key] = errorMessage(err);\n charRenderState[key] = \"error\";\n return;\n }\n const requestedFilePath = filePath.value;\n const response = await api.call(\"uploadCharacterImage\", { filePath: requestedFilePath, key, imageData });\n if (staleSince(requestedFilePath)) return;\n if (!response.ok) {\n charErrors[key] = response.error || \"Upload failed\";\n charRenderState[key] = \"error\";\n return;\n }\n charImages[key] = response.data.image ?? \"\";\n charRenderState[key] = \"done\";\n}\n\nfunction openCharacterLightbox(key: string) {\n // Stop both audio and silent timer — character lightbox is\n // outside the play loop (#1073).\n stopAllPlayback();\n lightbox.value = {\n src: charImages[key],\n text: key,\n index: -1,\n isCharacter: true,\n };\n}\n\nasync function loadExistingCharacterImage(key: string) {\n const requestedFilePath = filePath.value;\n const response = await api.call(\"characterImage\", { filePath: requestedFilePath, key });\n if (staleSince(requestedFilePath)) return;\n // silently ignore errors\n if (response.ok && response.data.image) {\n charImages[key] = response.data.image;\n charRenderState[key] = \"done\";\n }\n}\n\nfunction refreshMissingCharacterImages() {\n getMissingCharacterKeys(characterKeys.value, charImages, charRenderState).forEach((key) => loadExistingCharacterImage(key));\n}\n\nasync function renderCharacter(key: string, force: boolean) {\n const requestedFilePath = filePath.value;\n charRenderState[key] = \"rendering\";\n Reflect.deleteProperty(charErrors, key);\n const response = await api.call(\"renderCharacter\", {\n filePath: requestedFilePath,\n key,\n force,\n chatSessionId: chatSessionId.value,\n });\n if (staleSince(requestedFilePath)) return;\n if (!response.ok) {\n charErrors[key] = response.error || \"Render failed\";\n charRenderState[key] = \"error\";\n return;\n }\n charImages[key] = response.data.image ?? \"\";\n charRenderState[key] = \"done\";\n}\n\nasync function generateAllCharacters() {\n await Promise.all(characterKeys.value.filter((key) => charRenderState[key] !== \"rendering\").map((key) => renderCharacter(key, false)));\n}\n\n// Probe the server for an existing beat PNG before triggering any\n// generation. Only auto-renders when the disk is empty AND the beat\n// is a deterministic type — imagePrompt beats are left empty so the\n// user clicks Generate explicitly (avoids surprise paid text2image\n// calls on every page refresh).\nasync function hydrateBeatImage(beat: Beat, index: number, hasCharacters: boolean, autoRenderTypes: readonly string[]): Promise<void> {\n await loadExistingBeatImage(index);\n if (renderedImages[index]) return;\n if (shouldAutoRenderBeat(beat, hasCharacters, autoRenderTypes)) {\n await renderBeat(index);\n }\n}\n\n/**\n * #1074 — keep the in-memory toolResult in sync with the on-disk\n * script file. `updateBeat` / `updateScript` persist edits to\n * disk, but the session entry that backs\n * `props.selectedResult.data.script` is never rewritten, so a\n * page reload + session-restore would otherwise surface stale\n * pre-edit content.\n *\n * Why the reopen dispatch, not a generic file read: `filePath`\n * is the wire form `stories/<rel>` which only the mulmoScript save\n * op knows how to translate back to the real on-disk path under\n * `artifacts/stories/...`. The reopen op is read-only when `script`\n * is omitted; it does NOT trigger movie generation.\n *\n * The flow silently bails on every failure mode so a missing /\n * malformed / deleted script file never blocks the rest of\n * `initializeScript`.\n *\n * Stale-response guard: capture `uuid` + `filePath` before the\n * `await`. If either has changed by the time the response lands\n * (the user navigated to a different result while the request\n * was in flight, or `props.selectedResult` was swapped under us\n * by a parent watcher), drop the response on the floor — the new\n * `initializeScript` invocation triggered by that change will\n * issue its own refresh against the correct file.\n */\nasync function refreshScriptFromDisk(): Promise<void> {\n const requestedFilePath = filePath.value;\n if (!requestedFilePath) return;\n const requestedUuid = props.selectedResult.uuid;\n const response = await api.call(\"save\", { filePath: requestedFilePath });\n if (props.selectedResult.uuid !== requestedUuid || filePath.value !== requestedFilePath) return;\n if (!response.ok) return;\n const diskScript = response.data.script as MulmoScript | undefined;\n // The server-side reopen op already validated against\n // `mulmoScriptSchema`, so a non-null `script` is trusted here —\n // we only need a presence check.\n if (!diskScript) return;\n if (isSameScript(diskScript, script.value)) return;\n emit(\"updateResult\", {\n ...props.selectedResult,\n data: { ...props.selectedResult.data, script: diskScript },\n });\n}\n\nasync function initializeScript() {\n // Stop any in-flight playback BEFORE we tear down per-script state\n // — a pending `silentPlaybackTimer` or running audio from the\n // previous script would otherwise fire `advanceFromBeat()` against\n // the new script's lightbox / beat list and either crash or\n // silently jump the new presentation forward. Also close any open\n // lightbox so the user lands on the clean View for the new result\n // (Codex review iter-4 on #1365).\n stopAllPlayback();\n lightbox.value = null;\n // Reset scroll position so new results start at the top\n if (beatListEl.value) beatListEl.value.scrollTop = 0;\n // Reset per-script state\n Object.keys(renderState).forEach((key) => Reflect.deleteProperty(renderState, key));\n Object.keys(renderedImages).forEach((key) => Reflect.deleteProperty(renderedImages, key));\n Object.keys(renderErrors).forEach((key) => Reflect.deleteProperty(renderErrors, key));\n Object.keys(sourceOpen).forEach((key) => Reflect.deleteProperty(sourceOpen, key));\n Object.keys(sourceText).forEach((key) => Reflect.deleteProperty(sourceText, key));\n Object.keys(beatSaveErrors).forEach((key) => Reflect.deleteProperty(beatSaveErrors, key));\n Object.keys(beatSaving).forEach((key) => Reflect.deleteProperty(beatSaving, key));\n Object.keys(localOverrides).forEach((key) => Reflect.deleteProperty(localOverrides, key));\n Object.keys(beatAudios).forEach((key) => Reflect.deleteProperty(beatAudios, key));\n Object.keys(audioState).forEach((key) => Reflect.deleteProperty(audioState, key));\n Object.keys(audioErrors).forEach((key) => Reflect.deleteProperty(audioErrors, key));\n Object.keys(charRenderState).forEach((key) => Reflect.deleteProperty(charRenderState, key));\n Object.keys(charImages).forEach((key) => Reflect.deleteProperty(charImages, key));\n Object.keys(charErrors).forEach((key) => Reflect.deleteProperty(charErrors, key));\n Object.keys(beatDragOver).forEach((key) => Reflect.deleteProperty(beatDragOver, key));\n resetBeatMovies();\n moviePath.value = null;\n pdfPath.value = null;\n // Movie/PDF spinners are per-script: without this reset, switching\n // away from a generating script would leave the new script's toolbar\n // spinning. The pendingGenerations snapshot below re-lights them when\n // the NEW script really does have work in flight.\n movieGenerating.value = false;\n pdfGenerating.value = false;\n movieError.value = null;\n if (sourceDetails.value) sourceDetails.value.open = false;\n\n // #1074 — re-read the script file from disk before per-beat\n // hydration. When the user switches between tool results inside\n // the same SPA mount and switches back, the in-memory toolResult\n // still carries whatever script was captured earlier, and\n // `localOverrides` (the only thing showing the user's edit since\n // the last save) is reset by initializeScript on remount.\n // Re-fetching from disk via the reopen op covers that gap.\n await refreshScriptFromDisk();\n\n // Mount-time policy: prefer the existing PNG on the server. Every\n // beat — deterministic AND imagePrompt — first probes beatImage,\n // and we only fall through to renderBeat() when the disk has nothing\n // yet AND the type is safe to auto-render (deterministic content,\n // no characters waiting). Without this probe a refresh would re-fire\n // generateBeatImage for every beat, and for imagePrompt beats that\n // means a paid text2image call against an image we already have.\n //\n // Stale-after-edit: if the user edits the script source the on-disk\n // PNG is no longer in sync with the new content, but we don't try to\n // detect that here — the per-beat ↺ button is one click away and a\n // page refresh re-runs this same probe, so the user can opt back into\n // a fresh render whenever they need to.\n const AUTO_RENDER_TYPES = [\"textSlide\", \"markdown\", \"chart\", \"mermaid\", \"html_tailwind\", \"slide\"] as const;\n const hasCharacters = characterKeys.value.length > 0;\n beats.value.forEach((beat, index) => {\n void hydrateBeatImage(beat, index, hasCharacters, AUTO_RENDER_TYPES);\n if (beat.text) loadExistingBeatAudio(index);\n if (beatMayHaveMovie(beat)) void loadExistingBeatMovie(index);\n });\n\n characterKeys.value.forEach((key) => loadExistingCharacterImage(key));\n\n if (filePath.value) {\n // Stale-response guard: if the user navigates to a different result\n // while these calls are in flight, their answers describe the OLD\n // script — drop them instead of stamping them onto the new one.\n const requestedFilePath = filePath.value;\n const isStale = () => filePath.value !== requestedFilePath;\n\n const response = await api.call(\"movieStatus\", { filePath: requestedFilePath });\n if (isStale()) return;\n if (response.ok && response.data.moviePath) {\n moviePath.value = response.data.moviePath;\n }\n // ignore errors\n // Also check whether a PDF was previously generated and is still\n // newer than the source; status returns null otherwise so the UI\n // re-offers the Generate button.\n const pdfResponse = await api.call(\"pdfStatus\", { filePath: requestedFilePath });\n if (isStale()) return;\n if (pdfResponse.ok && pdfResponse.data.pdfPath) {\n pdfPath.value = pdfResponse.data.pdfPath;\n }\n\n // Reflect any generations that were already in flight when we\n // mounted (user switched away mid-generation and came back).\n // Snapshot via dispatch; live updates arrive on the pubsub\n // subscription below.\n const pending = await api.call(\"pendingGenerations\", { filePath: requestedFilePath });\n if (isStale()) return;\n if (pending.ok) {\n for (const entry of pending.data.pending) {\n reflectGenerationStart(entry);\n }\n }\n }\n}\n\nonMounted(initializeScript);\nwatch(() => props.selectedResult, initializeScript);\n\n// Keep the view in sync with generations running anywhere — this View's\n// own long-held dispatches, a parallel tab, the agent's background\n// autoGenerateMovie. The host publishes `generation` events on the\n// plugin pubsub channel (started + finished, per beat and per artifact);\n// on start we mirror the local \"rendering\" state so spinners show even\n// after a remount, on finish we reload the relevant asset off disk.\nconst unsubscribeGenerationEvents = api.onGenerationEvent(\n () => filePath.value,\n (event) => {\n if (!event.done) {\n reflectGenerationStart(event);\n return;\n }\n // Fire-and-forget: swallow + log so a failed reload doesn't\n // surface as an unhandled rejection.\n reflectGenerationFinish(event).catch((err) => {\n console.error(\"[presentMulmoScript] reload on finish failed:\", err);\n });\n },\n);\n\nfunction reflectGenerationStart(entry: MulmoScriptGenerationEvent): void {\n if (entry.kind === \"beatImage\") {\n const idx = Number(entry.key);\n if (!renderedImages[idx]) renderState[idx] = \"rendering\";\n } else if (entry.kind === \"beatAudio\") {\n const idx = Number(entry.key);\n if (!beatAudios[idx]) audioState[idx] = \"generating\";\n } else if (entry.kind === \"characterImage\") {\n if (!charImages[entry.key]) charRenderState[entry.key] = \"rendering\";\n } else if (entry.kind === \"movie\") {\n movieGenerating.value = true;\n } else if (entry.kind === \"pdf\") {\n pdfGenerating.value = true;\n }\n}\n\nasync function reflectGenerationFinish(entry: MulmoScriptGenerationEvent): Promise<void> {\n if (entry.kind === \"beatImage\") {\n const idx = Number(entry.key);\n await loadExistingBeatImage(idx);\n if (beatMayHaveMovie(effectiveBeat(idx))) await loadExistingBeatMovie(idx);\n if (renderState[idx] === \"rendering\") Reflect.deleteProperty(renderState, idx);\n refreshMissingCharacterImages();\n } else if (entry.kind === \"beatAudio\") {\n const idx = Number(entry.key);\n await loadExistingBeatAudio(idx);\n if (audioState[idx] === \"generating\") Reflect.deleteProperty(audioState, idx);\n } else if (entry.kind === \"characterImage\") {\n await loadExistingCharacterImage(entry.key);\n if (charRenderState[entry.key] === \"rendering\") {\n Reflect.deleteProperty(charRenderState, entry.key);\n }\n } else if (entry.kind === \"movie\") {\n movieGenerating.value = false;\n await refreshMoviePath();\n } else if (entry.kind === \"pdf\") {\n pdfGenerating.value = false;\n await refreshPdfPath();\n }\n}\n\nasync function refreshMoviePath(): Promise<void> {\n const requestedFilePath = filePath.value;\n if (!requestedFilePath) return;\n const response = await api.call(\"movieStatus\", { filePath: requestedFilePath });\n if (filePath.value !== requestedFilePath) return;\n if (response.ok && response.data.moviePath) {\n moviePath.value = response.data.moviePath;\n }\n}\n\n// Long-held dispatch: resolves when the whole images → audio → movie\n// pipeline finishes (or fails). Per-beat progress arrives on the\n// pubsub `generation` channel and is applied by\n// `reflectGenerationFinish`, which reloads each asset off disk —\n// replacing the pre-extraction SSE stream.\nasync function generateMovie() {\n // This dispatch is held open for the whole pipeline (minutes). If the\n // user navigates to a different result meanwhile, the resolution\n // describes the OLD script — drop it; the new script's own\n // initializeScript / pubsub subscription owns the visible state.\n const requestedFilePath = filePath.value;\n movieGenerating.value = true;\n movieError.value = null;\n const response = await api.call(\"generateMovie\", {\n filePath: requestedFilePath,\n chatSessionId: chatSessionId.value,\n });\n if (filePath.value !== requestedFilePath) return;\n movieGenerating.value = false;\n if (!response.ok) {\n // Surface inline (instead of `alert()` which blocks + has no\n // retry affordance). The error chip with a retry button lives\n // next to the generate button in the template (#1197).\n movieError.value = response.error;\n return;\n }\n moviePath.value = response.data.moviePath;\n}\n\n// Authenticated movie download through the host adapter (which attaches\n// whatever auth its media route needs — a plain `<a href download>`\n// cannot). The blob is hooked to a synthetic anchor whose `download`\n// attribute carries the filename — the browser still surfaces a native\n// save dialog.\nasync function downloadMovie() {\n const fetchMediaBlob = adapter.fetchMediaBlob;\n if (!fetchMediaBlob || !moviePath.value || movieDownloading.value) return;\n movieDownloading.value = true;\n let objectUrl: string | null = null;\n try {\n const blob = await fetchMediaBlob({ moviePath: moviePath.value });\n objectUrl = URL.createObjectURL(blob);\n const filename = moviePath.value.split(\"/\").pop() ?? \"movie.mp4\";\n const anchor = document.createElement(\"a\");\n anchor.href = objectUrl;\n anchor.download = filename;\n document.body.appendChild(anchor);\n anchor.click();\n anchor.remove();\n } catch (err) {\n alert(errorMessage(err));\n } finally {\n if (objectUrl) URL.revokeObjectURL(objectUrl);\n movieDownloading.value = false;\n }\n}\n\n// --- PDF (#1614) ---------------------------------------------------\n//\n// Same triple as movie: status poll → long-held generate dispatch →\n// authenticated download. Per-beat image progress arrives on the same\n// pubsub `generation` channel.\n\nasync function refreshPdfPath(): Promise<void> {\n const requestedFilePath = filePath.value;\n if (!requestedFilePath) return;\n const response = await api.call(\"pdfStatus\", { filePath: requestedFilePath });\n if (filePath.value !== requestedFilePath) return;\n if (response.ok && response.data.pdfPath) {\n pdfPath.value = response.data.pdfPath;\n }\n}\n\nasync function generatePdf() {\n // Long-held dispatch — same stale-navigation guard as generateMovie.\n const requestedFilePath = filePath.value;\n pdfGenerating.value = true;\n const response = await api.call(\"generatePdf\", {\n filePath: requestedFilePath,\n chatSessionId: chatSessionId.value,\n });\n if (filePath.value !== requestedFilePath) return;\n pdfGenerating.value = false;\n if (!response.ok) {\n alert(response.error);\n return;\n }\n pdfPath.value = response.data.pdfPath;\n}\n\nasync function downloadPdf() {\n const fetchMediaBlob = adapter.fetchMediaBlob;\n if (!fetchMediaBlob || !pdfPath.value || pdfDownloading.value) return;\n pdfDownloading.value = true;\n let objectUrl: string | null = null;\n try {\n const blob = await fetchMediaBlob({ pdfPath: pdfPath.value });\n objectUrl = URL.createObjectURL(blob);\n const filename = pdfPath.value.split(\"/\").pop() ?? \"deck.pdf\";\n const anchor = document.createElement(\"a\");\n anchor.href = objectUrl;\n anchor.download = filename;\n document.body.appendChild(anchor);\n anchor.click();\n anchor.remove();\n } catch (err) {\n alert(errorMessage(err));\n } finally {\n if (objectUrl) URL.revokeObjectURL(objectUrl);\n pdfDownloading.value = false;\n }\n}\n</script>\n\n<style scoped>\n.bottom-bar-wrapper {\n position: relative;\n flex-shrink: 0;\n}\n\n.script-source {\n padding: 0.5rem;\n background: #f5f5f5;\n border-top: 1px solid #e0e0e0;\n font-family: Consolas, \"MS Gothic\", \"BIZ UDGothic\", monospace;\n font-size: 0.85rem;\n}\n\n.script-source summary {\n cursor: pointer;\n user-select: none;\n padding: 0.5rem;\n background: #e8e8e8;\n border-radius: 4px;\n font-weight: 500;\n color: #333;\n}\n\n.script-source[open] summary {\n margin-bottom: 0.5rem;\n}\n\n.script-source summary:hover {\n background: #d8d8d8;\n}\n\n.script-editor {\n width: 100%;\n height: 40vh;\n padding: 1rem;\n background: #ffffff;\n border: 1px solid #ccc;\n border-radius: 4px;\n color: #333;\n font-family: \"Courier New\", \"MS Gothic\", \"BIZ UDGothic\", monospace;\n font-size: 0.9rem;\n resize: vertical;\n margin-bottom: 0.5rem;\n line-height: 1.5;\n}\n\n.script-editor:focus {\n outline: none;\n border-color: #4caf50;\n box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.1);\n}\n\n.script-editor-invalid {\n border-color: #ef4444;\n}\n\n.script-editor-invalid:focus {\n border-color: #ef4444;\n box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.1);\n}\n\n.editor-actions {\n display: flex;\n justify-content: space-between;\n}\n\n.apply-btn {\n padding: 0.5rem 1rem;\n background: #4caf50;\n color: white;\n border: none;\n border-radius: 4px;\n cursor: pointer;\n font-size: 0.9rem;\n transition: background 0.2s;\n font-weight: 500;\n}\n\n.apply-btn:hover {\n background: #45a049;\n}\n\n.apply-btn:disabled {\n background: #cccccc;\n color: #666666;\n cursor: not-allowed;\n opacity: 0.6;\n}\n\n.cancel-btn {\n padding: 0.5rem 1rem;\n background: #e0e0e0;\n color: #333;\n border: none;\n border-radius: 4px;\n cursor: pointer;\n font-size: 0.9rem;\n transition: background 0.2s;\n font-weight: 500;\n}\n\n.cancel-btn:hover {\n background: #d0d0d0;\n}\n\n.copy-btn {\n position: absolute;\n bottom: 0.3rem;\n right: 0.65rem;\n padding: 0.4rem;\n background: none;\n border: none;\n color: #333;\n cursor: pointer;\n z-index: 1;\n}\n\n.copy-btn:hover {\n color: #000;\n}\n\n.copy-btn .material-icons {\n font-size: 1.15rem;\n}\n</style>\n","<template>\n <div class=\"h-full bg-white flex flex-col overflow-hidden\">\n <!-- Header -->\n <div class=\"flex items-start justify-between px-6 py-4 border-b border-gray-100 shrink-0\">\n <div class=\"min-w-0 flex-1\">\n <h2 class=\"text-lg font-semibold text-gray-800 truncate\" data-testid=\"mulmo-script-title\">\n {{ script.title || \"Untitled Script\" }}\n </h2>\n <p v-if=\"script.description\" class=\"text-sm text-gray-500 mt-0.5 truncate\" data-testid=\"mulmo-script-description\">\n {{ script.description }}\n </p>\n <div class=\"flex items-center gap-3 mt-1 text-xs text-gray-400\">\n <span>{{ m.beatCount(beats.length) }}</span>\n <span v-if=\"script.lang\">{{ script.lang }}</span>\n <span v-if=\"filePath\" class=\"truncate\">{{ filePath }}</span>\n </div>\n </div>\n <div class=\"ml-4 shrink-0 flex items-center gap-2\">\n <!-- Play presentation: opens the lightbox at beat 0 and starts\n audio. Same gating as Download Movie — only when a movie has\n been generated, which is our proxy for \"every beat has both\n an image and audio on disk\". Green outline + green icon\n share the visual idiom with the (filled) Download button so\n both completed-artifact actions read as the same family.\n `isPlayReady` ensures we don't open the lightbox before the\n first beat's image (and audio, if it has text) finish their\n async load — moviePath can be set while loadExistingBeatImage\n is still in flight. -->\n <button\n v-if=\"moviePath && !movieGenerating\"\n class=\"h-8 w-8 flex items-center justify-center rounded border border-green-600 text-green-600 hover:bg-green-50 disabled:opacity-40 disabled:cursor-not-allowed transition-colors\"\n :disabled=\"!isPlayReady\"\n :title=\"m.playPresentation\"\n :aria-label=\"m.playPresentation\"\n @click=\"playPresentation\"\n >\n <span class=\"material-icons text-base\">play_arrow</span>\n </button>\n <!-- Download Movie: authenticated blob fetch through the host\n adapter, then a synthetic <a download> click. A plain\n <a href download> can't attach the host's auth headers, which\n would have forced an auth exemption on the media route — the\n host-injected `fetchMediaBlob` keeps the auth boundary intact\n (and hosts that don't provide it simply don't show this\n button). -->\n <button\n v-if=\"moviePath && !movieGenerating && canFetchMedia\"\n class=\"h-8 px-2.5 flex items-center gap-1 rounded bg-green-600 hover:bg-green-700 text-white text-sm disabled:opacity-60 disabled:cursor-not-allowed transition-colors\"\n :disabled=\"movieDownloading\"\n data-testid=\"mulmo-script-download-movie-button\"\n @click=\"downloadMovie\"\n >\n <span class=\"material-icons text-base\">download</span>\n <span>{{ m.movie }}</span>\n </button>\n <!-- Regenerate Movie (icon-only): collapses to a square once a\n movie exists — the adjacent Download / Play already make\n the subject clear, so the \"Movie\" label only adds noise. -->\n <button\n v-if=\"moviePath && !movieGenerating\"\n class=\"h-8 w-8 flex items-center justify-center rounded border border-gray-200 text-gray-600 hover:bg-gray-100 transition-colors\"\n :title=\"m.regenerateMovie\"\n :aria-label=\"m.regenerateMovie\"\n data-testid=\"mulmo-script-regenerate-movie-button\"\n @click=\"generateMovie\"\n >\n <span class=\"material-icons text-base\">refresh</span>\n </button>\n <!-- Generate Movie (pill): no movie yet, or one is currently\n generating. Keeps the label so first-time users know what\n they're triggering. -->\n <button\n v-else\n class=\"h-8 px-2.5 flex items-center gap-1 text-sm rounded border border-gray-200 text-gray-600 hover:bg-gray-100 disabled:opacity-40 disabled:cursor-not-allowed transition-colors\"\n :disabled=\"movieGenerating\"\n data-testid=\"mulmo-script-generate-movie-button\"\n @click=\"generateMovie\"\n >\n <svg v-if=\"movieGenerating\" class=\"animate-spin w-4 h-4 shrink-0\" viewBox=\"0 0 24 24\" fill=\"none\">\n <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\" />\n <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8v8H4z\" />\n </svg>\n <span v-if=\"movieGenerating\">{{ m.generating }}</span>\n <template v-else>\n <span class=\"material-icons text-sm\">refresh</span>\n <span>{{ m.movie }}</span>\n </template>\n </button>\n <!-- PDF (#1614): same Generate / Download / Regenerate pattern\n as the Movie cluster above, kept structurally separate so\n the two outputs can be requested independently and report\n status independently. -->\n <button\n v-if=\"pdfPath && !pdfGenerating && canFetchMedia\"\n class=\"h-8 px-2.5 flex items-center gap-1 rounded bg-red-600 hover:bg-red-700 text-white text-sm disabled:opacity-60 disabled:cursor-not-allowed transition-colors\"\n :disabled=\"pdfDownloading\"\n data-testid=\"mulmo-script-download-pdf-button\"\n @click=\"downloadPdf\"\n >\n <span class=\"material-icons text-base\">download</span>\n <span>{{ m.pdf }}</span>\n </button>\n <button\n v-if=\"pdfPath && !pdfGenerating\"\n class=\"h-8 w-8 flex items-center justify-center rounded border border-gray-200 text-gray-600 hover:bg-gray-100 transition-colors\"\n :title=\"m.regeneratePdf\"\n :aria-label=\"m.regeneratePdf\"\n data-testid=\"mulmo-script-regenerate-pdf-button\"\n @click=\"generatePdf\"\n >\n <span class=\"material-icons text-base\">refresh</span>\n </button>\n <button\n v-else\n class=\"h-8 px-2.5 flex items-center gap-1 text-sm rounded border border-gray-200 text-gray-600 hover:bg-gray-100 disabled:opacity-40 disabled:cursor-not-allowed transition-colors\"\n :disabled=\"pdfGenerating\"\n data-testid=\"mulmo-script-generate-pdf-button\"\n @click=\"generatePdf\"\n >\n <svg v-if=\"pdfGenerating\" class=\"animate-spin w-4 h-4 shrink-0\" viewBox=\"0 0 24 24\" fill=\"none\">\n <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\" />\n <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8v8H4z\" />\n </svg>\n <span v-if=\"pdfGenerating\">{{ m.generatingPdf }}</span>\n <template v-else>\n <span class=\"material-icons text-sm\">picture_as_pdf</span>\n <span>{{ m.pdf }}</span>\n </template>\n </button>\n </div>\n </div>\n\n <!--\n Inline error chip for movie-generation failures (#1197).\n Previously the catch arm of `generateMovie` raised an `alert()` —\n blocking, no retry path, and many users just dismissed the modal\n and saw a stalled spinner with no explanation. The chip stays\n visible until the next generate attempt clears it.\n -->\n <div\n v-if=\"movieError\"\n data-testid=\"mulmo-script-movie-error-chip\"\n class=\"bg-red-50 border border-red-200 text-red-800 text-xs px-3 py-2 mx-4 mt-3 mb-1 rounded flex items-start gap-2\"\n >\n <span class=\"material-icons text-base shrink-0 mt-px\">error_outline</span>\n <div class=\"flex-1 min-w-0\">\n <div class=\"font-medium\">{{ m.movieGenerationFailed }}</div>\n <div class=\"break-words whitespace-pre-wrap mt-0.5\">{{ movieError }}</div>\n </div>\n <button\n class=\"shrink-0 h-7 px-2 text-xs rounded border border-red-300 text-red-700 hover:bg-red-100 disabled:opacity-50\"\n :disabled=\"movieGenerating\"\n data-testid=\"mulmo-script-movie-retry-button\"\n @click=\"generateMovie\"\n >\n {{ m.retry }}\n </button>\n </div>\n\n <!-- Characters section -->\n <div v-if=\"characterKeys.length > 0\" class=\"border-b border-gray-100 shrink-0 px-4 py-3\">\n <div class=\"flex items-center justify-between mb-2\">\n <span class=\"text-xs font-semibold text-gray-500 uppercase tracking-wide\">{{ m.characters }}</span>\n <button\n class=\"px-2 py-0.5 text-xs rounded border border-gray-300 text-gray-500 hover:bg-gray-50 disabled:opacity-50\"\n :disabled=\"movieGenerating || anyBeatRendering || characterKeys.every((key) => charRenderState[key] === 'rendering')\"\n @click=\"generateAllCharacters\"\n >\n {{ m.generateAll }}\n </button>\n </div>\n <div class=\"flex gap-3 flex-wrap\">\n <div v-for=\"key in characterKeys\" :key=\"key\" class=\"flex flex-col items-center gap-1 w-36\">\n <!-- Character thumbnail -->\n <div\n class=\"relative w-36 h-36 rounded-lg border overflow-hidden bg-gray-50 flex items-center justify-center transition-colors\"\n :class=\"charDragOver[key] ? 'border-blue-400 bg-blue-50' : 'border-gray-200'\"\n @dragover=\"onCharDragOver($event, key)\"\n @dragleave=\"onCharDragLeave(key)\"\n @drop=\"onCharDrop($event, key)\"\n >\n <img\n v-if=\"charImages[key]\"\n :src=\"charImages[key]\"\n class=\"w-full h-full object-cover cursor-zoom-in\"\n :alt=\"key\"\n @click=\"openCharacterLightbox(key)\"\n />\n <template v-else-if=\"charRenderState[key] === 'rendering'\">\n <svg class=\"animate-spin w-4 h-4 text-green-400\" viewBox=\"0 0 24 24\" fill=\"none\">\n <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\" />\n <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8v8H4z\" />\n </svg>\n </template>\n <template v-else-if=\"charRenderState[key] === 'error'\">\n <span class=\"text-xs text-red-400 text-center px-1\">{{ charErrors[key] }}</span>\n </template>\n <template v-else>\n <span class=\"text-xs text-gray-300 text-center px-1 leading-tight\">{{ characterPrompt(key) }}</span>\n </template>\n <!-- Permanent drop hint -->\n <div v-if=\"!charDragOver[key]\" class=\"absolute bottom-0 inset-x-0 text-center text-xs text-gray-400 bg-white/70 py-0.5 pointer-events-none\">\n {{ m.orDropImage }}\n </div>\n <!-- Drop overlay -->\n <div v-if=\"charDragOver[key]\" class=\"absolute inset-0 flex items-center justify-center bg-blue-50/80 pointer-events-none\">\n <span class=\"text-xs text-blue-500 font-medium\">{{ m.drop }}</span>\n </div>\n <!-- Regenerate button -->\n <button\n v-if=\"charImages[key] && charRenderState[key] !== 'rendering'\"\n class=\"absolute top-0.5 right-0.5 px-1 py-0.5 text-xs rounded border bg-white\"\n :class=\"\n movieGenerating || anyBeatRendering ? 'border-yellow-400 text-yellow-500 cursor-not-allowed' : 'border-gray-400 text-gray-600 hover:bg-gray-50'\n \"\n :disabled=\"movieGenerating || anyBeatRendering\"\n @click.stop=\"renderCharacter(key, true)\"\n >\n <span v-if=\"movieGenerating || anyBeatRendering\" class=\"inline-block animate-spin\">↺</span>\n <span v-else>↺</span>\n </button>\n <!-- Generate button -->\n <button\n v-else-if=\"!charImages[key] && charRenderState[key] !== 'rendering'\"\n class=\"absolute top-0.5 right-0.5 px-1 py-0.5 text-xs rounded border bg-white\"\n :class=\"\n movieGenerating || anyBeatRendering ? 'border-yellow-400 text-yellow-500 cursor-not-allowed' : 'border-blue-400 text-blue-600 hover:bg-blue-50'\n \"\n :disabled=\"movieGenerating || anyBeatRendering\"\n @click.stop=\"renderCharacter(key, false)\"\n >\n <svg v-if=\"movieGenerating || anyBeatRendering\" class=\"animate-spin w-3 h-3\" viewBox=\"0 0 24 24\" fill=\"none\">\n <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\" />\n <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8v8H4z\" />\n </svg>\n <span v-else>{{ m.gen }}</span>\n </button>\n </div>\n <span class=\"text-xs text-gray-600 text-center truncate w-full\">{{ key }}</span>\n </div>\n </div>\n </div>\n\n <!-- Deck editor (#1575): every beat is a slide → mount the\n interactive deck editor from @mulmocast/deck-web. The Vue\n component is lazy-loaded via defineAsyncComponent, so users\n whose scripts aren't decks never pay the bundle cost. -->\n <div v-if=\"isDeck\" class=\"flex-1 overflow-hidden\" data-testid=\"mulmo-script-deck-editor\">\n <MulmoScriptDeckEditor :script=\"deckScriptInput\" layout=\"compact\" @update:script=\"onDeckUpdate\" />\n </div>\n\n <!-- Beat list (fallback when the script has any non-slide beat) -->\n <div v-else ref=\"beatListEl\" class=\"flex-1 overflow-y-auto p-2 space-y-1.5\">\n <div v-for=\"(beat, index) in beats\" :key=\"index\" class=\"rounded-lg border border-gray-200 overflow-hidden\">\n <!-- Beat body: thumbnail + narration side by side -->\n <div class=\"flex gap-3 items-stretch\">\n <!-- Thumbnail -->\n <div\n class=\"relative shrink-0 w-[45%] overflow-hidden bg-gray-50 transition-colors\"\n :class=\"beatDragOver[index] ? 'bg-blue-50' : ''\"\n @dragover=\"onBeatDragOver($event, index)\"\n @dragleave=\"onBeatDragLeave(index)\"\n @drop=\"onBeatDrop($event, index)\"\n >\n <!-- Inline player for the beat's generated video clip.\n Replaces the thumbnail while open; the close button\n returns to the still image. -->\n <template v-if=\"beatMovieOpen[index] && beatMovieUrls[index]\">\n <video :src=\"beatMovieUrls[index]\" class=\"w-full object-contain\" controls autoplay :data-testid=\"`mulmo-script-beat-movie-player-${index}`\" />\n <button\n class=\"absolute top-1.5 right-1.5 flex items-center justify-center w-6 h-6 rounded border border-gray-400 text-gray-600 bg-white hover:bg-gray-50\"\n :title=\"m.close\"\n :aria-label=\"m.close\"\n :data-testid=\"`mulmo-script-beat-movie-close-${index}`\"\n @click.stop=\"closeBeatMovie(index)\"\n >\n <span class=\"material-icons text-sm\">close</span>\n </button>\n </template>\n <template v-else>\n <img\n v-if=\"renderedImages[index]\"\n :src=\"renderedImages[index]\"\n class=\"w-full object-contain cursor-zoom-in\"\n :alt=\"`Beat ${index + 1}`\"\n @click=\"openLightbox(index)\"\n />\n <!-- Play overlay: shown when the beat-movie probe found a\n generated clip for this beat. Blob is fetched lazily\n on first click (host-authenticated), hence the spinner. -->\n <button\n v-if=\"renderedImages[index] && beatMovies[index] && canFetchMedia\"\n class=\"absolute inset-0 m-auto w-12 h-12 flex items-center justify-center rounded-full bg-black/50 text-white hover:bg-black/70\"\n :title=\"m.play\"\n :aria-label=\"m.play\"\n :data-testid=\"`mulmo-script-beat-movie-play-${index}`\"\n @click.stop=\"playBeatMovie(index)\"\n >\n <svg v-if=\"beatMovieLoading[index]\" class=\"animate-spin w-5 h-5\" viewBox=\"0 0 24 24\" fill=\"none\">\n <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\" />\n <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8v8H4z\" />\n </svg>\n <span v-else class=\"material-icons text-3xl\">play_arrow</span>\n </button>\n <button\n v-if=\"renderedImages[index] && renderState[index] !== 'rendering'\"\n class=\"absolute top-1.5 right-1.5 flex items-center gap-1 px-2 py-0.5 text-xs rounded border border-gray-400 text-gray-600 bg-white hover:bg-gray-50 disabled:opacity-60 disabled:cursor-not-allowed\"\n :disabled=\"movieGenerating\"\n @click.stop=\"regenerateBeat(index)\"\n >\n ↺\n </button>\n <div v-else-if=\"!renderedImages[index]\" class=\"w-full aspect-video flex flex-col items-center justify-center gap-1 p-2\">\n <template v-if=\"renderState[index] === 'rendering' || (movieGenerating && !renderedImages[index] && effectiveBeat(index).imagePrompt)\">\n <svg class=\"animate-spin w-4 h-4 text-green-400\" viewBox=\"0 0 24 24\" fill=\"none\">\n <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\" />\n <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8v8H4z\" />\n </svg>\n <span class=\"text-xs text-green-500\">{{ m.rendering }}</span>\n </template>\n <template v-else-if=\"renderState[index] === 'error'\">\n <span class=\"text-xs text-red-400 text-center\">{{ renderErrors[index] }}</span>\n </template>\n <template v-else>\n <span v-if=\"effectiveBeat(index).imagePrompt\" class=\"text-xs text-gray-400 text-center italic leading-relaxed px-1\">{{\n effectiveBeat(index).imagePrompt\n }}</span>\n <span v-else class=\"text-xs text-gray-300\">{{ beat.image?.type ?? \"—\" }}</span>\n </template>\n </div>\n </template>\n <!-- Beat drop hint / overlay -->\n <div v-if=\"beatDragOver[index]\" class=\"absolute inset-0 flex items-center justify-center bg-blue-50/80 pointer-events-none\">\n <span class=\"text-xs text-blue-500 font-medium\">{{ m.drop }}</span>\n </div>\n <div\n v-else-if=\"!renderedImages[index] && renderState[index] !== 'rendering'\"\n class=\"absolute bottom-0 inset-x-0 text-center text-xs text-gray-400 bg-white/70 py-0.5 pointer-events-none\"\n >\n {{ m.orDropImage }}\n </div>\n <!-- Generate button for any beat without a rendered image.\n renderBeat works for every beat type: imagePrompt /\n typed image beats render directly, moviePrompt beats\n get a frame extracted from the generated clip, and\n text-only beats fall back to a prompt derived from\n the narration text (mulmocast prompt.js). -->\n <button\n v-if=\"!renderedImages[index] && renderState[index] !== 'rendering' && !movieGenerating\"\n class=\"absolute top-1.5 right-1.5 flex items-center gap-1 px-2 py-0.5 text-xs rounded border border-blue-400 text-blue-600 bg-white hover:bg-blue-50\"\n @click=\"renderBeat(index)\"\n >\n {{ m.generate }}\n </button>\n </div>\n\n <!-- Narration text -->\n <div class=\"flex flex-col flex-1 min-w-0 px-2 py-1.5\">\n <span class=\"text-sm text-gray-800 leading-relaxed\">{{ effectiveBeat(index).text }}</span>\n <div class=\"flex justify-between mt-auto pt-1\">\n <!-- Audio controls -->\n <div class=\"flex items-center gap-1\">\n <template v-if=\"audioState[index] === 'generating' || (movieGenerating && !beatAudios[index] && effectiveBeat(index).text)\">\n <svg class=\"animate-spin w-3 h-3 text-green-400\" viewBox=\"0 0 24 24\" fill=\"none\">\n <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\" />\n <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8v8H4z\" />\n </svg>\n </template>\n <button\n v-else-if=\"beatAudios[index]\"\n class=\"text-xs px-2 py-0.5 rounded border\"\n :class=\"playingAudio?.index === index ? 'border-red-400 text-red-600 hover:bg-red-50' : 'border-green-400 text-green-600 hover:bg-green-50'\"\n @click=\"playAudio(index)\"\n >\n {{ playingAudio?.index === index ? m.stop : m.play }}\n </button>\n <template v-else-if=\"audioErrors[index]\">\n <span class=\"text-xs text-red-400 truncate min-w-0 max-w-[20rem]\" :title=\"audioErrors[index]\">\n {{ m.errPrefix }} {{ audioErrors[index] }}\n </span>\n <button\n v-if=\"effectiveBeat(index).text\"\n class=\"text-xs px-2 py-0.5 rounded border border-gray-300 text-gray-500 hover:bg-gray-50 disabled:opacity-50\"\n :disabled=\"movieGenerating\"\n @click=\"generateAudio(index)\"\n >\n ↺\n </button>\n </template>\n <button\n v-else-if=\"effectiveBeat(index).text\"\n class=\"text-xs px-2 py-0.5 rounded border border-gray-300 text-gray-500 hover:bg-gray-50\"\n @click=\"generateAudio(index)\"\n >\n {{ m.generateAudio }}\n </button>\n </div>\n <button\n class=\"text-gray-400 hover:text-gray-600\"\n :title=\"sourceOpen[index] ? 'Hide source' : 'Show source'\"\n :data-testid=\"`mulmo-script-beat-source-toggle-${index}`\"\n @click=\"toggleSource(index)\"\n >\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n class=\"w-3.5 h-3.5\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n >\n <polyline points=\"16 18 22 12 16 6\" />\n <polyline points=\"8 6 2 12 8 18\" />\n </svg>\n </button>\n </div>\n </div>\n </div>\n\n <!-- Source editor -->\n <div v-if=\"sourceOpen[index]\" class=\"border-t border-gray-100\">\n <textarea\n v-model=\"sourceText[index]\"\n class=\"w-full text-xs text-gray-600 bg-gray-50 p-2 font-mono resize-none\"\n :class=\"isValidBeat(index) ? 'outline-none' : 'outline outline-2 outline-red-400'\"\n rows=\"8\"\n spellcheck=\"false\"\n :data-testid=\"`mulmo-script-beat-source-textarea-${index}`\"\n />\n <div class=\"flex items-center justify-end gap-2 px-2 pb-2\">\n <span v-if=\"beatSaveErrors[index]\" class=\"text-xs text-red-600\" role=\"alert\">{{\n beatSaveErrors[index].kind === \"invalidJson\"\n ? m.saveErrorInvalidJson(beatSaveErrors[index].error)\n : m.saveErrorSaveFailed(beatSaveErrors[index].error)\n }}</span>\n <button\n class=\"px-2 py-1 text-xs rounded border\"\n :class=\"\n isValidBeat(index) && !beatSaving[index]\n ? 'border-blue-400 text-blue-600 hover:bg-blue-50 cursor-pointer'\n : 'border-gray-200 text-gray-300 cursor-not-allowed'\n \"\n :disabled=\"!isValidBeat(index) || !!beatSaving[index]\"\n :data-testid=\"`mulmo-script-beat-update-button-${index}`\"\n @click=\"updateBeat(index)\"\n >\n {{ beatSaving[index] ? m.saving : m.update }}\n </button>\n </div>\n </div>\n </div>\n\n <div v-if=\"beats.length === 0\" class=\"flex items-center justify-center h-32 text-gray-400 text-sm\">{{ m.noBeats }}</div>\n </div>\n\n <!-- Bottom bar: Edit Script Source + Copy -->\n <div class=\"bottom-bar-wrapper\">\n <details ref=\"sourceDetails\" class=\"script-source\" @toggle=\"onSourceToggle(($event.target as HTMLDetailsElement).open)\">\n <summary>{{ m.editSource }}</summary>\n <textarea\n v-model=\"editableSource\"\n class=\"script-editor\"\n :class=\"{ 'script-editor-invalid': sourceChanged && !sourceValid }\"\n spellcheck=\"false\"\n ></textarea>\n <div class=\"editor-actions\">\n <button class=\"apply-btn\" :disabled=\"!sourceChanged || !sourceValid\" @click=\"applySource\">{{ m.applyChanges }}</button>\n <button class=\"cancel-btn\" @click=\"cancelSourceEdit\">{{ m.cancel }}</button>\n </div>\n </details>\n <button v-show=\"!editing\" class=\"copy-btn\" :title=\"copied ? 'Copied!' : 'Copy'\" @click=\"copyText\">\n <span class=\"material-icons\">{{ copied ? \"check\" : \"content_copy\" }}</span>\n </button>\n </div>\n\n <!-- Lightbox -->\n <div v-if=\"lightbox\" class=\"fixed inset-0 z-50 bg-black/80 overflow-y-auto\" @click=\"closeLightbox\">\n <button class=\"fixed top-2 right-4 z-10 text-white/60 hover:text-white text-3xl leading-none\" :title=\"m.close\" @click.stop=\"closeLightbox\">✕</button>\n <div class=\"flex flex-col items-center gap-4 pt-4 pb-8\" @click.stop>\n <div class=\"flex items-center gap-4\">\n <button\n v-if=\"!lightbox.isCharacter\"\n class=\"text-white/60 hover:text-white disabled:opacity-20 text-5xl leading-none\"\n :disabled=\"!hasPrev\"\n @click=\"lightboxMove(-1)\"\n >\n ‹\n </button>\n <div class=\"flex flex-col items-center\">\n <img :src=\"lightbox.src\" class=\"max-w-[80vw] max-h-[85vh] object-contain rounded shadow-2xl\" />\n <div v-if=\"!lightbox.isCharacter && beats.length > 1\" class=\"relative w-full h-1\">\n <div class=\"flex gap-1 h-full\">\n <div\n v-for=\"i in beats.length\"\n :key=\"i - 1\"\n class=\"group flex-1 cursor-pointer relative transition-colors\"\n :class=\"\n i - 1 === lightbox.index\n ? 'bg-white/80 hover:bg-white'\n : i - 1 < lightbox.index\n ? 'bg-white/40 hover:bg-white/60'\n : 'bg-white/20 hover:bg-white/40'\n \"\n @click=\"jumpToBeat(i - 1)\"\n >\n <span class=\"absolute -inset-y-3 inset-x-0\" />\n <div\n v-if=\"beatTooltip(i - 1)\"\n class=\"absolute bottom-full mb-2 left-1/2 -translate-x-1/2 z-20 px-2 py-1 rounded bg-black/90 text-white text-xs leading-tight w-48 max-h-[53px] overflow-hidden opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity\"\n >\n {{ beatTooltip(i - 1) }}\n </div>\n </div>\n </div>\n <div\n v-if=\"playingAudio && playingAudio.index === lightbox.index\"\n class=\"absolute top-1/2 w-3.5 h-3.5 rounded-full bg-white shadow ring-2 ring-black/30 -translate-y-1/2 -translate-x-1/2 pointer-events-none\"\n :style=\"{ left: `${((lightbox.index + audioProgress) / beats.length) * 100}%` }\"\n />\n </div>\n </div>\n <button\n v-if=\"!lightbox.isCharacter\"\n class=\"text-white/60 hover:text-white disabled:opacity-20 text-5xl leading-none\"\n :disabled=\"!hasNext\"\n @click=\"lightboxMove(1)\"\n >\n ›\n </button>\n </div>\n <div v-if=\"lightbox.text || beatAudios[lightbox.index]\" class=\"relative w-screen flex justify-center px-16\">\n <p v-if=\"lightbox.text\" class=\"max-w-[80vw] text-center text-white leading-relaxed text-[clamp(0.8rem,1.76vw,1.6rem)]\">\n {{ lightbox.text }}\n </p>\n <button\n v-if=\"beatAudios[lightbox.index]\"\n class=\"absolute top-0 right-4 text-sm px-3 py-1 rounded border border-white/60 text-white/60 hover:bg-white/20\"\n @click=\"playAudio(lightbox.index)\"\n >\n {{ playingAudio?.index === lightbox.index ? m.stop : m.play }}\n </button>\n </div>\n </div>\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, defineAsyncComponent, onBeforeUnmount, onMounted, reactive, ref, watch } from \"vue\";\nimport type { ToolResultComplete } from \"gui-chat-protocol/vue\";\nimport { mulmoBeatSchema, mulmoScriptSchema } from \"@mulmocast/types\";\nimport type { SlideLayout, SlideTheme } from \"@mulmocast/deck-web\";\nimport type { MulmoScriptData } from \"../core/types\";\nimport type { MulmoScriptGenerationEvent } from \"../core/contract\";\nimport { getMissingCharacterKeys, isAllSlideDeck, isSameScript, beatMayHaveMovie, shouldAutoRenderBeat, validateBeatJSON } from \"./helpers\";\nimport { errorMessage, useClipboardCopy } from \"./support\";\nimport { useMulmoScriptTransport } from \"./transport\";\nimport { useHostAdapter } from \"./hostAdapter\";\nimport { useT } from \"../lang/index\";\n\n// Lazy-loaded so the deck editor's Vue / tailwind / SlidePreview chunk\n// stays out of the initial bundle for users whose scripts aren't decks\n// (movies, html_tailwind animations, mixed beats). `defineAsyncComponent`\n// triggers the dynamic import only when `isDeck` first flips true.\nconst MulmoScriptDeckEditor = defineAsyncComponent(() => import(\"@mulmocast/deck-web\").then((mod) => mod.MulmoScriptDeckEditor));\n\nconst api = useMulmoScriptTransport();\nconst adapter = useHostAdapter();\n// Media bytes (movie / PDF / beat clips) are served behind host auth; hosts\n// opt in by injecting `fetchMediaBlob`. Without it the download / clip-play\n// affordances are hidden (the probes still run — state stays warm for a\n// host that injects later at remount).\nconst canFetchMedia = computed(() => Boolean(adapter.fetchMediaBlob));\n\nconst m = useT();\n\ninterface Beat {\n speaker?: string;\n text?: string;\n id?: string;\n imagePrompt?: string;\n moviePrompt?: string;\n image?: { type: string; [key: string]: unknown };\n /** Beat duration in seconds. The mulmocast schema notes this is\n * \"Used only when the text is empty\" — when there's no TTS audio\n * to drive playback, the Play loop uses this as the auto-advance\n * timer (#1073). */\n duration?: number;\n}\n\ninterface ImageEntry {\n type: string;\n prompt?: string;\n [key: string]: unknown;\n}\n\ninterface MulmoScript {\n title?: string;\n description?: string;\n lang?: string;\n beats?: Beat[];\n imageParams?: {\n images?: Record<string, ImageEntry>;\n [key: string]: unknown;\n };\n [key: string]: unknown;\n}\n\nconst props = defineProps<{\n selectedResult: ToolResultComplete<MulmoScriptData>;\n}>();\nconst emit = defineEmits<{ updateResult: [result: ToolResultComplete] }>();\n\nconst data = computed(() => props.selectedResult.data);\nconst script = computed<MulmoScript>(() => data.value?.script ?? {});\nconst filePath = computed(() => data.value?.filePath ?? \"\");\nconst beats = computed<Beat[]>(() => script.value.beats ?? []);\n\n// Per-beat render state\ntype RenderState = \"idle\" | \"rendering\" | \"done\" | \"error\";\nconst renderState = reactive<Record<number, RenderState>>({});\nconst renderedImages = reactive<Record<number, string>>({});\nconst renderErrors = reactive<Record<number, string>>({});\nconst sourceOpen = reactive<Record<number, boolean>>({});\nconst sourceText = reactive<Record<number, string>>({});\n// Surface update-beat failures inline next to the Update button.\n// Cleared on next successful save or editor close. Store raw error +\n// kind tag so the template picks a localized message, instead of\n// pre-composing an English-prefixed string here.\ninterface BeatSaveError {\n kind: \"invalidJson\" | \"saveFailed\";\n error: string;\n}\nconst beatSaveErrors = reactive<Record<number, BeatSaveError>>({});\nconst beatSaving = reactive<Record<number, boolean>>({});\nconst localOverrides = reactive<Record<number, Beat>>({});\nconst movieGenerating = ref(false);\nconst movieDownloading = ref(false);\nconst moviePath = ref<string | null>(null);\n// Persists the most-recent movie-generation failure so the spinner\n// area can surface it inline with a retry button (#1197). Cleared\n// at the start of every generate / regenerate attempt.\nconst movieError = ref<string | null>(null);\n// PDF generation (#1614). Mirrors the movie triple — path / spinner /\n// downloading flag — kept independent so a PDF and a movie can be\n// generated for the same script without state collision.\nconst pdfGenerating = ref(false);\nconst pdfDownloading = ref(false);\nconst pdfPath = ref<string | null>(null);\nconst beatAudios = reactive<Record<number, string>>({});\nconst audioState = reactive<Record<number, \"generating\" | \"done\" | \"error\">>({});\nconst audioErrors = reactive<Record<number, string>>({});\n// Per-beat generated video clip (moviePrompt / animated beats).\n// `beatMovies` holds the \"stories/…\" wire path from the beat-movie\n// probe; the blob object URL is fetched lazily on first play through\n// the host adapter's authenticated `fetchMediaBlob` — a plain\n// <video src> can't attach the host's auth headers.\nconst beatMovies = reactive<Record<number, string>>({});\nconst beatMovieUrls = reactive<Record<number, string>>({});\nconst beatMovieOpen = reactive<Record<number, boolean>>({});\nconst beatMovieLoading = reactive<Record<number, boolean>>({});\nconst playingAudio = ref<{ index: number; audio: HTMLAudioElement } | null>(null);\n// Tracks the auto-advance timer running on a silent beat\n// (`beat.text === \"\"`). Beats without text generate no audio, so the\n// Play loop falls back to a `setTimeout(beat.duration)` for cues —\n// without this, Play would stall on the first silent beat (#1073).\nconst silentPlaybackTimer = ref<{ index: number; timer: ReturnType<typeof setTimeout> } | null>(null);\nconst audioProgress = ref(0);\n\n// Default duration (seconds) for a silent beat whose script doesn't\n// set `duration` either. Picked to roughly match the time it takes a\n// reader to scan a `textSlide` — long enough to read, short enough\n// not to feel stuck. The script's own `duration` always wins.\nconst SILENT_BEAT_DEFAULT_SEC = 3;\nconst MS_PER_SECOND = 1000;\nconst beatListEl = ref<HTMLElement | null>(null);\nconst lightbox = ref<{\n src: string;\n text?: string;\n index: number;\n isCharacter?: boolean;\n} | null>(null);\n// Character (imageParams.images) state\ntype CharRenderState = \"idle\" | \"rendering\" | \"done\" | \"error\";\nconst charRenderState = reactive<Record<string, CharRenderState>>({});\nconst charImages = reactive<Record<string, string>>({});\nconst charErrors = reactive<Record<string, string>>({});\nconst charDragOver = reactive<Record<string, boolean>>({});\nconst beatDragOver = reactive<Record<number, boolean>>({});\n\nconst anyBeatRendering = computed(() => Object.values(renderState).some((state) => state === \"rendering\"));\n\nconst characterKeys = computed(() => {\n const imgs = script.value.imageParams?.images ?? {};\n return Object.keys(imgs).filter((key) => imgs[key]?.type === \"imagePrompt\");\n});\n\n// Session tagging is host transport: MulmoClaude injects the active chat\n// session id so generations light its per-session sidebar indicator;\n// hosts without sessions leave the adapter empty and the field is simply\n// omitted from generation dispatches.\nconst chatSessionId = computed(() => adapter.chatSessionId?.value);\n\nfunction characterPrompt(key: string): string {\n return (script.value.imageParams?.images?.[key]?.prompt as string) ?? \"\";\n}\n\nfunction stopPlayingAudio() {\n // Single helper that clears both the audio path and the silent\n // auto-advance timer — callers (lightbox open / arrow nav / Stop\n // button) get consistent behaviour without remembering which\n // playback mode the current beat was using (#1073).\n stopAllPlayback();\n}\n\nfunction openLightbox(index: number) {\n stopPlayingAudio();\n lightbox.value = {\n src: renderedImages[index],\n text: effectiveBeat(index).text,\n index,\n };\n}\n\n// Backdrop click handler. Stops any in-flight narration so the audio\n// doesn't keep playing after the lightbox is dismissed — without this,\n// the HTMLAudioElement created by playAudio() outlives the modal and\n// the user hears disembodied narration with no UI to stop it.\nfunction closeLightbox() {\n stopPlayingAudio();\n lightbox.value = null;\n}\n\n// \"Play presentation\" toolbar action. Opens the lightbox at beat 0 and\n// kicks off its narration audio; the existing on-ended hook then chains\n// through the rest of the deck (lightboxMove(1) → playAudio if the next\n// beat has audio), so one click runs the whole presentation. Only wired\n// to the toolbar button when moviePath is set, which is our proxy for\n// \"every beat has both image and audio on disk\".\n//\n// `moviePath` arrives synchronously from movieStatus, but the per-beat\n// image and audio data URIs are populated asynchronously by\n// loadExistingBeatImage / loadExistingBeatAudio in initializeScript().\n// The Play button can therefore become visible before beat 0's assets\n// hydrate — `isPlayReady` gates the click so the lightbox never opens\n// with an undefined src or silent narration on a beat that does have\n// text.\nconst isPlayReady = computed<boolean>(() => {\n if (beats.value.length === 0) return false;\n if (!renderedImages[0]) return false;\n // Audio is only required when the beat has text (the source of TTS).\n // Beats without text are valid; they just play silently.\n if (effectiveBeat(0).text && !beatAudios[0]) return false;\n return true;\n});\n\nfunction playPresentation() {\n if (!isPlayReady.value) return;\n openLightbox(0);\n playBeat(0);\n}\n\n// Stop whichever playback handle is active. Idempotent. Called by\n// openLightbox, manual stop / pause buttons, and by `playBeat`\n// before kicking off a new beat so we never double-schedule. (#1073)\nfunction stopAllPlayback(): void {\n if (playingAudio.value) {\n playingAudio.value.audio.pause();\n playingAudio.value = null;\n audioProgress.value = 0;\n }\n if (silentPlaybackTimer.value) {\n clearTimeout(silentPlaybackTimer.value.timer);\n silentPlaybackTimer.value = null;\n }\n}\n\n// Single entry point for \"start playback at beat <index>\". Routes\n// on what the script DECLARED, not on what's currently hydrated:\n//\n// - `text` empty → silent path (`scheduleSilentAdvance`). The\n// schema says no audio is generated for empty-text beats, so\n// `duration` drives auto-advance.\n// - `text` present + audio loaded → audio path. `audio.ended`\n// chains via `advanceFromBeat`.\n// - `text` present + audio NOT loaded → stop. The Play button's\n// `isPlayReady` gate prevented this for beat 0, but mid-stream\n// a transient fetch miss must not silently skip the narration\n// by falling through to the silent timer (Codex review on\n// #1073 — gating on `beatAudios[index]` would do exactly that).\n//\n// Either path chains to the next beat via `advanceFromBeat`, so a\n// run of silent beats — or audio / silent / audio sequences —\n// plays through without manual interaction.\nfunction playBeat(index: number): void {\n stopAllPlayback();\n const hasText = Boolean(effectiveBeat(index).text);\n if (!hasText) {\n scheduleSilentAdvance(index);\n return;\n }\n if (beatAudios[index]) {\n playAudio(index);\n }\n // Text beat with no audio yet → stop. The user can re-click Play\n // once the audio finishes hydrating.\n}\n\nfunction scheduleSilentAdvance(index: number): void {\n // Defensively narrow the script-supplied duration. A bad value\n // (zero, negative, NaN, non-number) would otherwise collapse to\n // an immediate timeout and the Play loop would race through every\n // silent beat in a single tick (Codex review iter-5 on #1365).\n // Falling back to the default keeps the presentation watchable.\n const raw = effectiveBeat(index).duration;\n const seconds = typeof raw === \"number\" && Number.isFinite(raw) && raw > 0 ? raw : SILENT_BEAT_DEFAULT_SEC;\n const timer = setTimeout(() => {\n if (silentPlaybackTimer.value?.index !== index) return;\n silentPlaybackTimer.value = null;\n if (lightbox.value?.index === index) advanceFromBeat(index);\n }, seconds * MS_PER_SECOND);\n silentPlaybackTimer.value = { index, timer };\n}\n\nfunction advanceFromBeat(fromIndex: number): void {\n lightboxMove(1);\n const nextIndex = lightbox.value?.index;\n if (nextIndex === undefined || nextIndex === fromIndex) return;\n playBeat(nextIndex);\n}\n\nconst hasPrev = computed(() => {\n if (!lightbox.value) return false;\n for (let i = lightbox.value.index - 1; i >= 0; i--) {\n if (renderedImages[i]) return true;\n }\n return false;\n});\n\nconst hasNext = computed(() => {\n if (!lightbox.value) return false;\n for (let i = lightbox.value.index + 1; i < beats.value.length; i++) {\n if (renderedImages[i]) return true;\n }\n return false;\n});\n\nfunction jumpToBeat(index: number) {\n if (!lightbox.value) return;\n if (index === lightbox.value.index) return;\n if (!renderedImages[index]) return;\n // Carry the playback mode forward (audio OR silent timer) so a\n // user clicking the beat-strip thumbnail mid-playback keeps the\n // presentation rolling (#1073).\n const wasPlaying = playingAudio.value !== null || silentPlaybackTimer.value !== null;\n openLightbox(index);\n if (wasPlaying) playBeat(index);\n}\n\nfunction beatTooltip(index: number): string {\n const text = effectiveBeat(index).text ?? \"\";\n return text.length > 80 ? `${text.slice(0, 80)}…` : text;\n}\n\nfunction lightboxMove(delta: number) {\n if (!lightbox.value) return;\n const total = beats.value.length;\n // If a playback was in progress when the user clicked the arrow,\n // carry it forward to whichever beat we land on — `playBeat`\n // picks audio vs silent automatically. `openLightbox` stops the\n // current playback, so capture the flag BEFORE that and chain\n // AFTER. The on-ended / silent-advance paths already null their\n // own state before calling `lightboxMove`, so this branch won't\n // double-fire there.\n const wasPlaying = playingAudio.value !== null || silentPlaybackTimer.value !== null;\n let i = lightbox.value.index + delta;\n while (i >= 0 && i < total) {\n if (renderedImages[i]) {\n openLightbox(i);\n if (wasPlaying) playBeat(i);\n return;\n }\n i += delta;\n }\n}\nconst sourceDetails = ref<HTMLDetailsElement>();\nconst editing = ref(false);\nconst editableSource = ref(\"\");\nconst { copied, copy } = useClipboardCopy();\n\n// Beats may be edited in-place via `updateBeat()` and rendered through\n// `effectiveBeat()`, so the Copy / source-view text must read the merged\n// shape — otherwise the clipboard returns the original prop snapshot\n// until the full result is reloaded.\nconst effectiveScript = computed<MulmoScript>(() => ({\n ...script.value,\n beats: beats.value.map((beat, i) => localOverrides[i] ?? beat),\n}));\nconst scriptSourceText = computed(() => JSON.stringify(effectiveScript.value, null, 2));\n\n// #1575 — when every beat is a `slide`, swap the per-beat list UI for\n// the interactive deck editor (@mulmocast/deck-web). Mixed scripts\n// (any non-slide beat) fall back to the existing list so the user can\n// keep editing movie / textSlide / html_tailwind beats as before.\nconst isDeck = computed(() => isAllSlideDeck(effectiveScript.value));\n\n// `@mulmocast/deck-web` types its `script` prop as a *structural*\n// superset of MulmoScript (every key optional + index signature) using\n// `SlideLayout` / `SlideTheme` from `@mulmocast/deck`. Our strict\n// `MulmoScript` from `@mulmocast/types` doesn't unify with that shape\n// by name, so we re-type at the boundary. The cast is safe — any real\n// MulmoScript instance fits the structural shape. Mirrored on the way\n// out (`onDeckUpdate`).\ninterface DeckBeatShape {\n image?: {\n type?: string;\n slide?: SlideLayout;\n theme?: SlideTheme;\n [k: string]: unknown;\n };\n [k: string]: unknown;\n}\ninterface DeckScriptShape {\n beats?: DeckBeatShape[];\n presentationStyle?: { slideParams?: { theme?: SlideTheme } };\n slideParams?: { theme?: SlideTheme };\n [k: string]: unknown;\n}\nconst deckScriptInput = computed<DeckScriptShape>(() => effectiveScript.value as unknown as DeckScriptShape);\n\n// Debounce window for deck-editor → update-script. Drag a slide,\n// reorder, edit a field — each emit fires `update:script`, and we\n// only want one network round-trip per quiet stretch. 300ms is short\n// enough to feel live, long enough that typing in the Inspector\n// doesn't carpet-bomb the server.\nconst DECK_SAVE_DEBOUNCE_MS = 300;\nlet deckSaveTimer: ReturnType<typeof setTimeout> | null = null;\nlet pendingDeckScript: MulmoScript | null = null;\n\nfunction scheduleDeckSave(next: MulmoScript): void {\n pendingDeckScript = next;\n if (deckSaveTimer) clearTimeout(deckSaveTimer);\n deckSaveTimer = setTimeout(() => {\n void flushDeckSave();\n }, DECK_SAVE_DEBOUNCE_MS);\n}\n\nasync function flushDeckSave(): Promise<void> {\n deckSaveTimer = null;\n const next = pendingDeckScript;\n pendingDeckScript = null;\n if (!next || !filePath.value) return;\n const response = await api.call(\"updateScript\", {\n filePath: filePath.value,\n script: next,\n });\n if (!response.ok) {\n // Surface via console so the user can see what failed; a full\n // toast UI is P2. The deck editor still holds the latest edit\n // in its props until the next prop refresh, so the visible state\n // doesn't snap back on a transient failure.\n console.error(\"[presentMulmoScript] deck save failed:\", response.error);\n return;\n }\n // Mirror the JSON-source `applySource` flow so the parent's in-memory\n // script and our reactive beats[] stay in sync without a remount.\n emit(\"updateResult\", {\n ...props.selectedResult,\n data: { ...props.selectedResult.data, script: next },\n });\n}\n\nfunction onDeckUpdate(next: DeckScriptShape): void {\n scheduleDeckSave(next as unknown as MulmoScript);\n}\n\nonBeforeUnmount(() => {\n if (deckSaveTimer) {\n clearTimeout(deckSaveTimer);\n // Flush synchronously-scheduled work on unmount so a quick switch\n // away doesn't lose the last keystroke. Fire-and-forget — the\n // component is gone, we just want the bytes to land.\n void flushDeckSave();\n }\n // Release beat-clip blob object URLs — they outlive the component\n // otherwise (document-scoped, not GC'd with it).\n resetBeatMovies();\n unsubscribeGenerationEvents();\n});\nconst loadedSource = ref(\"\");\nconst sourceChanged = computed(() => editableSource.value !== loadedSource.value);\nconst sourceValid = computed(() => {\n try {\n const parsed = JSON.parse(editableSource.value);\n return mulmoScriptSchema.safeParse(parsed).success;\n } catch {\n return false;\n }\n});\n\nasync function onSourceToggle(open: boolean) {\n editing.value = open;\n if (open) {\n let text = scriptSourceText.value;\n // Re-read the current file from disk so beat-level edits made\n // since mount (other tabs, MCP, manual edits) surface in the\n // editor. Uses the reopen dispatch for the same reason\n // refreshScriptFromDisk does — `filePath.value` is the wire form\n // `stories/<rel>` and only the mulmoScript save/reopen op knows\n // how to map it to the on-disk path under `artifacts/stories/...`.\n if (filePath.value) {\n const response = await api.call(\"save\", { filePath: filePath.value });\n const diskScript = response.ok ? (response.data.script as MulmoScript | undefined) : undefined;\n if (diskScript) text = JSON.stringify(diskScript, null, 2);\n // fall through to in-memory script on failure\n }\n editableSource.value = text;\n loadedSource.value = text;\n }\n}\n\nfunction cancelSourceEdit() {\n if (sourceDetails.value) sourceDetails.value.open = false;\n}\n\nasync function applySource() {\n let parsed: MulmoScript;\n try {\n parsed = JSON.parse(editableSource.value);\n } catch (err) {\n alert(errorMessage(err));\n return;\n }\n const response = await api.call(\"updateScript\", {\n filePath: filePath.value,\n script: parsed,\n });\n if (!response.ok) {\n alert(response.error || \"Update failed\");\n return;\n }\n\n // Update the UI with the new script.\n // Note: the parent's handleUpdateResult uses Object.assign (in-place\n // mutation), so the watcher on props.selectedResult won't fire.\n // We emit first so the parent data is updated, then manually\n // re-initialize the view.\n emit(\"updateResult\", {\n ...props.selectedResult,\n data: { ...props.selectedResult.data, script: parsed },\n });\n\n if (sourceDetails.value) sourceDetails.value.open = false;\n await initializeScript();\n}\n\nasync function copyText() {\n await copy(scriptSourceText.value);\n}\n\nfunction effectiveBeat(index: number): Beat {\n return localOverrides[index] ?? beats.value[index] ?? {};\n}\n\nfunction toggleSource(index: number) {\n if (!sourceOpen[index]) {\n sourceText[index] = JSON.stringify(effectiveBeat(index), null, 2);\n Reflect.deleteProperty(beatSaveErrors, index);\n }\n sourceOpen[index] = !sourceOpen[index];\n}\n\nfunction isValidBeat(index: number): boolean {\n return validateBeatJSON(sourceText[index] ?? \"\", mulmoBeatSchema);\n}\n\nasync function updateBeat(index: number) {\n let beat: Beat;\n try {\n beat = JSON.parse(sourceText[index]);\n } catch (err) {\n beatSaveErrors[index] = { kind: \"invalidJson\", error: errorMessage(err) };\n return;\n }\n const prevImage = JSON.stringify(effectiveBeat(index).image);\n const prevText = effectiveBeat(index).text;\n\n const requestedFilePath = filePath.value;\n Reflect.deleteProperty(beatSaveErrors, index);\n beatSaving[index] = true;\n const response = await api.call(\"updateBeat\", {\n filePath: requestedFilePath,\n beatIndex: index,\n beat,\n });\n if (staleSince(requestedFilePath)) return;\n Reflect.deleteProperty(beatSaving, index);\n if (!response.ok) {\n beatSaveErrors[index] = { kind: \"saveFailed\", error: response.error };\n return;\n }\n\n localOverrides[index] = beat;\n sourceOpen[index] = false;\n\n if (JSON.stringify(beat.image) !== prevImage) {\n Reflect.deleteProperty(renderedImages, index);\n renderBeat(index);\n }\n\n // Audio files are content-addressed by the beat's text\n // (getBeatAudioPathOrUrl hashes text + voice), so after a text edit\n // the cached data URI belongs to the OLD narration. Drop it so the\n // \"Generate Audio\" button reappears, then re-probe — if the new text\n // matches previously generated audio (e.g. the edit was a revert),\n // the probe restores Play without a paid TTS call.\n if (beat.text !== prevText) {\n // If this beat's old narration is mid-playback, stop it first —\n // the deletes below remove the Play/Stop control from the row,\n // which would otherwise leave the stale audio playing with no\n // way to stop it (Codex review on #2143).\n if (playingAudio.value?.index === index) stopAllPlayback();\n Reflect.deleteProperty(beatAudios, index);\n Reflect.deleteProperty(audioState, index);\n Reflect.deleteProperty(audioErrors, index);\n if (beat.text) void loadExistingBeatAudio(index);\n }\n}\n\nasync function renderBeat(index: number) {\n const requestedFilePath = filePath.value;\n renderState[index] = \"rendering\";\n const response = await api.call(\"renderBeat\", {\n filePath: requestedFilePath,\n beatIndex: index,\n chatSessionId: chatSessionId.value,\n });\n if (staleSince(requestedFilePath)) return;\n if (!response.ok) {\n renderErrors[index] = response.error || \"Render failed\";\n renderState[index] = \"error\";\n return;\n }\n renderedImages[index] = response.data.image ?? \"\";\n renderState[index] = \"done\";\n refreshMissingCharacterImages();\n if (beatMayHaveMovie(effectiveBeat(index))) void loadExistingBeatMovie(index);\n}\n\nasync function regenerateBeat(index: number) {\n const requestedFilePath = filePath.value;\n Reflect.deleteProperty(renderedImages, index);\n invalidateBeatMovie(index);\n renderState[index] = \"rendering\";\n const response = await api.call(\"renderBeat\", {\n filePath: requestedFilePath,\n beatIndex: index,\n force: true,\n chatSessionId: chatSessionId.value,\n });\n if (staleSince(requestedFilePath)) return;\n if (!response.ok) {\n renderErrors[index] = response.error || \"Render failed\";\n renderState[index] = \"error\";\n return;\n }\n renderedImages[index] = response.data.image ?? \"\";\n renderState[index] = \"done\";\n if (beatMayHaveMovie(effectiveBeat(index))) void loadExistingBeatMovie(index);\n}\n\n// Stale-response guard shared by every per-beat/character loader and\n// mutator below: capture the wire path at call time and discard the\n// response when the user has navigated to a different result meanwhile —\n// otherwise late responses from script A's bulk mount-time probes would\n// write into the per-beat maps that now belong to script B.\nfunction staleSince(requestedFilePath: string): boolean {\n return filePath.value !== requestedFilePath;\n}\n\nasync function loadExistingBeatImage(index: number) {\n const requestedFilePath = filePath.value;\n const response = await api.call(\"beatImage\", { filePath: requestedFilePath, beatIndex: index });\n if (staleSince(requestedFilePath)) return;\n // silently ignore errors — image simply hasn't been generated yet\n if (response.ok && response.data.image) {\n renderedImages[index] = response.data.image;\n renderState[index] = \"done\";\n }\n}\n\nasync function loadExistingBeatAudio(index: number) {\n const requestedFilePath = filePath.value;\n const response = await api.call(\"beatAudio\", { filePath: requestedFilePath, beatIndex: index });\n if (staleSince(requestedFilePath)) return;\n // silently ignore errors\n if (response.ok && response.data.audio) {\n beatAudios[index] = response.data.audio;\n audioState[index] = \"done\";\n }\n}\n\nasync function loadExistingBeatMovie(index: number) {\n const requestedFilePath = filePath.value;\n const response = await api.call(\"beatMovie\", { filePath: requestedFilePath, beatIndex: index });\n if (staleSince(requestedFilePath)) return;\n // silently ignore errors — the clip simply hasn't been generated yet\n if (response.ok && response.data.moviePath) {\n beatMovies[index] = response.data.moviePath;\n }\n}\n\nasync function playBeatMovie(index: number) {\n const fetchMediaBlob = adapter.fetchMediaBlob;\n if (!fetchMediaBlob || !beatMovies[index] || beatMovieLoading[index]) return;\n if (beatMovieUrls[index]) {\n beatMovieOpen[index] = true;\n return;\n }\n beatMovieLoading[index] = true;\n try {\n // Re-type the .mov blob as video/mp4 — same ISO-BMFF family, and\n // <video> support for \"video/mp4\" is broader than \"video/quicktime\".\n const blob = new Blob([await fetchMediaBlob({ moviePath: beatMovies[index] })], { type: \"video/mp4\" });\n beatMovieUrls[index] = URL.createObjectURL(blob);\n beatMovieOpen[index] = true;\n } catch (err) {\n alert(errorMessage(err));\n } finally {\n Reflect.deleteProperty(beatMovieLoading, index);\n }\n}\n\nfunction closeBeatMovie(index: number) {\n Reflect.deleteProperty(beatMovieOpen, index);\n}\n\n// Drop one beat's cached clip (regenerate is about to replace it on\n// disk). Revoking the object URL frees the blob immediately.\nfunction invalidateBeatMovie(index: number): void {\n if (beatMovieUrls[index]) URL.revokeObjectURL(beatMovieUrls[index]);\n [beatMovies, beatMovieUrls, beatMovieOpen].forEach((map) => Reflect.deleteProperty(map, index));\n}\n\nfunction resetBeatMovies(): void {\n Object.values(beatMovieUrls).forEach((url) => URL.revokeObjectURL(url));\n [beatMovies, beatMovieUrls, beatMovieOpen, beatMovieLoading].forEach((map) => {\n Object.keys(map).forEach((key) => Reflect.deleteProperty(map, key));\n });\n}\n\nasync function generateAudio(index: number) {\n const requestedFilePath = filePath.value;\n audioState[index] = \"generating\";\n Reflect.deleteProperty(audioErrors, index);\n const response = await api.call(\"generateBeatAudio\", {\n filePath: requestedFilePath,\n beatIndex: index,\n chatSessionId: chatSessionId.value,\n });\n if (staleSince(requestedFilePath)) return;\n if (!response.ok) {\n audioErrors[index] = response.error || \"Audio generation failed\";\n audioState[index] = \"error\";\n return;\n }\n beatAudios[index] = response.data.audio ?? \"\";\n audioState[index] = \"done\";\n}\n\nfunction playAudio(index: number) {\n if (playingAudio.value) {\n playingAudio.value.audio.pause();\n const wasIndex = playingAudio.value.index;\n playingAudio.value = null;\n if (wasIndex === index) return;\n }\n const src = beatAudios[index];\n if (!src) return;\n const audio = new Audio(src);\n playingAudio.value = { index, audio };\n audioProgress.value = 0;\n audio.addEventListener(\"timeupdate\", () => {\n if (playingAudio.value?.index !== index) return;\n if (audio.duration > 0) audioProgress.value = audio.currentTime / audio.duration;\n });\n audio.addEventListener(\"ended\", () => {\n if (playingAudio.value?.index !== index) return;\n playingAudio.value = null;\n audioProgress.value = 0;\n if (lightbox.value?.index === index) advanceFromBeat(index);\n });\n audio.play();\n}\n\nfunction onBeatDragOver(event: DragEvent, index: number) {\n if (!event.dataTransfer?.types.includes(\"Files\")) return;\n event.preventDefault();\n beatDragOver[index] = true;\n}\n\nfunction onBeatDragLeave(index: number) {\n beatDragOver[index] = false;\n}\n\nasync function onBeatDrop(event: DragEvent, index: number) {\n event.preventDefault();\n beatDragOver[index] = false;\n const file = event.dataTransfer?.files[0];\n if (!file || !file.type.startsWith(\"image/\")) return;\n\n renderState[index] = \"rendering\";\n Reflect.deleteProperty(renderErrors, index);\n let imageData: string;\n try {\n imageData = await new Promise<string>((resolve, reject) => {\n const reader = new FileReader();\n reader.onload = () => resolve(reader.result as string);\n reader.onerror = reject;\n reader.readAsDataURL(file);\n });\n } catch (err) {\n renderErrors[index] = errorMessage(err);\n renderState[index] = \"error\";\n return;\n }\n const requestedFilePath = filePath.value;\n const response = await api.call(\"uploadBeatImage\", {\n filePath: requestedFilePath,\n beatIndex: index,\n imageData,\n });\n if (staleSince(requestedFilePath)) return;\n if (!response.ok) {\n renderErrors[index] = response.error || \"Upload failed\";\n renderState[index] = \"error\";\n return;\n }\n renderedImages[index] = response.data.image ?? \"\";\n renderState[index] = \"done\";\n}\n\nfunction onCharDragOver(event: DragEvent, key: string) {\n if (!event.dataTransfer?.types.includes(\"Files\")) return;\n event.preventDefault();\n charDragOver[key] = true;\n}\n\nfunction onCharDragLeave(key: string) {\n charDragOver[key] = false;\n}\n\nasync function onCharDrop(event: DragEvent, key: string) {\n event.preventDefault();\n charDragOver[key] = false;\n const file = event.dataTransfer?.files[0];\n if (!file || !file.type.startsWith(\"image/\")) return;\n\n charRenderState[key] = \"rendering\";\n Reflect.deleteProperty(charErrors, key);\n let imageData: string;\n try {\n imageData = await new Promise<string>((resolve, reject) => {\n const reader = new FileReader();\n reader.onload = () => resolve(reader.result as string);\n reader.onerror = reject;\n reader.readAsDataURL(file);\n });\n } catch (err) {\n charErrors[key] = errorMessage(err);\n charRenderState[key] = \"error\";\n return;\n }\n const requestedFilePath = filePath.value;\n const response = await api.call(\"uploadCharacterImage\", { filePath: requestedFilePath, key, imageData });\n if (staleSince(requestedFilePath)) return;\n if (!response.ok) {\n charErrors[key] = response.error || \"Upload failed\";\n charRenderState[key] = \"error\";\n return;\n }\n charImages[key] = response.data.image ?? \"\";\n charRenderState[key] = \"done\";\n}\n\nfunction openCharacterLightbox(key: string) {\n // Stop both audio and silent timer — character lightbox is\n // outside the play loop (#1073).\n stopAllPlayback();\n lightbox.value = {\n src: charImages[key],\n text: key,\n index: -1,\n isCharacter: true,\n };\n}\n\nasync function loadExistingCharacterImage(key: string) {\n const requestedFilePath = filePath.value;\n const response = await api.call(\"characterImage\", { filePath: requestedFilePath, key });\n if (staleSince(requestedFilePath)) return;\n // silently ignore errors\n if (response.ok && response.data.image) {\n charImages[key] = response.data.image;\n charRenderState[key] = \"done\";\n }\n}\n\nfunction refreshMissingCharacterImages() {\n getMissingCharacterKeys(characterKeys.value, charImages, charRenderState).forEach((key) => loadExistingCharacterImage(key));\n}\n\nasync function renderCharacter(key: string, force: boolean) {\n const requestedFilePath = filePath.value;\n charRenderState[key] = \"rendering\";\n Reflect.deleteProperty(charErrors, key);\n const response = await api.call(\"renderCharacter\", {\n filePath: requestedFilePath,\n key,\n force,\n chatSessionId: chatSessionId.value,\n });\n if (staleSince(requestedFilePath)) return;\n if (!response.ok) {\n charErrors[key] = response.error || \"Render failed\";\n charRenderState[key] = \"error\";\n return;\n }\n charImages[key] = response.data.image ?? \"\";\n charRenderState[key] = \"done\";\n}\n\nasync function generateAllCharacters() {\n await Promise.all(characterKeys.value.filter((key) => charRenderState[key] !== \"rendering\").map((key) => renderCharacter(key, false)));\n}\n\n// Probe the server for an existing beat PNG before triggering any\n// generation. Only auto-renders when the disk is empty AND the beat\n// is a deterministic type — imagePrompt beats are left empty so the\n// user clicks Generate explicitly (avoids surprise paid text2image\n// calls on every page refresh).\nasync function hydrateBeatImage(beat: Beat, index: number, hasCharacters: boolean, autoRenderTypes: readonly string[]): Promise<void> {\n await loadExistingBeatImage(index);\n if (renderedImages[index]) return;\n if (shouldAutoRenderBeat(beat, hasCharacters, autoRenderTypes)) {\n await renderBeat(index);\n }\n}\n\n/**\n * #1074 — keep the in-memory toolResult in sync with the on-disk\n * script file. `updateBeat` / `updateScript` persist edits to\n * disk, but the session entry that backs\n * `props.selectedResult.data.script` is never rewritten, so a\n * page reload + session-restore would otherwise surface stale\n * pre-edit content.\n *\n * Why the reopen dispatch, not a generic file read: `filePath`\n * is the wire form `stories/<rel>` which only the mulmoScript save\n * op knows how to translate back to the real on-disk path under\n * `artifacts/stories/...`. The reopen op is read-only when `script`\n * is omitted; it does NOT trigger movie generation.\n *\n * The flow silently bails on every failure mode so a missing /\n * malformed / deleted script file never blocks the rest of\n * `initializeScript`.\n *\n * Stale-response guard: capture `uuid` + `filePath` before the\n * `await`. If either has changed by the time the response lands\n * (the user navigated to a different result while the request\n * was in flight, or `props.selectedResult` was swapped under us\n * by a parent watcher), drop the response on the floor — the new\n * `initializeScript` invocation triggered by that change will\n * issue its own refresh against the correct file.\n */\nasync function refreshScriptFromDisk(): Promise<void> {\n const requestedFilePath = filePath.value;\n if (!requestedFilePath) return;\n const requestedUuid = props.selectedResult.uuid;\n const response = await api.call(\"save\", { filePath: requestedFilePath });\n if (props.selectedResult.uuid !== requestedUuid || filePath.value !== requestedFilePath) return;\n if (!response.ok) return;\n const diskScript = response.data.script as MulmoScript | undefined;\n // The server-side reopen op already validated against\n // `mulmoScriptSchema`, so a non-null `script` is trusted here —\n // we only need a presence check.\n if (!diskScript) return;\n if (isSameScript(diskScript, script.value)) return;\n emit(\"updateResult\", {\n ...props.selectedResult,\n data: { ...props.selectedResult.data, script: diskScript },\n });\n}\n\nasync function initializeScript() {\n // Stop any in-flight playback BEFORE we tear down per-script state\n // — a pending `silentPlaybackTimer` or running audio from the\n // previous script would otherwise fire `advanceFromBeat()` against\n // the new script's lightbox / beat list and either crash or\n // silently jump the new presentation forward. Also close any open\n // lightbox so the user lands on the clean View for the new result\n // (Codex review iter-4 on #1365).\n stopAllPlayback();\n lightbox.value = null;\n // Reset scroll position so new results start at the top\n if (beatListEl.value) beatListEl.value.scrollTop = 0;\n // Reset per-script state\n Object.keys(renderState).forEach((key) => Reflect.deleteProperty(renderState, key));\n Object.keys(renderedImages).forEach((key) => Reflect.deleteProperty(renderedImages, key));\n Object.keys(renderErrors).forEach((key) => Reflect.deleteProperty(renderErrors, key));\n Object.keys(sourceOpen).forEach((key) => Reflect.deleteProperty(sourceOpen, key));\n Object.keys(sourceText).forEach((key) => Reflect.deleteProperty(sourceText, key));\n Object.keys(beatSaveErrors).forEach((key) => Reflect.deleteProperty(beatSaveErrors, key));\n Object.keys(beatSaving).forEach((key) => Reflect.deleteProperty(beatSaving, key));\n Object.keys(localOverrides).forEach((key) => Reflect.deleteProperty(localOverrides, key));\n Object.keys(beatAudios).forEach((key) => Reflect.deleteProperty(beatAudios, key));\n Object.keys(audioState).forEach((key) => Reflect.deleteProperty(audioState, key));\n Object.keys(audioErrors).forEach((key) => Reflect.deleteProperty(audioErrors, key));\n Object.keys(charRenderState).forEach((key) => Reflect.deleteProperty(charRenderState, key));\n Object.keys(charImages).forEach((key) => Reflect.deleteProperty(charImages, key));\n Object.keys(charErrors).forEach((key) => Reflect.deleteProperty(charErrors, key));\n Object.keys(beatDragOver).forEach((key) => Reflect.deleteProperty(beatDragOver, key));\n resetBeatMovies();\n moviePath.value = null;\n pdfPath.value = null;\n // Movie/PDF spinners are per-script: without this reset, switching\n // away from a generating script would leave the new script's toolbar\n // spinning. The pendingGenerations snapshot below re-lights them when\n // the NEW script really does have work in flight.\n movieGenerating.value = false;\n pdfGenerating.value = false;\n movieError.value = null;\n if (sourceDetails.value) sourceDetails.value.open = false;\n\n // #1074 — re-read the script file from disk before per-beat\n // hydration. When the user switches between tool results inside\n // the same SPA mount and switches back, the in-memory toolResult\n // still carries whatever script was captured earlier, and\n // `localOverrides` (the only thing showing the user's edit since\n // the last save) is reset by initializeScript on remount.\n // Re-fetching from disk via the reopen op covers that gap.\n await refreshScriptFromDisk();\n\n // Mount-time policy: prefer the existing PNG on the server. Every\n // beat — deterministic AND imagePrompt — first probes beatImage,\n // and we only fall through to renderBeat() when the disk has nothing\n // yet AND the type is safe to auto-render (deterministic content,\n // no characters waiting). Without this probe a refresh would re-fire\n // generateBeatImage for every beat, and for imagePrompt beats that\n // means a paid text2image call against an image we already have.\n //\n // Stale-after-edit: if the user edits the script source the on-disk\n // PNG is no longer in sync with the new content, but we don't try to\n // detect that here — the per-beat ↺ button is one click away and a\n // page refresh re-runs this same probe, so the user can opt back into\n // a fresh render whenever they need to.\n const AUTO_RENDER_TYPES = [\"textSlide\", \"markdown\", \"chart\", \"mermaid\", \"html_tailwind\", \"slide\"] as const;\n const hasCharacters = characterKeys.value.length > 0;\n beats.value.forEach((beat, index) => {\n void hydrateBeatImage(beat, index, hasCharacters, AUTO_RENDER_TYPES);\n if (beat.text) loadExistingBeatAudio(index);\n if (beatMayHaveMovie(beat)) void loadExistingBeatMovie(index);\n });\n\n characterKeys.value.forEach((key) => loadExistingCharacterImage(key));\n\n if (filePath.value) {\n // Stale-response guard: if the user navigates to a different result\n // while these calls are in flight, their answers describe the OLD\n // script — drop them instead of stamping them onto the new one.\n const requestedFilePath = filePath.value;\n const isStale = () => filePath.value !== requestedFilePath;\n\n const response = await api.call(\"movieStatus\", { filePath: requestedFilePath });\n if (isStale()) return;\n if (response.ok && response.data.moviePath) {\n moviePath.value = response.data.moviePath;\n }\n // ignore errors\n // Also check whether a PDF was previously generated and is still\n // newer than the source; status returns null otherwise so the UI\n // re-offers the Generate button.\n const pdfResponse = await api.call(\"pdfStatus\", { filePath: requestedFilePath });\n if (isStale()) return;\n if (pdfResponse.ok && pdfResponse.data.pdfPath) {\n pdfPath.value = pdfResponse.data.pdfPath;\n }\n\n // Reflect any generations that were already in flight when we\n // mounted (user switched away mid-generation and came back).\n // Snapshot via dispatch; live updates arrive on the pubsub\n // subscription below.\n const pending = await api.call(\"pendingGenerations\", { filePath: requestedFilePath });\n if (isStale()) return;\n if (pending.ok) {\n for (const entry of pending.data.pending) {\n reflectGenerationStart(entry);\n }\n }\n }\n}\n\nonMounted(initializeScript);\nwatch(() => props.selectedResult, initializeScript);\n\n// Keep the view in sync with generations running anywhere — this View's\n// own long-held dispatches, a parallel tab, the agent's background\n// autoGenerateMovie. The host publishes `generation` events on the\n// plugin pubsub channel (started + finished, per beat and per artifact);\n// on start we mirror the local \"rendering\" state so spinners show even\n// after a remount, on finish we reload the relevant asset off disk.\nconst unsubscribeGenerationEvents = api.onGenerationEvent(\n () => filePath.value,\n (event) => {\n if (!event.done) {\n reflectGenerationStart(event);\n return;\n }\n // Fire-and-forget: swallow + log so a failed reload doesn't\n // surface as an unhandled rejection.\n reflectGenerationFinish(event).catch((err) => {\n console.error(\"[presentMulmoScript] reload on finish failed:\", err);\n });\n },\n);\n\nfunction reflectGenerationStart(entry: MulmoScriptGenerationEvent): void {\n if (entry.kind === \"beatImage\") {\n const idx = Number(entry.key);\n if (!renderedImages[idx]) renderState[idx] = \"rendering\";\n } else if (entry.kind === \"beatAudio\") {\n const idx = Number(entry.key);\n if (!beatAudios[idx]) audioState[idx] = \"generating\";\n } else if (entry.kind === \"characterImage\") {\n if (!charImages[entry.key]) charRenderState[entry.key] = \"rendering\";\n } else if (entry.kind === \"movie\") {\n movieGenerating.value = true;\n } else if (entry.kind === \"pdf\") {\n pdfGenerating.value = true;\n }\n}\n\nasync function reflectGenerationFinish(entry: MulmoScriptGenerationEvent): Promise<void> {\n if (entry.kind === \"beatImage\") {\n const idx = Number(entry.key);\n await loadExistingBeatImage(idx);\n if (beatMayHaveMovie(effectiveBeat(idx))) await loadExistingBeatMovie(idx);\n if (renderState[idx] === \"rendering\") Reflect.deleteProperty(renderState, idx);\n refreshMissingCharacterImages();\n } else if (entry.kind === \"beatAudio\") {\n const idx = Number(entry.key);\n await loadExistingBeatAudio(idx);\n if (audioState[idx] === \"generating\") Reflect.deleteProperty(audioState, idx);\n } else if (entry.kind === \"characterImage\") {\n await loadExistingCharacterImage(entry.key);\n if (charRenderState[entry.key] === \"rendering\") {\n Reflect.deleteProperty(charRenderState, entry.key);\n }\n } else if (entry.kind === \"movie\") {\n movieGenerating.value = false;\n await refreshMoviePath();\n } else if (entry.kind === \"pdf\") {\n pdfGenerating.value = false;\n await refreshPdfPath();\n }\n}\n\nasync function refreshMoviePath(): Promise<void> {\n const requestedFilePath = filePath.value;\n if (!requestedFilePath) return;\n const response = await api.call(\"movieStatus\", { filePath: requestedFilePath });\n if (filePath.value !== requestedFilePath) return;\n if (response.ok && response.data.moviePath) {\n moviePath.value = response.data.moviePath;\n }\n}\n\n// Long-held dispatch: resolves when the whole images → audio → movie\n// pipeline finishes (or fails). Per-beat progress arrives on the\n// pubsub `generation` channel and is applied by\n// `reflectGenerationFinish`, which reloads each asset off disk —\n// replacing the pre-extraction SSE stream.\nasync function generateMovie() {\n // This dispatch is held open for the whole pipeline (minutes). If the\n // user navigates to a different result meanwhile, the resolution\n // describes the OLD script — drop it; the new script's own\n // initializeScript / pubsub subscription owns the visible state.\n const requestedFilePath = filePath.value;\n movieGenerating.value = true;\n movieError.value = null;\n const response = await api.call(\"generateMovie\", {\n filePath: requestedFilePath,\n chatSessionId: chatSessionId.value,\n });\n if (filePath.value !== requestedFilePath) return;\n movieGenerating.value = false;\n if (!response.ok) {\n // Surface inline (instead of `alert()` which blocks + has no\n // retry affordance). The error chip with a retry button lives\n // next to the generate button in the template (#1197).\n movieError.value = response.error;\n return;\n }\n moviePath.value = response.data.moviePath;\n}\n\n// Authenticated movie download through the host adapter (which attaches\n// whatever auth its media route needs — a plain `<a href download>`\n// cannot). The blob is hooked to a synthetic anchor whose `download`\n// attribute carries the filename — the browser still surfaces a native\n// save dialog.\nasync function downloadMovie() {\n const fetchMediaBlob = adapter.fetchMediaBlob;\n if (!fetchMediaBlob || !moviePath.value || movieDownloading.value) return;\n movieDownloading.value = true;\n let objectUrl: string | null = null;\n try {\n const blob = await fetchMediaBlob({ moviePath: moviePath.value });\n objectUrl = URL.createObjectURL(blob);\n const filename = moviePath.value.split(\"/\").pop() ?? \"movie.mp4\";\n const anchor = document.createElement(\"a\");\n anchor.href = objectUrl;\n anchor.download = filename;\n document.body.appendChild(anchor);\n anchor.click();\n anchor.remove();\n } catch (err) {\n alert(errorMessage(err));\n } finally {\n if (objectUrl) URL.revokeObjectURL(objectUrl);\n movieDownloading.value = false;\n }\n}\n\n// --- PDF (#1614) ---------------------------------------------------\n//\n// Same triple as movie: status poll → long-held generate dispatch →\n// authenticated download. Per-beat image progress arrives on the same\n// pubsub `generation` channel.\n\nasync function refreshPdfPath(): Promise<void> {\n const requestedFilePath = filePath.value;\n if (!requestedFilePath) return;\n const response = await api.call(\"pdfStatus\", { filePath: requestedFilePath });\n if (filePath.value !== requestedFilePath) return;\n if (response.ok && response.data.pdfPath) {\n pdfPath.value = response.data.pdfPath;\n }\n}\n\nasync function generatePdf() {\n // Long-held dispatch — same stale-navigation guard as generateMovie.\n const requestedFilePath = filePath.value;\n pdfGenerating.value = true;\n const response = await api.call(\"generatePdf\", {\n filePath: requestedFilePath,\n chatSessionId: chatSessionId.value,\n });\n if (filePath.value !== requestedFilePath) return;\n pdfGenerating.value = false;\n if (!response.ok) {\n alert(response.error);\n return;\n }\n pdfPath.value = response.data.pdfPath;\n}\n\nasync function downloadPdf() {\n const fetchMediaBlob = adapter.fetchMediaBlob;\n if (!fetchMediaBlob || !pdfPath.value || pdfDownloading.value) return;\n pdfDownloading.value = true;\n let objectUrl: string | null = null;\n try {\n const blob = await fetchMediaBlob({ pdfPath: pdfPath.value });\n objectUrl = URL.createObjectURL(blob);\n const filename = pdfPath.value.split(\"/\").pop() ?? \"deck.pdf\";\n const anchor = document.createElement(\"a\");\n anchor.href = objectUrl;\n anchor.download = filename;\n document.body.appendChild(anchor);\n anchor.click();\n anchor.remove();\n } catch (err) {\n alert(errorMessage(err));\n } finally {\n if (objectUrl) URL.revokeObjectURL(objectUrl);\n pdfDownloading.value = false;\n }\n}\n</script>\n\n<style scoped>\n.bottom-bar-wrapper {\n position: relative;\n flex-shrink: 0;\n}\n\n.script-source {\n padding: 0.5rem;\n background: #f5f5f5;\n border-top: 1px solid #e0e0e0;\n font-family: Consolas, \"MS Gothic\", \"BIZ UDGothic\", monospace;\n font-size: 0.85rem;\n}\n\n.script-source summary {\n cursor: pointer;\n user-select: none;\n padding: 0.5rem;\n background: #e8e8e8;\n border-radius: 4px;\n font-weight: 500;\n color: #333;\n}\n\n.script-source[open] summary {\n margin-bottom: 0.5rem;\n}\n\n.script-source summary:hover {\n background: #d8d8d8;\n}\n\n.script-editor {\n width: 100%;\n height: 40vh;\n padding: 1rem;\n background: #ffffff;\n border: 1px solid #ccc;\n border-radius: 4px;\n color: #333;\n font-family: \"Courier New\", \"MS Gothic\", \"BIZ UDGothic\", monospace;\n font-size: 0.9rem;\n resize: vertical;\n margin-bottom: 0.5rem;\n line-height: 1.5;\n}\n\n.script-editor:focus {\n outline: none;\n border-color: #4caf50;\n box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.1);\n}\n\n.script-editor-invalid {\n border-color: #ef4444;\n}\n\n.script-editor-invalid:focus {\n border-color: #ef4444;\n box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.1);\n}\n\n.editor-actions {\n display: flex;\n justify-content: space-between;\n}\n\n.apply-btn {\n padding: 0.5rem 1rem;\n background: #4caf50;\n color: white;\n border: none;\n border-radius: 4px;\n cursor: pointer;\n font-size: 0.9rem;\n transition: background 0.2s;\n font-weight: 500;\n}\n\n.apply-btn:hover {\n background: #45a049;\n}\n\n.apply-btn:disabled {\n background: #cccccc;\n color: #666666;\n cursor: not-allowed;\n opacity: 0.6;\n}\n\n.cancel-btn {\n padding: 0.5rem 1rem;\n background: #e0e0e0;\n color: #333;\n border: none;\n border-radius: 4px;\n cursor: pointer;\n font-size: 0.9rem;\n transition: background 0.2s;\n font-weight: 500;\n}\n\n.cancel-btn:hover {\n background: #d0d0d0;\n}\n\n.copy-btn {\n position: absolute;\n bottom: 0.3rem;\n right: 0.65rem;\n padding: 0.4rem;\n background: none;\n border: none;\n color: #333;\n cursor: pointer;\n z-index: 1;\n}\n\n.copy-btn:hover {\n color: #000;\n}\n\n.copy-btn .material-icons {\n font-size: 1.15rem;\n}\n</style>\n","<template>\n <div class=\"p-2 text-sm\" data-testid=\"mulmo-script-preview\">\n <div class=\"font-medium text-gray-700 truncate mb-1\" data-testid=\"mulmo-script-preview-title\">\n {{ title }}\n </div>\n <div v-if=\"description\" class=\"text-xs text-gray-500 leading-relaxed\" data-testid=\"mulmo-script-preview-description\">\n {{ description }}\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed } from \"vue\";\nimport type { ToolResultComplete } from \"gui-chat-protocol/vue\";\nimport type { MulmoScriptData } from \"../core/types\";\n\nconst props = defineProps<{ result: ToolResultComplete<MulmoScriptData> }>();\n\nconst data = computed(() => props.result.data);\nconst script = computed(() => data.value?.script);\nconst title = computed(() => script.value?.title || data.value?.filePath?.split(\"/\").pop() || \"MulmoScript\");\nconst description = computed(() => script.value?.description);\n</script>\n","<template>\n <div class=\"p-2 text-sm\" data-testid=\"mulmo-script-preview\">\n <div class=\"font-medium text-gray-700 truncate mb-1\" data-testid=\"mulmo-script-preview-title\">\n {{ title }}\n </div>\n <div v-if=\"description\" class=\"text-xs text-gray-500 leading-relaxed\" data-testid=\"mulmo-script-preview-description\">\n {{ description }}\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed } from \"vue\";\nimport type { ToolResultComplete } from \"gui-chat-protocol/vue\";\nimport type { MulmoScriptData } from \"../core/types\";\n\nconst props = defineProps<{ result: ToolResultComplete<MulmoScriptData> }>();\n\nconst data = computed(() => props.result.data);\nconst script = computed(() => data.value?.script);\nconst title = computed(() => script.value?.title || data.value?.filePath?.split(\"/\").pop() || \"MulmoScript\");\nconst description = computed(() => script.value?.description);\n</script>\n","import \"../style.css\";\n\nimport type { ToolPlugin } from \"gui-chat-protocol/vue\";\nimport type { MulmoScriptData, SaveMulmoScriptArgs } from \"../core/types\";\nimport { pluginCore } from \"../core/plugin\";\nimport View from \"./View.vue\";\nimport Preview from \"./Preview.vue\";\n\nexport const plugin: ToolPlugin<MulmoScriptData, MulmoScriptData, SaveMulmoScriptArgs> = {\n ...pluginCore,\n viewComponent: View,\n previewComponent: Preview,\n};\n\nexport type { MulmoScriptData, MulmoScriptExecuteContext, SaveMulmoScriptArgs } from \"../core/types\";\nexport type { MulmoScriptDispatchArgs, MulmoScriptDispatchResult, MulmoScriptGenerationEvent, DispatchEnvelope, DispatchFailure } from \"../core/contract\";\nexport { GENERATION_EVENT } from \"../core/contract\";\nexport { TOOL_NAME, TOOL_DEFINITION } from \"../core/definition\";\nexport { MULMOSCRIPT_HOST_ADAPTER_KEY, useHostAdapter, type MulmoScriptHostAdapter } from \"./hostAdapter\";\nexport { useMulmoScriptTransport, type MulmoScriptTransport, type TransportResult } from \"./transport\";\nexport { View, Preview };\n\nexport default { plugin };\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"vue.cjs","names":[],"sources":["../src/vue/support.ts","../src/vue/helpers.ts","../src/vue/transport.ts","../src/vue/hostAdapter.ts","../src/vue/composables/useMediaExport.ts","../src/vue/composables/useBeatMovie.ts","../src/vue/composables/useCharacterImages.ts","../src/vue/composables/useDeckEditor.ts","../src/lang/de.ts","../src/lang/en.ts","../src/lang/es.ts","../src/lang/fr.ts","../src/lang/ja.ts","../src/lang/ko.ts","../src/lang/ptBR.ts","../src/lang/zh.ts","../src/lang/index.ts","../src/vue/components/BeatLightbox.vue","../src/vue/components/BeatLightbox.vue","../src/vue/components/CharacterStrip.vue","../src/vue/components/CharacterStrip.vue","../src/vue/components/MulmoScriptToolbar.vue","../src/vue/components/MulmoScriptToolbar.vue","../src/vue/View.vue","../src/vue/View.vue","../src/vue/Preview.vue","../src/vue/Preview.vue","../src/vue/index.ts"],"sourcesContent":["// Small host-independent utilities the View needs, ported from\n// MulmoClaude's `src/composables/useClipboardCopy.ts` so the package has no\n// host imports. (`errorMessage` moved to `@mulmoclaude/common`.)\n\nimport { ref, type Ref } from \"vue\";\n\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/** Read a dropped image File as a base64 data URL, the form the upload\n * dispatches expect. Shared by the beat and character drop handlers.\n * `readAsDataURL` always yields a string on load; the non-string reject\n * is an unreachable guard kept so the resolve type stays `string` without\n * a cast. */\nexport function readFileAsDataUrl(file: File): Promise<string> {\n return new Promise<string>((resolve, reject) => {\n const reader = new FileReader();\n reader.onload = () => {\n const { result } = reader;\n if (typeof result === \"string\") resolve(result);\n else reject(new Error(\"FileReader did not return a data URL string\"));\n };\n reader.onerror = reject;\n reader.readAsDataURL(file);\n });\n}\n\nexport interface UseClipboardCopyHandle {\n copied: Ref<boolean>;\n copy: (text: string) => Promise<void>;\n}\n\n/** Clipboard failures (permissions, insecure context) are swallowed on\n * purpose: the UI just leaves the \"Copied!\" hint off, which is what\n * `copied=false` already signals. */\nexport function useClipboardCopy(resetMs = 2000): UseClipboardCopyHandle {\n const copied = ref(false);\n\n async function copy(text: string): Promise<void> {\n try {\n await navigator.clipboard.writeText(text);\n copied.value = true;\n setTimeout(() => {\n copied.value = false;\n }, resetMs);\n } catch {\n // Clipboard API blocked (iframe without permissions, non-HTTPS origin) — leave `copied` false.\n }\n }\n\n return { copied, copy };\n}\n","// Pure helpers for the presentMulmoScript View. Kept separate so their\n// logic is unit-testable without mounting the Vue component. Ported from\n// the host's `src/plugins/presentMulmoScript/helpers.ts`; the SSE-stream\n// helpers did not move — per-beat generation progress now arrives on the\n// plugin pubsub channel (see `core/contract.ts`).\n\nimport { isRecord } from \"./support\";\n\n/**\n * Decide whether a beat should be rendered automatically at\n * script load time. Text-based beats (slides, charts, etc.) are\n * auto-rendered only when the script has no characters —\n * characters must be rendered first so they can be referenced by\n * any character-using beat.\n */\nexport function shouldAutoRenderBeat(beat: { image?: { type?: string } }, hasCharacters: boolean, autoRenderTypes: readonly string[]): boolean {\n if (hasCharacters) return false;\n const type = beat.image?.type;\n if (typeof type !== \"string\") return false;\n return autoRenderTypes.includes(type);\n}\n\n/**\n * Of the given character keys, return those whose image is not\n * yet loaded and is not currently rendering. Used to fetch only\n * what's missing after a movie-generation event arrives.\n */\nexport function getMissingCharacterKeys(keys: readonly string[], images: Record<string, unknown>, renderState: Record<string, string | undefined>): string[] {\n return keys.filter((charKey) => !images[charKey] && renderState[charKey] !== \"rendering\");\n}\n\n/**\n * A schema shape that exposes `safeParse` — matches Zod's API\n * without pulling the dep into this module.\n */\nexport interface SafeParseSchema {\n safeParse: (value: unknown) => { success: boolean };\n}\n\n/**\n * Validate a candidate Beat JSON string against a schema.\n * Returns false on any JSON parse error or schema mismatch.\n */\nexport function validateBeatJSON(json: string, schema: SafeParseSchema): boolean {\n let parsed: unknown;\n try {\n parsed = JSON.parse(json);\n } catch {\n return false;\n }\n return schema.safeParse(parsed).success;\n}\n\n/**\n * Stable structural equality for two MulmoScripts via JSON\n * canonicalisation. We compare the full re-serialised string\n * rather than walking keys because (a) MulmoScript is\n * deeply-nested and Object.keys-recursion would be ~50 lines, and\n * (b) `JSON.stringify` already preserves insertion order, which\n * `mulmoScriptSchema.safeParse` keeps stable across runs of the\n * same input. False positives (= \"differ\" when they don't) only\n * cost an extra `emit(\"updateResult\", ...)` which is a no-op when\n * data hasn't actually changed.\n */\nexport function isSameScript(left: unknown, right: unknown): boolean {\n return JSON.stringify(left) === JSON.stringify(right);\n}\n\n/**\n * True when a beat can have a generated video clip on disk — used to\n * decide whether to probe the beat-movie endpoint. `moviePrompt`\n * beats produce a per-beat movie file; `html_tailwind` beats with\n * `animation` set (either `true` or an options object) produce an\n * `_animated.mp4` render.\n */\nexport function beatMayHaveMovie(beat: { moviePrompt?: string; image?: { type?: string; animation?: unknown } }): boolean {\n if (beat.moviePrompt) return true;\n return beat.image?.type === \"html_tailwind\" && Boolean(beat.image.animation);\n}\n\n/**\n * True for a beat whose image merely REFERENCES another beat's image\n * (`image: { type: \"beat\", id }` — mulmoBeatReferenceMediaSchema). Such a\n * beat owns no asset of its own, so there is nothing to generate for it:\n * the View hides the Generate button (offering it produced a render that\n * could never succeed on its own terms).\n */\nexport function isBeatImageReference(beat: { image?: { type?: string; [key: string]: unknown } }): boolean {\n return beat.image?.type === \"beat\";\n}\n\n/** Pure check: is every beat in the script a `slide`-typed beat?\n * When true, the View mounts `@mulmocast/deck-web`'s\n * `MulmoScriptDeckEditor` instead of the per-beat list UI (#1575).\n * Empty / missing `beats[]` returns false — there's nothing to edit\n * as a deck, fall through to the existing UI which renders an empty\n * state. Mixed scripts (any non-`slide` beat) also return false; that\n * case is deferred to a future phase. */\nexport function isAllSlideDeck(script: unknown): boolean {\n if (!isRecord(script)) return false;\n const { beats } = script;\n if (!Array.isArray(beats) || beats.length === 0) return false;\n return beats.every((beat) => {\n if (!isRecord(beat)) return false;\n const { image } = beat;\n return isRecord(image) && image.type === \"slide\";\n });\n}\n\n/** A single MulmoScript beat as the View consumes it — every field\n * optional so the empty-beat fallback (`effectiveBeat` on an\n * out-of-range index) is a valid instance without a cast. */\nexport interface Beat {\n speaker?: string;\n text?: string;\n id?: string;\n imagePrompt?: string;\n moviePrompt?: string;\n image?: { type: string; [key: string]: unknown };\n /** Beat duration in seconds. The mulmocast schema notes this is\n * \"Used only when the text is empty\" — the silent-beat Play loop\n * uses it as the auto-advance timer (#1073). */\n duration?: number;\n}\n\n/** Resolve the beat the View should render at `index`: the user's\n * in-place edit (`overrides`) wins over the on-disk beat, and an\n * out-of-range index yields an empty beat so callers can read\n * `.text` / `.image` without a guard. */\nexport function effectiveBeat(overrides: Record<number, Beat>, beats: readonly Beat[], index: number): Beat {\n return overrides[index] ?? beats[index] ?? {};\n}\n\nconst BEAT_TOOLTIP_MAX_CHARS = 80;\n\n/** Beat-strip hover tooltip: the beat text, truncated with an ellipsis\n * past the cap. Missing text yields an empty string. Text of exactly\n * the cap length is returned whole (only a longer string is cut). */\nexport function beatTooltip(text: string | undefined): string {\n const value = text ?? \"\";\n return value.length > BEAT_TOOLTIP_MAX_CHARS ? `${value.slice(0, BEAT_TOOLTIP_MAX_CHARS)}…` : value;\n}\n\n/** The prompt for a character image, or \"\" when the key or its prompt\n * is absent — the character strip renders the empty string as no\n * caption rather than `undefined`. */\nexport function characterPrompt(images: Record<string, { prompt?: string }> | undefined, key: string): string {\n return images?.[key]?.prompt ?? \"\";\n}\n\n/** Is the in-editor JSON for a beat currently valid? A missing entry\n * (source editor never opened) validates the empty string, which is\n * not parseable JSON, so it reports invalid rather than throwing. */\nexport function isValidBeat(source: string | undefined, schema: SafeParseSchema): boolean {\n return validateBeatJSON(source ?? \"\", schema);\n}\n\n/** Stale-response guard: a per-beat / per-character response is stale\n * once the View has navigated to a different result, i.e. the current\n * file path no longer matches the one captured when the call was made.\n * Keeping the direction pinned matters — an inverted check would let\n * script A's late responses write into script B's state. */\nexport function staleSince(currentFilePath: string, requestedFilePath: string): boolean {\n return currentFilePath !== requestedFilePath;\n}\n\nconst JSON_INDENT = 2;\n\n/** Pretty-print a script (or any value) as the source-editor / clipboard\n * text — two-space indent, matching what the beat and disk views emit. */\nexport function scriptSourceText(value: unknown): string {\n return JSON.stringify(value, null, JSON_INDENT);\n}\n\n/** Basename for a download `<a download>` attribute, falling back when\n * the path has no basename. Mirrors the exact existing behaviour, and\n * it has a sharp edge: `.pop()` returns \"\" (not undefined) for a\n * trailing slash or empty path, and `??` does NOT replace \"\", so those\n * yield an empty filename rather than the fallback. Server paths always\n * carry a basename, so this never bites in practice — pinned so a later\n * reader doesn't \"simplify\" `??` to `||` and change behaviour. */\nexport function downloadFilename(path: string, fallback: string): string {\n return path.split(\"/\").pop() ?? fallback;\n}\n\n/** Narrow a script-supplied silent-beat duration to a safe positive number.\n * Zero / negative / NaN / Infinity / non-number collapse the auto-advance\n * timer to an immediate fire, which races the Play loop through every silent\n * beat in a single tick (#1365) — fall back to the default so a run of silent\n * beats stays watchable. The script's own valid `duration` always wins. */\nexport function resolveSilentAdvanceSeconds(raw: unknown, defaultSec: number): number {\n return typeof raw === \"number\" && Number.isFinite(raw) && raw > 0 ? raw : defaultSec;\n}\n\n/** Delete every own enumerable key of each record, in place. Used to reset the\n * View's per-beat / per-character reactive maps between scripts — passing the\n * reactive proxies mutates them so the template re-renders empty. Replaces a\n * wall of hand-rolled `Object.keys(map).forEach(delete)` loops. */\nexport function clearReactiveRecords(...records: object[]): void {\n records.forEach((record) => {\n Object.keys(record).forEach((key) => Reflect.deleteProperty(record, key));\n });\n}\n","// Host-agnostic transport for the presentMulmoScript View. Every operation\n// goes through `useRuntime().dispatch({ kind, … })` and returns the same\n// `{ ok, data | error }` shape the pre-extraction `apiGet`/`apiPost`\n// helpers produced, so the View's call sites stay structurally identical.\n//\n// Dispatch responses are `{ ok: … }` envelopes (see `core/contract.ts`):\n// business failures arrive as `{ ok: false, error }` data rather than HTTP\n// errors, keeping user-facing messages free of transport prefixes. A thrown\n// dispatch (network drop, host bug) is caught and folded into the same\n// failure shape.\n\nimport { useRuntime } from \"gui-chat-protocol/vue\";\nimport type { MulmoScriptDispatchArgs, MulmoScriptDispatchResult, MulmoScriptGenerationEvent } from \"../core/contract\";\nimport { GENERATION_EVENT } from \"../core/contract\";\nimport { errorMessage } from \"@mulmoclaude/common\";\nimport { isRecord } from \"./support\";\n\nexport type TransportResult<T> = { ok: true; data: T } | { ok: false; error: string };\n\ntype ArgsFor<K extends MulmoScriptDispatchArgs[\"kind\"]> = Omit<Extract<MulmoScriptDispatchArgs, { kind: K }>, \"kind\">;\n\nconst GENERATION_EVENT_KINDS: ReadonlySet<string> = new Set([\"beatImage\", \"beatAudio\", \"characterImage\", \"movie\", \"pdf\"]);\n\nfunction parseGenerationEvent(payload: unknown): MulmoScriptGenerationEvent | null {\n if (!isRecord(payload)) return null;\n const { kind, filePath, key, done, error } = payload;\n if (typeof kind !== \"string\" || !GENERATION_EVENT_KINDS.has(kind)) return null;\n if (typeof filePath !== \"string\" || typeof key !== \"string\" || typeof done !== \"boolean\") return null;\n return {\n kind: kind as MulmoScriptGenerationEvent[\"kind\"],\n filePath,\n key,\n done,\n ...(typeof error === \"string\" ? { error } : {}),\n };\n}\n\nexport interface MulmoScriptTransport {\n call<K extends MulmoScriptDispatchArgs[\"kind\"]>(kind: K, args: ArgsFor<K>): Promise<TransportResult<MulmoScriptDispatchResult[K]>>;\n /** Subscribe to the host's generation channel, pre-filtered to one\n * script's wire path. Returns the unsubscribe function. */\n onGenerationEvent(filePath: () => string, handler: (event: MulmoScriptGenerationEvent) => void): () => void;\n}\n\nexport function useMulmoScriptTransport(): MulmoScriptTransport {\n const runtime = useRuntime();\n\n async function call<K extends MulmoScriptDispatchArgs[\"kind\"]>(kind: K, args: ArgsFor<K>): Promise<TransportResult<MulmoScriptDispatchResult[K]>> {\n let result: unknown;\n try {\n result = await runtime.dispatch({ kind, ...args });\n } catch (err) {\n return { ok: false, error: errorMessage(err) };\n }\n if (!isRecord(result) || result.ok !== true) {\n const error = isRecord(result) && typeof result.error === \"string\" ? result.error : `dispatch ${kind} returned an unexpected response`;\n return { ok: false, error };\n }\n return { ok: true, data: result as MulmoScriptDispatchResult[K] };\n }\n\n function onGenerationEvent(filePath: () => string, handler: (event: MulmoScriptGenerationEvent) => void): () => void {\n return runtime.pubsub.subscribe(GENERATION_EVENT, (payload: unknown) => {\n const event = parseGenerationEvent(payload);\n if (!event) return;\n const current = filePath();\n if (!current || event.filePath !== current) return;\n handler(event);\n });\n }\n\n return { call, onGenerationEvent };\n}\n","// Optional host-supplied capabilities that are genuinely host TRANSPORT,\n// not plugin logic — the browser-side sibling of html-plugin's host-injected\n// `previewUrl`. The generic runtime covers JSON dispatch + pubsub; what it\n// can't cover is (a) which chat session a generation should be tagged to\n// (MulmoClaude's sidebar indicator) and (b) how to fetch movie/PDF bytes,\n// which every host serves behind its own auth (MulmoClaude keeps them on\n// bearer-guarded /api routes by explicit review decision — see the\n// downloadMovie comment trail in the pre-extraction View).\n//\n// Hosts provide the adapter with Vue's provide() around the View; absent\n// capabilities degrade gracefully (no session tagging; download / clip-play\n// UI hidden).\n\nimport { inject, type InjectionKey, type Ref } from \"vue\";\n\nexport interface MulmoScriptHostAdapter {\n /** Active chat session id, forwarded on generation dispatches so the\n * host can light its per-session progress indicators. */\n chatSessionId?: Ref<string | undefined>;\n /** Authenticated media download. Exactly one of `moviePath` / `pdfPath`\n * is set — both are the wire `stories/…` paths the status/probe\n * dispatches return. Rejects on transport/HTTP failure. */\n fetchMediaBlob?: (query: { moviePath?: string; pdfPath?: string }) => Promise<Blob>;\n}\n\nexport const MULMOSCRIPT_HOST_ADAPTER_KEY: InjectionKey<MulmoScriptHostAdapter> = Symbol(\"mulmoscript-host-adapter\");\n\nconst EMPTY_ADAPTER: MulmoScriptHostAdapter = {};\n\nexport function useHostAdapter(): MulmoScriptHostAdapter {\n return inject(MULMOSCRIPT_HOST_ADAPTER_KEY, EMPTY_ADAPTER);\n}\n","// Movie + PDF export (#1614): each output has the same status-poll →\n// long-held generate dispatch → authenticated download triple, kept as\n// independent state so a movie and a PDF can be requested for the same\n// script without collision. Media bytes are served behind host auth; the\n// host-injected `fetchMediaBlob` keeps the auth boundary intact (a plain\n// `<a href download>` can't attach the host's headers).\n\nimport { ref, type ComputedRef, type Ref } from \"vue\";\nimport { errorMessage } from \"@mulmoclaude/common\";\nimport { downloadFilename } from \"../helpers\";\nimport type { MulmoScriptTransport } from \"../transport\";\nimport type { MulmoScriptHostAdapter } from \"../hostAdapter\";\n\nexport interface UseMediaExportOptions {\n api: MulmoScriptTransport;\n adapter: MulmoScriptHostAdapter;\n filePath: ComputedRef<string>;\n chatSessionId: ComputedRef<string | undefined>;\n}\n\ntype MediaKind = \"movie\" | \"pdf\";\n\nexport function useMediaExport({ api, adapter, filePath, chatSessionId }: UseMediaExportOptions) {\n const movieGenerating = ref(false);\n const movieDownloading = ref(false);\n const moviePath = ref<string | null>(null);\n // Persists the most-recent movie-generation failure so the toolbar can\n // surface it inline with a retry button (#1197). Cleared at the start of\n // every generate / regenerate attempt.\n const movieError = ref<string | null>(null);\n const pdfGenerating = ref(false);\n const pdfDownloading = ref(false);\n const pdfPath = ref<string | null>(null);\n\n // Long-held dispatch — resolves when the whole pipeline finishes (minutes).\n // If the user navigates to a different result meanwhile the resolution\n // describes the OLD script, so drop it; the new script's own\n // initializeScript / pubsub subscription owns the visible state.\n async function generateMovie(): Promise<void> {\n const requestedFilePath = filePath.value;\n movieGenerating.value = true;\n movieError.value = null;\n const response = await api.call(\"generateMovie\", { filePath: requestedFilePath, chatSessionId: chatSessionId.value });\n if (filePath.value !== requestedFilePath) return;\n movieGenerating.value = false;\n if (!response.ok) {\n // Surface inline (instead of `alert()` which blocks + has no retry\n // affordance). The error chip with a retry button lives in the toolbar.\n movieError.value = response.error;\n return;\n }\n moviePath.value = response.data.moviePath;\n }\n\n async function generatePdf(): Promise<void> {\n const requestedFilePath = filePath.value;\n pdfGenerating.value = true;\n const response = await api.call(\"generatePdf\", { filePath: requestedFilePath, chatSessionId: chatSessionId.value });\n if (filePath.value !== requestedFilePath) return;\n pdfGenerating.value = false;\n if (!response.ok) {\n alert(response.error);\n return;\n }\n pdfPath.value = response.data.pdfPath;\n }\n\n async function refreshMoviePath(): Promise<void> {\n const requestedFilePath = filePath.value;\n if (!requestedFilePath) return;\n const response = await api.call(\"movieStatus\", { filePath: requestedFilePath });\n if (filePath.value !== requestedFilePath) return;\n if (response.ok && response.data.moviePath) moviePath.value = response.data.moviePath;\n }\n\n async function refreshPdfPath(): Promise<void> {\n const requestedFilePath = filePath.value;\n if (!requestedFilePath) return;\n const response = await api.call(\"pdfStatus\", { filePath: requestedFilePath });\n if (filePath.value !== requestedFilePath) return;\n if (response.ok && response.data.pdfPath) pdfPath.value = response.data.pdfPath;\n }\n\n // Authenticated blob → synthetic <a download> click. The download attribute\n // carries the filename so the browser still surfaces a native save dialog.\n async function downloadMedia(kind: MediaKind, sourcePath: string | null, fallbackName: string, downloading: Ref<boolean>): Promise<void> {\n const fetchMediaBlob = adapter.fetchMediaBlob;\n if (!fetchMediaBlob || !sourcePath || downloading.value) return;\n downloading.value = true;\n let objectUrl: string | null = null;\n try {\n const blob = await fetchMediaBlob(kind === \"movie\" ? { moviePath: sourcePath } : { pdfPath: sourcePath });\n objectUrl = URL.createObjectURL(blob);\n clickDownloadAnchor(objectUrl, downloadFilename(sourcePath, fallbackName));\n } catch (err) {\n alert(errorMessage(err));\n } finally {\n if (objectUrl) URL.revokeObjectURL(objectUrl);\n downloading.value = false;\n }\n }\n\n function downloadMovie(): Promise<void> {\n return downloadMedia(\"movie\", moviePath.value, \"movie.mp4\", movieDownloading);\n }\n\n function downloadPdf(): Promise<void> {\n return downloadMedia(\"pdf\", pdfPath.value, \"deck.pdf\", pdfDownloading);\n }\n\n // Movie/PDF spinners + paths are per-script: without a reset, switching away\n // from a generating script would leave the new script's toolbar spinning.\n function resetMedia(): void {\n moviePath.value = null;\n pdfPath.value = null;\n movieGenerating.value = false;\n pdfGenerating.value = false;\n movieError.value = null;\n }\n\n return {\n moviePath,\n movieGenerating,\n movieDownloading,\n movieError,\n pdfPath,\n pdfGenerating,\n pdfDownloading,\n generateMovie,\n downloadMovie,\n refreshMoviePath,\n generatePdf,\n downloadPdf,\n refreshPdfPath,\n resetMedia,\n };\n}\n\nfunction clickDownloadAnchor(href: string, filename: string): void {\n const anchor = document.createElement(\"a\");\n anchor.href = href;\n anchor.download = filename;\n document.body.appendChild(anchor);\n anchor.click();\n anchor.remove();\n}\n","// Per-beat generated video clip state (moviePrompt / animated beats). The wire\n// `stories/…` path comes from the beat-movie probe; the blob object URL is\n// fetched lazily on first play through the host adapter's authenticated\n// `fetchMediaBlob` — a plain <video src> can't attach the host's auth headers.\n\nimport { reactive, type ComputedRef } from \"vue\";\nimport { errorMessage } from \"@mulmoclaude/common\";\nimport { clearReactiveRecords, staleSince as staleSinceOf } from \"../helpers\";\nimport type { MulmoScriptTransport } from \"../transport\";\nimport type { MulmoScriptHostAdapter } from \"../hostAdapter\";\n\nexport interface UseBeatMovieOptions {\n api: MulmoScriptTransport;\n adapter: MulmoScriptHostAdapter;\n filePath: ComputedRef<string>;\n}\n\nexport function useBeatMovie({ api, adapter, filePath }: UseBeatMovieOptions) {\n const beatMovies = reactive<Record<number, string>>({});\n const beatMovieUrls = reactive<Record<number, string>>({});\n const beatMovieOpen = reactive<Record<number, boolean>>({});\n const beatMovieLoading = reactive<Record<number, boolean>>({});\n\n const staleSince = (requestedFilePath: string): boolean => staleSinceOf(filePath.value, requestedFilePath);\n\n async function loadExistingBeatMovie(index: number): Promise<void> {\n const requestedFilePath = filePath.value;\n const response = await api.call(\"beatMovie\", { filePath: requestedFilePath, beatIndex: index });\n if (staleSince(requestedFilePath)) return;\n // silently ignore errors — the clip simply hasn't been generated yet\n if (response.ok && response.data.moviePath) {\n beatMovies[index] = response.data.moviePath;\n }\n }\n\n async function playBeatMovie(index: number): Promise<void> {\n const fetchMediaBlob = adapter.fetchMediaBlob;\n if (!fetchMediaBlob || !beatMovies[index] || beatMovieLoading[index]) return;\n if (beatMovieUrls[index]) {\n beatMovieOpen[index] = true;\n return;\n }\n beatMovieLoading[index] = true;\n try {\n // Re-type the .mov blob as video/mp4 — same ISO-BMFF family, and\n // <video> support for \"video/mp4\" is broader than \"video/quicktime\".\n const blob = new Blob([await fetchMediaBlob({ moviePath: beatMovies[index] })], { type: \"video/mp4\" });\n beatMovieUrls[index] = URL.createObjectURL(blob);\n beatMovieOpen[index] = true;\n } catch (err) {\n alert(errorMessage(err));\n } finally {\n Reflect.deleteProperty(beatMovieLoading, index);\n }\n }\n\n function closeBeatMovie(index: number): void {\n Reflect.deleteProperty(beatMovieOpen, index);\n }\n\n // Drop one beat's cached clip (regenerate is about to replace it on\n // disk). Revoking the object URL frees the blob immediately.\n function invalidateBeatMovie(index: number): void {\n if (beatMovieUrls[index]) URL.revokeObjectURL(beatMovieUrls[index]);\n [beatMovies, beatMovieUrls, beatMovieOpen].forEach((map) => Reflect.deleteProperty(map, index));\n }\n\n function resetBeatMovies(): void {\n Object.values(beatMovieUrls).forEach((url) => URL.revokeObjectURL(url));\n clearReactiveRecords(beatMovies, beatMovieUrls, beatMovieOpen, beatMovieLoading);\n }\n\n return {\n beatMovies,\n beatMovieUrls,\n beatMovieOpen,\n beatMovieLoading,\n loadExistingBeatMovie,\n playBeatMovie,\n closeBeatMovie,\n invalidateBeatMovie,\n resetBeatMovies,\n };\n}\n","// Character (imageParams.images) strip: thumbnails, drag-and-drop upload, and\n// render / generate-all for the `imagePrompt` characters a script references.\n// Characters must be rendered before the beats that use them, so the View\n// probes these on mount and after every beat render.\n\nimport { computed, reactive, type ComputedRef } from \"vue\";\nimport { errorMessage } from \"@mulmoclaude/common\";\nimport { characterPrompt as characterPromptOf, clearReactiveRecords, getMissingCharacterKeys, staleSince as staleSinceOf } from \"../helpers\";\nimport { readFileAsDataUrl } from \"../support\";\nimport type { MulmoScriptTransport } from \"../transport\";\n\ntype CharRenderState = \"idle\" | \"rendering\" | \"done\" | \"error\";\n\ntype ScriptImages = Record<string, { type?: string; prompt?: string }> | undefined;\n\nexport interface UseCharacterImagesOptions {\n api: MulmoScriptTransport;\n filePath: ComputedRef<string>;\n chatSessionId: ComputedRef<string | undefined>;\n getImages: () => ScriptImages;\n}\n\nexport function useCharacterImages({ api, filePath, chatSessionId, getImages }: UseCharacterImagesOptions) {\n const charRenderState = reactive<Record<string, CharRenderState>>({});\n const charImages = reactive<Record<string, string>>({});\n const charErrors = reactive<Record<string, string>>({});\n const charDragOver = reactive<Record<string, boolean>>({});\n\n const staleSince = (requestedFilePath: string): boolean => staleSinceOf(filePath.value, requestedFilePath);\n\n const characterKeys = computed(() => {\n const imgs = getImages() ?? {};\n return Object.keys(imgs).filter((key) => imgs[key]?.type === \"imagePrompt\");\n });\n\n function characterPrompt(key: string): string {\n return characterPromptOf(getImages(), key);\n }\n\n function onCharDragOver(event: DragEvent, key: string): void {\n if (!event.dataTransfer?.types.includes(\"Files\")) return;\n event.preventDefault();\n charDragOver[key] = true;\n }\n\n function onCharDragLeave(key: string): void {\n charDragOver[key] = false;\n }\n\n async function onCharDrop(event: DragEvent, key: string): Promise<void> {\n event.preventDefault();\n charDragOver[key] = false;\n const file = event.dataTransfer?.files[0];\n if (!file || !file.type.startsWith(\"image/\")) return;\n\n charRenderState[key] = \"rendering\";\n Reflect.deleteProperty(charErrors, key);\n let imageData: string;\n try {\n imageData = await readFileAsDataUrl(file);\n } catch (err) {\n charErrors[key] = errorMessage(err);\n charRenderState[key] = \"error\";\n return;\n }\n const requestedFilePath = filePath.value;\n const response = await api.call(\"uploadCharacterImage\", { filePath: requestedFilePath, key, imageData });\n if (staleSince(requestedFilePath)) return;\n if (!response.ok) {\n charErrors[key] = response.error || \"Upload failed\";\n charRenderState[key] = \"error\";\n return;\n }\n charImages[key] = response.data.image ?? \"\";\n charRenderState[key] = \"done\";\n }\n\n async function loadExistingCharacterImage(key: string): Promise<void> {\n const requestedFilePath = filePath.value;\n const response = await api.call(\"characterImage\", { filePath: requestedFilePath, key });\n if (staleSince(requestedFilePath)) return;\n // silently ignore errors\n if (response.ok && response.data.image) {\n charImages[key] = response.data.image;\n charRenderState[key] = \"done\";\n }\n }\n\n function refreshMissingCharacterImages(): void {\n getMissingCharacterKeys(characterKeys.value, charImages, charRenderState).forEach((key) => loadExistingCharacterImage(key));\n }\n\n async function renderCharacter(key: string, force: boolean): Promise<void> {\n const requestedFilePath = filePath.value;\n charRenderState[key] = \"rendering\";\n Reflect.deleteProperty(charErrors, key);\n const response = await api.call(\"renderCharacter\", { filePath: requestedFilePath, key, force, chatSessionId: chatSessionId.value });\n if (staleSince(requestedFilePath)) return;\n if (!response.ok) {\n charErrors[key] = response.error || \"Render failed\";\n charRenderState[key] = \"error\";\n return;\n }\n charImages[key] = response.data.image ?? \"\";\n charRenderState[key] = \"done\";\n }\n\n async function generateAllCharacters(): Promise<void> {\n await Promise.all(characterKeys.value.filter((key) => charRenderState[key] !== \"rendering\").map((key) => renderCharacter(key, false)));\n }\n\n function resetCharacters(): void {\n clearReactiveRecords(charRenderState, charImages, charErrors, charDragOver);\n }\n\n return {\n charRenderState,\n charImages,\n charErrors,\n charDragOver,\n characterKeys,\n characterPrompt,\n onCharDragOver,\n onCharDragLeave,\n onCharDrop,\n loadExistingCharacterImage,\n refreshMissingCharacterImages,\n renderCharacter,\n generateAllCharacters,\n resetCharacters,\n };\n}\n","// #1575 — when every beat is a `slide`, the View swaps the per-beat list for\n// the interactive deck editor (@mulmocast/deck-web). Each editor emit fires\n// `update:script`; this debounces them into one updateScript round-trip per\n// quiet stretch (300ms — short enough to feel live, long enough that typing in\n// the Inspector doesn't carpet-bomb the server).\n\nimport { computed, type ComputedRef } from \"vue\";\nimport { isAllSlideDeck } from \"../helpers\";\nimport type { MulmoScriptTransport } from \"../transport\";\nimport type { DeckScriptShape, MulmoScript } from \"../viewTypes\";\n\nconst DECK_SAVE_DEBOUNCE_MS = 300;\n\nexport interface UseDeckEditorOptions {\n api: MulmoScriptTransport;\n filePath: ComputedRef<string>;\n effectiveScript: ComputedRef<MulmoScript>;\n /** Persist the saved script back into the parent's toolResult so the\n * in-memory script and reactive beats[] stay in sync without a remount. */\n commitScript: (next: MulmoScript) => void;\n}\n\nexport function useDeckEditor({ api, filePath, effectiveScript, commitScript }: UseDeckEditorOptions) {\n const isDeck = computed(() => isAllSlideDeck(effectiveScript.value));\n const deckScriptInput = computed<DeckScriptShape>(() => effectiveScript.value as unknown as DeckScriptShape);\n\n let deckSaveTimer: ReturnType<typeof setTimeout> | null = null;\n let pendingDeckScript: MulmoScript | null = null;\n\n function scheduleDeckSave(next: MulmoScript): void {\n pendingDeckScript = next;\n if (deckSaveTimer) clearTimeout(deckSaveTimer);\n deckSaveTimer = setTimeout(() => {\n void flushDeckSave();\n }, DECK_SAVE_DEBOUNCE_MS);\n }\n\n async function flushDeckSave(): Promise<void> {\n deckSaveTimer = null;\n const next = pendingDeckScript;\n pendingDeckScript = null;\n if (!next || !filePath.value) return;\n const response = await api.call(\"updateScript\", { filePath: filePath.value, script: next });\n if (!response.ok) {\n // Surface via console; the deck editor still holds the latest edit in\n // its props until the next refresh, so the view doesn't snap back on a\n // transient failure. A full toast UI is P2.\n console.error(\"[presentMulmoScript] deck save failed:\", response.error);\n return;\n }\n commitScript(next);\n }\n\n function onDeckUpdate(next: DeckScriptShape): void {\n scheduleDeckSave(next as unknown as MulmoScript);\n }\n\n // Flush synchronously-scheduled work on unmount so a quick switch away\n // doesn't lose the last keystroke. Fire-and-forget — the component is gone,\n // we just want the bytes to land.\n function flushPendingDeckSave(): void {\n if (deckSaveTimer) {\n clearTimeout(deckSaveTimer);\n void flushDeckSave();\n }\n }\n\n return { isDeck, deckScriptInput, onDeckUpdate, flushPendingDeckSave };\n}\n","import type { Messages } from \"./messages\";\n\nconst de: Messages = {\n beatCount: (count) => (count === 1 ? `${count} Beat` : `${count} Beats`),\n movie: \"Video\",\n generating: \"Wird generiert…\",\n rendering: \"Wird gerendert…\",\n saving: \"Wird gespeichert…\",\n update: \"Aktualisieren\",\n characters: \"Charaktere\",\n drop: \"Ablegen\",\n gen: \"Generieren\",\n play: \"▶ Abspielen\",\n stop: \"■ Stoppen\",\n playPresentation: \"Präsentation abspielen\",\n regenerateMovie: \"Video neu generieren\",\n movieGenerationFailed: \"Videoerstellung fehlgeschlagen\",\n pdf: \"PDF\",\n regeneratePdf: \"PDF neu generieren\",\n generatingPdf: \"PDF wird erstellt…\",\n retry: \"Erneut versuchen\",\n errPrefix: \"⚠ Fehler\",\n noBeats: \"Keine Beats im Skript gefunden\",\n editSource: \"Skript-Quelle bearbeiten\",\n applyChanges: \"Änderungen übernehmen\",\n generateAll: \"Alle generieren\",\n orDropImage: \"oder Bild ablegen\",\n generate: \"Generieren\",\n generateAudio: \"♪ Generieren\",\n saveErrorInvalidJson: (error) => `⚠ Ungültiges JSON: ${error}`,\n saveErrorSaveFailed: (error) => `⚠ Speichern fehlgeschlagen: ${error}`,\n close: \"Schließen\",\n cancel: \"Abbrechen\",\n};\n\nexport default de;\n","import type { Messages } from \"./messages\";\n\nconst en: Messages = {\n beatCount: (count) => (count === 1 ? `${count} beat` : `${count} beats`),\n movie: \"Movie\",\n generating: \"Generating…\",\n rendering: \"Rendering…\",\n saving: \"Saving…\",\n update: \"Update\",\n characters: \"Characters\",\n drop: \"Drop\",\n gen: \"Gen\",\n play: \"▶ Play\",\n stop: \"■ Stop\",\n playPresentation: \"Play presentation\",\n regenerateMovie: \"Regenerate movie\",\n movieGenerationFailed: \"Movie generation failed\",\n pdf: \"PDF\",\n regeneratePdf: \"Regenerate PDF\",\n generatingPdf: \"Generating PDF…\",\n retry: \"Retry\",\n errPrefix: \"⚠ Error\",\n noBeats: \"No beats found in script\",\n editSource: \"Edit Script Source\",\n applyChanges: \"Apply Changes\",\n generateAll: \"Generate All\",\n orDropImage: \"or drop image\",\n generate: \"Generate\",\n generateAudio: \"♪ Generate\",\n saveErrorInvalidJson: (error) => `⚠ Invalid JSON: ${error}`,\n saveErrorSaveFailed: (error) => `⚠ Save failed: ${error}`,\n close: \"Close\",\n cancel: \"Cancel\",\n};\n\nexport default en;\n","import type { Messages } from \"./messages\";\n\nconst es: Messages = {\n beatCount: (count) => (count === 1 ? `${count} beat` : `${count} beats`),\n movie: \"Vídeo\",\n generating: \"Generando…\",\n rendering: \"Renderizando…\",\n saving: \"Guardando…\",\n update: \"Actualizar\",\n characters: \"Personajes\",\n drop: \"Soltar\",\n gen: \"Generar\",\n play: \"▶ Reproducir\",\n stop: \"■ Detener\",\n playPresentation: \"Reproducir presentación\",\n regenerateMovie: \"Regenerar vídeo\",\n movieGenerationFailed: \"Error al generar el vídeo\",\n pdf: \"PDF\",\n regeneratePdf: \"Regenerar PDF\",\n generatingPdf: \"Generando PDF…\",\n retry: \"Reintentar\",\n errPrefix: \"⚠ Error\",\n noBeats: \"No se encontraron beats en el script\",\n editSource: \"Editar fuente del script\",\n applyChanges: \"Aplicar cambios\",\n generateAll: \"Generar todo\",\n orDropImage: \"o arrastra una imagen\",\n generate: \"Generar\",\n generateAudio: \"♪ Generar\",\n saveErrorInvalidJson: (error) => `⚠ JSON no válido: ${error}`,\n saveErrorSaveFailed: (error) => `⚠ Error al guardar: ${error}`,\n close: \"Cerrar\",\n cancel: \"Cancelar\",\n};\n\nexport default es;\n","import type { Messages } from \"./messages\";\n\nconst fr: Messages = {\n beatCount: (count) => (count === 1 ? `${count} beat` : `${count} beats`),\n movie: \"Film\",\n generating: \"Génération…\",\n rendering: \"Rendu…\",\n saving: \"Enregistrement…\",\n update: \"Mettre à jour\",\n characters: \"Personnages\",\n drop: \"Déposer\",\n gen: \"Générer\",\n play: \"▶ Lire\",\n stop: \"■ Arrêter\",\n playPresentation: \"Lire la présentation\",\n regenerateMovie: \"Régénérer la vidéo\",\n movieGenerationFailed: \"Échec de la génération de la vidéo\",\n pdf: \"PDF\",\n regeneratePdf: \"Régénérer le PDF\",\n generatingPdf: \"Génération du PDF…\",\n retry: \"Réessayer\",\n errPrefix: \"⚠ Erreur\",\n noBeats: \"Aucun beat trouvé dans le script\",\n editSource: \"Modifier la source du script\",\n applyChanges: \"Appliquer les modifications\",\n generateAll: \"Tout générer\",\n orDropImage: \"ou déposez une image\",\n generate: \"Générer\",\n generateAudio: \"♪ Générer\",\n saveErrorInvalidJson: (error) => `⚠ JSON invalide : ${error}`,\n saveErrorSaveFailed: (error) => `⚠ Échec de la sauvegarde : ${error}`,\n close: \"Fermer\",\n cancel: \"Annuler\",\n};\n\nexport default fr;\n","import type { Messages } from \"./messages\";\n\nconst ja: Messages = {\n beatCount: (count) => `${count} ビート`,\n movie: \"動画\",\n generating: \"生成中…\",\n rendering: \"レンダリング中…\",\n saving: \"保存中…\",\n update: \"更新\",\n characters: \"キャラクター\",\n drop: \"ドロップ\",\n gen: \"生成\",\n play: \"▶ 再生\",\n stop: \"■ 停止\",\n playPresentation: \"プレゼンテーション再生\",\n regenerateMovie: \"動画を再生成\",\n movieGenerationFailed: \"動画の生成に失敗しました\",\n pdf: \"PDF\",\n regeneratePdf: \"PDF を再生成\",\n generatingPdf: \"PDF を生成中…\",\n retry: \"再試行\",\n errPrefix: \"⚠ エラー\",\n noBeats: \"スクリプトにビートが見つかりません\",\n editSource: \"スクリプトソースを編集\",\n applyChanges: \"変更を適用\",\n generateAll: \"すべて生成\",\n orDropImage: \"画像をドロップ\",\n generate: \"生成\",\n generateAudio: \"♪ 生成\",\n saveErrorInvalidJson: (error) => `⚠ 不正な JSON: ${error}`,\n saveErrorSaveFailed: (error) => `⚠ 保存失敗: ${error}`,\n close: \"閉じる\",\n cancel: \"キャンセル\",\n};\n\nexport default ja;\n","import type { Messages } from \"./messages\";\n\nconst ko: Messages = {\n beatCount: (count) => `${count}개 비트`,\n movie: \"영상\",\n generating: \"생성 중…\",\n rendering: \"렌더링 중…\",\n saving: \"저장 중…\",\n update: \"업데이트\",\n characters: \"캐릭터\",\n drop: \"드롭\",\n gen: \"생성\",\n play: \"▶ 재생\",\n stop: \"■ 정지\",\n playPresentation: \"프레젠테이션 재생\",\n regenerateMovie: \"동영상 재생성\",\n movieGenerationFailed: \"동영상 생성에 실패했습니다\",\n pdf: \"PDF\",\n regeneratePdf: \"PDF 재생성\",\n generatingPdf: \"PDF 생성 중…\",\n retry: \"다시 시도\",\n errPrefix: \"⚠ 오류\",\n noBeats: \"스크립트에서 비트를 찾을 수 없습니다\",\n editSource: \"스크립트 원본 편집\",\n applyChanges: \"변경 사항 적용\",\n generateAll: \"전체 생성\",\n orDropImage: \"또는 이미지 드롭\",\n generate: \"생성\",\n generateAudio: \"♪ 생성\",\n saveErrorInvalidJson: (error) => `⚠ 잘못된 JSON: ${error}`,\n saveErrorSaveFailed: (error) => `⚠ 저장 실패: ${error}`,\n close: \"닫기\",\n cancel: \"취소\",\n};\n\nexport default ko;\n","import type { Messages } from \"./messages\";\n\nconst ptBR: Messages = {\n beatCount: (count) => (count === 1 ? `${count} beat` : `${count} beats`),\n movie: \"Vídeo\",\n generating: \"Gerando…\",\n rendering: \"Renderizando…\",\n saving: \"Salvando…\",\n update: \"Atualizar\",\n characters: \"Personagens\",\n drop: \"Soltar\",\n gen: \"Gerar\",\n play: \"▶ Reproduzir\",\n stop: \"■ Parar\",\n playPresentation: \"Reproduzir apresentação\",\n regenerateMovie: \"Regenerar vídeo\",\n movieGenerationFailed: \"Falha ao gerar o vídeo\",\n pdf: \"PDF\",\n regeneratePdf: \"Regenerar PDF\",\n generatingPdf: \"Gerando PDF…\",\n retry: \"Tentar novamente\",\n errPrefix: \"⚠ Erro\",\n noBeats: \"Nenhum beat encontrado no script\",\n editSource: \"Editar fonte do script\",\n applyChanges: \"Aplicar alterações\",\n generateAll: \"Gerar tudo\",\n orDropImage: \"ou solte uma imagem\",\n generate: \"Gerar\",\n generateAudio: \"♪ Gerar\",\n saveErrorInvalidJson: (error) => `⚠ JSON inválido: ${error}`,\n saveErrorSaveFailed: (error) => `⚠ Falha ao salvar: ${error}`,\n close: \"Fechar\",\n cancel: \"Cancelar\",\n};\n\nexport default ptBR;\n","import type { Messages } from \"./messages\";\n\nconst zh: Messages = {\n beatCount: (count) => `${count} 个 beat`,\n movie: \"视频\",\n generating: \"生成中…\",\n rendering: \"渲染中…\",\n saving: \"保存中…\",\n update: \"更新\",\n characters: \"角色\",\n drop: \"拖放\",\n gen: \"生成\",\n play: \"▶ 播放\",\n stop: \"■ 停止\",\n playPresentation: \"播放演示\",\n regenerateMovie: \"重新生成视频\",\n movieGenerationFailed: \"视频生成失败\",\n pdf: \"PDF\",\n regeneratePdf: \"重新生成 PDF\",\n generatingPdf: \"生成 PDF…\",\n retry: \"重试\",\n errPrefix: \"⚠ 错误\",\n noBeats: \"脚本中没有找到 beat\",\n editSource: \"编辑脚本源\",\n applyChanges: \"应用更改\",\n generateAll: \"全部生成\",\n orDropImage: \"或拖入图片\",\n generate: \"生成\",\n generateAudio: \"♪ 生成\",\n saveErrorInvalidJson: (error) => `⚠ JSON 无效: ${error}`,\n saveErrorSaveFailed: (error) => `⚠ 保存失败: ${error}`,\n close: \"关闭\",\n cancel: \"取消\",\n};\n\nexport default zh;\n","import { createUseT } from \"gui-chat-protocol/vue\";\nimport type { Messages } from \"./messages\";\nimport de from \"./de\";\nimport en from \"./en\";\nimport es from \"./es\";\nimport fr from \"./fr\";\nimport ja from \"./ja\";\nimport ko from \"./ko\";\nimport ptBR from \"./ptBR\";\nimport zh from \"./zh\";\n\nconst MESSAGES = { de, en, es, fr, ja, ko, \"pt-BR\": ptBR, zh } as const;\n\n/** Reactive message bundle for the active host locale. The plugin carries its\n * own translations (no host i18n dependency); it reads the locale off the\n * injected `BrowserPluginRuntime.locale` ref and falls back to English.\n * Same pattern as @mulmoclaude/html-plugin. */\nexport const useT = createUseT(MESSAGES);\n\nexport type { Messages };\n","<template>\n <div class=\"fixed inset-0 z-50 bg-black/80 overflow-y-auto\" @click=\"emit('close')\">\n <button class=\"fixed top-2 right-4 z-10 text-white/60 hover:text-white text-3xl leading-none\" :title=\"m.close\" @click.stop=\"emit('close')\">✕</button>\n <div class=\"flex flex-col items-center gap-4 pt-4 pb-8\" @click.stop>\n <div class=\"flex items-center gap-4\">\n <button\n v-if=\"!lightbox.isCharacter\"\n class=\"text-white/60 hover:text-white disabled:opacity-20 text-5xl leading-none\"\n :disabled=\"!hasPrev\"\n @click=\"emit('move', -1)\"\n >\n ‹\n </button>\n <div class=\"flex flex-col items-center\">\n <img :src=\"lightbox.src\" class=\"max-w-[80vw] max-h-[85vh] object-contain rounded shadow-2xl\" />\n <div v-if=\"!lightbox.isCharacter && beatCount > 1\" class=\"relative w-full h-1\">\n <div class=\"flex gap-1 h-full\">\n <div\n v-for=\"i in beatCount\"\n :key=\"i - 1\"\n class=\"group flex-1 cursor-pointer relative transition-colors\"\n :class=\"\n i - 1 === lightbox.index\n ? 'bg-white/80 hover:bg-white'\n : i - 1 < lightbox.index\n ? 'bg-white/40 hover:bg-white/60'\n : 'bg-white/20 hover:bg-white/40'\n \"\n @click=\"emit('jump', i - 1)\"\n >\n <span class=\"absolute -inset-y-3 inset-x-0\" />\n <div\n v-if=\"beatTooltip(beatTexts[i - 1])\"\n class=\"absolute bottom-full mb-2 left-1/2 -translate-x-1/2 z-20 px-2 py-1 rounded bg-black/90 text-white text-xs leading-tight w-48 max-h-[53px] overflow-hidden opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity\"\n >\n {{ beatTooltip(beatTexts[i - 1]) }}\n </div>\n </div>\n </div>\n <div\n v-if=\"playingAudioIndex !== null && playingAudioIndex === lightbox.index\"\n class=\"absolute top-1/2 w-3.5 h-3.5 rounded-full bg-white shadow ring-2 ring-black/30 -translate-y-1/2 -translate-x-1/2 pointer-events-none\"\n :style=\"{ left: `${((lightbox.index + audioProgress) / beatCount) * 100}%` }\"\n />\n </div>\n </div>\n <button\n v-if=\"!lightbox.isCharacter\"\n class=\"text-white/60 hover:text-white disabled:opacity-20 text-5xl leading-none\"\n :disabled=\"!hasNext\"\n @click=\"emit('move', 1)\"\n >\n ›\n </button>\n </div>\n <div v-if=\"lightbox.text || hasCurrentAudio\" class=\"relative w-screen flex justify-center px-16\">\n <p v-if=\"lightbox.text\" class=\"max-w-[80vw] text-center text-white leading-relaxed text-[clamp(0.8rem,1.76vw,1.6rem)]\">\n {{ lightbox.text }}\n </p>\n <button\n v-if=\"hasCurrentAudio\"\n class=\"absolute top-0 right-4 text-sm px-3 py-1 rounded border border-white/60 text-white/60 hover:bg-white/20\"\n @click=\"emit('playAudio', lightbox.index)\"\n >\n {{ playingAudioIndex === lightbox.index ? m.stop : m.play }}\n </button>\n </div>\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\n// Full-screen beat / character image viewer with the beat strip, prev-next\n// arrows and the narration Play control. Pure Tailwind — the parent's\n// `<style scoped>` block only targets the bottom-bar region, so nothing here\n// relies on styles that stop at the component boundary.\n//\n// The parent owns `v-if=\"lightbox\"`, so `lightbox` is never null in here and\n// the template can read `.index` / `.src` without a guard.\nimport { beatTooltip } from \"../helpers\";\nimport type { LightboxState } from \"../viewTypes\";\nimport { useT } from \"../../lang/index\";\n\nconst m = useT();\n\ndefineProps<{\n lightbox: LightboxState;\n beatCount: number;\n beatTexts: (string | undefined)[];\n hasPrev: boolean;\n hasNext: boolean;\n playingAudioIndex: number | null;\n audioProgress: number;\n hasCurrentAudio: boolean;\n}>();\n\nconst emit = defineEmits<{\n close: [];\n move: [delta: number];\n jump: [index: number];\n playAudio: [index: number];\n}>();\n</script>\n","<template>\n <div class=\"fixed inset-0 z-50 bg-black/80 overflow-y-auto\" @click=\"emit('close')\">\n <button class=\"fixed top-2 right-4 z-10 text-white/60 hover:text-white text-3xl leading-none\" :title=\"m.close\" @click.stop=\"emit('close')\">✕</button>\n <div class=\"flex flex-col items-center gap-4 pt-4 pb-8\" @click.stop>\n <div class=\"flex items-center gap-4\">\n <button\n v-if=\"!lightbox.isCharacter\"\n class=\"text-white/60 hover:text-white disabled:opacity-20 text-5xl leading-none\"\n :disabled=\"!hasPrev\"\n @click=\"emit('move', -1)\"\n >\n ‹\n </button>\n <div class=\"flex flex-col items-center\">\n <img :src=\"lightbox.src\" class=\"max-w-[80vw] max-h-[85vh] object-contain rounded shadow-2xl\" />\n <div v-if=\"!lightbox.isCharacter && beatCount > 1\" class=\"relative w-full h-1\">\n <div class=\"flex gap-1 h-full\">\n <div\n v-for=\"i in beatCount\"\n :key=\"i - 1\"\n class=\"group flex-1 cursor-pointer relative transition-colors\"\n :class=\"\n i - 1 === lightbox.index\n ? 'bg-white/80 hover:bg-white'\n : i - 1 < lightbox.index\n ? 'bg-white/40 hover:bg-white/60'\n : 'bg-white/20 hover:bg-white/40'\n \"\n @click=\"emit('jump', i - 1)\"\n >\n <span class=\"absolute -inset-y-3 inset-x-0\" />\n <div\n v-if=\"beatTooltip(beatTexts[i - 1])\"\n class=\"absolute bottom-full mb-2 left-1/2 -translate-x-1/2 z-20 px-2 py-1 rounded bg-black/90 text-white text-xs leading-tight w-48 max-h-[53px] overflow-hidden opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity\"\n >\n {{ beatTooltip(beatTexts[i - 1]) }}\n </div>\n </div>\n </div>\n <div\n v-if=\"playingAudioIndex !== null && playingAudioIndex === lightbox.index\"\n class=\"absolute top-1/2 w-3.5 h-3.5 rounded-full bg-white shadow ring-2 ring-black/30 -translate-y-1/2 -translate-x-1/2 pointer-events-none\"\n :style=\"{ left: `${((lightbox.index + audioProgress) / beatCount) * 100}%` }\"\n />\n </div>\n </div>\n <button\n v-if=\"!lightbox.isCharacter\"\n class=\"text-white/60 hover:text-white disabled:opacity-20 text-5xl leading-none\"\n :disabled=\"!hasNext\"\n @click=\"emit('move', 1)\"\n >\n ›\n </button>\n </div>\n <div v-if=\"lightbox.text || hasCurrentAudio\" class=\"relative w-screen flex justify-center px-16\">\n <p v-if=\"lightbox.text\" class=\"max-w-[80vw] text-center text-white leading-relaxed text-[clamp(0.8rem,1.76vw,1.6rem)]\">\n {{ lightbox.text }}\n </p>\n <button\n v-if=\"hasCurrentAudio\"\n class=\"absolute top-0 right-4 text-sm px-3 py-1 rounded border border-white/60 text-white/60 hover:bg-white/20\"\n @click=\"emit('playAudio', lightbox.index)\"\n >\n {{ playingAudioIndex === lightbox.index ? m.stop : m.play }}\n </button>\n </div>\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\n// Full-screen beat / character image viewer with the beat strip, prev-next\n// arrows and the narration Play control. Pure Tailwind — the parent's\n// `<style scoped>` block only targets the bottom-bar region, so nothing here\n// relies on styles that stop at the component boundary.\n//\n// The parent owns `v-if=\"lightbox\"`, so `lightbox` is never null in here and\n// the template can read `.index` / `.src` without a guard.\nimport { beatTooltip } from \"../helpers\";\nimport type { LightboxState } from \"../viewTypes\";\nimport { useT } from \"../../lang/index\";\n\nconst m = useT();\n\ndefineProps<{\n lightbox: LightboxState;\n beatCount: number;\n beatTexts: (string | undefined)[];\n hasPrev: boolean;\n hasNext: boolean;\n playingAudioIndex: number | null;\n audioProgress: number;\n hasCurrentAudio: boolean;\n}>();\n\nconst emit = defineEmits<{\n close: [];\n move: [delta: number];\n jump: [index: number];\n playAudio: [index: number];\n}>();\n</script>\n","<template>\n <div class=\"border-b border-gray-100 shrink-0 px-4 py-3\">\n <div class=\"flex items-center justify-between mb-2\">\n <span class=\"text-xs font-semibold text-gray-500 uppercase tracking-wide\">{{ m.characters }}</span>\n <button\n class=\"px-2 py-0.5 text-xs rounded border border-gray-300 text-gray-500 hover:bg-gray-50 disabled:opacity-50\"\n :disabled=\"busy || characterKeys.every((key) => renderState[key] === 'rendering')\"\n @click=\"emit('generateAll')\"\n >\n {{ m.generateAll }}\n </button>\n </div>\n <div class=\"flex gap-3 flex-wrap\">\n <div v-for=\"key in characterKeys\" :key=\"key\" class=\"flex flex-col items-center gap-1 w-36\">\n <!-- Character thumbnail -->\n <div\n class=\"relative w-36 h-36 rounded-lg border overflow-hidden bg-gray-50 flex items-center justify-center transition-colors\"\n :class=\"dragOver[key] ? 'border-blue-400 bg-blue-50' : 'border-gray-200'\"\n @dragover=\"emit('charDragOver', $event, key)\"\n @dragleave=\"emit('charDragLeave', key)\"\n @drop=\"emit('charDrop', $event, key)\"\n >\n <img v-if=\"thumbnails[key]\" :src=\"thumbnails[key]\" class=\"w-full h-full object-cover cursor-zoom-in\" :alt=\"key\" @click=\"emit('openLightbox', key)\" />\n <template v-else-if=\"renderState[key] === 'rendering'\">\n <svg class=\"animate-spin w-4 h-4 text-green-400\" viewBox=\"0 0 24 24\" fill=\"none\">\n <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\" />\n <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8v8H4z\" />\n </svg>\n </template>\n <template v-else-if=\"renderState[key] === 'error'\">\n <span class=\"text-xs text-red-400 text-center px-1\">{{ errors[key] }}</span>\n </template>\n <template v-else>\n <span class=\"text-xs text-gray-300 text-center px-1 leading-tight\">{{ characterPrompt(images, key) }}</span>\n </template>\n <!-- Permanent drop hint -->\n <div v-if=\"!dragOver[key]\" class=\"absolute bottom-0 inset-x-0 text-center text-xs text-gray-400 bg-white/70 py-0.5 pointer-events-none\">\n {{ m.orDropImage }}\n </div>\n <!-- Drop overlay -->\n <div v-if=\"dragOver[key]\" class=\"absolute inset-0 flex items-center justify-center bg-blue-50/80 pointer-events-none\">\n <span class=\"text-xs text-blue-500 font-medium\">{{ m.drop }}</span>\n </div>\n <!-- Regenerate button -->\n <button\n v-if=\"thumbnails[key] && renderState[key] !== 'rendering'\"\n class=\"absolute top-0.5 right-0.5 px-1 py-0.5 text-xs rounded border bg-white\"\n :class=\"busy ? 'border-yellow-400 text-yellow-500 cursor-not-allowed' : 'border-gray-400 text-gray-600 hover:bg-gray-50'\"\n :disabled=\"busy\"\n @click.stop=\"emit('renderCharacter', key, true)\"\n >\n <span v-if=\"busy\" class=\"inline-block animate-spin\">↺</span>\n <span v-else>↺</span>\n </button>\n <!-- Generate button -->\n <button\n v-else-if=\"!thumbnails[key] && renderState[key] !== 'rendering'\"\n class=\"absolute top-0.5 right-0.5 px-1 py-0.5 text-xs rounded border bg-white\"\n :class=\"busy ? 'border-yellow-400 text-yellow-500 cursor-not-allowed' : 'border-blue-400 text-blue-600 hover:bg-blue-50'\"\n :disabled=\"busy\"\n @click.stop=\"emit('renderCharacter', key, false)\"\n >\n <svg v-if=\"busy\" class=\"animate-spin w-3 h-3\" viewBox=\"0 0 24 24\" fill=\"none\">\n <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\" />\n <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8v8H4z\" />\n </svg>\n <span v-else>{{ m.gen }}</span>\n </button>\n </div>\n <span class=\"text-xs text-gray-600 text-center truncate w-full\">{{ key }}</span>\n </div>\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\n// `imageParams.images` character thumbnails: drag-and-drop upload, per-character\n// render, and generate-all. Pure Tailwind — the parent's `<style scoped>` block\n// only targets the bottom-bar region, so nothing here relies on styles that stop\n// at the component boundary. The `char` prefix the parent uses to disambiguate\n// character state from beat state is redundant inside this component.\nimport { computed } from \"vue\";\nimport { characterPrompt } from \"../helpers\";\nimport type { ImageEntry } from \"../viewTypes\";\nimport { useT } from \"../../lang/index\";\n\nconst m = useT();\n\nconst props = defineProps<{\n characterKeys: string[];\n images: Record<string, ImageEntry> | undefined;\n thumbnails: Record<string, string>;\n renderState: Record<string, string>;\n errors: Record<string, string>;\n dragOver: Record<string, boolean>;\n movieGenerating: boolean;\n anyBeatRendering: boolean;\n}>();\n\nconst emit = defineEmits<{\n generateAll: [];\n charDragOver: [event: DragEvent, key: string];\n charDragLeave: [key: string];\n charDrop: [event: DragEvent, key: string];\n openLightbox: [key: string];\n renderCharacter: [key: string, force: boolean];\n}>();\n\n// A movie render or any in-flight beat render locks every per-character\n// action — the generated frames must not change underneath them.\nconst busy = computed(() => props.movieGenerating || props.anyBeatRendering);\n</script>\n","<template>\n <div class=\"border-b border-gray-100 shrink-0 px-4 py-3\">\n <div class=\"flex items-center justify-between mb-2\">\n <span class=\"text-xs font-semibold text-gray-500 uppercase tracking-wide\">{{ m.characters }}</span>\n <button\n class=\"px-2 py-0.5 text-xs rounded border border-gray-300 text-gray-500 hover:bg-gray-50 disabled:opacity-50\"\n :disabled=\"busy || characterKeys.every((key) => renderState[key] === 'rendering')\"\n @click=\"emit('generateAll')\"\n >\n {{ m.generateAll }}\n </button>\n </div>\n <div class=\"flex gap-3 flex-wrap\">\n <div v-for=\"key in characterKeys\" :key=\"key\" class=\"flex flex-col items-center gap-1 w-36\">\n <!-- Character thumbnail -->\n <div\n class=\"relative w-36 h-36 rounded-lg border overflow-hidden bg-gray-50 flex items-center justify-center transition-colors\"\n :class=\"dragOver[key] ? 'border-blue-400 bg-blue-50' : 'border-gray-200'\"\n @dragover=\"emit('charDragOver', $event, key)\"\n @dragleave=\"emit('charDragLeave', key)\"\n @drop=\"emit('charDrop', $event, key)\"\n >\n <img v-if=\"thumbnails[key]\" :src=\"thumbnails[key]\" class=\"w-full h-full object-cover cursor-zoom-in\" :alt=\"key\" @click=\"emit('openLightbox', key)\" />\n <template v-else-if=\"renderState[key] === 'rendering'\">\n <svg class=\"animate-spin w-4 h-4 text-green-400\" viewBox=\"0 0 24 24\" fill=\"none\">\n <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\" />\n <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8v8H4z\" />\n </svg>\n </template>\n <template v-else-if=\"renderState[key] === 'error'\">\n <span class=\"text-xs text-red-400 text-center px-1\">{{ errors[key] }}</span>\n </template>\n <template v-else>\n <span class=\"text-xs text-gray-300 text-center px-1 leading-tight\">{{ characterPrompt(images, key) }}</span>\n </template>\n <!-- Permanent drop hint -->\n <div v-if=\"!dragOver[key]\" class=\"absolute bottom-0 inset-x-0 text-center text-xs text-gray-400 bg-white/70 py-0.5 pointer-events-none\">\n {{ m.orDropImage }}\n </div>\n <!-- Drop overlay -->\n <div v-if=\"dragOver[key]\" class=\"absolute inset-0 flex items-center justify-center bg-blue-50/80 pointer-events-none\">\n <span class=\"text-xs text-blue-500 font-medium\">{{ m.drop }}</span>\n </div>\n <!-- Regenerate button -->\n <button\n v-if=\"thumbnails[key] && renderState[key] !== 'rendering'\"\n class=\"absolute top-0.5 right-0.5 px-1 py-0.5 text-xs rounded border bg-white\"\n :class=\"busy ? 'border-yellow-400 text-yellow-500 cursor-not-allowed' : 'border-gray-400 text-gray-600 hover:bg-gray-50'\"\n :disabled=\"busy\"\n @click.stop=\"emit('renderCharacter', key, true)\"\n >\n <span v-if=\"busy\" class=\"inline-block animate-spin\">↺</span>\n <span v-else>↺</span>\n </button>\n <!-- Generate button -->\n <button\n v-else-if=\"!thumbnails[key] && renderState[key] !== 'rendering'\"\n class=\"absolute top-0.5 right-0.5 px-1 py-0.5 text-xs rounded border bg-white\"\n :class=\"busy ? 'border-yellow-400 text-yellow-500 cursor-not-allowed' : 'border-blue-400 text-blue-600 hover:bg-blue-50'\"\n :disabled=\"busy\"\n @click.stop=\"emit('renderCharacter', key, false)\"\n >\n <svg v-if=\"busy\" class=\"animate-spin w-3 h-3\" viewBox=\"0 0 24 24\" fill=\"none\">\n <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\" />\n <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8v8H4z\" />\n </svg>\n <span v-else>{{ m.gen }}</span>\n </button>\n </div>\n <span class=\"text-xs text-gray-600 text-center truncate w-full\">{{ key }}</span>\n </div>\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\n// `imageParams.images` character thumbnails: drag-and-drop upload, per-character\n// render, and generate-all. Pure Tailwind — the parent's `<style scoped>` block\n// only targets the bottom-bar region, so nothing here relies on styles that stop\n// at the component boundary. The `char` prefix the parent uses to disambiguate\n// character state from beat state is redundant inside this component.\nimport { computed } from \"vue\";\nimport { characterPrompt } from \"../helpers\";\nimport type { ImageEntry } from \"../viewTypes\";\nimport { useT } from \"../../lang/index\";\n\nconst m = useT();\n\nconst props = defineProps<{\n characterKeys: string[];\n images: Record<string, ImageEntry> | undefined;\n thumbnails: Record<string, string>;\n renderState: Record<string, string>;\n errors: Record<string, string>;\n dragOver: Record<string, boolean>;\n movieGenerating: boolean;\n anyBeatRendering: boolean;\n}>();\n\nconst emit = defineEmits<{\n generateAll: [];\n charDragOver: [event: DragEvent, key: string];\n charDragLeave: [key: string];\n charDrop: [event: DragEvent, key: string];\n openLightbox: [key: string];\n renderCharacter: [key: string, force: boolean];\n}>();\n\n// A movie render or any in-flight beat render locks every per-character\n// action — the generated frames must not change underneath them.\nconst busy = computed(() => props.movieGenerating || props.anyBeatRendering);\n</script>\n","<template>\n <div class=\"ml-4 shrink-0 flex items-center gap-2\">\n <!-- Play presentation: opens the lightbox at beat 0 and starts\n audio. Same gating as Download Movie — only when a movie has\n been generated, which is our proxy for \"every beat has both\n an image and audio on disk\". Green outline + green icon\n share the visual idiom with the (filled) Download button so\n both completed-artifact actions read as the same family.\n `isPlayReady` ensures we don't open the lightbox before the\n first beat's image (and audio, if it has text) finish their\n async load — moviePath can be set while loadExistingBeatImage\n is still in flight. -->\n <button\n v-if=\"moviePath && !movieGenerating\"\n class=\"h-8 w-8 flex items-center justify-center rounded border border-green-600 text-green-600 hover:bg-green-50 disabled:opacity-40 disabled:cursor-not-allowed transition-colors\"\n :disabled=\"!isPlayReady\"\n :title=\"m.playPresentation\"\n :aria-label=\"m.playPresentation\"\n @click=\"emit('play')\"\n >\n <span class=\"material-icons text-base\">play_arrow</span>\n </button>\n <!-- Download Movie: authenticated blob fetch through the host\n adapter, then a synthetic <a download> click. A plain\n <a href download> can't attach the host's auth headers, which\n would have forced an auth exemption on the media route — the\n host-injected `fetchMediaBlob` keeps the auth boundary intact\n (and hosts that don't provide it simply don't show this\n button). -->\n <button\n v-if=\"moviePath && !movieGenerating && canFetchMedia\"\n class=\"h-8 px-2.5 flex items-center gap-1 rounded bg-green-600 hover:bg-green-700 text-white text-sm disabled:opacity-60 disabled:cursor-not-allowed transition-colors\"\n :disabled=\"movieDownloading\"\n data-testid=\"mulmo-script-download-movie-button\"\n @click=\"emit('downloadMovie')\"\n >\n <span class=\"material-icons text-base\">download</span>\n <span>{{ m.movie }}</span>\n </button>\n <!-- Regenerate Movie (icon-only): collapses to a square once a\n movie exists — the adjacent Download / Play already make\n the subject clear, so the \"Movie\" label only adds noise. -->\n <button\n v-if=\"moviePath && !movieGenerating\"\n class=\"h-8 w-8 flex items-center justify-center rounded border border-gray-200 text-gray-600 hover:bg-gray-100 transition-colors\"\n :title=\"m.regenerateMovie\"\n :aria-label=\"m.regenerateMovie\"\n data-testid=\"mulmo-script-regenerate-movie-button\"\n @click=\"emit('generateMovie')\"\n >\n <span class=\"material-icons text-base\">refresh</span>\n </button>\n <!-- Generate Movie (pill): no movie yet, or one is currently\n generating. Keeps the label so first-time users know what\n they're triggering. -->\n <button\n v-else\n class=\"h-8 px-2.5 flex items-center gap-1 text-sm rounded border border-gray-200 text-gray-600 hover:bg-gray-100 disabled:opacity-40 disabled:cursor-not-allowed transition-colors\"\n :disabled=\"movieGenerating\"\n data-testid=\"mulmo-script-generate-movie-button\"\n @click=\"emit('generateMovie')\"\n >\n <svg v-if=\"movieGenerating\" class=\"animate-spin w-4 h-4 shrink-0\" viewBox=\"0 0 24 24\" fill=\"none\">\n <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\" />\n <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8v8H4z\" />\n </svg>\n <span v-if=\"movieGenerating\">{{ m.generating }}</span>\n <template v-else>\n <span class=\"material-icons text-sm\">refresh</span>\n <span>{{ m.movie }}</span>\n </template>\n </button>\n <!-- PDF (#1614): same Generate / Download / Regenerate pattern\n as the Movie cluster above, kept structurally separate so\n the two outputs can be requested independently and report\n status independently. -->\n <button\n v-if=\"pdfPath && !pdfGenerating && canFetchMedia\"\n class=\"h-8 px-2.5 flex items-center gap-1 rounded bg-red-600 hover:bg-red-700 text-white text-sm disabled:opacity-60 disabled:cursor-not-allowed transition-colors\"\n :disabled=\"pdfDownloading\"\n data-testid=\"mulmo-script-download-pdf-button\"\n @click=\"emit('downloadPdf')\"\n >\n <span class=\"material-icons text-base\">download</span>\n <span>{{ m.pdf }}</span>\n </button>\n <button\n v-if=\"pdfPath && !pdfGenerating\"\n class=\"h-8 w-8 flex items-center justify-center rounded border border-gray-200 text-gray-600 hover:bg-gray-100 transition-colors\"\n :title=\"m.regeneratePdf\"\n :aria-label=\"m.regeneratePdf\"\n data-testid=\"mulmo-script-regenerate-pdf-button\"\n @click=\"emit('generatePdf')\"\n >\n <span class=\"material-icons text-base\">refresh</span>\n </button>\n <button\n v-else\n class=\"h-8 px-2.5 flex items-center gap-1 text-sm rounded border border-gray-200 text-gray-600 hover:bg-gray-100 disabled:opacity-40 disabled:cursor-not-allowed transition-colors\"\n :disabled=\"pdfGenerating\"\n data-testid=\"mulmo-script-generate-pdf-button\"\n @click=\"emit('generatePdf')\"\n >\n <svg v-if=\"pdfGenerating\" class=\"animate-spin w-4 h-4 shrink-0\" viewBox=\"0 0 24 24\" fill=\"none\">\n <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\" />\n <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8v8H4z\" />\n </svg>\n <span v-if=\"pdfGenerating\">{{ m.generatingPdf }}</span>\n <template v-else>\n <span class=\"material-icons text-sm\">picture_as_pdf</span>\n <span>{{ m.pdf }}</span>\n </template>\n </button>\n </div>\n</template>\n\n<script setup lang=\"ts\">\n// Movie + PDF action cluster from the View header. Pure Tailwind — the\n// parent's `<style scoped>` block only targets the bottom-bar region, so\n// nothing here depends on styles that stop at the component boundary.\nimport { useT } from \"../../lang/index\";\n\nconst m = useT();\n\ndefineProps<{\n moviePath: string | null;\n movieGenerating: boolean;\n movieDownloading: boolean;\n isPlayReady: boolean;\n canFetchMedia: boolean;\n pdfPath: string | null;\n pdfGenerating: boolean;\n pdfDownloading: boolean;\n}>();\n\nconst emit = defineEmits<{\n play: [];\n generateMovie: [];\n downloadMovie: [];\n generatePdf: [];\n downloadPdf: [];\n}>();\n</script>\n","<template>\n <div class=\"ml-4 shrink-0 flex items-center gap-2\">\n <!-- Play presentation: opens the lightbox at beat 0 and starts\n audio. Same gating as Download Movie — only when a movie has\n been generated, which is our proxy for \"every beat has both\n an image and audio on disk\". Green outline + green icon\n share the visual idiom with the (filled) Download button so\n both completed-artifact actions read as the same family.\n `isPlayReady` ensures we don't open the lightbox before the\n first beat's image (and audio, if it has text) finish their\n async load — moviePath can be set while loadExistingBeatImage\n is still in flight. -->\n <button\n v-if=\"moviePath && !movieGenerating\"\n class=\"h-8 w-8 flex items-center justify-center rounded border border-green-600 text-green-600 hover:bg-green-50 disabled:opacity-40 disabled:cursor-not-allowed transition-colors\"\n :disabled=\"!isPlayReady\"\n :title=\"m.playPresentation\"\n :aria-label=\"m.playPresentation\"\n @click=\"emit('play')\"\n >\n <span class=\"material-icons text-base\">play_arrow</span>\n </button>\n <!-- Download Movie: authenticated blob fetch through the host\n adapter, then a synthetic <a download> click. A plain\n <a href download> can't attach the host's auth headers, which\n would have forced an auth exemption on the media route — the\n host-injected `fetchMediaBlob` keeps the auth boundary intact\n (and hosts that don't provide it simply don't show this\n button). -->\n <button\n v-if=\"moviePath && !movieGenerating && canFetchMedia\"\n class=\"h-8 px-2.5 flex items-center gap-1 rounded bg-green-600 hover:bg-green-700 text-white text-sm disabled:opacity-60 disabled:cursor-not-allowed transition-colors\"\n :disabled=\"movieDownloading\"\n data-testid=\"mulmo-script-download-movie-button\"\n @click=\"emit('downloadMovie')\"\n >\n <span class=\"material-icons text-base\">download</span>\n <span>{{ m.movie }}</span>\n </button>\n <!-- Regenerate Movie (icon-only): collapses to a square once a\n movie exists — the adjacent Download / Play already make\n the subject clear, so the \"Movie\" label only adds noise. -->\n <button\n v-if=\"moviePath && !movieGenerating\"\n class=\"h-8 w-8 flex items-center justify-center rounded border border-gray-200 text-gray-600 hover:bg-gray-100 transition-colors\"\n :title=\"m.regenerateMovie\"\n :aria-label=\"m.regenerateMovie\"\n data-testid=\"mulmo-script-regenerate-movie-button\"\n @click=\"emit('generateMovie')\"\n >\n <span class=\"material-icons text-base\">refresh</span>\n </button>\n <!-- Generate Movie (pill): no movie yet, or one is currently\n generating. Keeps the label so first-time users know what\n they're triggering. -->\n <button\n v-else\n class=\"h-8 px-2.5 flex items-center gap-1 text-sm rounded border border-gray-200 text-gray-600 hover:bg-gray-100 disabled:opacity-40 disabled:cursor-not-allowed transition-colors\"\n :disabled=\"movieGenerating\"\n data-testid=\"mulmo-script-generate-movie-button\"\n @click=\"emit('generateMovie')\"\n >\n <svg v-if=\"movieGenerating\" class=\"animate-spin w-4 h-4 shrink-0\" viewBox=\"0 0 24 24\" fill=\"none\">\n <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\" />\n <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8v8H4z\" />\n </svg>\n <span v-if=\"movieGenerating\">{{ m.generating }}</span>\n <template v-else>\n <span class=\"material-icons text-sm\">refresh</span>\n <span>{{ m.movie }}</span>\n </template>\n </button>\n <!-- PDF (#1614): same Generate / Download / Regenerate pattern\n as the Movie cluster above, kept structurally separate so\n the two outputs can be requested independently and report\n status independently. -->\n <button\n v-if=\"pdfPath && !pdfGenerating && canFetchMedia\"\n class=\"h-8 px-2.5 flex items-center gap-1 rounded bg-red-600 hover:bg-red-700 text-white text-sm disabled:opacity-60 disabled:cursor-not-allowed transition-colors\"\n :disabled=\"pdfDownloading\"\n data-testid=\"mulmo-script-download-pdf-button\"\n @click=\"emit('downloadPdf')\"\n >\n <span class=\"material-icons text-base\">download</span>\n <span>{{ m.pdf }}</span>\n </button>\n <button\n v-if=\"pdfPath && !pdfGenerating\"\n class=\"h-8 w-8 flex items-center justify-center rounded border border-gray-200 text-gray-600 hover:bg-gray-100 transition-colors\"\n :title=\"m.regeneratePdf\"\n :aria-label=\"m.regeneratePdf\"\n data-testid=\"mulmo-script-regenerate-pdf-button\"\n @click=\"emit('generatePdf')\"\n >\n <span class=\"material-icons text-base\">refresh</span>\n </button>\n <button\n v-else\n class=\"h-8 px-2.5 flex items-center gap-1 text-sm rounded border border-gray-200 text-gray-600 hover:bg-gray-100 disabled:opacity-40 disabled:cursor-not-allowed transition-colors\"\n :disabled=\"pdfGenerating\"\n data-testid=\"mulmo-script-generate-pdf-button\"\n @click=\"emit('generatePdf')\"\n >\n <svg v-if=\"pdfGenerating\" class=\"animate-spin w-4 h-4 shrink-0\" viewBox=\"0 0 24 24\" fill=\"none\">\n <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\" />\n <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8v8H4z\" />\n </svg>\n <span v-if=\"pdfGenerating\">{{ m.generatingPdf }}</span>\n <template v-else>\n <span class=\"material-icons text-sm\">picture_as_pdf</span>\n <span>{{ m.pdf }}</span>\n </template>\n </button>\n </div>\n</template>\n\n<script setup lang=\"ts\">\n// Movie + PDF action cluster from the View header. Pure Tailwind — the\n// parent's `<style scoped>` block only targets the bottom-bar region, so\n// nothing here depends on styles that stop at the component boundary.\nimport { useT } from \"../../lang/index\";\n\nconst m = useT();\n\ndefineProps<{\n moviePath: string | null;\n movieGenerating: boolean;\n movieDownloading: boolean;\n isPlayReady: boolean;\n canFetchMedia: boolean;\n pdfPath: string | null;\n pdfGenerating: boolean;\n pdfDownloading: boolean;\n}>();\n\nconst emit = defineEmits<{\n play: [];\n generateMovie: [];\n downloadMovie: [];\n generatePdf: [];\n downloadPdf: [];\n}>();\n</script>\n","<template>\n <div class=\"h-full bg-white flex flex-col overflow-hidden\">\n <!-- Header -->\n <div class=\"flex items-start justify-between px-6 py-4 border-b border-gray-100 shrink-0\">\n <div class=\"min-w-0 flex-1\">\n <h2 class=\"text-lg font-semibold text-gray-800 truncate\" data-testid=\"mulmo-script-title\">\n {{ script.title || \"Untitled Script\" }}\n </h2>\n <p v-if=\"script.description\" class=\"text-sm text-gray-500 mt-0.5 truncate\" data-testid=\"mulmo-script-description\">\n {{ script.description }}\n </p>\n <div class=\"flex items-center gap-3 mt-1 text-xs text-gray-400\">\n <span>{{ m.beatCount(beats.length) }}</span>\n <span v-if=\"script.lang\">{{ script.lang }}</span>\n <span v-if=\"filePath\" class=\"truncate\">{{ filePath }}</span>\n </div>\n </div>\n <MulmoScriptToolbar\n :movie-path=\"moviePath\"\n :movie-generating=\"movieGenerating\"\n :movie-downloading=\"movieDownloading\"\n :is-play-ready=\"isPlayReady\"\n :can-fetch-media=\"canFetchMedia\"\n :pdf-path=\"pdfPath\"\n :pdf-generating=\"pdfGenerating\"\n :pdf-downloading=\"pdfDownloading\"\n @play=\"playPresentation\"\n @generate-movie=\"generateMovie\"\n @download-movie=\"downloadMovie\"\n @generate-pdf=\"generatePdf\"\n @download-pdf=\"downloadPdf\"\n />\n </div>\n\n <!--\n Inline error chip for movie-generation failures (#1197).\n Previously the catch arm of `generateMovie` raised an `alert()` —\n blocking, no retry path, and many users just dismissed the modal\n and saw a stalled spinner with no explanation. The chip stays\n visible until the next generate attempt clears it.\n -->\n <div\n v-if=\"movieError\"\n data-testid=\"mulmo-script-movie-error-chip\"\n class=\"bg-red-50 border border-red-200 text-red-800 text-xs px-3 py-2 mx-4 mt-3 mb-1 rounded flex items-start gap-2\"\n >\n <span class=\"material-icons text-base shrink-0 mt-px\">error_outline</span>\n <div class=\"flex-1 min-w-0\">\n <div class=\"font-medium\">{{ m.movieGenerationFailed }}</div>\n <div class=\"break-words whitespace-pre-wrap mt-0.5\">{{ movieError }}</div>\n </div>\n <button\n class=\"shrink-0 h-7 px-2 text-xs rounded border border-red-300 text-red-700 hover:bg-red-100 disabled:opacity-50\"\n :disabled=\"movieGenerating\"\n data-testid=\"mulmo-script-movie-retry-button\"\n @click=\"generateMovie\"\n >\n {{ m.retry }}\n </button>\n </div>\n\n <!-- Characters section -->\n <CharacterStrip\n v-if=\"characterKeys.length > 0\"\n :character-keys=\"characterKeys\"\n :images=\"script.imageParams?.images\"\n :thumbnails=\"charImages\"\n :render-state=\"charRenderState\"\n :errors=\"charErrors\"\n :drag-over=\"charDragOver\"\n :movie-generating=\"movieGenerating\"\n :any-beat-rendering=\"anyBeatRendering\"\n @generate-all=\"generateAllCharacters\"\n @char-drag-over=\"onCharDragOver\"\n @char-drag-leave=\"onCharDragLeave\"\n @char-drop=\"onCharDrop\"\n @open-lightbox=\"openCharacterLightbox\"\n @render-character=\"renderCharacter\"\n />\n\n <!-- Deck editor (#1575): every beat is a slide → mount the\n interactive deck editor from @mulmocast/deck-web. The Vue\n component is lazy-loaded via defineAsyncComponent, so users\n whose scripts aren't decks never pay the bundle cost. -->\n <div v-if=\"isDeck\" class=\"flex-1 overflow-hidden\" data-testid=\"mulmo-script-deck-editor\">\n <MulmoScriptDeckEditor :script=\"deckScriptInput\" layout=\"compact\" @update:script=\"onDeckUpdate\" />\n </div>\n\n <!-- Beat list (fallback when the script has any non-slide beat) -->\n <div v-else ref=\"beatListEl\" class=\"flex-1 overflow-y-auto p-2 space-y-1.5\">\n <div v-for=\"(beat, index) in beats\" :key=\"index\" class=\"rounded-lg border border-gray-200 overflow-hidden\">\n <!-- Beat body: thumbnail + narration side by side -->\n <div class=\"flex gap-3 items-stretch\">\n <!-- Thumbnail -->\n <div\n class=\"relative shrink-0 w-[45%] overflow-hidden bg-gray-50 transition-colors\"\n :class=\"beatDragOver[index] ? 'bg-blue-50' : ''\"\n @dragover=\"onBeatDragOver($event, index)\"\n @dragleave=\"onBeatDragLeave(index)\"\n @drop=\"onBeatDrop($event, index)\"\n >\n <!-- Beat number badge (1-based). Sits above the drop-hint\n overlay and the inline video player so the index stays\n readable in every beat state. -->\n <div\n class=\"absolute top-1.5 left-1.5 z-10 px-1.5 py-0.5 rounded bg-black/55 text-white text-xs font-medium leading-none pointer-events-none\"\n :data-testid=\"`mulmo-script-beat-number-${index}`\"\n >\n {{ index + 1 }}\n </div>\n <!-- Inline player for the beat's generated video clip.\n Replaces the thumbnail while open; the close button\n returns to the still image. -->\n <template v-if=\"beatMovieOpen[index] && beatMovieUrls[index]\">\n <video :src=\"beatMovieUrls[index]\" class=\"w-full object-contain\" controls autoplay :data-testid=\"`mulmo-script-beat-movie-player-${index}`\" />\n <button\n class=\"absolute top-1.5 right-1.5 flex items-center justify-center w-6 h-6 rounded border border-gray-400 text-gray-600 bg-white hover:bg-gray-50\"\n :title=\"m.close\"\n :aria-label=\"m.close\"\n :data-testid=\"`mulmo-script-beat-movie-close-${index}`\"\n @click.stop=\"closeBeatMovie(index)\"\n >\n <span class=\"material-icons text-sm\">close</span>\n </button>\n </template>\n <template v-else>\n <img\n v-if=\"renderedImages[index]\"\n :src=\"renderedImages[index]\"\n class=\"w-full object-contain cursor-zoom-in\"\n :alt=\"`Beat ${index + 1}`\"\n @click=\"openLightbox(index)\"\n />\n <!-- Play overlay: shown when the beat-movie probe found a\n generated clip for this beat. Blob is fetched lazily\n on first click (host-authenticated), hence the spinner. -->\n <button\n v-if=\"renderedImages[index] && beatMovies[index] && canFetchMedia\"\n class=\"absolute inset-0 m-auto w-12 h-12 flex items-center justify-center rounded-full bg-black/50 text-white hover:bg-black/70\"\n :title=\"m.play\"\n :aria-label=\"m.play\"\n :data-testid=\"`mulmo-script-beat-movie-play-${index}`\"\n @click.stop=\"playBeatMovie(index)\"\n >\n <svg v-if=\"beatMovieLoading[index]\" class=\"animate-spin w-5 h-5\" viewBox=\"0 0 24 24\" fill=\"none\">\n <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\" />\n <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8v8H4z\" />\n </svg>\n <span v-else class=\"material-icons text-3xl\">play_arrow</span>\n </button>\n <button\n v-if=\"renderedImages[index] && renderState[index] !== 'rendering'\"\n class=\"absolute top-1.5 right-1.5 flex items-center gap-1 px-2 py-0.5 text-xs rounded border border-gray-400 text-gray-600 bg-white hover:bg-gray-50 disabled:opacity-60 disabled:cursor-not-allowed\"\n :disabled=\"movieGenerating\"\n @click.stop=\"regenerateBeat(index)\"\n >\n ↺\n </button>\n <div v-else-if=\"!renderedImages[index]\" class=\"w-full aspect-video flex flex-col items-center justify-center gap-1 p-2\">\n <template v-if=\"renderState[index] === 'rendering' || (movieGenerating && !renderedImages[index] && effectiveBeat(index).imagePrompt)\">\n <svg class=\"animate-spin w-4 h-4 text-green-400\" viewBox=\"0 0 24 24\" fill=\"none\">\n <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\" />\n <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8v8H4z\" />\n </svg>\n <span class=\"text-xs text-green-500\">{{ m.rendering }}</span>\n </template>\n <template v-else-if=\"renderState[index] === 'error'\">\n <span class=\"text-xs text-red-400 text-center\">{{ renderErrors[index] }}</span>\n </template>\n <template v-else>\n <span v-if=\"effectiveBeat(index).imagePrompt\" class=\"text-xs text-gray-400 text-center italic leading-relaxed px-1\">{{\n effectiveBeat(index).imagePrompt\n }}</span>\n <span v-else class=\"text-xs text-gray-300\">{{ beat.image?.type ?? \"—\" }}</span>\n </template>\n </div>\n </template>\n <!-- Beat drop hint / overlay -->\n <div v-if=\"beatDragOver[index]\" class=\"absolute inset-0 flex items-center justify-center bg-blue-50/80 pointer-events-none\">\n <span class=\"text-xs text-blue-500 font-medium\">{{ m.drop }}</span>\n </div>\n <div\n v-else-if=\"!renderedImages[index] && renderState[index] !== 'rendering'\"\n class=\"absolute bottom-0 inset-x-0 text-center text-xs text-gray-400 bg-white/70 py-0.5 pointer-events-none\"\n >\n {{ m.orDropImage }}\n </div>\n <!-- Generate button for any beat without a rendered image.\n renderBeat works for every beat type: imagePrompt /\n typed image beats render directly, moviePrompt beats\n get a frame extracted from the generated clip, and\n text-only beats fall back to a prompt derived from\n the narration text (mulmocast prompt.js). -->\n <button\n v-if=\"!renderedImages[index] && renderState[index] !== 'rendering' && !movieGenerating && !isBeatImageReference(effectiveBeat(index))\"\n class=\"absolute top-1.5 right-1.5 flex items-center gap-1 px-2 py-0.5 text-xs rounded border border-blue-400 text-blue-600 bg-white hover:bg-blue-50\"\n @click=\"renderBeat(index)\"\n >\n {{ m.generate }}\n </button>\n </div>\n\n <!-- Narration text -->\n <div class=\"flex flex-col flex-1 min-w-0 px-2 py-1.5\">\n <span class=\"text-sm text-gray-800 leading-relaxed\">{{ effectiveBeat(index).text }}</span>\n <div class=\"flex justify-between mt-auto pt-1\">\n <!-- Audio controls -->\n <div class=\"flex items-center gap-1\">\n <template v-if=\"audioState[index] === 'generating' || (movieGenerating && !beatAudios[index] && effectiveBeat(index).text)\">\n <svg class=\"animate-spin w-3 h-3 text-green-400\" viewBox=\"0 0 24 24\" fill=\"none\">\n <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\" />\n <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8v8H4z\" />\n </svg>\n </template>\n <button\n v-else-if=\"beatAudios[index]\"\n class=\"text-xs px-2 py-0.5 rounded border\"\n :class=\"playingAudio?.index === index ? 'border-red-400 text-red-600 hover:bg-red-50' : 'border-green-400 text-green-600 hover:bg-green-50'\"\n @click=\"playAudio(index)\"\n >\n {{ playingAudio?.index === index ? m.stop : m.play }}\n </button>\n <template v-else-if=\"audioErrors[index]\">\n <span class=\"text-xs text-red-400 truncate min-w-0 max-w-[20rem]\" :title=\"audioErrors[index]\">\n {{ m.errPrefix }} {{ audioErrors[index] }}\n </span>\n <button\n v-if=\"effectiveBeat(index).text\"\n class=\"text-xs px-2 py-0.5 rounded border border-gray-300 text-gray-500 hover:bg-gray-50 disabled:opacity-50\"\n :disabled=\"movieGenerating\"\n @click=\"generateAudio(index)\"\n >\n ↺\n </button>\n </template>\n <button\n v-else-if=\"effectiveBeat(index).text\"\n class=\"text-xs px-2 py-0.5 rounded border border-gray-300 text-gray-500 hover:bg-gray-50\"\n @click=\"generateAudio(index)\"\n >\n {{ m.generateAudio }}\n </button>\n </div>\n <button\n class=\"text-gray-400 hover:text-gray-600\"\n :title=\"sourceOpen[index] ? 'Hide source' : 'Show source'\"\n :data-testid=\"`mulmo-script-beat-source-toggle-${index}`\"\n @click=\"toggleSource(index)\"\n >\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n class=\"w-3.5 h-3.5\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n >\n <polyline points=\"16 18 22 12 16 6\" />\n <polyline points=\"8 6 2 12 8 18\" />\n </svg>\n </button>\n </div>\n </div>\n </div>\n\n <!-- Source editor -->\n <div v-if=\"sourceOpen[index]\" class=\"border-t border-gray-100\">\n <textarea\n v-model=\"sourceText[index]\"\n class=\"w-full text-xs text-gray-600 bg-gray-50 p-2 font-mono resize-none\"\n :class=\"isValidBeat(index) ? 'outline-none' : 'outline outline-2 outline-red-400'\"\n rows=\"8\"\n spellcheck=\"false\"\n :data-testid=\"`mulmo-script-beat-source-textarea-${index}`\"\n />\n <div class=\"flex items-center justify-end gap-2 px-2 pb-2\">\n <span v-if=\"beatSaveErrors[index]\" class=\"text-xs text-red-600\" role=\"alert\">{{\n beatSaveErrors[index].kind === \"invalidJson\"\n ? m.saveErrorInvalidJson(beatSaveErrors[index].error)\n : m.saveErrorSaveFailed(beatSaveErrors[index].error)\n }}</span>\n <button\n class=\"px-2 py-1 text-xs rounded border\"\n :class=\"\n isValidBeat(index) && !beatSaving[index]\n ? 'border-blue-400 text-blue-600 hover:bg-blue-50 cursor-pointer'\n : 'border-gray-200 text-gray-300 cursor-not-allowed'\n \"\n :disabled=\"!isValidBeat(index) || !!beatSaving[index]\"\n :data-testid=\"`mulmo-script-beat-update-button-${index}`\"\n @click=\"updateBeat(index)\"\n >\n {{ beatSaving[index] ? m.saving : m.update }}\n </button>\n </div>\n </div>\n </div>\n\n <div v-if=\"beats.length === 0\" class=\"flex items-center justify-center h-32 text-gray-400 text-sm\">{{ m.noBeats }}</div>\n </div>\n\n <!-- Bottom bar: Edit Script Source + Copy -->\n <div class=\"bottom-bar-wrapper\">\n <details ref=\"sourceDetails\" class=\"script-source\" @toggle=\"onSourceToggle(($event.target as HTMLDetailsElement).open)\">\n <summary>{{ m.editSource }}</summary>\n <textarea\n v-model=\"editableSource\"\n class=\"script-editor\"\n :class=\"{ 'script-editor-invalid': sourceChanged && !sourceValid }\"\n spellcheck=\"false\"\n ></textarea>\n <div class=\"editor-actions\">\n <button class=\"apply-btn\" :disabled=\"!sourceChanged || !sourceValid\" @click=\"applySource\">{{ m.applyChanges }}</button>\n <button class=\"cancel-btn\" @click=\"cancelSourceEdit\">{{ m.cancel }}</button>\n </div>\n </details>\n <button v-show=\"!editing\" class=\"copy-btn\" :title=\"copied ? 'Copied!' : 'Copy'\" @click=\"copyText\">\n <span class=\"material-icons\">{{ copied ? \"check\" : \"content_copy\" }}</span>\n </button>\n </div>\n\n <!-- Lightbox -->\n <BeatLightbox\n v-if=\"lightbox\"\n :lightbox=\"lightbox\"\n :beat-count=\"beats.length\"\n :beat-texts=\"beatTexts\"\n :has-prev=\"hasPrev\"\n :has-next=\"hasNext\"\n :playing-audio-index=\"playingAudio?.index ?? null\"\n :audio-progress=\"audioProgress\"\n :has-current-audio=\"Boolean(beatAudios[lightbox.index])\"\n @close=\"closeLightbox\"\n @move=\"lightboxMove\"\n @jump=\"jumpToBeat\"\n @play-audio=\"playAudio\"\n />\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, defineAsyncComponent, onBeforeUnmount, onMounted, reactive, ref, watch } from \"vue\";\nimport type { ToolResultComplete } from \"gui-chat-protocol/vue\";\nimport { mulmoBeatSchema, mulmoScriptSchema } from \"@mulmocast/types\";\nimport type { MulmoScriptData } from \"../core/types\";\nimport type { MulmoScriptGenerationEvent } from \"../core/contract\";\nimport {\n isSameScript,\n beatMayHaveMovie,\n shouldAutoRenderBeat,\n effectiveBeat as effectiveBeatOf,\n isBeatImageReference,\n isValidBeat as isValidBeatOf,\n staleSince as staleSinceOf,\n scriptSourceText as toScriptSourceText,\n resolveSilentAdvanceSeconds,\n clearReactiveRecords,\n type Beat,\n} from \"./helpers\";\nimport { errorMessage } from \"@mulmoclaude/common\";\nimport { readFileAsDataUrl, useClipboardCopy } from \"./support\";\nimport { useMulmoScriptTransport } from \"./transport\";\nimport { useHostAdapter } from \"./hostAdapter\";\nimport { useMediaExport } from \"./composables/useMediaExport\";\nimport { useBeatMovie } from \"./composables/useBeatMovie\";\nimport { useCharacterImages } from \"./composables/useCharacterImages\";\nimport { useDeckEditor } from \"./composables/useDeckEditor\";\nimport type { LightboxState, MulmoScript } from \"./viewTypes\";\nimport BeatLightbox from \"./components/BeatLightbox.vue\";\nimport CharacterStrip from \"./components/CharacterStrip.vue\";\nimport MulmoScriptToolbar from \"./components/MulmoScriptToolbar.vue\";\nimport { useT } from \"../lang/index\";\n\n// Lazy-loaded so the deck editor's Vue / tailwind / SlidePreview chunk\n// stays out of the initial bundle for users whose scripts aren't decks\n// (movies, html_tailwind animations, mixed beats). `defineAsyncComponent`\n// triggers the dynamic import only when `isDeck` first flips true.\nconst MulmoScriptDeckEditor = defineAsyncComponent(() => import(\"@mulmocast/deck-web\").then((mod) => mod.MulmoScriptDeckEditor));\n\nconst api = useMulmoScriptTransport();\nconst adapter = useHostAdapter();\n// Media bytes (movie / PDF / beat clips) are served behind host auth; hosts\n// opt in by injecting `fetchMediaBlob`. Without it the download / clip-play\n// affordances are hidden (the probes still run — state stays warm for a\n// host that injects later at remount).\nconst canFetchMedia = computed(() => Boolean(adapter.fetchMediaBlob));\n\nconst m = useT();\n\nconst props = defineProps<{\n selectedResult: ToolResultComplete<MulmoScriptData>;\n}>();\nconst emit = defineEmits<{ updateResult: [result: ToolResultComplete] }>();\n\nconst data = computed(() => props.selectedResult.data);\nconst script = computed<MulmoScript>(() => data.value?.script ?? {});\nconst filePath = computed(() => data.value?.filePath ?? \"\");\nconst beats = computed<Beat[]>(() => script.value.beats ?? []);\n\n// Per-beat render state\ntype RenderState = \"idle\" | \"rendering\" | \"done\" | \"error\";\nconst renderState = reactive<Record<number, RenderState>>({});\nconst renderedImages = reactive<Record<number, string>>({});\nconst renderErrors = reactive<Record<number, string>>({});\nconst sourceOpen = reactive<Record<number, boolean>>({});\nconst sourceText = reactive<Record<number, string>>({});\n// Surface update-beat failures inline next to the Update button.\n// Cleared on next successful save or editor close. Store raw error +\n// kind tag so the template picks a localized message, instead of\n// pre-composing an English-prefixed string here.\ninterface BeatSaveError {\n kind: \"invalidJson\" | \"saveFailed\";\n error: string;\n}\nconst beatSaveErrors = reactive<Record<number, BeatSaveError>>({});\nconst beatSaving = reactive<Record<number, boolean>>({});\nconst localOverrides = reactive<Record<number, Beat>>({});\nconst beatAudios = reactive<Record<number, string>>({});\nconst audioState = reactive<Record<number, \"generating\" | \"done\" | \"error\">>({});\nconst audioErrors = reactive<Record<number, string>>({});\nconst playingAudio = ref<{ index: number; audio: HTMLAudioElement } | null>(null);\n// Tracks the auto-advance timer running on a silent beat\n// (`beat.text === \"\"`). Beats without text generate no audio, so the\n// Play loop falls back to a `setTimeout(beat.duration)` for cues —\n// without this, Play would stall on the first silent beat (#1073).\nconst silentPlaybackTimer = ref<{ index: number; timer: ReturnType<typeof setTimeout> } | null>(null);\nconst audioProgress = ref(0);\n\n// Default duration (seconds) for a silent beat whose script doesn't\n// set `duration` either. Picked to roughly match the time it takes a\n// reader to scan a `textSlide` — long enough to read, short enough\n// not to feel stuck. The script's own `duration` always wins.\nconst SILENT_BEAT_DEFAULT_SEC = 3;\nconst MS_PER_SECOND = 1000;\nconst beatListEl = ref<HTMLElement | null>(null);\nconst lightbox = ref<LightboxState | null>(null);\nconst beatDragOver = reactive<Record<number, boolean>>({});\n\nconst anyBeatRendering = computed(() => Object.values(renderState).some((state) => state === \"rendering\"));\n\n// Session tagging is host transport: MulmoClaude injects the active chat\n// session id so generations light its per-session sidebar indicator;\n// hosts without sessions leave the adapter empty and the field is simply\n// omitted from generation dispatches.\nconst chatSessionId = computed(() => adapter.chatSessionId?.value);\n\nconst {\n moviePath,\n movieGenerating,\n movieDownloading,\n movieError,\n pdfPath,\n pdfGenerating,\n pdfDownloading,\n generateMovie,\n downloadMovie,\n refreshMoviePath,\n generatePdf,\n downloadPdf,\n refreshPdfPath,\n resetMedia,\n} = useMediaExport({ api, adapter, filePath, chatSessionId });\n\nconst {\n beatMovies,\n beatMovieUrls,\n beatMovieOpen,\n beatMovieLoading,\n loadExistingBeatMovie,\n playBeatMovie,\n closeBeatMovie,\n invalidateBeatMovie,\n resetBeatMovies,\n} = useBeatMovie({ api, adapter, filePath });\n\nconst {\n charRenderState,\n charImages,\n charErrors,\n charDragOver,\n characterKeys,\n onCharDragOver,\n onCharDragLeave,\n onCharDrop,\n loadExistingCharacterImage,\n refreshMissingCharacterImages,\n renderCharacter,\n generateAllCharacters,\n resetCharacters,\n} = useCharacterImages({ api, filePath, chatSessionId, getImages: () => script.value.imageParams?.images });\n\nfunction stopPlayingAudio() {\n // Single helper that clears both the audio path and the silent\n // auto-advance timer — callers (lightbox open / arrow nav / Stop\n // button) get consistent behaviour without remembering which\n // playback mode the current beat was using (#1073).\n stopAllPlayback();\n}\n\nfunction openLightbox(index: number) {\n stopPlayingAudio();\n lightbox.value = {\n src: renderedImages[index],\n text: effectiveBeat(index).text,\n index,\n };\n}\n\n// Backdrop click handler. Stops any in-flight narration so the audio\n// doesn't keep playing after the lightbox is dismissed — without this,\n// the HTMLAudioElement created by playAudio() outlives the modal and\n// the user hears disembodied narration with no UI to stop it.\nfunction closeLightbox() {\n stopPlayingAudio();\n lightbox.value = null;\n}\n\n// \"Play presentation\" toolbar action. Opens the lightbox at beat 0 and\n// kicks off its narration audio; the existing on-ended hook then chains\n// through the rest of the deck (lightboxMove(1) → playAudio if the next\n// beat has audio), so one click runs the whole presentation. Only wired\n// to the toolbar button when moviePath is set, which is our proxy for\n// \"every beat has both image and audio on disk\".\n//\n// `moviePath` arrives synchronously from movieStatus, but the per-beat\n// image and audio data URIs are populated asynchronously by\n// loadExistingBeatImage / loadExistingBeatAudio in initializeScript().\n// The Play button can therefore become visible before beat 0's assets\n// hydrate — `isPlayReady` gates the click so the lightbox never opens\n// with an undefined src or silent narration on a beat that does have\n// text.\nconst isPlayReady = computed<boolean>(() => {\n if (beats.value.length === 0) return false;\n if (!renderedImages[0]) return false;\n // Audio is only required when the beat has text (the source of TTS).\n // Beats without text are valid; they just play silently.\n if (effectiveBeat(0).text && !beatAudios[0]) return false;\n return true;\n});\n\nfunction playPresentation() {\n if (!isPlayReady.value) return;\n openLightbox(0);\n playBeat(0);\n}\n\n// Stop whichever playback handle is active. Idempotent. Called by\n// openLightbox, manual stop / pause buttons, and by `playBeat`\n// before kicking off a new beat so we never double-schedule. (#1073)\nfunction stopAllPlayback(): void {\n if (playingAudio.value) {\n playingAudio.value.audio.pause();\n playingAudio.value = null;\n audioProgress.value = 0;\n }\n if (silentPlaybackTimer.value) {\n clearTimeout(silentPlaybackTimer.value.timer);\n silentPlaybackTimer.value = null;\n }\n}\n\n// Single entry point for \"start playback at beat <index>\". Routes\n// on what the script DECLARED, not on what's currently hydrated:\n//\n// - `text` empty → silent path (`scheduleSilentAdvance`). The\n// schema says no audio is generated for empty-text beats, so\n// `duration` drives auto-advance.\n// - `text` present + audio loaded → audio path. `audio.ended`\n// chains via `advanceFromBeat`.\n// - `text` present + audio NOT loaded → stop. The Play button's\n// `isPlayReady` gate prevented this for beat 0, but mid-stream\n// a transient fetch miss must not silently skip the narration\n// by falling through to the silent timer (Codex review on\n// #1073 — gating on `beatAudios[index]` would do exactly that).\n//\n// Either path chains to the next beat via `advanceFromBeat`, so a\n// run of silent beats — or audio / silent / audio sequences —\n// plays through without manual interaction.\nfunction playBeat(index: number): void {\n stopAllPlayback();\n const hasText = Boolean(effectiveBeat(index).text);\n if (!hasText) {\n scheduleSilentAdvance(index);\n return;\n }\n if (beatAudios[index]) {\n playAudio(index);\n }\n // Text beat with no audio yet → stop. The user can re-click Play\n // once the audio finishes hydrating.\n}\n\nfunction scheduleSilentAdvance(index: number): void {\n // Defensively narrow the script-supplied duration (zero / negative / NaN /\n // non-number → default) — a bad value would otherwise collapse to an\n // immediate timeout and the Play loop would race through every silent beat\n // in a single tick (Codex review iter-5 on #1365).\n const seconds = resolveSilentAdvanceSeconds(effectiveBeat(index).duration, SILENT_BEAT_DEFAULT_SEC);\n const timer = setTimeout(() => {\n if (silentPlaybackTimer.value?.index !== index) return;\n silentPlaybackTimer.value = null;\n if (lightbox.value?.index === index) advanceFromBeat(index);\n }, seconds * MS_PER_SECOND);\n silentPlaybackTimer.value = { index, timer };\n}\n\nfunction advanceFromBeat(fromIndex: number): void {\n lightboxMove(1);\n const nextIndex = lightbox.value?.index;\n if (nextIndex === undefined || nextIndex === fromIndex) return;\n playBeat(nextIndex);\n}\n\nconst hasPrev = computed(() => {\n if (!lightbox.value) return false;\n for (let i = lightbox.value.index - 1; i >= 0; i--) {\n if (renderedImages[i]) return true;\n }\n return false;\n});\n\nconst hasNext = computed(() => {\n if (!lightbox.value) return false;\n for (let i = lightbox.value.index + 1; i < beats.value.length; i++) {\n if (renderedImages[i]) return true;\n }\n return false;\n});\n\n// Narration text per beat, for the lightbox beat-strip hover tooltips. Reads\n// through `effectiveBeat` so an unsaved in-place edit shows its new text.\nconst beatTexts = computed(() => beats.value.map((_, index) => effectiveBeat(index).text));\n\nfunction jumpToBeat(index: number) {\n if (!lightbox.value) return;\n if (index === lightbox.value.index) return;\n if (!renderedImages[index]) return;\n // Carry the playback mode forward (audio OR silent timer) so a\n // user clicking the beat-strip thumbnail mid-playback keeps the\n // presentation rolling (#1073).\n const wasPlaying = playingAudio.value !== null || silentPlaybackTimer.value !== null;\n openLightbox(index);\n if (wasPlaying) playBeat(index);\n}\n\nfunction lightboxMove(delta: number) {\n if (!lightbox.value) return;\n const total = beats.value.length;\n // If a playback was in progress when the user clicked the arrow,\n // carry it forward to whichever beat we land on — `playBeat`\n // picks audio vs silent automatically. `openLightbox` stops the\n // current playback, so capture the flag BEFORE that and chain\n // AFTER. The on-ended / silent-advance paths already null their\n // own state before calling `lightboxMove`, so this branch won't\n // double-fire there.\n const wasPlaying = playingAudio.value !== null || silentPlaybackTimer.value !== null;\n let i = lightbox.value.index + delta;\n while (i >= 0 && i < total) {\n if (renderedImages[i]) {\n openLightbox(i);\n if (wasPlaying) playBeat(i);\n return;\n }\n i += delta;\n }\n}\nconst sourceDetails = ref<HTMLDetailsElement>();\nconst editing = ref(false);\nconst editableSource = ref(\"\");\nconst { copied, copy } = useClipboardCopy();\n\n// Beats may be edited in-place via `updateBeat()` and rendered through\n// `effectiveBeat()`, so the Copy / source-view text must read the merged\n// shape — otherwise the clipboard returns the original prop snapshot\n// until the full result is reloaded.\nconst effectiveScript = computed<MulmoScript>(() => ({\n ...script.value,\n beats: beats.value.map((beat, i) => localOverrides[i] ?? beat),\n}));\nconst scriptSourceText = computed(() => toScriptSourceText(effectiveScript.value));\n\n// Persist a saved script back into the parent's toolResult so the in-memory\n// script and reactive beats[] stay in sync without a remount. The parent's\n// handleUpdateResult uses Object.assign (in-place), so the prop watcher won't\n// fire — callers that need a re-read drive initializeScript themselves.\nfunction commitScript(next: MulmoScript): void {\n emit(\"updateResult\", {\n ...props.selectedResult,\n data: { ...props.selectedResult.data, script: next },\n });\n}\n\n// #1575 — when every beat is a `slide`, swap the per-beat list UI for the\n// interactive deck editor (@mulmocast/deck-web). Mixed scripts (any non-slide\n// beat) fall back to the existing list. The debounce + flush-on-unmount live\n// in the composable.\nconst { isDeck, deckScriptInput, onDeckUpdate, flushPendingDeckSave } = useDeckEditor({ api, filePath, effectiveScript, commitScript });\n\nonBeforeUnmount(() => {\n flushPendingDeckSave();\n // Release beat-clip blob object URLs — they outlive the component\n // otherwise (document-scoped, not GC'd with it).\n resetBeatMovies();\n unsubscribeGenerationEvents();\n});\nconst loadedSource = ref(\"\");\nconst sourceChanged = computed(() => editableSource.value !== loadedSource.value);\nconst sourceValid = computed(() => {\n try {\n const parsed = JSON.parse(editableSource.value);\n return mulmoScriptSchema.safeParse(parsed).success;\n } catch {\n return false;\n }\n});\n\nasync function onSourceToggle(open: boolean) {\n editing.value = open;\n if (open) {\n let text = scriptSourceText.value;\n // Re-read the current file from disk so beat-level edits made\n // since mount (other tabs, MCP, manual edits) surface in the\n // editor. Uses the reopen dispatch for the same reason\n // refreshScriptFromDisk does — `filePath.value` is the wire form\n // `stories/<rel>` and only the mulmoScript save/reopen op knows\n // how to map it to the on-disk path under `artifacts/stories/...`.\n if (filePath.value) {\n const response = await api.call(\"save\", { filePath: filePath.value });\n const diskScript = response.ok ? (response.data.script as MulmoScript | undefined) : undefined;\n if (diskScript) text = toScriptSourceText(diskScript);\n // fall through to in-memory script on failure\n }\n editableSource.value = text;\n loadedSource.value = text;\n }\n}\n\nfunction cancelSourceEdit() {\n if (sourceDetails.value) sourceDetails.value.open = false;\n}\n\nasync function applySource() {\n let parsed: MulmoScript;\n try {\n parsed = JSON.parse(editableSource.value);\n } catch (err) {\n alert(errorMessage(err));\n return;\n }\n const response = await api.call(\"updateScript\", {\n filePath: filePath.value,\n script: parsed,\n });\n if (!response.ok) {\n alert(response.error || \"Update failed\");\n return;\n }\n\n // Update the UI with the new script. commitScript emits first so the parent\n // data is updated (its handleUpdateResult uses in-place Object.assign, so\n // the prop watcher won't fire), then we manually re-initialize the view.\n commitScript(parsed);\n\n if (sourceDetails.value) sourceDetails.value.open = false;\n await initializeScript();\n}\n\nasync function copyText() {\n await copy(scriptSourceText.value);\n}\n\nfunction effectiveBeat(index: number): Beat {\n return effectiveBeatOf(localOverrides, beats.value, index);\n}\n\nfunction toggleSource(index: number) {\n if (!sourceOpen[index]) {\n sourceText[index] = toScriptSourceText(effectiveBeat(index));\n Reflect.deleteProperty(beatSaveErrors, index);\n }\n sourceOpen[index] = !sourceOpen[index];\n}\n\nfunction isValidBeat(index: number): boolean {\n return isValidBeatOf(sourceText[index], mulmoBeatSchema);\n}\n\nasync function updateBeat(index: number) {\n let beat: Beat;\n try {\n beat = JSON.parse(sourceText[index]);\n } catch (err) {\n beatSaveErrors[index] = { kind: \"invalidJson\", error: errorMessage(err) };\n return;\n }\n const prevImage = JSON.stringify(effectiveBeat(index).image);\n const prevText = effectiveBeat(index).text;\n\n const requestedFilePath = filePath.value;\n Reflect.deleteProperty(beatSaveErrors, index);\n beatSaving[index] = true;\n const response = await api.call(\"updateBeat\", {\n filePath: requestedFilePath,\n beatIndex: index,\n beat,\n });\n if (staleSince(requestedFilePath)) return;\n Reflect.deleteProperty(beatSaving, index);\n if (!response.ok) {\n beatSaveErrors[index] = { kind: \"saveFailed\", error: response.error };\n return;\n }\n\n localOverrides[index] = beat;\n sourceOpen[index] = false;\n\n if (JSON.stringify(beat.image) !== prevImage) {\n Reflect.deleteProperty(renderedImages, index);\n renderBeat(index);\n }\n\n // Audio files are content-addressed by the beat's text\n // (getBeatAudioPathOrUrl hashes text + voice), so after a text edit\n // the cached data URI belongs to the OLD narration. Drop it so the\n // \"Generate Audio\" button reappears, then re-probe — if the new text\n // matches previously generated audio (e.g. the edit was a revert),\n // the probe restores Play without a paid TTS call.\n if (beat.text !== prevText) {\n // If this beat's old narration is mid-playback, stop it first —\n // the deletes below remove the Play/Stop control from the row,\n // which would otherwise leave the stale audio playing with no\n // way to stop it (Codex review on #2143).\n if (playingAudio.value?.index === index) stopAllPlayback();\n Reflect.deleteProperty(beatAudios, index);\n Reflect.deleteProperty(audioState, index);\n Reflect.deleteProperty(audioErrors, index);\n if (beat.text) void loadExistingBeatAudio(index);\n }\n}\n\nasync function renderBeat(index: number) {\n const requestedFilePath = filePath.value;\n renderState[index] = \"rendering\";\n const response = await api.call(\"renderBeat\", {\n filePath: requestedFilePath,\n beatIndex: index,\n chatSessionId: chatSessionId.value,\n });\n if (staleSince(requestedFilePath)) return;\n if (!response.ok) {\n renderErrors[index] = response.error || \"Render failed\";\n renderState[index] = \"error\";\n return;\n }\n renderedImages[index] = response.data.image ?? \"\";\n renderState[index] = \"done\";\n refreshMissingCharacterImages();\n if (beatMayHaveMovie(effectiveBeat(index))) void loadExistingBeatMovie(index);\n}\n\nasync function regenerateBeat(index: number) {\n const requestedFilePath = filePath.value;\n Reflect.deleteProperty(renderedImages, index);\n invalidateBeatMovie(index);\n renderState[index] = \"rendering\";\n const response = await api.call(\"renderBeat\", {\n filePath: requestedFilePath,\n beatIndex: index,\n force: true,\n chatSessionId: chatSessionId.value,\n });\n if (staleSince(requestedFilePath)) return;\n if (!response.ok) {\n renderErrors[index] = response.error || \"Render failed\";\n renderState[index] = \"error\";\n return;\n }\n renderedImages[index] = response.data.image ?? \"\";\n renderState[index] = \"done\";\n if (beatMayHaveMovie(effectiveBeat(index))) void loadExistingBeatMovie(index);\n}\n\n// Stale-response guard shared by every per-beat/character loader and\n// mutator below: capture the wire path at call time and discard the\n// response when the user has navigated to a different result meanwhile —\n// otherwise late responses from script A's bulk mount-time probes would\n// write into the per-beat maps that now belong to script B.\nfunction staleSince(requestedFilePath: string): boolean {\n return staleSinceOf(filePath.value, requestedFilePath);\n}\n\nasync function loadExistingBeatImage(index: number) {\n const requestedFilePath = filePath.value;\n const response = await api.call(\"beatImage\", { filePath: requestedFilePath, beatIndex: index });\n if (staleSince(requestedFilePath)) return;\n // silently ignore errors — image simply hasn't been generated yet\n if (response.ok && response.data.image) {\n renderedImages[index] = response.data.image;\n renderState[index] = \"done\";\n }\n}\n\nasync function loadExistingBeatAudio(index: number) {\n const requestedFilePath = filePath.value;\n const response = await api.call(\"beatAudio\", { filePath: requestedFilePath, beatIndex: index });\n if (staleSince(requestedFilePath)) return;\n // silently ignore errors\n if (response.ok && response.data.audio) {\n beatAudios[index] = response.data.audio;\n audioState[index] = \"done\";\n }\n}\n\nasync function generateAudio(index: number) {\n const requestedFilePath = filePath.value;\n audioState[index] = \"generating\";\n Reflect.deleteProperty(audioErrors, index);\n const response = await api.call(\"generateBeatAudio\", {\n filePath: requestedFilePath,\n beatIndex: index,\n chatSessionId: chatSessionId.value,\n });\n if (staleSince(requestedFilePath)) return;\n if (!response.ok) {\n audioErrors[index] = response.error || \"Audio generation failed\";\n audioState[index] = \"error\";\n return;\n }\n beatAudios[index] = response.data.audio ?? \"\";\n audioState[index] = \"done\";\n}\n\nfunction playAudio(index: number) {\n if (playingAudio.value) {\n playingAudio.value.audio.pause();\n const wasIndex = playingAudio.value.index;\n playingAudio.value = null;\n if (wasIndex === index) return;\n }\n const src = beatAudios[index];\n if (!src) return;\n const audio = new Audio(src);\n playingAudio.value = { index, audio };\n audioProgress.value = 0;\n audio.addEventListener(\"timeupdate\", () => {\n if (playingAudio.value?.index !== index) return;\n if (audio.duration > 0) audioProgress.value = audio.currentTime / audio.duration;\n });\n audio.addEventListener(\"ended\", () => {\n if (playingAudio.value?.index !== index) return;\n playingAudio.value = null;\n audioProgress.value = 0;\n if (lightbox.value?.index === index) advanceFromBeat(index);\n });\n audio.play();\n}\n\nfunction onBeatDragOver(event: DragEvent, index: number) {\n if (!event.dataTransfer?.types.includes(\"Files\")) return;\n event.preventDefault();\n beatDragOver[index] = true;\n}\n\nfunction onBeatDragLeave(index: number) {\n beatDragOver[index] = false;\n}\n\nasync function onBeatDrop(event: DragEvent, index: number) {\n event.preventDefault();\n beatDragOver[index] = false;\n const file = event.dataTransfer?.files[0];\n if (!file || !file.type.startsWith(\"image/\")) return;\n\n renderState[index] = \"rendering\";\n Reflect.deleteProperty(renderErrors, index);\n let imageData: string;\n try {\n imageData = await readFileAsDataUrl(file);\n } catch (err) {\n renderErrors[index] = errorMessage(err);\n renderState[index] = \"error\";\n return;\n }\n const requestedFilePath = filePath.value;\n const response = await api.call(\"uploadBeatImage\", {\n filePath: requestedFilePath,\n beatIndex: index,\n imageData,\n });\n if (staleSince(requestedFilePath)) return;\n if (!response.ok) {\n renderErrors[index] = response.error || \"Upload failed\";\n renderState[index] = \"error\";\n return;\n }\n renderedImages[index] = response.data.image ?? \"\";\n renderState[index] = \"done\";\n}\n\nfunction openCharacterLightbox(key: string) {\n // Stop both audio and silent timer — character lightbox is\n // outside the play loop (#1073).\n stopAllPlayback();\n lightbox.value = {\n src: charImages[key],\n text: key,\n index: -1,\n isCharacter: true,\n };\n}\n\n// Probe the server for an existing beat PNG before triggering any\n// generation. Only auto-renders when the disk is empty AND the beat\n// is a deterministic type — imagePrompt beats are left empty so the\n// user clicks Generate explicitly (avoids surprise paid text2image\n// calls on every page refresh).\nasync function hydrateBeatImage(beat: Beat, index: number, hasCharacters: boolean, autoRenderTypes: readonly string[]): Promise<void> {\n await loadExistingBeatImage(index);\n if (renderedImages[index]) return;\n if (shouldAutoRenderBeat(beat, hasCharacters, autoRenderTypes)) {\n await renderBeat(index);\n }\n}\n\n/**\n * #1074 — keep the in-memory toolResult in sync with the on-disk\n * script file. `updateBeat` / `updateScript` persist edits to\n * disk, but the session entry that backs\n * `props.selectedResult.data.script` is never rewritten, so a\n * page reload + session-restore would otherwise surface stale\n * pre-edit content.\n *\n * Why the reopen dispatch, not a generic file read: `filePath`\n * is the wire form `stories/<rel>` which only the mulmoScript save\n * op knows how to translate back to the real on-disk path under\n * `artifacts/stories/...`. The reopen op is read-only when `script`\n * is omitted; it does NOT trigger movie generation.\n *\n * The flow silently bails on every failure mode so a missing /\n * malformed / deleted script file never blocks the rest of\n * `initializeScript`.\n *\n * Stale-response guard: capture `uuid` + `filePath` before the\n * `await`. If either has changed by the time the response lands\n * (the user navigated to a different result while the request\n * was in flight, or `props.selectedResult` was swapped under us\n * by a parent watcher), drop the response on the floor — the new\n * `initializeScript` invocation triggered by that change will\n * issue its own refresh against the correct file.\n */\nasync function refreshScriptFromDisk(): Promise<void> {\n const requestedFilePath = filePath.value;\n if (!requestedFilePath) return;\n const requestedUuid = props.selectedResult.uuid;\n const response = await api.call(\"save\", { filePath: requestedFilePath });\n if (props.selectedResult.uuid !== requestedUuid || filePath.value !== requestedFilePath) return;\n if (!response.ok) return;\n const diskScript = response.data.script as MulmoScript | undefined;\n // The server-side reopen op already validated against\n // `mulmoScriptSchema`, so a non-null `script` is trusted here —\n // we only need a presence check.\n if (!diskScript) return;\n if (isSameScript(diskScript, script.value)) return;\n commitScript(diskScript);\n}\n\nasync function initializeScript() {\n // Stop any in-flight playback BEFORE we tear down per-script state\n // — a pending `silentPlaybackTimer` or running audio from the\n // previous script would otherwise fire `advanceFromBeat()` against\n // the new script's lightbox / beat list and either crash or\n // silently jump the new presentation forward. Also close any open\n // lightbox so the user lands on the clean View for the new result\n // (Codex review iter-4 on #1365).\n stopAllPlayback();\n lightbox.value = null;\n // Reset scroll position so new results start at the top\n if (beatListEl.value) beatListEl.value.scrollTop = 0;\n // Reset per-script state. resetMedia clears the movie/PDF spinners too —\n // per-script, so switching away from a generating script doesn't leave the\n // new script's toolbar spinning; the pendingGenerations snapshot below\n // re-lights them when the NEW script really does have work in flight.\n clearReactiveRecords(\n renderState,\n renderedImages,\n renderErrors,\n sourceOpen,\n sourceText,\n beatSaveErrors,\n beatSaving,\n localOverrides,\n beatAudios,\n audioState,\n audioErrors,\n beatDragOver,\n );\n resetCharacters();\n resetBeatMovies();\n resetMedia();\n if (sourceDetails.value) sourceDetails.value.open = false;\n\n // #1074 — re-read the script file from disk before per-beat\n // hydration. When the user switches between tool results inside\n // the same SPA mount and switches back, the in-memory toolResult\n // still carries whatever script was captured earlier, and\n // `localOverrides` (the only thing showing the user's edit since\n // the last save) is reset by initializeScript on remount.\n // Re-fetching from disk via the reopen op covers that gap.\n await refreshScriptFromDisk();\n\n // Mount-time policy: prefer the existing PNG on the server. Every\n // beat — deterministic AND imagePrompt — first probes beatImage,\n // and we only fall through to renderBeat() when the disk has nothing\n // yet AND the type is safe to auto-render (deterministic content,\n // no characters waiting). Without this probe a refresh would re-fire\n // generateBeatImage for every beat, and for imagePrompt beats that\n // means a paid text2image call against an image we already have.\n //\n // Stale-after-edit: if the user edits the script source the on-disk\n // PNG is no longer in sync with the new content, but we don't try to\n // detect that here — the per-beat ↺ button is one click away and a\n // page refresh re-runs this same probe, so the user can opt back into\n // a fresh render whenever they need to.\n const AUTO_RENDER_TYPES = [\"textSlide\", \"markdown\", \"chart\", \"mermaid\", \"html_tailwind\", \"slide\"] as const;\n const hasCharacters = characterKeys.value.length > 0;\n beats.value.forEach((beat, index) => {\n void hydrateBeatImage(beat, index, hasCharacters, AUTO_RENDER_TYPES);\n if (beat.text) loadExistingBeatAudio(index);\n if (beatMayHaveMovie(beat)) void loadExistingBeatMovie(index);\n });\n\n characterKeys.value.forEach((key) => loadExistingCharacterImage(key));\n\n if (filePath.value) {\n // Stale-response guard: if the user navigates to a different result\n // while these calls are in flight, their answers describe the OLD\n // script — drop them instead of stamping them onto the new one.\n const requestedFilePath = filePath.value;\n const isStale = () => filePath.value !== requestedFilePath;\n\n const response = await api.call(\"movieStatus\", { filePath: requestedFilePath });\n if (isStale()) return;\n if (response.ok && response.data.moviePath) {\n moviePath.value = response.data.moviePath;\n }\n // ignore errors\n // Also check whether a PDF was previously generated and is still\n // newer than the source; status returns null otherwise so the UI\n // re-offers the Generate button.\n const pdfResponse = await api.call(\"pdfStatus\", { filePath: requestedFilePath });\n if (isStale()) return;\n if (pdfResponse.ok && pdfResponse.data.pdfPath) {\n pdfPath.value = pdfResponse.data.pdfPath;\n }\n\n // Reflect any generations that were already in flight when we\n // mounted (user switched away mid-generation and came back).\n // Snapshot via dispatch; live updates arrive on the pubsub\n // subscription below.\n const pending = await api.call(\"pendingGenerations\", { filePath: requestedFilePath });\n if (isStale()) return;\n if (pending.ok) {\n for (const entry of pending.data.pending) {\n reflectGenerationStart(entry);\n }\n }\n }\n}\n\nonMounted(initializeScript);\nwatch(() => props.selectedResult, initializeScript);\n\n// Keep the view in sync with generations running anywhere — this View's\n// own long-held dispatches, a parallel tab, the agent's background\n// autoGenerateMovie. The host publishes `generation` events on the\n// plugin pubsub channel (started + finished, per beat and per artifact);\n// on start we mirror the local \"rendering\" state so spinners show even\n// after a remount, on finish we reload the relevant asset off disk.\nconst unsubscribeGenerationEvents = api.onGenerationEvent(\n () => filePath.value,\n (event) => {\n if (!event.done) {\n reflectGenerationStart(event);\n return;\n }\n // Fire-and-forget: swallow + log so a failed reload doesn't\n // surface as an unhandled rejection.\n reflectGenerationFinish(event).catch((err) => {\n console.error(\"[presentMulmoScript] reload on finish failed:\", err);\n });\n },\n);\n\nfunction reflectGenerationStart(entry: MulmoScriptGenerationEvent): void {\n if (entry.kind === \"beatImage\") {\n const idx = Number(entry.key);\n if (!renderedImages[idx]) renderState[idx] = \"rendering\";\n } else if (entry.kind === \"beatAudio\") {\n const idx = Number(entry.key);\n if (!beatAudios[idx]) audioState[idx] = \"generating\";\n } else if (entry.kind === \"characterImage\") {\n if (!charImages[entry.key]) charRenderState[entry.key] = \"rendering\";\n } else if (entry.kind === \"movie\") {\n movieGenerating.value = true;\n } else if (entry.kind === \"pdf\") {\n pdfGenerating.value = true;\n }\n}\n\nasync function reflectGenerationFinish(entry: MulmoScriptGenerationEvent): Promise<void> {\n if (entry.kind === \"beatImage\") {\n const idx = Number(entry.key);\n await loadExistingBeatImage(idx);\n if (beatMayHaveMovie(effectiveBeat(idx))) await loadExistingBeatMovie(idx);\n if (renderState[idx] === \"rendering\") Reflect.deleteProperty(renderState, idx);\n refreshMissingCharacterImages();\n } else if (entry.kind === \"beatAudio\") {\n const idx = Number(entry.key);\n await loadExistingBeatAudio(idx);\n if (audioState[idx] === \"generating\") Reflect.deleteProperty(audioState, idx);\n } else if (entry.kind === \"characterImage\") {\n await loadExistingCharacterImage(entry.key);\n if (charRenderState[entry.key] === \"rendering\") {\n Reflect.deleteProperty(charRenderState, entry.key);\n }\n } else if (entry.kind === \"movie\") {\n movieGenerating.value = false;\n await refreshMoviePath();\n } else if (entry.kind === \"pdf\") {\n pdfGenerating.value = false;\n await refreshPdfPath();\n }\n}\n</script>\n\n<style scoped>\n.bottom-bar-wrapper {\n position: relative;\n flex-shrink: 0;\n}\n\n.script-source {\n padding: 0.5rem;\n background: #f5f5f5;\n border-top: 1px solid #e0e0e0;\n font-family: Consolas, \"MS Gothic\", \"BIZ UDGothic\", monospace;\n font-size: 0.85rem;\n}\n\n.script-source summary {\n cursor: pointer;\n user-select: none;\n padding: 0.5rem;\n background: #e8e8e8;\n border-radius: 4px;\n font-weight: 500;\n color: #333;\n}\n\n.script-source[open] summary {\n margin-bottom: 0.5rem;\n}\n\n.script-source summary:hover {\n background: #d8d8d8;\n}\n\n.script-editor {\n width: 100%;\n height: 40vh;\n padding: 1rem;\n background: #ffffff;\n border: 1px solid #ccc;\n border-radius: 4px;\n color: #333;\n font-family: \"Courier New\", \"MS Gothic\", \"BIZ UDGothic\", monospace;\n font-size: 0.9rem;\n resize: vertical;\n margin-bottom: 0.5rem;\n line-height: 1.5;\n}\n\n.script-editor:focus {\n outline: none;\n border-color: #4caf50;\n box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.1);\n}\n\n.script-editor-invalid {\n border-color: #ef4444;\n}\n\n.script-editor-invalid:focus {\n border-color: #ef4444;\n box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.1);\n}\n\n.editor-actions {\n display: flex;\n justify-content: space-between;\n}\n\n.apply-btn {\n padding: 0.5rem 1rem;\n background: #4caf50;\n color: white;\n border: none;\n border-radius: 4px;\n cursor: pointer;\n font-size: 0.9rem;\n transition: background 0.2s;\n font-weight: 500;\n}\n\n.apply-btn:hover {\n background: #45a049;\n}\n\n.apply-btn:disabled {\n background: #cccccc;\n color: #666666;\n cursor: not-allowed;\n opacity: 0.6;\n}\n\n.cancel-btn {\n padding: 0.5rem 1rem;\n background: #e0e0e0;\n color: #333;\n border: none;\n border-radius: 4px;\n cursor: pointer;\n font-size: 0.9rem;\n transition: background 0.2s;\n font-weight: 500;\n}\n\n.cancel-btn:hover {\n background: #d0d0d0;\n}\n\n.copy-btn {\n position: absolute;\n bottom: 0.3rem;\n right: 0.65rem;\n padding: 0.4rem;\n background: none;\n border: none;\n color: #333;\n cursor: pointer;\n z-index: 1;\n}\n\n.copy-btn:hover {\n color: #000;\n}\n\n.copy-btn .material-icons {\n font-size: 1.15rem;\n}\n</style>\n","<template>\n <div class=\"h-full bg-white flex flex-col overflow-hidden\">\n <!-- Header -->\n <div class=\"flex items-start justify-between px-6 py-4 border-b border-gray-100 shrink-0\">\n <div class=\"min-w-0 flex-1\">\n <h2 class=\"text-lg font-semibold text-gray-800 truncate\" data-testid=\"mulmo-script-title\">\n {{ script.title || \"Untitled Script\" }}\n </h2>\n <p v-if=\"script.description\" class=\"text-sm text-gray-500 mt-0.5 truncate\" data-testid=\"mulmo-script-description\">\n {{ script.description }}\n </p>\n <div class=\"flex items-center gap-3 mt-1 text-xs text-gray-400\">\n <span>{{ m.beatCount(beats.length) }}</span>\n <span v-if=\"script.lang\">{{ script.lang }}</span>\n <span v-if=\"filePath\" class=\"truncate\">{{ filePath }}</span>\n </div>\n </div>\n <MulmoScriptToolbar\n :movie-path=\"moviePath\"\n :movie-generating=\"movieGenerating\"\n :movie-downloading=\"movieDownloading\"\n :is-play-ready=\"isPlayReady\"\n :can-fetch-media=\"canFetchMedia\"\n :pdf-path=\"pdfPath\"\n :pdf-generating=\"pdfGenerating\"\n :pdf-downloading=\"pdfDownloading\"\n @play=\"playPresentation\"\n @generate-movie=\"generateMovie\"\n @download-movie=\"downloadMovie\"\n @generate-pdf=\"generatePdf\"\n @download-pdf=\"downloadPdf\"\n />\n </div>\n\n <!--\n Inline error chip for movie-generation failures (#1197).\n Previously the catch arm of `generateMovie` raised an `alert()` —\n blocking, no retry path, and many users just dismissed the modal\n and saw a stalled spinner with no explanation. The chip stays\n visible until the next generate attempt clears it.\n -->\n <div\n v-if=\"movieError\"\n data-testid=\"mulmo-script-movie-error-chip\"\n class=\"bg-red-50 border border-red-200 text-red-800 text-xs px-3 py-2 mx-4 mt-3 mb-1 rounded flex items-start gap-2\"\n >\n <span class=\"material-icons text-base shrink-0 mt-px\">error_outline</span>\n <div class=\"flex-1 min-w-0\">\n <div class=\"font-medium\">{{ m.movieGenerationFailed }}</div>\n <div class=\"break-words whitespace-pre-wrap mt-0.5\">{{ movieError }}</div>\n </div>\n <button\n class=\"shrink-0 h-7 px-2 text-xs rounded border border-red-300 text-red-700 hover:bg-red-100 disabled:opacity-50\"\n :disabled=\"movieGenerating\"\n data-testid=\"mulmo-script-movie-retry-button\"\n @click=\"generateMovie\"\n >\n {{ m.retry }}\n </button>\n </div>\n\n <!-- Characters section -->\n <CharacterStrip\n v-if=\"characterKeys.length > 0\"\n :character-keys=\"characterKeys\"\n :images=\"script.imageParams?.images\"\n :thumbnails=\"charImages\"\n :render-state=\"charRenderState\"\n :errors=\"charErrors\"\n :drag-over=\"charDragOver\"\n :movie-generating=\"movieGenerating\"\n :any-beat-rendering=\"anyBeatRendering\"\n @generate-all=\"generateAllCharacters\"\n @char-drag-over=\"onCharDragOver\"\n @char-drag-leave=\"onCharDragLeave\"\n @char-drop=\"onCharDrop\"\n @open-lightbox=\"openCharacterLightbox\"\n @render-character=\"renderCharacter\"\n />\n\n <!-- Deck editor (#1575): every beat is a slide → mount the\n interactive deck editor from @mulmocast/deck-web. The Vue\n component is lazy-loaded via defineAsyncComponent, so users\n whose scripts aren't decks never pay the bundle cost. -->\n <div v-if=\"isDeck\" class=\"flex-1 overflow-hidden\" data-testid=\"mulmo-script-deck-editor\">\n <MulmoScriptDeckEditor :script=\"deckScriptInput\" layout=\"compact\" @update:script=\"onDeckUpdate\" />\n </div>\n\n <!-- Beat list (fallback when the script has any non-slide beat) -->\n <div v-else ref=\"beatListEl\" class=\"flex-1 overflow-y-auto p-2 space-y-1.5\">\n <div v-for=\"(beat, index) in beats\" :key=\"index\" class=\"rounded-lg border border-gray-200 overflow-hidden\">\n <!-- Beat body: thumbnail + narration side by side -->\n <div class=\"flex gap-3 items-stretch\">\n <!-- Thumbnail -->\n <div\n class=\"relative shrink-0 w-[45%] overflow-hidden bg-gray-50 transition-colors\"\n :class=\"beatDragOver[index] ? 'bg-blue-50' : ''\"\n @dragover=\"onBeatDragOver($event, index)\"\n @dragleave=\"onBeatDragLeave(index)\"\n @drop=\"onBeatDrop($event, index)\"\n >\n <!-- Beat number badge (1-based). Sits above the drop-hint\n overlay and the inline video player so the index stays\n readable in every beat state. -->\n <div\n class=\"absolute top-1.5 left-1.5 z-10 px-1.5 py-0.5 rounded bg-black/55 text-white text-xs font-medium leading-none pointer-events-none\"\n :data-testid=\"`mulmo-script-beat-number-${index}`\"\n >\n {{ index + 1 }}\n </div>\n <!-- Inline player for the beat's generated video clip.\n Replaces the thumbnail while open; the close button\n returns to the still image. -->\n <template v-if=\"beatMovieOpen[index] && beatMovieUrls[index]\">\n <video :src=\"beatMovieUrls[index]\" class=\"w-full object-contain\" controls autoplay :data-testid=\"`mulmo-script-beat-movie-player-${index}`\" />\n <button\n class=\"absolute top-1.5 right-1.5 flex items-center justify-center w-6 h-6 rounded border border-gray-400 text-gray-600 bg-white hover:bg-gray-50\"\n :title=\"m.close\"\n :aria-label=\"m.close\"\n :data-testid=\"`mulmo-script-beat-movie-close-${index}`\"\n @click.stop=\"closeBeatMovie(index)\"\n >\n <span class=\"material-icons text-sm\">close</span>\n </button>\n </template>\n <template v-else>\n <img\n v-if=\"renderedImages[index]\"\n :src=\"renderedImages[index]\"\n class=\"w-full object-contain cursor-zoom-in\"\n :alt=\"`Beat ${index + 1}`\"\n @click=\"openLightbox(index)\"\n />\n <!-- Play overlay: shown when the beat-movie probe found a\n generated clip for this beat. Blob is fetched lazily\n on first click (host-authenticated), hence the spinner. -->\n <button\n v-if=\"renderedImages[index] && beatMovies[index] && canFetchMedia\"\n class=\"absolute inset-0 m-auto w-12 h-12 flex items-center justify-center rounded-full bg-black/50 text-white hover:bg-black/70\"\n :title=\"m.play\"\n :aria-label=\"m.play\"\n :data-testid=\"`mulmo-script-beat-movie-play-${index}`\"\n @click.stop=\"playBeatMovie(index)\"\n >\n <svg v-if=\"beatMovieLoading[index]\" class=\"animate-spin w-5 h-5\" viewBox=\"0 0 24 24\" fill=\"none\">\n <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\" />\n <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8v8H4z\" />\n </svg>\n <span v-else class=\"material-icons text-3xl\">play_arrow</span>\n </button>\n <button\n v-if=\"renderedImages[index] && renderState[index] !== 'rendering'\"\n class=\"absolute top-1.5 right-1.5 flex items-center gap-1 px-2 py-0.5 text-xs rounded border border-gray-400 text-gray-600 bg-white hover:bg-gray-50 disabled:opacity-60 disabled:cursor-not-allowed\"\n :disabled=\"movieGenerating\"\n @click.stop=\"regenerateBeat(index)\"\n >\n ↺\n </button>\n <div v-else-if=\"!renderedImages[index]\" class=\"w-full aspect-video flex flex-col items-center justify-center gap-1 p-2\">\n <template v-if=\"renderState[index] === 'rendering' || (movieGenerating && !renderedImages[index] && effectiveBeat(index).imagePrompt)\">\n <svg class=\"animate-spin w-4 h-4 text-green-400\" viewBox=\"0 0 24 24\" fill=\"none\">\n <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\" />\n <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8v8H4z\" />\n </svg>\n <span class=\"text-xs text-green-500\">{{ m.rendering }}</span>\n </template>\n <template v-else-if=\"renderState[index] === 'error'\">\n <span class=\"text-xs text-red-400 text-center\">{{ renderErrors[index] }}</span>\n </template>\n <template v-else>\n <span v-if=\"effectiveBeat(index).imagePrompt\" class=\"text-xs text-gray-400 text-center italic leading-relaxed px-1\">{{\n effectiveBeat(index).imagePrompt\n }}</span>\n <span v-else class=\"text-xs text-gray-300\">{{ beat.image?.type ?? \"—\" }}</span>\n </template>\n </div>\n </template>\n <!-- Beat drop hint / overlay -->\n <div v-if=\"beatDragOver[index]\" class=\"absolute inset-0 flex items-center justify-center bg-blue-50/80 pointer-events-none\">\n <span class=\"text-xs text-blue-500 font-medium\">{{ m.drop }}</span>\n </div>\n <div\n v-else-if=\"!renderedImages[index] && renderState[index] !== 'rendering'\"\n class=\"absolute bottom-0 inset-x-0 text-center text-xs text-gray-400 bg-white/70 py-0.5 pointer-events-none\"\n >\n {{ m.orDropImage }}\n </div>\n <!-- Generate button for any beat without a rendered image.\n renderBeat works for every beat type: imagePrompt /\n typed image beats render directly, moviePrompt beats\n get a frame extracted from the generated clip, and\n text-only beats fall back to a prompt derived from\n the narration text (mulmocast prompt.js). -->\n <button\n v-if=\"!renderedImages[index] && renderState[index] !== 'rendering' && !movieGenerating && !isBeatImageReference(effectiveBeat(index))\"\n class=\"absolute top-1.5 right-1.5 flex items-center gap-1 px-2 py-0.5 text-xs rounded border border-blue-400 text-blue-600 bg-white hover:bg-blue-50\"\n @click=\"renderBeat(index)\"\n >\n {{ m.generate }}\n </button>\n </div>\n\n <!-- Narration text -->\n <div class=\"flex flex-col flex-1 min-w-0 px-2 py-1.5\">\n <span class=\"text-sm text-gray-800 leading-relaxed\">{{ effectiveBeat(index).text }}</span>\n <div class=\"flex justify-between mt-auto pt-1\">\n <!-- Audio controls -->\n <div class=\"flex items-center gap-1\">\n <template v-if=\"audioState[index] === 'generating' || (movieGenerating && !beatAudios[index] && effectiveBeat(index).text)\">\n <svg class=\"animate-spin w-3 h-3 text-green-400\" viewBox=\"0 0 24 24\" fill=\"none\">\n <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\" />\n <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8v8H4z\" />\n </svg>\n </template>\n <button\n v-else-if=\"beatAudios[index]\"\n class=\"text-xs px-2 py-0.5 rounded border\"\n :class=\"playingAudio?.index === index ? 'border-red-400 text-red-600 hover:bg-red-50' : 'border-green-400 text-green-600 hover:bg-green-50'\"\n @click=\"playAudio(index)\"\n >\n {{ playingAudio?.index === index ? m.stop : m.play }}\n </button>\n <template v-else-if=\"audioErrors[index]\">\n <span class=\"text-xs text-red-400 truncate min-w-0 max-w-[20rem]\" :title=\"audioErrors[index]\">\n {{ m.errPrefix }} {{ audioErrors[index] }}\n </span>\n <button\n v-if=\"effectiveBeat(index).text\"\n class=\"text-xs px-2 py-0.5 rounded border border-gray-300 text-gray-500 hover:bg-gray-50 disabled:opacity-50\"\n :disabled=\"movieGenerating\"\n @click=\"generateAudio(index)\"\n >\n ↺\n </button>\n </template>\n <button\n v-else-if=\"effectiveBeat(index).text\"\n class=\"text-xs px-2 py-0.5 rounded border border-gray-300 text-gray-500 hover:bg-gray-50\"\n @click=\"generateAudio(index)\"\n >\n {{ m.generateAudio }}\n </button>\n </div>\n <button\n class=\"text-gray-400 hover:text-gray-600\"\n :title=\"sourceOpen[index] ? 'Hide source' : 'Show source'\"\n :data-testid=\"`mulmo-script-beat-source-toggle-${index}`\"\n @click=\"toggleSource(index)\"\n >\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n class=\"w-3.5 h-3.5\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n >\n <polyline points=\"16 18 22 12 16 6\" />\n <polyline points=\"8 6 2 12 8 18\" />\n </svg>\n </button>\n </div>\n </div>\n </div>\n\n <!-- Source editor -->\n <div v-if=\"sourceOpen[index]\" class=\"border-t border-gray-100\">\n <textarea\n v-model=\"sourceText[index]\"\n class=\"w-full text-xs text-gray-600 bg-gray-50 p-2 font-mono resize-none\"\n :class=\"isValidBeat(index) ? 'outline-none' : 'outline outline-2 outline-red-400'\"\n rows=\"8\"\n spellcheck=\"false\"\n :data-testid=\"`mulmo-script-beat-source-textarea-${index}`\"\n />\n <div class=\"flex items-center justify-end gap-2 px-2 pb-2\">\n <span v-if=\"beatSaveErrors[index]\" class=\"text-xs text-red-600\" role=\"alert\">{{\n beatSaveErrors[index].kind === \"invalidJson\"\n ? m.saveErrorInvalidJson(beatSaveErrors[index].error)\n : m.saveErrorSaveFailed(beatSaveErrors[index].error)\n }}</span>\n <button\n class=\"px-2 py-1 text-xs rounded border\"\n :class=\"\n isValidBeat(index) && !beatSaving[index]\n ? 'border-blue-400 text-blue-600 hover:bg-blue-50 cursor-pointer'\n : 'border-gray-200 text-gray-300 cursor-not-allowed'\n \"\n :disabled=\"!isValidBeat(index) || !!beatSaving[index]\"\n :data-testid=\"`mulmo-script-beat-update-button-${index}`\"\n @click=\"updateBeat(index)\"\n >\n {{ beatSaving[index] ? m.saving : m.update }}\n </button>\n </div>\n </div>\n </div>\n\n <div v-if=\"beats.length === 0\" class=\"flex items-center justify-center h-32 text-gray-400 text-sm\">{{ m.noBeats }}</div>\n </div>\n\n <!-- Bottom bar: Edit Script Source + Copy -->\n <div class=\"bottom-bar-wrapper\">\n <details ref=\"sourceDetails\" class=\"script-source\" @toggle=\"onSourceToggle(($event.target as HTMLDetailsElement).open)\">\n <summary>{{ m.editSource }}</summary>\n <textarea\n v-model=\"editableSource\"\n class=\"script-editor\"\n :class=\"{ 'script-editor-invalid': sourceChanged && !sourceValid }\"\n spellcheck=\"false\"\n ></textarea>\n <div class=\"editor-actions\">\n <button class=\"apply-btn\" :disabled=\"!sourceChanged || !sourceValid\" @click=\"applySource\">{{ m.applyChanges }}</button>\n <button class=\"cancel-btn\" @click=\"cancelSourceEdit\">{{ m.cancel }}</button>\n </div>\n </details>\n <button v-show=\"!editing\" class=\"copy-btn\" :title=\"copied ? 'Copied!' : 'Copy'\" @click=\"copyText\">\n <span class=\"material-icons\">{{ copied ? \"check\" : \"content_copy\" }}</span>\n </button>\n </div>\n\n <!-- Lightbox -->\n <BeatLightbox\n v-if=\"lightbox\"\n :lightbox=\"lightbox\"\n :beat-count=\"beats.length\"\n :beat-texts=\"beatTexts\"\n :has-prev=\"hasPrev\"\n :has-next=\"hasNext\"\n :playing-audio-index=\"playingAudio?.index ?? null\"\n :audio-progress=\"audioProgress\"\n :has-current-audio=\"Boolean(beatAudios[lightbox.index])\"\n @close=\"closeLightbox\"\n @move=\"lightboxMove\"\n @jump=\"jumpToBeat\"\n @play-audio=\"playAudio\"\n />\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, defineAsyncComponent, onBeforeUnmount, onMounted, reactive, ref, watch } from \"vue\";\nimport type { ToolResultComplete } from \"gui-chat-protocol/vue\";\nimport { mulmoBeatSchema, mulmoScriptSchema } from \"@mulmocast/types\";\nimport type { MulmoScriptData } from \"../core/types\";\nimport type { MulmoScriptGenerationEvent } from \"../core/contract\";\nimport {\n isSameScript,\n beatMayHaveMovie,\n shouldAutoRenderBeat,\n effectiveBeat as effectiveBeatOf,\n isBeatImageReference,\n isValidBeat as isValidBeatOf,\n staleSince as staleSinceOf,\n scriptSourceText as toScriptSourceText,\n resolveSilentAdvanceSeconds,\n clearReactiveRecords,\n type Beat,\n} from \"./helpers\";\nimport { errorMessage } from \"@mulmoclaude/common\";\nimport { readFileAsDataUrl, useClipboardCopy } from \"./support\";\nimport { useMulmoScriptTransport } from \"./transport\";\nimport { useHostAdapter } from \"./hostAdapter\";\nimport { useMediaExport } from \"./composables/useMediaExport\";\nimport { useBeatMovie } from \"./composables/useBeatMovie\";\nimport { useCharacterImages } from \"./composables/useCharacterImages\";\nimport { useDeckEditor } from \"./composables/useDeckEditor\";\nimport type { LightboxState, MulmoScript } from \"./viewTypes\";\nimport BeatLightbox from \"./components/BeatLightbox.vue\";\nimport CharacterStrip from \"./components/CharacterStrip.vue\";\nimport MulmoScriptToolbar from \"./components/MulmoScriptToolbar.vue\";\nimport { useT } from \"../lang/index\";\n\n// Lazy-loaded so the deck editor's Vue / tailwind / SlidePreview chunk\n// stays out of the initial bundle for users whose scripts aren't decks\n// (movies, html_tailwind animations, mixed beats). `defineAsyncComponent`\n// triggers the dynamic import only when `isDeck` first flips true.\nconst MulmoScriptDeckEditor = defineAsyncComponent(() => import(\"@mulmocast/deck-web\").then((mod) => mod.MulmoScriptDeckEditor));\n\nconst api = useMulmoScriptTransport();\nconst adapter = useHostAdapter();\n// Media bytes (movie / PDF / beat clips) are served behind host auth; hosts\n// opt in by injecting `fetchMediaBlob`. Without it the download / clip-play\n// affordances are hidden (the probes still run — state stays warm for a\n// host that injects later at remount).\nconst canFetchMedia = computed(() => Boolean(adapter.fetchMediaBlob));\n\nconst m = useT();\n\nconst props = defineProps<{\n selectedResult: ToolResultComplete<MulmoScriptData>;\n}>();\nconst emit = defineEmits<{ updateResult: [result: ToolResultComplete] }>();\n\nconst data = computed(() => props.selectedResult.data);\nconst script = computed<MulmoScript>(() => data.value?.script ?? {});\nconst filePath = computed(() => data.value?.filePath ?? \"\");\nconst beats = computed<Beat[]>(() => script.value.beats ?? []);\n\n// Per-beat render state\ntype RenderState = \"idle\" | \"rendering\" | \"done\" | \"error\";\nconst renderState = reactive<Record<number, RenderState>>({});\nconst renderedImages = reactive<Record<number, string>>({});\nconst renderErrors = reactive<Record<number, string>>({});\nconst sourceOpen = reactive<Record<number, boolean>>({});\nconst sourceText = reactive<Record<number, string>>({});\n// Surface update-beat failures inline next to the Update button.\n// Cleared on next successful save or editor close. Store raw error +\n// kind tag so the template picks a localized message, instead of\n// pre-composing an English-prefixed string here.\ninterface BeatSaveError {\n kind: \"invalidJson\" | \"saveFailed\";\n error: string;\n}\nconst beatSaveErrors = reactive<Record<number, BeatSaveError>>({});\nconst beatSaving = reactive<Record<number, boolean>>({});\nconst localOverrides = reactive<Record<number, Beat>>({});\nconst beatAudios = reactive<Record<number, string>>({});\nconst audioState = reactive<Record<number, \"generating\" | \"done\" | \"error\">>({});\nconst audioErrors = reactive<Record<number, string>>({});\nconst playingAudio = ref<{ index: number; audio: HTMLAudioElement } | null>(null);\n// Tracks the auto-advance timer running on a silent beat\n// (`beat.text === \"\"`). Beats without text generate no audio, so the\n// Play loop falls back to a `setTimeout(beat.duration)` for cues —\n// without this, Play would stall on the first silent beat (#1073).\nconst silentPlaybackTimer = ref<{ index: number; timer: ReturnType<typeof setTimeout> } | null>(null);\nconst audioProgress = ref(0);\n\n// Default duration (seconds) for a silent beat whose script doesn't\n// set `duration` either. Picked to roughly match the time it takes a\n// reader to scan a `textSlide` — long enough to read, short enough\n// not to feel stuck. The script's own `duration` always wins.\nconst SILENT_BEAT_DEFAULT_SEC = 3;\nconst MS_PER_SECOND = 1000;\nconst beatListEl = ref<HTMLElement | null>(null);\nconst lightbox = ref<LightboxState | null>(null);\nconst beatDragOver = reactive<Record<number, boolean>>({});\n\nconst anyBeatRendering = computed(() => Object.values(renderState).some((state) => state === \"rendering\"));\n\n// Session tagging is host transport: MulmoClaude injects the active chat\n// session id so generations light its per-session sidebar indicator;\n// hosts without sessions leave the adapter empty and the field is simply\n// omitted from generation dispatches.\nconst chatSessionId = computed(() => adapter.chatSessionId?.value);\n\nconst {\n moviePath,\n movieGenerating,\n movieDownloading,\n movieError,\n pdfPath,\n pdfGenerating,\n pdfDownloading,\n generateMovie,\n downloadMovie,\n refreshMoviePath,\n generatePdf,\n downloadPdf,\n refreshPdfPath,\n resetMedia,\n} = useMediaExport({ api, adapter, filePath, chatSessionId });\n\nconst {\n beatMovies,\n beatMovieUrls,\n beatMovieOpen,\n beatMovieLoading,\n loadExistingBeatMovie,\n playBeatMovie,\n closeBeatMovie,\n invalidateBeatMovie,\n resetBeatMovies,\n} = useBeatMovie({ api, adapter, filePath });\n\nconst {\n charRenderState,\n charImages,\n charErrors,\n charDragOver,\n characterKeys,\n onCharDragOver,\n onCharDragLeave,\n onCharDrop,\n loadExistingCharacterImage,\n refreshMissingCharacterImages,\n renderCharacter,\n generateAllCharacters,\n resetCharacters,\n} = useCharacterImages({ api, filePath, chatSessionId, getImages: () => script.value.imageParams?.images });\n\nfunction stopPlayingAudio() {\n // Single helper that clears both the audio path and the silent\n // auto-advance timer — callers (lightbox open / arrow nav / Stop\n // button) get consistent behaviour without remembering which\n // playback mode the current beat was using (#1073).\n stopAllPlayback();\n}\n\nfunction openLightbox(index: number) {\n stopPlayingAudio();\n lightbox.value = {\n src: renderedImages[index],\n text: effectiveBeat(index).text,\n index,\n };\n}\n\n// Backdrop click handler. Stops any in-flight narration so the audio\n// doesn't keep playing after the lightbox is dismissed — without this,\n// the HTMLAudioElement created by playAudio() outlives the modal and\n// the user hears disembodied narration with no UI to stop it.\nfunction closeLightbox() {\n stopPlayingAudio();\n lightbox.value = null;\n}\n\n// \"Play presentation\" toolbar action. Opens the lightbox at beat 0 and\n// kicks off its narration audio; the existing on-ended hook then chains\n// through the rest of the deck (lightboxMove(1) → playAudio if the next\n// beat has audio), so one click runs the whole presentation. Only wired\n// to the toolbar button when moviePath is set, which is our proxy for\n// \"every beat has both image and audio on disk\".\n//\n// `moviePath` arrives synchronously from movieStatus, but the per-beat\n// image and audio data URIs are populated asynchronously by\n// loadExistingBeatImage / loadExistingBeatAudio in initializeScript().\n// The Play button can therefore become visible before beat 0's assets\n// hydrate — `isPlayReady` gates the click so the lightbox never opens\n// with an undefined src or silent narration on a beat that does have\n// text.\nconst isPlayReady = computed<boolean>(() => {\n if (beats.value.length === 0) return false;\n if (!renderedImages[0]) return false;\n // Audio is only required when the beat has text (the source of TTS).\n // Beats without text are valid; they just play silently.\n if (effectiveBeat(0).text && !beatAudios[0]) return false;\n return true;\n});\n\nfunction playPresentation() {\n if (!isPlayReady.value) return;\n openLightbox(0);\n playBeat(0);\n}\n\n// Stop whichever playback handle is active. Idempotent. Called by\n// openLightbox, manual stop / pause buttons, and by `playBeat`\n// before kicking off a new beat so we never double-schedule. (#1073)\nfunction stopAllPlayback(): void {\n if (playingAudio.value) {\n playingAudio.value.audio.pause();\n playingAudio.value = null;\n audioProgress.value = 0;\n }\n if (silentPlaybackTimer.value) {\n clearTimeout(silentPlaybackTimer.value.timer);\n silentPlaybackTimer.value = null;\n }\n}\n\n// Single entry point for \"start playback at beat <index>\". Routes\n// on what the script DECLARED, not on what's currently hydrated:\n//\n// - `text` empty → silent path (`scheduleSilentAdvance`). The\n// schema says no audio is generated for empty-text beats, so\n// `duration` drives auto-advance.\n// - `text` present + audio loaded → audio path. `audio.ended`\n// chains via `advanceFromBeat`.\n// - `text` present + audio NOT loaded → stop. The Play button's\n// `isPlayReady` gate prevented this for beat 0, but mid-stream\n// a transient fetch miss must not silently skip the narration\n// by falling through to the silent timer (Codex review on\n// #1073 — gating on `beatAudios[index]` would do exactly that).\n//\n// Either path chains to the next beat via `advanceFromBeat`, so a\n// run of silent beats — or audio / silent / audio sequences —\n// plays through without manual interaction.\nfunction playBeat(index: number): void {\n stopAllPlayback();\n const hasText = Boolean(effectiveBeat(index).text);\n if (!hasText) {\n scheduleSilentAdvance(index);\n return;\n }\n if (beatAudios[index]) {\n playAudio(index);\n }\n // Text beat with no audio yet → stop. The user can re-click Play\n // once the audio finishes hydrating.\n}\n\nfunction scheduleSilentAdvance(index: number): void {\n // Defensively narrow the script-supplied duration (zero / negative / NaN /\n // non-number → default) — a bad value would otherwise collapse to an\n // immediate timeout and the Play loop would race through every silent beat\n // in a single tick (Codex review iter-5 on #1365).\n const seconds = resolveSilentAdvanceSeconds(effectiveBeat(index).duration, SILENT_BEAT_DEFAULT_SEC);\n const timer = setTimeout(() => {\n if (silentPlaybackTimer.value?.index !== index) return;\n silentPlaybackTimer.value = null;\n if (lightbox.value?.index === index) advanceFromBeat(index);\n }, seconds * MS_PER_SECOND);\n silentPlaybackTimer.value = { index, timer };\n}\n\nfunction advanceFromBeat(fromIndex: number): void {\n lightboxMove(1);\n const nextIndex = lightbox.value?.index;\n if (nextIndex === undefined || nextIndex === fromIndex) return;\n playBeat(nextIndex);\n}\n\nconst hasPrev = computed(() => {\n if (!lightbox.value) return false;\n for (let i = lightbox.value.index - 1; i >= 0; i--) {\n if (renderedImages[i]) return true;\n }\n return false;\n});\n\nconst hasNext = computed(() => {\n if (!lightbox.value) return false;\n for (let i = lightbox.value.index + 1; i < beats.value.length; i++) {\n if (renderedImages[i]) return true;\n }\n return false;\n});\n\n// Narration text per beat, for the lightbox beat-strip hover tooltips. Reads\n// through `effectiveBeat` so an unsaved in-place edit shows its new text.\nconst beatTexts = computed(() => beats.value.map((_, index) => effectiveBeat(index).text));\n\nfunction jumpToBeat(index: number) {\n if (!lightbox.value) return;\n if (index === lightbox.value.index) return;\n if (!renderedImages[index]) return;\n // Carry the playback mode forward (audio OR silent timer) so a\n // user clicking the beat-strip thumbnail mid-playback keeps the\n // presentation rolling (#1073).\n const wasPlaying = playingAudio.value !== null || silentPlaybackTimer.value !== null;\n openLightbox(index);\n if (wasPlaying) playBeat(index);\n}\n\nfunction lightboxMove(delta: number) {\n if (!lightbox.value) return;\n const total = beats.value.length;\n // If a playback was in progress when the user clicked the arrow,\n // carry it forward to whichever beat we land on — `playBeat`\n // picks audio vs silent automatically. `openLightbox` stops the\n // current playback, so capture the flag BEFORE that and chain\n // AFTER. The on-ended / silent-advance paths already null their\n // own state before calling `lightboxMove`, so this branch won't\n // double-fire there.\n const wasPlaying = playingAudio.value !== null || silentPlaybackTimer.value !== null;\n let i = lightbox.value.index + delta;\n while (i >= 0 && i < total) {\n if (renderedImages[i]) {\n openLightbox(i);\n if (wasPlaying) playBeat(i);\n return;\n }\n i += delta;\n }\n}\nconst sourceDetails = ref<HTMLDetailsElement>();\nconst editing = ref(false);\nconst editableSource = ref(\"\");\nconst { copied, copy } = useClipboardCopy();\n\n// Beats may be edited in-place via `updateBeat()` and rendered through\n// `effectiveBeat()`, so the Copy / source-view text must read the merged\n// shape — otherwise the clipboard returns the original prop snapshot\n// until the full result is reloaded.\nconst effectiveScript = computed<MulmoScript>(() => ({\n ...script.value,\n beats: beats.value.map((beat, i) => localOverrides[i] ?? beat),\n}));\nconst scriptSourceText = computed(() => toScriptSourceText(effectiveScript.value));\n\n// Persist a saved script back into the parent's toolResult so the in-memory\n// script and reactive beats[] stay in sync without a remount. The parent's\n// handleUpdateResult uses Object.assign (in-place), so the prop watcher won't\n// fire — callers that need a re-read drive initializeScript themselves.\nfunction commitScript(next: MulmoScript): void {\n emit(\"updateResult\", {\n ...props.selectedResult,\n data: { ...props.selectedResult.data, script: next },\n });\n}\n\n// #1575 — when every beat is a `slide`, swap the per-beat list UI for the\n// interactive deck editor (@mulmocast/deck-web). Mixed scripts (any non-slide\n// beat) fall back to the existing list. The debounce + flush-on-unmount live\n// in the composable.\nconst { isDeck, deckScriptInput, onDeckUpdate, flushPendingDeckSave } = useDeckEditor({ api, filePath, effectiveScript, commitScript });\n\nonBeforeUnmount(() => {\n flushPendingDeckSave();\n // Release beat-clip blob object URLs — they outlive the component\n // otherwise (document-scoped, not GC'd with it).\n resetBeatMovies();\n unsubscribeGenerationEvents();\n});\nconst loadedSource = ref(\"\");\nconst sourceChanged = computed(() => editableSource.value !== loadedSource.value);\nconst sourceValid = computed(() => {\n try {\n const parsed = JSON.parse(editableSource.value);\n return mulmoScriptSchema.safeParse(parsed).success;\n } catch {\n return false;\n }\n});\n\nasync function onSourceToggle(open: boolean) {\n editing.value = open;\n if (open) {\n let text = scriptSourceText.value;\n // Re-read the current file from disk so beat-level edits made\n // since mount (other tabs, MCP, manual edits) surface in the\n // editor. Uses the reopen dispatch for the same reason\n // refreshScriptFromDisk does — `filePath.value` is the wire form\n // `stories/<rel>` and only the mulmoScript save/reopen op knows\n // how to map it to the on-disk path under `artifacts/stories/...`.\n if (filePath.value) {\n const response = await api.call(\"save\", { filePath: filePath.value });\n const diskScript = response.ok ? (response.data.script as MulmoScript | undefined) : undefined;\n if (diskScript) text = toScriptSourceText(diskScript);\n // fall through to in-memory script on failure\n }\n editableSource.value = text;\n loadedSource.value = text;\n }\n}\n\nfunction cancelSourceEdit() {\n if (sourceDetails.value) sourceDetails.value.open = false;\n}\n\nasync function applySource() {\n let parsed: MulmoScript;\n try {\n parsed = JSON.parse(editableSource.value);\n } catch (err) {\n alert(errorMessage(err));\n return;\n }\n const response = await api.call(\"updateScript\", {\n filePath: filePath.value,\n script: parsed,\n });\n if (!response.ok) {\n alert(response.error || \"Update failed\");\n return;\n }\n\n // Update the UI with the new script. commitScript emits first so the parent\n // data is updated (its handleUpdateResult uses in-place Object.assign, so\n // the prop watcher won't fire), then we manually re-initialize the view.\n commitScript(parsed);\n\n if (sourceDetails.value) sourceDetails.value.open = false;\n await initializeScript();\n}\n\nasync function copyText() {\n await copy(scriptSourceText.value);\n}\n\nfunction effectiveBeat(index: number): Beat {\n return effectiveBeatOf(localOverrides, beats.value, index);\n}\n\nfunction toggleSource(index: number) {\n if (!sourceOpen[index]) {\n sourceText[index] = toScriptSourceText(effectiveBeat(index));\n Reflect.deleteProperty(beatSaveErrors, index);\n }\n sourceOpen[index] = !sourceOpen[index];\n}\n\nfunction isValidBeat(index: number): boolean {\n return isValidBeatOf(sourceText[index], mulmoBeatSchema);\n}\n\nasync function updateBeat(index: number) {\n let beat: Beat;\n try {\n beat = JSON.parse(sourceText[index]);\n } catch (err) {\n beatSaveErrors[index] = { kind: \"invalidJson\", error: errorMessage(err) };\n return;\n }\n const prevImage = JSON.stringify(effectiveBeat(index).image);\n const prevText = effectiveBeat(index).text;\n\n const requestedFilePath = filePath.value;\n Reflect.deleteProperty(beatSaveErrors, index);\n beatSaving[index] = true;\n const response = await api.call(\"updateBeat\", {\n filePath: requestedFilePath,\n beatIndex: index,\n beat,\n });\n if (staleSince(requestedFilePath)) return;\n Reflect.deleteProperty(beatSaving, index);\n if (!response.ok) {\n beatSaveErrors[index] = { kind: \"saveFailed\", error: response.error };\n return;\n }\n\n localOverrides[index] = beat;\n sourceOpen[index] = false;\n\n if (JSON.stringify(beat.image) !== prevImage) {\n Reflect.deleteProperty(renderedImages, index);\n renderBeat(index);\n }\n\n // Audio files are content-addressed by the beat's text\n // (getBeatAudioPathOrUrl hashes text + voice), so after a text edit\n // the cached data URI belongs to the OLD narration. Drop it so the\n // \"Generate Audio\" button reappears, then re-probe — if the new text\n // matches previously generated audio (e.g. the edit was a revert),\n // the probe restores Play without a paid TTS call.\n if (beat.text !== prevText) {\n // If this beat's old narration is mid-playback, stop it first —\n // the deletes below remove the Play/Stop control from the row,\n // which would otherwise leave the stale audio playing with no\n // way to stop it (Codex review on #2143).\n if (playingAudio.value?.index === index) stopAllPlayback();\n Reflect.deleteProperty(beatAudios, index);\n Reflect.deleteProperty(audioState, index);\n Reflect.deleteProperty(audioErrors, index);\n if (beat.text) void loadExistingBeatAudio(index);\n }\n}\n\nasync function renderBeat(index: number) {\n const requestedFilePath = filePath.value;\n renderState[index] = \"rendering\";\n const response = await api.call(\"renderBeat\", {\n filePath: requestedFilePath,\n beatIndex: index,\n chatSessionId: chatSessionId.value,\n });\n if (staleSince(requestedFilePath)) return;\n if (!response.ok) {\n renderErrors[index] = response.error || \"Render failed\";\n renderState[index] = \"error\";\n return;\n }\n renderedImages[index] = response.data.image ?? \"\";\n renderState[index] = \"done\";\n refreshMissingCharacterImages();\n if (beatMayHaveMovie(effectiveBeat(index))) void loadExistingBeatMovie(index);\n}\n\nasync function regenerateBeat(index: number) {\n const requestedFilePath = filePath.value;\n Reflect.deleteProperty(renderedImages, index);\n invalidateBeatMovie(index);\n renderState[index] = \"rendering\";\n const response = await api.call(\"renderBeat\", {\n filePath: requestedFilePath,\n beatIndex: index,\n force: true,\n chatSessionId: chatSessionId.value,\n });\n if (staleSince(requestedFilePath)) return;\n if (!response.ok) {\n renderErrors[index] = response.error || \"Render failed\";\n renderState[index] = \"error\";\n return;\n }\n renderedImages[index] = response.data.image ?? \"\";\n renderState[index] = \"done\";\n if (beatMayHaveMovie(effectiveBeat(index))) void loadExistingBeatMovie(index);\n}\n\n// Stale-response guard shared by every per-beat/character loader and\n// mutator below: capture the wire path at call time and discard the\n// response when the user has navigated to a different result meanwhile —\n// otherwise late responses from script A's bulk mount-time probes would\n// write into the per-beat maps that now belong to script B.\nfunction staleSince(requestedFilePath: string): boolean {\n return staleSinceOf(filePath.value, requestedFilePath);\n}\n\nasync function loadExistingBeatImage(index: number) {\n const requestedFilePath = filePath.value;\n const response = await api.call(\"beatImage\", { filePath: requestedFilePath, beatIndex: index });\n if (staleSince(requestedFilePath)) return;\n // silently ignore errors — image simply hasn't been generated yet\n if (response.ok && response.data.image) {\n renderedImages[index] = response.data.image;\n renderState[index] = \"done\";\n }\n}\n\nasync function loadExistingBeatAudio(index: number) {\n const requestedFilePath = filePath.value;\n const response = await api.call(\"beatAudio\", { filePath: requestedFilePath, beatIndex: index });\n if (staleSince(requestedFilePath)) return;\n // silently ignore errors\n if (response.ok && response.data.audio) {\n beatAudios[index] = response.data.audio;\n audioState[index] = \"done\";\n }\n}\n\nasync function generateAudio(index: number) {\n const requestedFilePath = filePath.value;\n audioState[index] = \"generating\";\n Reflect.deleteProperty(audioErrors, index);\n const response = await api.call(\"generateBeatAudio\", {\n filePath: requestedFilePath,\n beatIndex: index,\n chatSessionId: chatSessionId.value,\n });\n if (staleSince(requestedFilePath)) return;\n if (!response.ok) {\n audioErrors[index] = response.error || \"Audio generation failed\";\n audioState[index] = \"error\";\n return;\n }\n beatAudios[index] = response.data.audio ?? \"\";\n audioState[index] = \"done\";\n}\n\nfunction playAudio(index: number) {\n if (playingAudio.value) {\n playingAudio.value.audio.pause();\n const wasIndex = playingAudio.value.index;\n playingAudio.value = null;\n if (wasIndex === index) return;\n }\n const src = beatAudios[index];\n if (!src) return;\n const audio = new Audio(src);\n playingAudio.value = { index, audio };\n audioProgress.value = 0;\n audio.addEventListener(\"timeupdate\", () => {\n if (playingAudio.value?.index !== index) return;\n if (audio.duration > 0) audioProgress.value = audio.currentTime / audio.duration;\n });\n audio.addEventListener(\"ended\", () => {\n if (playingAudio.value?.index !== index) return;\n playingAudio.value = null;\n audioProgress.value = 0;\n if (lightbox.value?.index === index) advanceFromBeat(index);\n });\n audio.play();\n}\n\nfunction onBeatDragOver(event: DragEvent, index: number) {\n if (!event.dataTransfer?.types.includes(\"Files\")) return;\n event.preventDefault();\n beatDragOver[index] = true;\n}\n\nfunction onBeatDragLeave(index: number) {\n beatDragOver[index] = false;\n}\n\nasync function onBeatDrop(event: DragEvent, index: number) {\n event.preventDefault();\n beatDragOver[index] = false;\n const file = event.dataTransfer?.files[0];\n if (!file || !file.type.startsWith(\"image/\")) return;\n\n renderState[index] = \"rendering\";\n Reflect.deleteProperty(renderErrors, index);\n let imageData: string;\n try {\n imageData = await readFileAsDataUrl(file);\n } catch (err) {\n renderErrors[index] = errorMessage(err);\n renderState[index] = \"error\";\n return;\n }\n const requestedFilePath = filePath.value;\n const response = await api.call(\"uploadBeatImage\", {\n filePath: requestedFilePath,\n beatIndex: index,\n imageData,\n });\n if (staleSince(requestedFilePath)) return;\n if (!response.ok) {\n renderErrors[index] = response.error || \"Upload failed\";\n renderState[index] = \"error\";\n return;\n }\n renderedImages[index] = response.data.image ?? \"\";\n renderState[index] = \"done\";\n}\n\nfunction openCharacterLightbox(key: string) {\n // Stop both audio and silent timer — character lightbox is\n // outside the play loop (#1073).\n stopAllPlayback();\n lightbox.value = {\n src: charImages[key],\n text: key,\n index: -1,\n isCharacter: true,\n };\n}\n\n// Probe the server for an existing beat PNG before triggering any\n// generation. Only auto-renders when the disk is empty AND the beat\n// is a deterministic type — imagePrompt beats are left empty so the\n// user clicks Generate explicitly (avoids surprise paid text2image\n// calls on every page refresh).\nasync function hydrateBeatImage(beat: Beat, index: number, hasCharacters: boolean, autoRenderTypes: readonly string[]): Promise<void> {\n await loadExistingBeatImage(index);\n if (renderedImages[index]) return;\n if (shouldAutoRenderBeat(beat, hasCharacters, autoRenderTypes)) {\n await renderBeat(index);\n }\n}\n\n/**\n * #1074 — keep the in-memory toolResult in sync with the on-disk\n * script file. `updateBeat` / `updateScript` persist edits to\n * disk, but the session entry that backs\n * `props.selectedResult.data.script` is never rewritten, so a\n * page reload + session-restore would otherwise surface stale\n * pre-edit content.\n *\n * Why the reopen dispatch, not a generic file read: `filePath`\n * is the wire form `stories/<rel>` which only the mulmoScript save\n * op knows how to translate back to the real on-disk path under\n * `artifacts/stories/...`. The reopen op is read-only when `script`\n * is omitted; it does NOT trigger movie generation.\n *\n * The flow silently bails on every failure mode so a missing /\n * malformed / deleted script file never blocks the rest of\n * `initializeScript`.\n *\n * Stale-response guard: capture `uuid` + `filePath` before the\n * `await`. If either has changed by the time the response lands\n * (the user navigated to a different result while the request\n * was in flight, or `props.selectedResult` was swapped under us\n * by a parent watcher), drop the response on the floor — the new\n * `initializeScript` invocation triggered by that change will\n * issue its own refresh against the correct file.\n */\nasync function refreshScriptFromDisk(): Promise<void> {\n const requestedFilePath = filePath.value;\n if (!requestedFilePath) return;\n const requestedUuid = props.selectedResult.uuid;\n const response = await api.call(\"save\", { filePath: requestedFilePath });\n if (props.selectedResult.uuid !== requestedUuid || filePath.value !== requestedFilePath) return;\n if (!response.ok) return;\n const diskScript = response.data.script as MulmoScript | undefined;\n // The server-side reopen op already validated against\n // `mulmoScriptSchema`, so a non-null `script` is trusted here —\n // we only need a presence check.\n if (!diskScript) return;\n if (isSameScript(diskScript, script.value)) return;\n commitScript(diskScript);\n}\n\nasync function initializeScript() {\n // Stop any in-flight playback BEFORE we tear down per-script state\n // — a pending `silentPlaybackTimer` or running audio from the\n // previous script would otherwise fire `advanceFromBeat()` against\n // the new script's lightbox / beat list and either crash or\n // silently jump the new presentation forward. Also close any open\n // lightbox so the user lands on the clean View for the new result\n // (Codex review iter-4 on #1365).\n stopAllPlayback();\n lightbox.value = null;\n // Reset scroll position so new results start at the top\n if (beatListEl.value) beatListEl.value.scrollTop = 0;\n // Reset per-script state. resetMedia clears the movie/PDF spinners too —\n // per-script, so switching away from a generating script doesn't leave the\n // new script's toolbar spinning; the pendingGenerations snapshot below\n // re-lights them when the NEW script really does have work in flight.\n clearReactiveRecords(\n renderState,\n renderedImages,\n renderErrors,\n sourceOpen,\n sourceText,\n beatSaveErrors,\n beatSaving,\n localOverrides,\n beatAudios,\n audioState,\n audioErrors,\n beatDragOver,\n );\n resetCharacters();\n resetBeatMovies();\n resetMedia();\n if (sourceDetails.value) sourceDetails.value.open = false;\n\n // #1074 — re-read the script file from disk before per-beat\n // hydration. When the user switches between tool results inside\n // the same SPA mount and switches back, the in-memory toolResult\n // still carries whatever script was captured earlier, and\n // `localOverrides` (the only thing showing the user's edit since\n // the last save) is reset by initializeScript on remount.\n // Re-fetching from disk via the reopen op covers that gap.\n await refreshScriptFromDisk();\n\n // Mount-time policy: prefer the existing PNG on the server. Every\n // beat — deterministic AND imagePrompt — first probes beatImage,\n // and we only fall through to renderBeat() when the disk has nothing\n // yet AND the type is safe to auto-render (deterministic content,\n // no characters waiting). Without this probe a refresh would re-fire\n // generateBeatImage for every beat, and for imagePrompt beats that\n // means a paid text2image call against an image we already have.\n //\n // Stale-after-edit: if the user edits the script source the on-disk\n // PNG is no longer in sync with the new content, but we don't try to\n // detect that here — the per-beat ↺ button is one click away and a\n // page refresh re-runs this same probe, so the user can opt back into\n // a fresh render whenever they need to.\n const AUTO_RENDER_TYPES = [\"textSlide\", \"markdown\", \"chart\", \"mermaid\", \"html_tailwind\", \"slide\"] as const;\n const hasCharacters = characterKeys.value.length > 0;\n beats.value.forEach((beat, index) => {\n void hydrateBeatImage(beat, index, hasCharacters, AUTO_RENDER_TYPES);\n if (beat.text) loadExistingBeatAudio(index);\n if (beatMayHaveMovie(beat)) void loadExistingBeatMovie(index);\n });\n\n characterKeys.value.forEach((key) => loadExistingCharacterImage(key));\n\n if (filePath.value) {\n // Stale-response guard: if the user navigates to a different result\n // while these calls are in flight, their answers describe the OLD\n // script — drop them instead of stamping them onto the new one.\n const requestedFilePath = filePath.value;\n const isStale = () => filePath.value !== requestedFilePath;\n\n const response = await api.call(\"movieStatus\", { filePath: requestedFilePath });\n if (isStale()) return;\n if (response.ok && response.data.moviePath) {\n moviePath.value = response.data.moviePath;\n }\n // ignore errors\n // Also check whether a PDF was previously generated and is still\n // newer than the source; status returns null otherwise so the UI\n // re-offers the Generate button.\n const pdfResponse = await api.call(\"pdfStatus\", { filePath: requestedFilePath });\n if (isStale()) return;\n if (pdfResponse.ok && pdfResponse.data.pdfPath) {\n pdfPath.value = pdfResponse.data.pdfPath;\n }\n\n // Reflect any generations that were already in flight when we\n // mounted (user switched away mid-generation and came back).\n // Snapshot via dispatch; live updates arrive on the pubsub\n // subscription below.\n const pending = await api.call(\"pendingGenerations\", { filePath: requestedFilePath });\n if (isStale()) return;\n if (pending.ok) {\n for (const entry of pending.data.pending) {\n reflectGenerationStart(entry);\n }\n }\n }\n}\n\nonMounted(initializeScript);\nwatch(() => props.selectedResult, initializeScript);\n\n// Keep the view in sync with generations running anywhere — this View's\n// own long-held dispatches, a parallel tab, the agent's background\n// autoGenerateMovie. The host publishes `generation` events on the\n// plugin pubsub channel (started + finished, per beat and per artifact);\n// on start we mirror the local \"rendering\" state so spinners show even\n// after a remount, on finish we reload the relevant asset off disk.\nconst unsubscribeGenerationEvents = api.onGenerationEvent(\n () => filePath.value,\n (event) => {\n if (!event.done) {\n reflectGenerationStart(event);\n return;\n }\n // Fire-and-forget: swallow + log so a failed reload doesn't\n // surface as an unhandled rejection.\n reflectGenerationFinish(event).catch((err) => {\n console.error(\"[presentMulmoScript] reload on finish failed:\", err);\n });\n },\n);\n\nfunction reflectGenerationStart(entry: MulmoScriptGenerationEvent): void {\n if (entry.kind === \"beatImage\") {\n const idx = Number(entry.key);\n if (!renderedImages[idx]) renderState[idx] = \"rendering\";\n } else if (entry.kind === \"beatAudio\") {\n const idx = Number(entry.key);\n if (!beatAudios[idx]) audioState[idx] = \"generating\";\n } else if (entry.kind === \"characterImage\") {\n if (!charImages[entry.key]) charRenderState[entry.key] = \"rendering\";\n } else if (entry.kind === \"movie\") {\n movieGenerating.value = true;\n } else if (entry.kind === \"pdf\") {\n pdfGenerating.value = true;\n }\n}\n\nasync function reflectGenerationFinish(entry: MulmoScriptGenerationEvent): Promise<void> {\n if (entry.kind === \"beatImage\") {\n const idx = Number(entry.key);\n await loadExistingBeatImage(idx);\n if (beatMayHaveMovie(effectiveBeat(idx))) await loadExistingBeatMovie(idx);\n if (renderState[idx] === \"rendering\") Reflect.deleteProperty(renderState, idx);\n refreshMissingCharacterImages();\n } else if (entry.kind === \"beatAudio\") {\n const idx = Number(entry.key);\n await loadExistingBeatAudio(idx);\n if (audioState[idx] === \"generating\") Reflect.deleteProperty(audioState, idx);\n } else if (entry.kind === \"characterImage\") {\n await loadExistingCharacterImage(entry.key);\n if (charRenderState[entry.key] === \"rendering\") {\n Reflect.deleteProperty(charRenderState, entry.key);\n }\n } else if (entry.kind === \"movie\") {\n movieGenerating.value = false;\n await refreshMoviePath();\n } else if (entry.kind === \"pdf\") {\n pdfGenerating.value = false;\n await refreshPdfPath();\n }\n}\n</script>\n\n<style scoped>\n.bottom-bar-wrapper {\n position: relative;\n flex-shrink: 0;\n}\n\n.script-source {\n padding: 0.5rem;\n background: #f5f5f5;\n border-top: 1px solid #e0e0e0;\n font-family: Consolas, \"MS Gothic\", \"BIZ UDGothic\", monospace;\n font-size: 0.85rem;\n}\n\n.script-source summary {\n cursor: pointer;\n user-select: none;\n padding: 0.5rem;\n background: #e8e8e8;\n border-radius: 4px;\n font-weight: 500;\n color: #333;\n}\n\n.script-source[open] summary {\n margin-bottom: 0.5rem;\n}\n\n.script-source summary:hover {\n background: #d8d8d8;\n}\n\n.script-editor {\n width: 100%;\n height: 40vh;\n padding: 1rem;\n background: #ffffff;\n border: 1px solid #ccc;\n border-radius: 4px;\n color: #333;\n font-family: \"Courier New\", \"MS Gothic\", \"BIZ UDGothic\", monospace;\n font-size: 0.9rem;\n resize: vertical;\n margin-bottom: 0.5rem;\n line-height: 1.5;\n}\n\n.script-editor:focus {\n outline: none;\n border-color: #4caf50;\n box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.1);\n}\n\n.script-editor-invalid {\n border-color: #ef4444;\n}\n\n.script-editor-invalid:focus {\n border-color: #ef4444;\n box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.1);\n}\n\n.editor-actions {\n display: flex;\n justify-content: space-between;\n}\n\n.apply-btn {\n padding: 0.5rem 1rem;\n background: #4caf50;\n color: white;\n border: none;\n border-radius: 4px;\n cursor: pointer;\n font-size: 0.9rem;\n transition: background 0.2s;\n font-weight: 500;\n}\n\n.apply-btn:hover {\n background: #45a049;\n}\n\n.apply-btn:disabled {\n background: #cccccc;\n color: #666666;\n cursor: not-allowed;\n opacity: 0.6;\n}\n\n.cancel-btn {\n padding: 0.5rem 1rem;\n background: #e0e0e0;\n color: #333;\n border: none;\n border-radius: 4px;\n cursor: pointer;\n font-size: 0.9rem;\n transition: background 0.2s;\n font-weight: 500;\n}\n\n.cancel-btn:hover {\n background: #d0d0d0;\n}\n\n.copy-btn {\n position: absolute;\n bottom: 0.3rem;\n right: 0.65rem;\n padding: 0.4rem;\n background: none;\n border: none;\n color: #333;\n cursor: pointer;\n z-index: 1;\n}\n\n.copy-btn:hover {\n color: #000;\n}\n\n.copy-btn .material-icons {\n font-size: 1.15rem;\n}\n</style>\n","<template>\n <div class=\"p-2 text-sm\" data-testid=\"mulmo-script-preview\">\n <div class=\"font-medium text-gray-700 truncate mb-1\" data-testid=\"mulmo-script-preview-title\">\n {{ title }}\n </div>\n <div v-if=\"description\" class=\"text-xs text-gray-500 leading-relaxed\" data-testid=\"mulmo-script-preview-description\">\n {{ description }}\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed } from \"vue\";\nimport type { ToolResultComplete } from \"gui-chat-protocol/vue\";\nimport type { MulmoScriptData } from \"../core/types\";\n\nconst props = defineProps<{ result: ToolResultComplete<MulmoScriptData> }>();\n\nconst data = computed(() => props.result.data);\nconst script = computed(() => data.value?.script);\nconst title = computed(() => script.value?.title || data.value?.filePath?.split(\"/\").pop() || \"MulmoScript\");\nconst description = computed(() => script.value?.description);\n</script>\n","<template>\n <div class=\"p-2 text-sm\" data-testid=\"mulmo-script-preview\">\n <div class=\"font-medium text-gray-700 truncate mb-1\" data-testid=\"mulmo-script-preview-title\">\n {{ title }}\n </div>\n <div v-if=\"description\" class=\"text-xs text-gray-500 leading-relaxed\" data-testid=\"mulmo-script-preview-description\">\n {{ description }}\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed } from \"vue\";\nimport type { ToolResultComplete } from \"gui-chat-protocol/vue\";\nimport type { MulmoScriptData } from \"../core/types\";\n\nconst props = defineProps<{ result: ToolResultComplete<MulmoScriptData> }>();\n\nconst data = computed(() => props.result.data);\nconst script = computed(() => data.value?.script);\nconst title = computed(() => script.value?.title || data.value?.filePath?.split(\"/\").pop() || \"MulmoScript\");\nconst description = computed(() => script.value?.description);\n</script>\n","import \"../style.css\";\n\nimport type { ToolPlugin } from \"gui-chat-protocol/vue\";\nimport type { MulmoScriptData, SaveMulmoScriptArgs } from \"../core/types\";\nimport { pluginCore } from \"../core/plugin\";\nimport View from \"./View.vue\";\nimport Preview from \"./Preview.vue\";\n\nexport const plugin: ToolPlugin<MulmoScriptData, MulmoScriptData, SaveMulmoScriptArgs> = {\n ...pluginCore,\n viewComponent: View,\n previewComponent: Preview,\n};\n\nexport type { MulmoScriptData, MulmoScriptExecuteContext, SaveMulmoScriptArgs } from \"../core/types\";\nexport type { MulmoScriptDispatchArgs, MulmoScriptDispatchResult, MulmoScriptGenerationEvent, DispatchEnvelope, DispatchFailure } from \"../core/contract\";\nexport { GENERATION_EVENT } from \"../core/contract\";\nexport { TOOL_NAME, TOOL_DEFINITION } from \"../core/definition\";\nexport { MULMOSCRIPT_HOST_ADAPTER_KEY, useHostAdapter, type MulmoScriptHostAdapter } from \"./hostAdapter\";\nexport { useMulmoScriptTransport, type MulmoScriptTransport, type TransportResult } from \"./transport\";\nexport { View, Preview };\n\nexport default { plugin };\n"],"mappings":"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"}
|