@mulmoclaude/mulmoscript-plugin 0.1.0

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.
Files changed (62) hide show
  1. package/dist/core/contract.d.ts +162 -0
  2. package/dist/core/contract.d.ts.map +1 -0
  3. package/dist/core/definition.d.ts +4 -0
  4. package/dist/core/definition.d.ts.map +1 -0
  5. package/dist/core/index.d.ts +6 -0
  6. package/dist/core/index.d.ts.map +1 -0
  7. package/dist/core/paths.d.ts +21 -0
  8. package/dist/core/paths.d.ts.map +1 -0
  9. package/dist/core/plugin.d.ts +44 -0
  10. package/dist/core/plugin.d.ts.map +1 -0
  11. package/dist/core/types.d.ts +29 -0
  12. package/dist/core/types.d.ts.map +1 -0
  13. package/dist/core/validate.d.ts +27 -0
  14. package/dist/core/validate.d.ts.map +1 -0
  15. package/dist/index.cjs +14 -0
  16. package/dist/index.d.ts +2 -0
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/index.js +2 -0
  19. package/dist/lang/de.d.ts +4 -0
  20. package/dist/lang/de.d.ts.map +1 -0
  21. package/dist/lang/en.d.ts +4 -0
  22. package/dist/lang/en.d.ts.map +1 -0
  23. package/dist/lang/es.d.ts +4 -0
  24. package/dist/lang/es.d.ts.map +1 -0
  25. package/dist/lang/fr.d.ts +4 -0
  26. package/dist/lang/fr.d.ts.map +1 -0
  27. package/dist/lang/index.d.ts +9 -0
  28. package/dist/lang/index.d.ts.map +1 -0
  29. package/dist/lang/ja.d.ts +4 -0
  30. package/dist/lang/ja.d.ts.map +1 -0
  31. package/dist/lang/ko.d.ts +4 -0
  32. package/dist/lang/ko.d.ts.map +1 -0
  33. package/dist/lang/messages.d.ts +36 -0
  34. package/dist/lang/messages.d.ts.map +1 -0
  35. package/dist/lang/ptBR.d.ts +4 -0
  36. package/dist/lang/ptBR.d.ts.map +1 -0
  37. package/dist/lang/zh.d.ts +4 -0
  38. package/dist/lang/zh.d.ts.map +1 -0
  39. package/dist/plugin-CtKUt8DX.cjs +451 -0
  40. package/dist/plugin-CtKUt8DX.cjs.map +1 -0
  41. package/dist/plugin-W1ppnyhR.js +380 -0
  42. package/dist/plugin-W1ppnyhR.js.map +1 -0
  43. package/dist/style.css +1635 -0
  44. package/dist/vue/Preview.vue.d.ts +9 -0
  45. package/dist/vue/Preview.vue.d.ts.map +1 -0
  46. package/dist/vue/View.vue.d.ts +13 -0
  47. package/dist/vue/View.vue.d.ts.map +1 -0
  48. package/dist/vue/helpers.d.ts +67 -0
  49. package/dist/vue/helpers.d.ts.map +1 -0
  50. package/dist/vue/hostAdapter.d.ts +16 -0
  51. package/dist/vue/hostAdapter.d.ts.map +1 -0
  52. package/dist/vue/index.d.ts +18 -0
  53. package/dist/vue/index.d.ts.map +1 -0
  54. package/dist/vue/support.d.ts +15 -0
  55. package/dist/vue/support.d.ts.map +1 -0
  56. package/dist/vue/transport.d.ts +20 -0
  57. package/dist/vue/transport.d.ts.map +1 -0
  58. package/dist/vue.cjs +1984 -0
  59. package/dist/vue.cjs.map +1 -0
  60. package/dist/vue.js +1971 -0
  61. package/dist/vue.js.map +1 -0
  62. package/package.json +61 -0
package/dist/vue.js ADDED
@@ -0,0 +1,1971 @@
1
+ import { a as pluginCore, d as TOOL_DEFINITION, f as TOOL_NAME } from "./plugin-W1ppnyhR.js";
2
+ import { mulmoBeatSchema, mulmoScriptSchema } from "@mulmocast/types";
3
+ import { Fragment, computed, createCommentVNode, createElementBlock, createElementVNode, createVNode, defineAsyncComponent, defineComponent, inject, normalizeClass, normalizeStyle, onBeforeUnmount, onMounted, openBlock, reactive, ref, renderList, toDisplayString, unref, vModelText, vShow, watch, withDirectives, withModifiers } from "vue";
4
+ import { PLUGIN_RUNTIME_KEY, useRuntime } from "gui-chat-protocol/vue";
5
+ //#region src/vue/support.ts
6
+ function isRecord(value) {
7
+ return typeof value === "object" && value !== null && !Array.isArray(value);
8
+ }
9
+ /** Canonical unknown-caught-value → human-readable string. Non-Error
10
+ * objects with a `details` (gRPC convention) or `message` string field
11
+ * have that field surfaced. */
12
+ function errorMessage(err, fallback) {
13
+ if (err instanceof Error) return err.message;
14
+ if (err !== null && typeof err === "object") {
15
+ const obj = err;
16
+ if (typeof obj.details === "string" && obj.details) return obj.details;
17
+ if (typeof obj.message === "string" && obj.message) return obj.message;
18
+ }
19
+ if (fallback !== void 0) return fallback;
20
+ return String(err);
21
+ }
22
+ /** Clipboard failures (permissions, insecure context) are swallowed on
23
+ * purpose: the UI just leaves the "Copied!" hint off, which is what
24
+ * `copied=false` already signals. */
25
+ function useClipboardCopy(resetMs = 2e3) {
26
+ const copied = ref(false);
27
+ async function copy(text) {
28
+ try {
29
+ await navigator.clipboard.writeText(text);
30
+ copied.value = true;
31
+ setTimeout(() => {
32
+ copied.value = false;
33
+ }, resetMs);
34
+ } catch {}
35
+ }
36
+ return {
37
+ copied,
38
+ copy
39
+ };
40
+ }
41
+ //#endregion
42
+ //#region src/vue/helpers.ts
43
+ /**
44
+ * Decide whether a beat should be rendered automatically at
45
+ * script load time. Text-based beats (slides, charts, etc.) are
46
+ * auto-rendered only when the script has no characters —
47
+ * characters must be rendered first so they can be referenced by
48
+ * any character-using beat.
49
+ */
50
+ function shouldAutoRenderBeat(beat, hasCharacters, autoRenderTypes) {
51
+ if (hasCharacters) return false;
52
+ const type = beat.image?.type;
53
+ if (typeof type !== "string") return false;
54
+ return autoRenderTypes.includes(type);
55
+ }
56
+ /**
57
+ * Of the given character keys, return those whose image is not
58
+ * yet loaded and is not currently rendering. Used to fetch only
59
+ * what's missing after a movie-generation event arrives.
60
+ */
61
+ function getMissingCharacterKeys(keys, images, renderState) {
62
+ return keys.filter((charKey) => !images[charKey] && renderState[charKey] !== "rendering");
63
+ }
64
+ /**
65
+ * Validate a candidate Beat JSON string against a schema.
66
+ * Returns false on any JSON parse error or schema mismatch.
67
+ */
68
+ function validateBeatJSON(json, schema) {
69
+ let parsed;
70
+ try {
71
+ parsed = JSON.parse(json);
72
+ } catch {
73
+ return false;
74
+ }
75
+ return schema.safeParse(parsed).success;
76
+ }
77
+ /**
78
+ * Stable structural equality for two MulmoScripts via JSON
79
+ * canonicalisation. We compare the full re-serialised string
80
+ * rather than walking keys because (a) MulmoScript is
81
+ * deeply-nested and Object.keys-recursion would be ~50 lines, and
82
+ * (b) `JSON.stringify` already preserves insertion order, which
83
+ * `mulmoScriptSchema.safeParse` keeps stable across runs of the
84
+ * same input. False positives (= "differ" when they don't) only
85
+ * cost an extra `emit("updateResult", ...)` which is a no-op when
86
+ * data hasn't actually changed.
87
+ */
88
+ function isSameScript(left, right) {
89
+ return JSON.stringify(left) === JSON.stringify(right);
90
+ }
91
+ /**
92
+ * True when a beat can have a generated video clip on disk — used to
93
+ * decide whether to probe the beat-movie endpoint. `moviePrompt`
94
+ * beats produce a per-beat movie file; `html_tailwind` beats with
95
+ * `animation` set (either `true` or an options object) produce an
96
+ * `_animated.mp4` render.
97
+ */
98
+ function beatMayHaveMovie(beat) {
99
+ if (beat.moviePrompt) return true;
100
+ return beat.image?.type === "html_tailwind" && Boolean(beat.image.animation);
101
+ }
102
+ /** Pure check: is every beat in the script a `slide`-typed beat?
103
+ * When true, the View mounts `@mulmocast/deck-web`'s
104
+ * `MulmoScriptDeckEditor` instead of the per-beat list UI (#1575).
105
+ * Empty / missing `beats[]` returns false — there's nothing to edit
106
+ * as a deck, fall through to the existing UI which renders an empty
107
+ * state. Mixed scripts (any non-`slide` beat) also return false; that
108
+ * case is deferred to a future phase. */
109
+ function isAllSlideDeck(script) {
110
+ if (!isRecord(script)) return false;
111
+ const { beats } = script;
112
+ if (!Array.isArray(beats) || beats.length === 0) return false;
113
+ return beats.every((beat) => {
114
+ if (!isRecord(beat)) return false;
115
+ const { image } = beat;
116
+ return isRecord(image) && image.type === "slide";
117
+ });
118
+ }
119
+ //#endregion
120
+ //#region src/core/contract.ts
121
+ /** Plugin pubsub event name the host publishes generation events on
122
+ * (full channel: `plugin:<scope>:generation`). */
123
+ var GENERATION_EVENT = "generation";
124
+ //#endregion
125
+ //#region src/vue/transport.ts
126
+ var GENERATION_EVENT_KINDS = /* @__PURE__ */ new Set([
127
+ "beatImage",
128
+ "beatAudio",
129
+ "characterImage",
130
+ "movie",
131
+ "pdf"
132
+ ]);
133
+ function parseGenerationEvent(payload) {
134
+ if (!isRecord(payload)) return null;
135
+ const { kind, filePath, key, done, error } = payload;
136
+ if (typeof kind !== "string" || !GENERATION_EVENT_KINDS.has(kind)) return null;
137
+ if (typeof filePath !== "string" || typeof key !== "string" || typeof done !== "boolean") return null;
138
+ return {
139
+ kind,
140
+ filePath,
141
+ key,
142
+ done,
143
+ ...typeof error === "string" ? { error } : {}
144
+ };
145
+ }
146
+ function useMulmoScriptTransport() {
147
+ const runtime = useRuntime();
148
+ async function call(kind, args) {
149
+ let result;
150
+ try {
151
+ result = await runtime.dispatch({
152
+ kind,
153
+ ...args
154
+ });
155
+ } catch (err) {
156
+ return {
157
+ ok: false,
158
+ error: errorMessage(err)
159
+ };
160
+ }
161
+ if (!isRecord(result) || result.ok !== true) return {
162
+ ok: false,
163
+ error: isRecord(result) && typeof result.error === "string" ? result.error : `dispatch ${kind} returned an unexpected response`
164
+ };
165
+ return {
166
+ ok: true,
167
+ data: result
168
+ };
169
+ }
170
+ function onGenerationEvent(filePath, handler) {
171
+ return runtime.pubsub.subscribe(GENERATION_EVENT, (payload) => {
172
+ const event = parseGenerationEvent(payload);
173
+ if (!event) return;
174
+ const current = filePath();
175
+ if (!current || event.filePath !== current) return;
176
+ handler(event);
177
+ });
178
+ }
179
+ return {
180
+ call,
181
+ onGenerationEvent
182
+ };
183
+ }
184
+ //#endregion
185
+ //#region src/vue/hostAdapter.ts
186
+ var MULMOSCRIPT_HOST_ADAPTER_KEY = Symbol("mulmoscript-host-adapter");
187
+ var EMPTY_ADAPTER = {};
188
+ function useHostAdapter() {
189
+ return inject(MULMOSCRIPT_HOST_ADAPTER_KEY, EMPTY_ADAPTER);
190
+ }
191
+ //#endregion
192
+ //#region src/lang/index.ts
193
+ var MESSAGES = {
194
+ de: {
195
+ beatCount: (count) => count === 1 ? `${count} Beat` : `${count} Beats`,
196
+ movie: "Video",
197
+ generating: "Wird generiert…",
198
+ rendering: "Wird gerendert…",
199
+ saving: "Wird gespeichert…",
200
+ update: "Aktualisieren",
201
+ characters: "Charaktere",
202
+ drop: "Ablegen",
203
+ gen: "Generieren",
204
+ play: "▶ Abspielen",
205
+ stop: "■ Stoppen",
206
+ playPresentation: "Präsentation abspielen",
207
+ regenerateMovie: "Video neu generieren",
208
+ movieGenerationFailed: "Videoerstellung fehlgeschlagen",
209
+ pdf: "PDF",
210
+ regeneratePdf: "PDF neu generieren",
211
+ generatingPdf: "PDF wird erstellt…",
212
+ retry: "Erneut versuchen",
213
+ errPrefix: "⚠ Fehler",
214
+ noBeats: "Keine Beats im Skript gefunden",
215
+ editSource: "Skript-Quelle bearbeiten",
216
+ applyChanges: "Änderungen übernehmen",
217
+ generateAll: "Alle generieren",
218
+ orDropImage: "oder Bild ablegen",
219
+ generate: "Generieren",
220
+ generateAudio: "♪ Generieren",
221
+ saveErrorInvalidJson: (error) => `⚠ Ungültiges JSON: ${error}`,
222
+ saveErrorSaveFailed: (error) => `⚠ Speichern fehlgeschlagen: ${error}`,
223
+ close: "Schließen",
224
+ cancel: "Abbrechen"
225
+ },
226
+ en: {
227
+ beatCount: (count) => count === 1 ? `${count} beat` : `${count} beats`,
228
+ movie: "Movie",
229
+ generating: "Generating…",
230
+ rendering: "Rendering…",
231
+ saving: "Saving…",
232
+ update: "Update",
233
+ characters: "Characters",
234
+ drop: "Drop",
235
+ gen: "Gen",
236
+ play: "▶ Play",
237
+ stop: "■ Stop",
238
+ playPresentation: "Play presentation",
239
+ regenerateMovie: "Regenerate movie",
240
+ movieGenerationFailed: "Movie generation failed",
241
+ pdf: "PDF",
242
+ regeneratePdf: "Regenerate PDF",
243
+ generatingPdf: "Generating PDF…",
244
+ retry: "Retry",
245
+ errPrefix: "⚠ Error",
246
+ noBeats: "No beats found in script",
247
+ editSource: "Edit Script Source",
248
+ applyChanges: "Apply Changes",
249
+ generateAll: "Generate All",
250
+ orDropImage: "or drop image",
251
+ generate: "Generate",
252
+ generateAudio: "♪ Generate",
253
+ saveErrorInvalidJson: (error) => `⚠ Invalid JSON: ${error}`,
254
+ saveErrorSaveFailed: (error) => `⚠ Save failed: ${error}`,
255
+ close: "Close",
256
+ cancel: "Cancel"
257
+ },
258
+ es: {
259
+ beatCount: (count) => count === 1 ? `${count} beat` : `${count} beats`,
260
+ movie: "Vídeo",
261
+ generating: "Generando…",
262
+ rendering: "Renderizando…",
263
+ saving: "Guardando…",
264
+ update: "Actualizar",
265
+ characters: "Personajes",
266
+ drop: "Soltar",
267
+ gen: "Generar",
268
+ play: "▶ Reproducir",
269
+ stop: "■ Detener",
270
+ playPresentation: "Reproducir presentación",
271
+ regenerateMovie: "Regenerar vídeo",
272
+ movieGenerationFailed: "Error al generar el vídeo",
273
+ pdf: "PDF",
274
+ regeneratePdf: "Regenerar PDF",
275
+ generatingPdf: "Generando PDF…",
276
+ retry: "Reintentar",
277
+ errPrefix: "⚠ Error",
278
+ noBeats: "No se encontraron beats en el script",
279
+ editSource: "Editar fuente del script",
280
+ applyChanges: "Aplicar cambios",
281
+ generateAll: "Generar todo",
282
+ orDropImage: "o arrastra una imagen",
283
+ generate: "Generar",
284
+ generateAudio: "♪ Generar",
285
+ saveErrorInvalidJson: (error) => `⚠ JSON no válido: ${error}`,
286
+ saveErrorSaveFailed: (error) => `⚠ Error al guardar: ${error}`,
287
+ close: "Cerrar",
288
+ cancel: "Cancelar"
289
+ },
290
+ fr: {
291
+ beatCount: (count) => count === 1 ? `${count} beat` : `${count} beats`,
292
+ movie: "Film",
293
+ generating: "Génération…",
294
+ rendering: "Rendu…",
295
+ saving: "Enregistrement…",
296
+ update: "Mettre à jour",
297
+ characters: "Personnages",
298
+ drop: "Déposer",
299
+ gen: "Générer",
300
+ play: "▶ Lire",
301
+ stop: "■ Arrêter",
302
+ playPresentation: "Lire la présentation",
303
+ regenerateMovie: "Régénérer la vidéo",
304
+ movieGenerationFailed: "Échec de la génération de la vidéo",
305
+ pdf: "PDF",
306
+ regeneratePdf: "Régénérer le PDF",
307
+ generatingPdf: "Génération du PDF…",
308
+ retry: "Réessayer",
309
+ errPrefix: "⚠ Erreur",
310
+ noBeats: "Aucun beat trouvé dans le script",
311
+ editSource: "Modifier la source du script",
312
+ applyChanges: "Appliquer les modifications",
313
+ generateAll: "Tout générer",
314
+ orDropImage: "ou déposez une image",
315
+ generate: "Générer",
316
+ generateAudio: "♪ Générer",
317
+ saveErrorInvalidJson: (error) => `⚠ JSON invalide : ${error}`,
318
+ saveErrorSaveFailed: (error) => `⚠ Échec de la sauvegarde : ${error}`,
319
+ close: "Fermer",
320
+ cancel: "Annuler"
321
+ },
322
+ ja: {
323
+ beatCount: (count) => `${count} ビート`,
324
+ movie: "動画",
325
+ generating: "生成中…",
326
+ rendering: "レンダリング中…",
327
+ saving: "保存中…",
328
+ update: "更新",
329
+ characters: "キャラクター",
330
+ drop: "ドロップ",
331
+ gen: "生成",
332
+ play: "▶ 再生",
333
+ stop: "■ 停止",
334
+ playPresentation: "プレゼンテーション再生",
335
+ regenerateMovie: "動画を再生成",
336
+ movieGenerationFailed: "動画の生成に失敗しました",
337
+ pdf: "PDF",
338
+ regeneratePdf: "PDF を再生成",
339
+ generatingPdf: "PDF を生成中…",
340
+ retry: "再試行",
341
+ errPrefix: "⚠ エラー",
342
+ noBeats: "スクリプトにビートが見つかりません",
343
+ editSource: "スクリプトソースを編集",
344
+ applyChanges: "変更を適用",
345
+ generateAll: "すべて生成",
346
+ orDropImage: "画像をドロップ",
347
+ generate: "生成",
348
+ generateAudio: "♪ 生成",
349
+ saveErrorInvalidJson: (error) => `⚠ 不正な JSON: ${error}`,
350
+ saveErrorSaveFailed: (error) => `⚠ 保存失敗: ${error}`,
351
+ close: "閉じる",
352
+ cancel: "キャンセル"
353
+ },
354
+ ko: {
355
+ beatCount: (count) => `${count}개 비트`,
356
+ movie: "영상",
357
+ generating: "생성 중…",
358
+ rendering: "렌더링 중…",
359
+ saving: "저장 중…",
360
+ update: "업데이트",
361
+ characters: "캐릭터",
362
+ drop: "드롭",
363
+ gen: "생성",
364
+ play: "▶ 재생",
365
+ stop: "■ 정지",
366
+ playPresentation: "프레젠테이션 재생",
367
+ regenerateMovie: "동영상 재생성",
368
+ movieGenerationFailed: "동영상 생성에 실패했습니다",
369
+ pdf: "PDF",
370
+ regeneratePdf: "PDF 재생성",
371
+ generatingPdf: "PDF 생성 중…",
372
+ retry: "다시 시도",
373
+ errPrefix: "⚠ 오류",
374
+ noBeats: "스크립트에서 비트를 찾을 수 없습니다",
375
+ editSource: "스크립트 원본 편집",
376
+ applyChanges: "변경 사항 적용",
377
+ generateAll: "전체 생성",
378
+ orDropImage: "또는 이미지 드롭",
379
+ generate: "생성",
380
+ generateAudio: "♪ 생성",
381
+ saveErrorInvalidJson: (error) => `⚠ 잘못된 JSON: ${error}`,
382
+ saveErrorSaveFailed: (error) => `⚠ 저장 실패: ${error}`,
383
+ close: "닫기",
384
+ cancel: "취소"
385
+ },
386
+ "pt-BR": {
387
+ beatCount: (count) => count === 1 ? `${count} beat` : `${count} beats`,
388
+ movie: "Vídeo",
389
+ generating: "Gerando…",
390
+ rendering: "Renderizando…",
391
+ saving: "Salvando…",
392
+ update: "Atualizar",
393
+ characters: "Personagens",
394
+ drop: "Soltar",
395
+ gen: "Gerar",
396
+ play: "▶ Reproduzir",
397
+ stop: "■ Parar",
398
+ playPresentation: "Reproduzir apresentação",
399
+ regenerateMovie: "Regenerar vídeo",
400
+ movieGenerationFailed: "Falha ao gerar o vídeo",
401
+ pdf: "PDF",
402
+ regeneratePdf: "Regenerar PDF",
403
+ generatingPdf: "Gerando PDF…",
404
+ retry: "Tentar novamente",
405
+ errPrefix: "⚠ Erro",
406
+ noBeats: "Nenhum beat encontrado no script",
407
+ editSource: "Editar fonte do script",
408
+ applyChanges: "Aplicar alterações",
409
+ generateAll: "Gerar tudo",
410
+ orDropImage: "ou solte uma imagem",
411
+ generate: "Gerar",
412
+ generateAudio: "♪ Gerar",
413
+ saveErrorInvalidJson: (error) => `⚠ JSON inválido: ${error}`,
414
+ saveErrorSaveFailed: (error) => `⚠ Falha ao salvar: ${error}`,
415
+ close: "Fechar",
416
+ cancel: "Cancelar"
417
+ },
418
+ zh: {
419
+ beatCount: (count) => `${count} 个 beat`,
420
+ movie: "视频",
421
+ generating: "生成中…",
422
+ rendering: "渲染中…",
423
+ saving: "保存中…",
424
+ update: "更新",
425
+ characters: "角色",
426
+ drop: "拖放",
427
+ gen: "生成",
428
+ play: "▶ 播放",
429
+ stop: "■ 停止",
430
+ playPresentation: "播放演示",
431
+ regenerateMovie: "重新生成视频",
432
+ movieGenerationFailed: "视频生成失败",
433
+ pdf: "PDF",
434
+ regeneratePdf: "重新生成 PDF",
435
+ generatingPdf: "生成 PDF…",
436
+ retry: "重试",
437
+ errPrefix: "⚠ 错误",
438
+ noBeats: "脚本中没有找到 beat",
439
+ editSource: "编辑脚本源",
440
+ applyChanges: "应用更改",
441
+ generateAll: "全部生成",
442
+ orDropImage: "或拖入图片",
443
+ generate: "生成",
444
+ generateAudio: "♪ 生成",
445
+ saveErrorInvalidJson: (error) => `⚠ JSON 无效: ${error}`,
446
+ saveErrorSaveFailed: (error) => `⚠ 保存失败: ${error}`,
447
+ close: "关闭",
448
+ cancel: "取消"
449
+ }
450
+ };
451
+ function isSupportedLocale(value) {
452
+ return Object.hasOwn(MESSAGES, value);
453
+ }
454
+ /** Reactive message bundle for the active host locale. The plugin carries its
455
+ * own translations (no host i18n dependency); it reads the locale off the
456
+ * injected `BrowserPluginRuntime.locale` ref and falls back to English.
457
+ * Same pattern as @mulmoclaude/html-plugin. */
458
+ function useT() {
459
+ const locale = inject(PLUGIN_RUNTIME_KEY, void 0)?.locale ?? ref("en");
460
+ return computed(() => isSupportedLocale(locale.value) ? MESSAGES[locale.value] : MESSAGES.en);
461
+ }
462
+ //#endregion
463
+ //#region src/vue/View.vue?vue&type=script&setup=true&lang.ts
464
+ var _hoisted_1$1 = { class: "h-full bg-white flex flex-col overflow-hidden" };
465
+ var _hoisted_2$1 = { class: "flex items-start justify-between px-6 py-4 border-b border-gray-100 shrink-0" };
466
+ var _hoisted_3$1 = { class: "min-w-0 flex-1" };
467
+ var _hoisted_4 = {
468
+ class: "text-lg font-semibold text-gray-800 truncate",
469
+ "data-testid": "mulmo-script-title"
470
+ };
471
+ var _hoisted_5 = {
472
+ key: 0,
473
+ class: "text-sm text-gray-500 mt-0.5 truncate",
474
+ "data-testid": "mulmo-script-description"
475
+ };
476
+ var _hoisted_6 = { class: "flex items-center gap-3 mt-1 text-xs text-gray-400" };
477
+ var _hoisted_7 = { key: 0 };
478
+ var _hoisted_8 = {
479
+ key: 1,
480
+ class: "truncate"
481
+ };
482
+ var _hoisted_9 = { class: "ml-4 shrink-0 flex items-center gap-2" };
483
+ var _hoisted_10 = [
484
+ "disabled",
485
+ "title",
486
+ "aria-label"
487
+ ];
488
+ var _hoisted_11 = ["disabled"];
489
+ var _hoisted_12 = ["title", "aria-label"];
490
+ var _hoisted_13 = ["disabled"];
491
+ var _hoisted_14 = {
492
+ key: 0,
493
+ class: "animate-spin w-4 h-4 shrink-0",
494
+ viewBox: "0 0 24 24",
495
+ fill: "none"
496
+ };
497
+ var _hoisted_15 = { key: 1 };
498
+ var _hoisted_16 = ["disabled"];
499
+ var _hoisted_17 = ["title", "aria-label"];
500
+ var _hoisted_18 = ["disabled"];
501
+ var _hoisted_19 = {
502
+ key: 0,
503
+ class: "animate-spin w-4 h-4 shrink-0",
504
+ viewBox: "0 0 24 24",
505
+ fill: "none"
506
+ };
507
+ var _hoisted_20 = { key: 1 };
508
+ var _hoisted_21 = {
509
+ key: 0,
510
+ "data-testid": "mulmo-script-movie-error-chip",
511
+ 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"
512
+ };
513
+ var _hoisted_22 = { class: "flex-1 min-w-0" };
514
+ var _hoisted_23 = { class: "font-medium" };
515
+ var _hoisted_24 = { class: "break-words whitespace-pre-wrap mt-0.5" };
516
+ var _hoisted_25 = ["disabled"];
517
+ var _hoisted_26 = {
518
+ key: 1,
519
+ class: "border-b border-gray-100 shrink-0 px-4 py-3"
520
+ };
521
+ var _hoisted_27 = { class: "flex items-center justify-between mb-2" };
522
+ var _hoisted_28 = { class: "text-xs font-semibold text-gray-500 uppercase tracking-wide" };
523
+ var _hoisted_29 = ["disabled"];
524
+ var _hoisted_30 = { class: "flex gap-3 flex-wrap" };
525
+ var _hoisted_31 = [
526
+ "onDragover",
527
+ "onDragleave",
528
+ "onDrop"
529
+ ];
530
+ var _hoisted_32 = [
531
+ "src",
532
+ "alt",
533
+ "onClick"
534
+ ];
535
+ var _hoisted_33 = {
536
+ key: 1,
537
+ class: "animate-spin w-4 h-4 text-green-400",
538
+ viewBox: "0 0 24 24",
539
+ fill: "none"
540
+ };
541
+ var _hoisted_34 = {
542
+ key: 2,
543
+ class: "text-xs text-red-400 text-center px-1"
544
+ };
545
+ var _hoisted_35 = {
546
+ key: 3,
547
+ class: "text-xs text-gray-300 text-center px-1 leading-tight"
548
+ };
549
+ var _hoisted_36 = {
550
+ key: 4,
551
+ class: "absolute bottom-0 inset-x-0 text-center text-xs text-gray-400 bg-white/70 py-0.5 pointer-events-none"
552
+ };
553
+ var _hoisted_37 = {
554
+ key: 5,
555
+ class: "absolute inset-0 flex items-center justify-center bg-blue-50/80 pointer-events-none"
556
+ };
557
+ var _hoisted_38 = { class: "text-xs text-blue-500 font-medium" };
558
+ var _hoisted_39 = ["disabled", "onClick"];
559
+ var _hoisted_40 = {
560
+ key: 0,
561
+ class: "inline-block animate-spin"
562
+ };
563
+ var _hoisted_41 = { key: 1 };
564
+ var _hoisted_42 = ["disabled", "onClick"];
565
+ var _hoisted_43 = {
566
+ key: 0,
567
+ class: "animate-spin w-3 h-3",
568
+ viewBox: "0 0 24 24",
569
+ fill: "none"
570
+ };
571
+ var _hoisted_44 = { key: 1 };
572
+ var _hoisted_45 = { class: "text-xs text-gray-600 text-center truncate w-full" };
573
+ var _hoisted_46 = {
574
+ key: 2,
575
+ class: "flex-1 overflow-hidden",
576
+ "data-testid": "mulmo-script-deck-editor"
577
+ };
578
+ var _hoisted_47 = { class: "flex gap-3 items-stretch" };
579
+ var _hoisted_48 = [
580
+ "onDragover",
581
+ "onDragleave",
582
+ "onDrop"
583
+ ];
584
+ var _hoisted_49 = ["src", "data-testid"];
585
+ var _hoisted_50 = [
586
+ "title",
587
+ "aria-label",
588
+ "data-testid",
589
+ "onClick"
590
+ ];
591
+ var _hoisted_51 = [
592
+ "src",
593
+ "alt",
594
+ "onClick"
595
+ ];
596
+ var _hoisted_52 = [
597
+ "title",
598
+ "aria-label",
599
+ "data-testid",
600
+ "onClick"
601
+ ];
602
+ var _hoisted_53 = {
603
+ key: 0,
604
+ class: "animate-spin w-5 h-5",
605
+ viewBox: "0 0 24 24",
606
+ fill: "none"
607
+ };
608
+ var _hoisted_54 = {
609
+ key: 1,
610
+ class: "material-icons text-3xl"
611
+ };
612
+ var _hoisted_55 = ["disabled", "onClick"];
613
+ var _hoisted_56 = {
614
+ key: 3,
615
+ class: "w-full aspect-video flex flex-col items-center justify-center gap-1 p-2"
616
+ };
617
+ var _hoisted_57 = { class: "text-xs text-green-500" };
618
+ var _hoisted_58 = {
619
+ key: 1,
620
+ class: "text-xs text-red-400 text-center"
621
+ };
622
+ var _hoisted_59 = {
623
+ key: 0,
624
+ class: "text-xs text-gray-400 text-center italic leading-relaxed px-1"
625
+ };
626
+ var _hoisted_60 = {
627
+ key: 1,
628
+ class: "text-xs text-gray-300"
629
+ };
630
+ var _hoisted_61 = {
631
+ key: 2,
632
+ class: "absolute inset-0 flex items-center justify-center bg-blue-50/80 pointer-events-none"
633
+ };
634
+ var _hoisted_62 = { class: "text-xs text-blue-500 font-medium" };
635
+ var _hoisted_63 = {
636
+ key: 3,
637
+ class: "absolute bottom-0 inset-x-0 text-center text-xs text-gray-400 bg-white/70 py-0.5 pointer-events-none"
638
+ };
639
+ var _hoisted_64 = ["onClick"];
640
+ var _hoisted_65 = { class: "flex flex-col flex-1 min-w-0 px-2 py-1.5" };
641
+ var _hoisted_66 = { class: "text-sm text-gray-800 leading-relaxed" };
642
+ var _hoisted_67 = { class: "flex justify-between mt-auto pt-1" };
643
+ var _hoisted_68 = { class: "flex items-center gap-1" };
644
+ var _hoisted_69 = {
645
+ key: 0,
646
+ class: "animate-spin w-3 h-3 text-green-400",
647
+ viewBox: "0 0 24 24",
648
+ fill: "none"
649
+ };
650
+ var _hoisted_70 = ["onClick"];
651
+ var _hoisted_71 = ["title"];
652
+ var _hoisted_72 = ["disabled", "onClick"];
653
+ var _hoisted_73 = ["onClick"];
654
+ var _hoisted_74 = [
655
+ "title",
656
+ "data-testid",
657
+ "onClick"
658
+ ];
659
+ var _hoisted_75 = {
660
+ key: 0,
661
+ class: "border-t border-gray-100"
662
+ };
663
+ var _hoisted_76 = ["onUpdate:modelValue", "data-testid"];
664
+ var _hoisted_77 = { class: "flex items-center justify-end gap-2 px-2 pb-2" };
665
+ var _hoisted_78 = {
666
+ key: 0,
667
+ class: "text-xs text-red-600",
668
+ role: "alert"
669
+ };
670
+ var _hoisted_79 = [
671
+ "disabled",
672
+ "data-testid",
673
+ "onClick"
674
+ ];
675
+ var _hoisted_80 = {
676
+ key: 0,
677
+ class: "flex items-center justify-center h-32 text-gray-400 text-sm"
678
+ };
679
+ var _hoisted_81 = { class: "bottom-bar-wrapper" };
680
+ var _hoisted_82 = { class: "editor-actions" };
681
+ var _hoisted_83 = ["disabled"];
682
+ var _hoisted_84 = ["title"];
683
+ var _hoisted_85 = { class: "material-icons" };
684
+ var _hoisted_86 = ["title"];
685
+ var _hoisted_87 = { class: "flex items-center gap-4" };
686
+ var _hoisted_88 = ["disabled"];
687
+ var _hoisted_89 = { class: "flex flex-col items-center" };
688
+ var _hoisted_90 = ["src"];
689
+ var _hoisted_91 = {
690
+ key: 0,
691
+ class: "relative w-full h-1"
692
+ };
693
+ var _hoisted_92 = { class: "flex gap-1 h-full" };
694
+ var _hoisted_93 = ["onClick"];
695
+ var _hoisted_94 = {
696
+ key: 0,
697
+ 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"
698
+ };
699
+ var _hoisted_95 = ["disabled"];
700
+ var _hoisted_96 = {
701
+ key: 0,
702
+ class: "relative w-screen flex justify-center px-16"
703
+ };
704
+ var _hoisted_97 = {
705
+ key: 0,
706
+ class: "max-w-[80vw] text-center text-white leading-relaxed text-[clamp(0.8rem,1.76vw,1.6rem)]"
707
+ };
708
+ var SILENT_BEAT_DEFAULT_SEC = 3;
709
+ var MS_PER_SECOND = 1e3;
710
+ var DECK_SAVE_DEBOUNCE_MS = 300;
711
+ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineComponent({
712
+ __name: "View",
713
+ props: { selectedResult: {} },
714
+ emits: ["updateResult"],
715
+ setup(__props, { emit: __emit }) {
716
+ const MulmoScriptDeckEditor = defineAsyncComponent(() => import("@mulmocast/deck-web").then((mod) => mod.MulmoScriptDeckEditor));
717
+ const api = useMulmoScriptTransport();
718
+ const adapter = useHostAdapter();
719
+ const canFetchMedia = computed(() => Boolean(adapter.fetchMediaBlob));
720
+ const m = useT();
721
+ const props = __props;
722
+ const emit = __emit;
723
+ const data = computed(() => props.selectedResult.data);
724
+ const script = computed(() => data.value?.script ?? {});
725
+ const filePath = computed(() => data.value?.filePath ?? "");
726
+ const beats = computed(() => script.value.beats ?? []);
727
+ const renderState = reactive({});
728
+ const renderedImages = reactive({});
729
+ const renderErrors = reactive({});
730
+ const sourceOpen = reactive({});
731
+ const sourceText = reactive({});
732
+ const beatSaveErrors = reactive({});
733
+ const beatSaving = reactive({});
734
+ const localOverrides = reactive({});
735
+ const movieGenerating = ref(false);
736
+ const movieDownloading = ref(false);
737
+ const moviePath = ref(null);
738
+ const movieError = ref(null);
739
+ const pdfGenerating = ref(false);
740
+ const pdfDownloading = ref(false);
741
+ const pdfPath = ref(null);
742
+ const beatAudios = reactive({});
743
+ const audioState = reactive({});
744
+ const audioErrors = reactive({});
745
+ const beatMovies = reactive({});
746
+ const beatMovieUrls = reactive({});
747
+ const beatMovieOpen = reactive({});
748
+ const beatMovieLoading = reactive({});
749
+ const playingAudio = ref(null);
750
+ const silentPlaybackTimer = ref(null);
751
+ const audioProgress = ref(0);
752
+ const beatListEl = ref(null);
753
+ const lightbox = ref(null);
754
+ const charRenderState = reactive({});
755
+ const charImages = reactive({});
756
+ const charErrors = reactive({});
757
+ const charDragOver = reactive({});
758
+ const beatDragOver = reactive({});
759
+ const anyBeatRendering = computed(() => Object.values(renderState).some((state) => state === "rendering"));
760
+ const characterKeys = computed(() => {
761
+ const imgs = script.value.imageParams?.images ?? {};
762
+ return Object.keys(imgs).filter((key) => imgs[key]?.type === "imagePrompt");
763
+ });
764
+ const chatSessionId = computed(() => adapter.chatSessionId?.value);
765
+ function characterPrompt(key) {
766
+ return script.value.imageParams?.images?.[key]?.prompt ?? "";
767
+ }
768
+ function stopPlayingAudio() {
769
+ stopAllPlayback();
770
+ }
771
+ function openLightbox(index) {
772
+ stopPlayingAudio();
773
+ lightbox.value = {
774
+ src: renderedImages[index],
775
+ text: effectiveBeat(index).text,
776
+ index
777
+ };
778
+ }
779
+ function closeLightbox() {
780
+ stopPlayingAudio();
781
+ lightbox.value = null;
782
+ }
783
+ const isPlayReady = computed(() => {
784
+ if (beats.value.length === 0) return false;
785
+ if (!renderedImages[0]) return false;
786
+ if (effectiveBeat(0).text && !beatAudios[0]) return false;
787
+ return true;
788
+ });
789
+ function playPresentation() {
790
+ if (!isPlayReady.value) return;
791
+ openLightbox(0);
792
+ playBeat(0);
793
+ }
794
+ function stopAllPlayback() {
795
+ if (playingAudio.value) {
796
+ playingAudio.value.audio.pause();
797
+ playingAudio.value = null;
798
+ audioProgress.value = 0;
799
+ }
800
+ if (silentPlaybackTimer.value) {
801
+ clearTimeout(silentPlaybackTimer.value.timer);
802
+ silentPlaybackTimer.value = null;
803
+ }
804
+ }
805
+ function playBeat(index) {
806
+ stopAllPlayback();
807
+ if (!Boolean(effectiveBeat(index).text)) {
808
+ scheduleSilentAdvance(index);
809
+ return;
810
+ }
811
+ if (beatAudios[index]) playAudio(index);
812
+ }
813
+ function scheduleSilentAdvance(index) {
814
+ const raw = effectiveBeat(index).duration;
815
+ const timer = setTimeout(() => {
816
+ if (silentPlaybackTimer.value?.index !== index) return;
817
+ silentPlaybackTimer.value = null;
818
+ if (lightbox.value?.index === index) advanceFromBeat(index);
819
+ }, (typeof raw === "number" && Number.isFinite(raw) && raw > 0 ? raw : SILENT_BEAT_DEFAULT_SEC) * MS_PER_SECOND);
820
+ silentPlaybackTimer.value = {
821
+ index,
822
+ timer
823
+ };
824
+ }
825
+ function advanceFromBeat(fromIndex) {
826
+ lightboxMove(1);
827
+ const nextIndex = lightbox.value?.index;
828
+ if (nextIndex === void 0 || nextIndex === fromIndex) return;
829
+ playBeat(nextIndex);
830
+ }
831
+ const hasPrev = computed(() => {
832
+ if (!lightbox.value) return false;
833
+ for (let i = lightbox.value.index - 1; i >= 0; i--) if (renderedImages[i]) return true;
834
+ return false;
835
+ });
836
+ const hasNext = computed(() => {
837
+ if (!lightbox.value) return false;
838
+ for (let i = lightbox.value.index + 1; i < beats.value.length; i++) if (renderedImages[i]) return true;
839
+ return false;
840
+ });
841
+ function jumpToBeat(index) {
842
+ if (!lightbox.value) return;
843
+ if (index === lightbox.value.index) return;
844
+ if (!renderedImages[index]) return;
845
+ const wasPlaying = playingAudio.value !== null || silentPlaybackTimer.value !== null;
846
+ openLightbox(index);
847
+ if (wasPlaying) playBeat(index);
848
+ }
849
+ function beatTooltip(index) {
850
+ const text = effectiveBeat(index).text ?? "";
851
+ return text.length > 80 ? `${text.slice(0, 80)}…` : text;
852
+ }
853
+ function lightboxMove(delta) {
854
+ if (!lightbox.value) return;
855
+ const total = beats.value.length;
856
+ const wasPlaying = playingAudio.value !== null || silentPlaybackTimer.value !== null;
857
+ let i = lightbox.value.index + delta;
858
+ while (i >= 0 && i < total) {
859
+ if (renderedImages[i]) {
860
+ openLightbox(i);
861
+ if (wasPlaying) playBeat(i);
862
+ return;
863
+ }
864
+ i += delta;
865
+ }
866
+ }
867
+ const sourceDetails = ref();
868
+ const editing = ref(false);
869
+ const editableSource = ref("");
870
+ const { copied, copy } = useClipboardCopy();
871
+ const effectiveScript = computed(() => ({
872
+ ...script.value,
873
+ beats: beats.value.map((beat, i) => localOverrides[i] ?? beat)
874
+ }));
875
+ const scriptSourceText = computed(() => JSON.stringify(effectiveScript.value, null, 2));
876
+ const isDeck = computed(() => isAllSlideDeck(effectiveScript.value));
877
+ const deckScriptInput = computed(() => effectiveScript.value);
878
+ let deckSaveTimer = null;
879
+ let pendingDeckScript = null;
880
+ function scheduleDeckSave(next) {
881
+ pendingDeckScript = next;
882
+ if (deckSaveTimer) clearTimeout(deckSaveTimer);
883
+ deckSaveTimer = setTimeout(() => {
884
+ flushDeckSave();
885
+ }, DECK_SAVE_DEBOUNCE_MS);
886
+ }
887
+ async function flushDeckSave() {
888
+ deckSaveTimer = null;
889
+ const next = pendingDeckScript;
890
+ pendingDeckScript = null;
891
+ if (!next || !filePath.value) return;
892
+ const response = await api.call("updateScript", {
893
+ filePath: filePath.value,
894
+ script: next
895
+ });
896
+ if (!response.ok) {
897
+ console.error("[presentMulmoScript] deck save failed:", response.error);
898
+ return;
899
+ }
900
+ emit("updateResult", {
901
+ ...props.selectedResult,
902
+ data: {
903
+ ...props.selectedResult.data,
904
+ script: next
905
+ }
906
+ });
907
+ }
908
+ function onDeckUpdate(next) {
909
+ scheduleDeckSave(next);
910
+ }
911
+ onBeforeUnmount(() => {
912
+ if (deckSaveTimer) {
913
+ clearTimeout(deckSaveTimer);
914
+ flushDeckSave();
915
+ }
916
+ resetBeatMovies();
917
+ unsubscribeGenerationEvents();
918
+ });
919
+ const loadedSource = ref("");
920
+ const sourceChanged = computed(() => editableSource.value !== loadedSource.value);
921
+ const sourceValid = computed(() => {
922
+ try {
923
+ const parsed = JSON.parse(editableSource.value);
924
+ return mulmoScriptSchema.safeParse(parsed).success;
925
+ } catch {
926
+ return false;
927
+ }
928
+ });
929
+ async function onSourceToggle(open) {
930
+ editing.value = open;
931
+ if (open) {
932
+ let text = scriptSourceText.value;
933
+ if (filePath.value) {
934
+ const response = await api.call("save", { filePath: filePath.value });
935
+ const diskScript = response.ok ? response.data.script : void 0;
936
+ if (diskScript) text = JSON.stringify(diskScript, null, 2);
937
+ }
938
+ editableSource.value = text;
939
+ loadedSource.value = text;
940
+ }
941
+ }
942
+ function cancelSourceEdit() {
943
+ if (sourceDetails.value) sourceDetails.value.open = false;
944
+ }
945
+ async function applySource() {
946
+ let parsed;
947
+ try {
948
+ parsed = JSON.parse(editableSource.value);
949
+ } catch (err) {
950
+ alert(errorMessage(err));
951
+ return;
952
+ }
953
+ const response = await api.call("updateScript", {
954
+ filePath: filePath.value,
955
+ script: parsed
956
+ });
957
+ if (!response.ok) {
958
+ alert(response.error || "Update failed");
959
+ return;
960
+ }
961
+ emit("updateResult", {
962
+ ...props.selectedResult,
963
+ data: {
964
+ ...props.selectedResult.data,
965
+ script: parsed
966
+ }
967
+ });
968
+ if (sourceDetails.value) sourceDetails.value.open = false;
969
+ await initializeScript();
970
+ }
971
+ async function copyText() {
972
+ await copy(scriptSourceText.value);
973
+ }
974
+ function effectiveBeat(index) {
975
+ return localOverrides[index] ?? beats.value[index] ?? {};
976
+ }
977
+ function toggleSource(index) {
978
+ if (!sourceOpen[index]) {
979
+ sourceText[index] = JSON.stringify(effectiveBeat(index), null, 2);
980
+ Reflect.deleteProperty(beatSaveErrors, index);
981
+ }
982
+ sourceOpen[index] = !sourceOpen[index];
983
+ }
984
+ function isValidBeat(index) {
985
+ return validateBeatJSON(sourceText[index] ?? "", mulmoBeatSchema);
986
+ }
987
+ async function updateBeat(index) {
988
+ let beat;
989
+ try {
990
+ beat = JSON.parse(sourceText[index]);
991
+ } catch (err) {
992
+ beatSaveErrors[index] = {
993
+ kind: "invalidJson",
994
+ error: errorMessage(err)
995
+ };
996
+ return;
997
+ }
998
+ const prevImage = JSON.stringify(effectiveBeat(index).image);
999
+ const requestedFilePath = filePath.value;
1000
+ Reflect.deleteProperty(beatSaveErrors, index);
1001
+ beatSaving[index] = true;
1002
+ const response = await api.call("updateBeat", {
1003
+ filePath: requestedFilePath,
1004
+ beatIndex: index,
1005
+ beat
1006
+ });
1007
+ if (staleSince(requestedFilePath)) return;
1008
+ Reflect.deleteProperty(beatSaving, index);
1009
+ if (!response.ok) {
1010
+ beatSaveErrors[index] = {
1011
+ kind: "saveFailed",
1012
+ error: response.error
1013
+ };
1014
+ return;
1015
+ }
1016
+ localOverrides[index] = beat;
1017
+ sourceOpen[index] = false;
1018
+ if (JSON.stringify(beat.image) !== prevImage) {
1019
+ Reflect.deleteProperty(renderedImages, index);
1020
+ renderBeat(index);
1021
+ }
1022
+ }
1023
+ async function renderBeat(index) {
1024
+ const requestedFilePath = filePath.value;
1025
+ renderState[index] = "rendering";
1026
+ const response = await api.call("renderBeat", {
1027
+ filePath: requestedFilePath,
1028
+ beatIndex: index,
1029
+ chatSessionId: chatSessionId.value
1030
+ });
1031
+ if (staleSince(requestedFilePath)) return;
1032
+ if (!response.ok) {
1033
+ renderErrors[index] = response.error || "Render failed";
1034
+ renderState[index] = "error";
1035
+ return;
1036
+ }
1037
+ renderedImages[index] = response.data.image ?? "";
1038
+ renderState[index] = "done";
1039
+ refreshMissingCharacterImages();
1040
+ if (beatMayHaveMovie(effectiveBeat(index))) loadExistingBeatMovie(index);
1041
+ }
1042
+ async function regenerateBeat(index) {
1043
+ const requestedFilePath = filePath.value;
1044
+ Reflect.deleteProperty(renderedImages, index);
1045
+ invalidateBeatMovie(index);
1046
+ renderState[index] = "rendering";
1047
+ const response = await api.call("renderBeat", {
1048
+ filePath: requestedFilePath,
1049
+ beatIndex: index,
1050
+ force: true,
1051
+ chatSessionId: chatSessionId.value
1052
+ });
1053
+ if (staleSince(requestedFilePath)) return;
1054
+ if (!response.ok) {
1055
+ renderErrors[index] = response.error || "Render failed";
1056
+ renderState[index] = "error";
1057
+ return;
1058
+ }
1059
+ renderedImages[index] = response.data.image ?? "";
1060
+ renderState[index] = "done";
1061
+ if (beatMayHaveMovie(effectiveBeat(index))) loadExistingBeatMovie(index);
1062
+ }
1063
+ function staleSince(requestedFilePath) {
1064
+ return filePath.value !== requestedFilePath;
1065
+ }
1066
+ async function loadExistingBeatImage(index) {
1067
+ const requestedFilePath = filePath.value;
1068
+ const response = await api.call("beatImage", {
1069
+ filePath: requestedFilePath,
1070
+ beatIndex: index
1071
+ });
1072
+ if (staleSince(requestedFilePath)) return;
1073
+ if (response.ok && response.data.image) {
1074
+ renderedImages[index] = response.data.image;
1075
+ renderState[index] = "done";
1076
+ }
1077
+ }
1078
+ async function loadExistingBeatAudio(index) {
1079
+ const requestedFilePath = filePath.value;
1080
+ const response = await api.call("beatAudio", {
1081
+ filePath: requestedFilePath,
1082
+ beatIndex: index
1083
+ });
1084
+ if (staleSince(requestedFilePath)) return;
1085
+ if (response.ok && response.data.audio) {
1086
+ beatAudios[index] = response.data.audio;
1087
+ audioState[index] = "done";
1088
+ }
1089
+ }
1090
+ async function loadExistingBeatMovie(index) {
1091
+ const requestedFilePath = filePath.value;
1092
+ const response = await api.call("beatMovie", {
1093
+ filePath: requestedFilePath,
1094
+ beatIndex: index
1095
+ });
1096
+ if (staleSince(requestedFilePath)) return;
1097
+ if (response.ok && response.data.moviePath) beatMovies[index] = response.data.moviePath;
1098
+ }
1099
+ async function playBeatMovie(index) {
1100
+ const fetchMediaBlob = adapter.fetchMediaBlob;
1101
+ if (!fetchMediaBlob || !beatMovies[index] || beatMovieLoading[index]) return;
1102
+ if (beatMovieUrls[index]) {
1103
+ beatMovieOpen[index] = true;
1104
+ return;
1105
+ }
1106
+ beatMovieLoading[index] = true;
1107
+ try {
1108
+ const blob = new Blob([await fetchMediaBlob({ moviePath: beatMovies[index] })], { type: "video/mp4" });
1109
+ beatMovieUrls[index] = URL.createObjectURL(blob);
1110
+ beatMovieOpen[index] = true;
1111
+ } catch (err) {
1112
+ alert(errorMessage(err));
1113
+ } finally {
1114
+ Reflect.deleteProperty(beatMovieLoading, index);
1115
+ }
1116
+ }
1117
+ function closeBeatMovie(index) {
1118
+ Reflect.deleteProperty(beatMovieOpen, index);
1119
+ }
1120
+ function invalidateBeatMovie(index) {
1121
+ if (beatMovieUrls[index]) URL.revokeObjectURL(beatMovieUrls[index]);
1122
+ [
1123
+ beatMovies,
1124
+ beatMovieUrls,
1125
+ beatMovieOpen
1126
+ ].forEach((map) => Reflect.deleteProperty(map, index));
1127
+ }
1128
+ function resetBeatMovies() {
1129
+ Object.values(beatMovieUrls).forEach((url) => URL.revokeObjectURL(url));
1130
+ [
1131
+ beatMovies,
1132
+ beatMovieUrls,
1133
+ beatMovieOpen,
1134
+ beatMovieLoading
1135
+ ].forEach((map) => {
1136
+ Object.keys(map).forEach((key) => Reflect.deleteProperty(map, key));
1137
+ });
1138
+ }
1139
+ async function generateAudio(index) {
1140
+ const requestedFilePath = filePath.value;
1141
+ audioState[index] = "generating";
1142
+ Reflect.deleteProperty(audioErrors, index);
1143
+ const response = await api.call("generateBeatAudio", {
1144
+ filePath: requestedFilePath,
1145
+ beatIndex: index,
1146
+ chatSessionId: chatSessionId.value
1147
+ });
1148
+ if (staleSince(requestedFilePath)) return;
1149
+ if (!response.ok) {
1150
+ audioErrors[index] = response.error || "Audio generation failed";
1151
+ audioState[index] = "error";
1152
+ return;
1153
+ }
1154
+ beatAudios[index] = response.data.audio ?? "";
1155
+ audioState[index] = "done";
1156
+ }
1157
+ function playAudio(index) {
1158
+ if (playingAudio.value) {
1159
+ playingAudio.value.audio.pause();
1160
+ const wasIndex = playingAudio.value.index;
1161
+ playingAudio.value = null;
1162
+ if (wasIndex === index) return;
1163
+ }
1164
+ const src = beatAudios[index];
1165
+ if (!src) return;
1166
+ const audio = new Audio(src);
1167
+ playingAudio.value = {
1168
+ index,
1169
+ audio
1170
+ };
1171
+ audioProgress.value = 0;
1172
+ audio.addEventListener("timeupdate", () => {
1173
+ if (playingAudio.value?.index !== index) return;
1174
+ if (audio.duration > 0) audioProgress.value = audio.currentTime / audio.duration;
1175
+ });
1176
+ audio.addEventListener("ended", () => {
1177
+ if (playingAudio.value?.index !== index) return;
1178
+ playingAudio.value = null;
1179
+ audioProgress.value = 0;
1180
+ if (lightbox.value?.index === index) advanceFromBeat(index);
1181
+ });
1182
+ audio.play();
1183
+ }
1184
+ function onBeatDragOver(event, index) {
1185
+ if (!event.dataTransfer?.types.includes("Files")) return;
1186
+ event.preventDefault();
1187
+ beatDragOver[index] = true;
1188
+ }
1189
+ function onBeatDragLeave(index) {
1190
+ beatDragOver[index] = false;
1191
+ }
1192
+ async function onBeatDrop(event, index) {
1193
+ event.preventDefault();
1194
+ beatDragOver[index] = false;
1195
+ const file = event.dataTransfer?.files[0];
1196
+ if (!file || !file.type.startsWith("image/")) return;
1197
+ renderState[index] = "rendering";
1198
+ Reflect.deleteProperty(renderErrors, index);
1199
+ let imageData;
1200
+ try {
1201
+ imageData = await new Promise((resolve, reject) => {
1202
+ const reader = new FileReader();
1203
+ reader.onload = () => resolve(reader.result);
1204
+ reader.onerror = reject;
1205
+ reader.readAsDataURL(file);
1206
+ });
1207
+ } catch (err) {
1208
+ renderErrors[index] = errorMessage(err);
1209
+ renderState[index] = "error";
1210
+ return;
1211
+ }
1212
+ const requestedFilePath = filePath.value;
1213
+ const response = await api.call("uploadBeatImage", {
1214
+ filePath: requestedFilePath,
1215
+ beatIndex: index,
1216
+ imageData
1217
+ });
1218
+ if (staleSince(requestedFilePath)) return;
1219
+ if (!response.ok) {
1220
+ renderErrors[index] = response.error || "Upload failed";
1221
+ renderState[index] = "error";
1222
+ return;
1223
+ }
1224
+ renderedImages[index] = response.data.image ?? "";
1225
+ renderState[index] = "done";
1226
+ }
1227
+ function onCharDragOver(event, key) {
1228
+ if (!event.dataTransfer?.types.includes("Files")) return;
1229
+ event.preventDefault();
1230
+ charDragOver[key] = true;
1231
+ }
1232
+ function onCharDragLeave(key) {
1233
+ charDragOver[key] = false;
1234
+ }
1235
+ async function onCharDrop(event, key) {
1236
+ event.preventDefault();
1237
+ charDragOver[key] = false;
1238
+ const file = event.dataTransfer?.files[0];
1239
+ if (!file || !file.type.startsWith("image/")) return;
1240
+ charRenderState[key] = "rendering";
1241
+ Reflect.deleteProperty(charErrors, key);
1242
+ let imageData;
1243
+ try {
1244
+ imageData = await new Promise((resolve, reject) => {
1245
+ const reader = new FileReader();
1246
+ reader.onload = () => resolve(reader.result);
1247
+ reader.onerror = reject;
1248
+ reader.readAsDataURL(file);
1249
+ });
1250
+ } catch (err) {
1251
+ charErrors[key] = errorMessage(err);
1252
+ charRenderState[key] = "error";
1253
+ return;
1254
+ }
1255
+ const requestedFilePath = filePath.value;
1256
+ const response = await api.call("uploadCharacterImage", {
1257
+ filePath: requestedFilePath,
1258
+ key,
1259
+ imageData
1260
+ });
1261
+ if (staleSince(requestedFilePath)) return;
1262
+ if (!response.ok) {
1263
+ charErrors[key] = response.error || "Upload failed";
1264
+ charRenderState[key] = "error";
1265
+ return;
1266
+ }
1267
+ charImages[key] = response.data.image ?? "";
1268
+ charRenderState[key] = "done";
1269
+ }
1270
+ function openCharacterLightbox(key) {
1271
+ stopAllPlayback();
1272
+ lightbox.value = {
1273
+ src: charImages[key],
1274
+ text: key,
1275
+ index: -1,
1276
+ isCharacter: true
1277
+ };
1278
+ }
1279
+ async function loadExistingCharacterImage(key) {
1280
+ const requestedFilePath = filePath.value;
1281
+ const response = await api.call("characterImage", {
1282
+ filePath: requestedFilePath,
1283
+ key
1284
+ });
1285
+ if (staleSince(requestedFilePath)) return;
1286
+ if (response.ok && response.data.image) {
1287
+ charImages[key] = response.data.image;
1288
+ charRenderState[key] = "done";
1289
+ }
1290
+ }
1291
+ function refreshMissingCharacterImages() {
1292
+ getMissingCharacterKeys(characterKeys.value, charImages, charRenderState).forEach((key) => loadExistingCharacterImage(key));
1293
+ }
1294
+ async function renderCharacter(key, force) {
1295
+ const requestedFilePath = filePath.value;
1296
+ charRenderState[key] = "rendering";
1297
+ Reflect.deleteProperty(charErrors, key);
1298
+ const response = await api.call("renderCharacter", {
1299
+ filePath: requestedFilePath,
1300
+ key,
1301
+ force,
1302
+ chatSessionId: chatSessionId.value
1303
+ });
1304
+ if (staleSince(requestedFilePath)) return;
1305
+ if (!response.ok) {
1306
+ charErrors[key] = response.error || "Render failed";
1307
+ charRenderState[key] = "error";
1308
+ return;
1309
+ }
1310
+ charImages[key] = response.data.image ?? "";
1311
+ charRenderState[key] = "done";
1312
+ }
1313
+ async function generateAllCharacters() {
1314
+ await Promise.all(characterKeys.value.filter((key) => charRenderState[key] !== "rendering").map((key) => renderCharacter(key, false)));
1315
+ }
1316
+ async function hydrateBeatImage(beat, index, hasCharacters, autoRenderTypes) {
1317
+ await loadExistingBeatImage(index);
1318
+ if (renderedImages[index]) return;
1319
+ if (shouldAutoRenderBeat(beat, hasCharacters, autoRenderTypes)) await renderBeat(index);
1320
+ }
1321
+ /**
1322
+ * #1074 — keep the in-memory toolResult in sync with the on-disk
1323
+ * script file. `updateBeat` / `updateScript` persist edits to
1324
+ * disk, but the session entry that backs
1325
+ * `props.selectedResult.data.script` is never rewritten, so a
1326
+ * page reload + session-restore would otherwise surface stale
1327
+ * pre-edit content.
1328
+ *
1329
+ * Why the reopen dispatch, not a generic file read: `filePath`
1330
+ * is the wire form `stories/<rel>` which only the mulmoScript save
1331
+ * op knows how to translate back to the real on-disk path under
1332
+ * `artifacts/stories/...`. The reopen op is read-only when `script`
1333
+ * is omitted; it does NOT trigger movie generation.
1334
+ *
1335
+ * The flow silently bails on every failure mode so a missing /
1336
+ * malformed / deleted script file never blocks the rest of
1337
+ * `initializeScript`.
1338
+ *
1339
+ * Stale-response guard: capture `uuid` + `filePath` before the
1340
+ * `await`. If either has changed by the time the response lands
1341
+ * (the user navigated to a different result while the request
1342
+ * was in flight, or `props.selectedResult` was swapped under us
1343
+ * by a parent watcher), drop the response on the floor — the new
1344
+ * `initializeScript` invocation triggered by that change will
1345
+ * issue its own refresh against the correct file.
1346
+ */
1347
+ async function refreshScriptFromDisk() {
1348
+ const requestedFilePath = filePath.value;
1349
+ if (!requestedFilePath) return;
1350
+ const requestedUuid = props.selectedResult.uuid;
1351
+ const response = await api.call("save", { filePath: requestedFilePath });
1352
+ if (props.selectedResult.uuid !== requestedUuid || filePath.value !== requestedFilePath) return;
1353
+ if (!response.ok) return;
1354
+ const diskScript = response.data.script;
1355
+ if (!diskScript) return;
1356
+ if (isSameScript(diskScript, script.value)) return;
1357
+ emit("updateResult", {
1358
+ ...props.selectedResult,
1359
+ data: {
1360
+ ...props.selectedResult.data,
1361
+ script: diskScript
1362
+ }
1363
+ });
1364
+ }
1365
+ async function initializeScript() {
1366
+ stopAllPlayback();
1367
+ lightbox.value = null;
1368
+ if (beatListEl.value) beatListEl.value.scrollTop = 0;
1369
+ Object.keys(renderState).forEach((key) => Reflect.deleteProperty(renderState, key));
1370
+ Object.keys(renderedImages).forEach((key) => Reflect.deleteProperty(renderedImages, key));
1371
+ Object.keys(renderErrors).forEach((key) => Reflect.deleteProperty(renderErrors, key));
1372
+ Object.keys(sourceOpen).forEach((key) => Reflect.deleteProperty(sourceOpen, key));
1373
+ Object.keys(sourceText).forEach((key) => Reflect.deleteProperty(sourceText, key));
1374
+ Object.keys(beatSaveErrors).forEach((key) => Reflect.deleteProperty(beatSaveErrors, key));
1375
+ Object.keys(beatSaving).forEach((key) => Reflect.deleteProperty(beatSaving, key));
1376
+ Object.keys(localOverrides).forEach((key) => Reflect.deleteProperty(localOverrides, key));
1377
+ Object.keys(beatAudios).forEach((key) => Reflect.deleteProperty(beatAudios, key));
1378
+ Object.keys(audioState).forEach((key) => Reflect.deleteProperty(audioState, key));
1379
+ Object.keys(audioErrors).forEach((key) => Reflect.deleteProperty(audioErrors, key));
1380
+ Object.keys(charRenderState).forEach((key) => Reflect.deleteProperty(charRenderState, key));
1381
+ Object.keys(charImages).forEach((key) => Reflect.deleteProperty(charImages, key));
1382
+ Object.keys(charErrors).forEach((key) => Reflect.deleteProperty(charErrors, key));
1383
+ Object.keys(beatDragOver).forEach((key) => Reflect.deleteProperty(beatDragOver, key));
1384
+ resetBeatMovies();
1385
+ moviePath.value = null;
1386
+ pdfPath.value = null;
1387
+ movieGenerating.value = false;
1388
+ pdfGenerating.value = false;
1389
+ movieError.value = null;
1390
+ if (sourceDetails.value) sourceDetails.value.open = false;
1391
+ await refreshScriptFromDisk();
1392
+ const AUTO_RENDER_TYPES = [
1393
+ "textSlide",
1394
+ "markdown",
1395
+ "chart",
1396
+ "mermaid",
1397
+ "html_tailwind",
1398
+ "slide"
1399
+ ];
1400
+ const hasCharacters = characterKeys.value.length > 0;
1401
+ beats.value.forEach((beat, index) => {
1402
+ hydrateBeatImage(beat, index, hasCharacters, AUTO_RENDER_TYPES);
1403
+ if (beat.text) loadExistingBeatAudio(index);
1404
+ if (beatMayHaveMovie(beat)) loadExistingBeatMovie(index);
1405
+ });
1406
+ characterKeys.value.forEach((key) => loadExistingCharacterImage(key));
1407
+ if (filePath.value) {
1408
+ const requestedFilePath = filePath.value;
1409
+ const isStale = () => filePath.value !== requestedFilePath;
1410
+ const response = await api.call("movieStatus", { filePath: requestedFilePath });
1411
+ if (isStale()) return;
1412
+ if (response.ok && response.data.moviePath) moviePath.value = response.data.moviePath;
1413
+ const pdfResponse = await api.call("pdfStatus", { filePath: requestedFilePath });
1414
+ if (isStale()) return;
1415
+ if (pdfResponse.ok && pdfResponse.data.pdfPath) pdfPath.value = pdfResponse.data.pdfPath;
1416
+ const pending = await api.call("pendingGenerations", { filePath: requestedFilePath });
1417
+ if (isStale()) return;
1418
+ if (pending.ok) for (const entry of pending.data.pending) reflectGenerationStart(entry);
1419
+ }
1420
+ }
1421
+ onMounted(initializeScript);
1422
+ watch(() => props.selectedResult, initializeScript);
1423
+ const unsubscribeGenerationEvents = api.onGenerationEvent(() => filePath.value, (event) => {
1424
+ if (!event.done) {
1425
+ reflectGenerationStart(event);
1426
+ return;
1427
+ }
1428
+ reflectGenerationFinish(event).catch((err) => {
1429
+ console.error("[presentMulmoScript] reload on finish failed:", err);
1430
+ });
1431
+ });
1432
+ function reflectGenerationStart(entry) {
1433
+ if (entry.kind === "beatImage") {
1434
+ const idx = Number(entry.key);
1435
+ if (!renderedImages[idx]) renderState[idx] = "rendering";
1436
+ } else if (entry.kind === "beatAudio") {
1437
+ const idx = Number(entry.key);
1438
+ if (!beatAudios[idx]) audioState[idx] = "generating";
1439
+ } else if (entry.kind === "characterImage") {
1440
+ if (!charImages[entry.key]) charRenderState[entry.key] = "rendering";
1441
+ } else if (entry.kind === "movie") movieGenerating.value = true;
1442
+ else if (entry.kind === "pdf") pdfGenerating.value = true;
1443
+ }
1444
+ async function reflectGenerationFinish(entry) {
1445
+ if (entry.kind === "beatImage") {
1446
+ const idx = Number(entry.key);
1447
+ await loadExistingBeatImage(idx);
1448
+ if (beatMayHaveMovie(effectiveBeat(idx))) await loadExistingBeatMovie(idx);
1449
+ if (renderState[idx] === "rendering") Reflect.deleteProperty(renderState, idx);
1450
+ refreshMissingCharacterImages();
1451
+ } else if (entry.kind === "beatAudio") {
1452
+ const idx = Number(entry.key);
1453
+ await loadExistingBeatAudio(idx);
1454
+ if (audioState[idx] === "generating") Reflect.deleteProperty(audioState, idx);
1455
+ } else if (entry.kind === "characterImage") {
1456
+ await loadExistingCharacterImage(entry.key);
1457
+ if (charRenderState[entry.key] === "rendering") Reflect.deleteProperty(charRenderState, entry.key);
1458
+ } else if (entry.kind === "movie") {
1459
+ movieGenerating.value = false;
1460
+ await refreshMoviePath();
1461
+ } else if (entry.kind === "pdf") {
1462
+ pdfGenerating.value = false;
1463
+ await refreshPdfPath();
1464
+ }
1465
+ }
1466
+ async function refreshMoviePath() {
1467
+ const requestedFilePath = filePath.value;
1468
+ if (!requestedFilePath) return;
1469
+ const response = await api.call("movieStatus", { filePath: requestedFilePath });
1470
+ if (filePath.value !== requestedFilePath) return;
1471
+ if (response.ok && response.data.moviePath) moviePath.value = response.data.moviePath;
1472
+ }
1473
+ async function generateMovie() {
1474
+ const requestedFilePath = filePath.value;
1475
+ movieGenerating.value = true;
1476
+ movieError.value = null;
1477
+ const response = await api.call("generateMovie", {
1478
+ filePath: requestedFilePath,
1479
+ chatSessionId: chatSessionId.value
1480
+ });
1481
+ if (filePath.value !== requestedFilePath) return;
1482
+ movieGenerating.value = false;
1483
+ if (!response.ok) {
1484
+ movieError.value = response.error;
1485
+ return;
1486
+ }
1487
+ moviePath.value = response.data.moviePath;
1488
+ }
1489
+ async function downloadMovie() {
1490
+ const fetchMediaBlob = adapter.fetchMediaBlob;
1491
+ if (!fetchMediaBlob || !moviePath.value || movieDownloading.value) return;
1492
+ movieDownloading.value = true;
1493
+ let objectUrl = null;
1494
+ try {
1495
+ const blob = await fetchMediaBlob({ moviePath: moviePath.value });
1496
+ objectUrl = URL.createObjectURL(blob);
1497
+ const filename = moviePath.value.split("/").pop() ?? "movie.mp4";
1498
+ const anchor = document.createElement("a");
1499
+ anchor.href = objectUrl;
1500
+ anchor.download = filename;
1501
+ document.body.appendChild(anchor);
1502
+ anchor.click();
1503
+ anchor.remove();
1504
+ } catch (err) {
1505
+ alert(errorMessage(err));
1506
+ } finally {
1507
+ if (objectUrl) URL.revokeObjectURL(objectUrl);
1508
+ movieDownloading.value = false;
1509
+ }
1510
+ }
1511
+ async function refreshPdfPath() {
1512
+ const requestedFilePath = filePath.value;
1513
+ if (!requestedFilePath) return;
1514
+ const response = await api.call("pdfStatus", { filePath: requestedFilePath });
1515
+ if (filePath.value !== requestedFilePath) return;
1516
+ if (response.ok && response.data.pdfPath) pdfPath.value = response.data.pdfPath;
1517
+ }
1518
+ async function generatePdf() {
1519
+ const requestedFilePath = filePath.value;
1520
+ pdfGenerating.value = true;
1521
+ const response = await api.call("generatePdf", {
1522
+ filePath: requestedFilePath,
1523
+ chatSessionId: chatSessionId.value
1524
+ });
1525
+ if (filePath.value !== requestedFilePath) return;
1526
+ pdfGenerating.value = false;
1527
+ if (!response.ok) {
1528
+ alert(response.error);
1529
+ return;
1530
+ }
1531
+ pdfPath.value = response.data.pdfPath;
1532
+ }
1533
+ async function downloadPdf() {
1534
+ const fetchMediaBlob = adapter.fetchMediaBlob;
1535
+ if (!fetchMediaBlob || !pdfPath.value || pdfDownloading.value) return;
1536
+ pdfDownloading.value = true;
1537
+ let objectUrl = null;
1538
+ try {
1539
+ const blob = await fetchMediaBlob({ pdfPath: pdfPath.value });
1540
+ objectUrl = URL.createObjectURL(blob);
1541
+ const filename = pdfPath.value.split("/").pop() ?? "deck.pdf";
1542
+ const anchor = document.createElement("a");
1543
+ anchor.href = objectUrl;
1544
+ anchor.download = filename;
1545
+ document.body.appendChild(anchor);
1546
+ anchor.click();
1547
+ anchor.remove();
1548
+ } catch (err) {
1549
+ alert(errorMessage(err));
1550
+ } finally {
1551
+ if (objectUrl) URL.revokeObjectURL(objectUrl);
1552
+ pdfDownloading.value = false;
1553
+ }
1554
+ }
1555
+ return (_ctx, _cache) => {
1556
+ return openBlock(), createElementBlock("div", _hoisted_1$1, [
1557
+ createElementVNode("div", _hoisted_2$1, [createElementVNode("div", _hoisted_3$1, [
1558
+ createElementVNode("h2", _hoisted_4, toDisplayString(script.value.title || "Untitled Script"), 1),
1559
+ script.value.description ? (openBlock(), createElementBlock("p", _hoisted_5, toDisplayString(script.value.description), 1)) : createCommentVNode("", true),
1560
+ createElementVNode("div", _hoisted_6, [
1561
+ createElementVNode("span", null, toDisplayString(unref(m).beatCount(beats.value.length)), 1),
1562
+ script.value.lang ? (openBlock(), createElementBlock("span", _hoisted_7, toDisplayString(script.value.lang), 1)) : createCommentVNode("", true),
1563
+ filePath.value ? (openBlock(), createElementBlock("span", _hoisted_8, toDisplayString(filePath.value), 1)) : createCommentVNode("", true)
1564
+ ])
1565
+ ]), createElementVNode("div", _hoisted_9, [
1566
+ moviePath.value && !movieGenerating.value ? (openBlock(), createElementBlock("button", {
1567
+ key: 0,
1568
+ 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",
1569
+ disabled: !isPlayReady.value,
1570
+ title: unref(m).playPresentation,
1571
+ "aria-label": unref(m).playPresentation,
1572
+ onClick: playPresentation
1573
+ }, [..._cache[6] || (_cache[6] = [createElementVNode("span", { class: "material-icons text-base" }, "play_arrow", -1)])], 8, _hoisted_10)) : createCommentVNode("", true),
1574
+ moviePath.value && !movieGenerating.value && canFetchMedia.value ? (openBlock(), createElementBlock("button", {
1575
+ key: 1,
1576
+ 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",
1577
+ disabled: movieDownloading.value,
1578
+ "data-testid": "mulmo-script-download-movie-button",
1579
+ onClick: downloadMovie
1580
+ }, [_cache[7] || (_cache[7] = createElementVNode("span", { class: "material-icons text-base" }, "download", -1)), createElementVNode("span", null, toDisplayString(unref(m).movie), 1)], 8, _hoisted_11)) : createCommentVNode("", true),
1581
+ moviePath.value && !movieGenerating.value ? (openBlock(), createElementBlock("button", {
1582
+ key: 2,
1583
+ class: "h-8 w-8 flex items-center justify-center rounded border border-gray-200 text-gray-600 hover:bg-gray-100 transition-colors",
1584
+ title: unref(m).regenerateMovie,
1585
+ "aria-label": unref(m).regenerateMovie,
1586
+ "data-testid": "mulmo-script-regenerate-movie-button",
1587
+ onClick: generateMovie
1588
+ }, [..._cache[8] || (_cache[8] = [createElementVNode("span", { class: "material-icons text-base" }, "refresh", -1)])], 8, _hoisted_12)) : (openBlock(), createElementBlock("button", {
1589
+ key: 3,
1590
+ 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",
1591
+ disabled: movieGenerating.value,
1592
+ "data-testid": "mulmo-script-generate-movie-button",
1593
+ onClick: generateMovie
1594
+ }, [movieGenerating.value ? (openBlock(), createElementBlock("svg", _hoisted_14, [..._cache[9] || (_cache[9] = [createElementVNode("circle", {
1595
+ class: "opacity-25",
1596
+ cx: "12",
1597
+ cy: "12",
1598
+ r: "10",
1599
+ stroke: "currentColor",
1600
+ "stroke-width": "4"
1601
+ }, null, -1), createElementVNode("path", {
1602
+ class: "opacity-75",
1603
+ fill: "currentColor",
1604
+ d: "M4 12a8 8 0 018-8v8H4z"
1605
+ }, null, -1)])])) : createCommentVNode("", true), movieGenerating.value ? (openBlock(), createElementBlock("span", _hoisted_15, toDisplayString(unref(m).generating), 1)) : (openBlock(), createElementBlock(Fragment, { key: 2 }, [_cache[10] || (_cache[10] = createElementVNode("span", { class: "material-icons text-sm" }, "refresh", -1)), createElementVNode("span", null, toDisplayString(unref(m).movie), 1)], 64))], 8, _hoisted_13)),
1606
+ pdfPath.value && !pdfGenerating.value && canFetchMedia.value ? (openBlock(), createElementBlock("button", {
1607
+ key: 4,
1608
+ 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",
1609
+ disabled: pdfDownloading.value,
1610
+ "data-testid": "mulmo-script-download-pdf-button",
1611
+ onClick: downloadPdf
1612
+ }, [_cache[11] || (_cache[11] = createElementVNode("span", { class: "material-icons text-base" }, "download", -1)), createElementVNode("span", null, toDisplayString(unref(m).pdf), 1)], 8, _hoisted_16)) : createCommentVNode("", true),
1613
+ pdfPath.value && !pdfGenerating.value ? (openBlock(), createElementBlock("button", {
1614
+ key: 5,
1615
+ class: "h-8 w-8 flex items-center justify-center rounded border border-gray-200 text-gray-600 hover:bg-gray-100 transition-colors",
1616
+ title: unref(m).regeneratePdf,
1617
+ "aria-label": unref(m).regeneratePdf,
1618
+ "data-testid": "mulmo-script-regenerate-pdf-button",
1619
+ onClick: generatePdf
1620
+ }, [..._cache[12] || (_cache[12] = [createElementVNode("span", { class: "material-icons text-base" }, "refresh", -1)])], 8, _hoisted_17)) : (openBlock(), createElementBlock("button", {
1621
+ key: 6,
1622
+ 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",
1623
+ disabled: pdfGenerating.value,
1624
+ "data-testid": "mulmo-script-generate-pdf-button",
1625
+ onClick: generatePdf
1626
+ }, [pdfGenerating.value ? (openBlock(), createElementBlock("svg", _hoisted_19, [..._cache[13] || (_cache[13] = [createElementVNode("circle", {
1627
+ class: "opacity-25",
1628
+ cx: "12",
1629
+ cy: "12",
1630
+ r: "10",
1631
+ stroke: "currentColor",
1632
+ "stroke-width": "4"
1633
+ }, null, -1), createElementVNode("path", {
1634
+ class: "opacity-75",
1635
+ fill: "currentColor",
1636
+ d: "M4 12a8 8 0 018-8v8H4z"
1637
+ }, null, -1)])])) : createCommentVNode("", true), pdfGenerating.value ? (openBlock(), createElementBlock("span", _hoisted_20, toDisplayString(unref(m).generatingPdf), 1)) : (openBlock(), createElementBlock(Fragment, { key: 2 }, [_cache[14] || (_cache[14] = createElementVNode("span", { class: "material-icons text-sm" }, "picture_as_pdf", -1)), createElementVNode("span", null, toDisplayString(unref(m).pdf), 1)], 64))], 8, _hoisted_18))
1638
+ ])]),
1639
+ movieError.value ? (openBlock(), createElementBlock("div", _hoisted_21, [
1640
+ _cache[15] || (_cache[15] = createElementVNode("span", { class: "material-icons text-base shrink-0 mt-px" }, "error_outline", -1)),
1641
+ createElementVNode("div", _hoisted_22, [createElementVNode("div", _hoisted_23, toDisplayString(unref(m).movieGenerationFailed), 1), createElementVNode("div", _hoisted_24, toDisplayString(movieError.value), 1)]),
1642
+ createElementVNode("button", {
1643
+ class: "shrink-0 h-7 px-2 text-xs rounded border border-red-300 text-red-700 hover:bg-red-100 disabled:opacity-50",
1644
+ disabled: movieGenerating.value,
1645
+ "data-testid": "mulmo-script-movie-retry-button",
1646
+ onClick: generateMovie
1647
+ }, toDisplayString(unref(m).retry), 9, _hoisted_25)
1648
+ ])) : createCommentVNode("", true),
1649
+ characterKeys.value.length > 0 ? (openBlock(), createElementBlock("div", _hoisted_26, [createElementVNode("div", _hoisted_27, [createElementVNode("span", _hoisted_28, toDisplayString(unref(m).characters), 1), createElementVNode("button", {
1650
+ class: "px-2 py-0.5 text-xs rounded border border-gray-300 text-gray-500 hover:bg-gray-50 disabled:opacity-50",
1651
+ disabled: movieGenerating.value || anyBeatRendering.value || characterKeys.value.every((key) => charRenderState[key] === "rendering"),
1652
+ onClick: generateAllCharacters
1653
+ }, toDisplayString(unref(m).generateAll), 9, _hoisted_29)]), createElementVNode("div", _hoisted_30, [(openBlock(true), createElementBlock(Fragment, null, renderList(characterKeys.value, (key) => {
1654
+ return openBlock(), createElementBlock("div", {
1655
+ key,
1656
+ class: "flex flex-col items-center gap-1 w-36"
1657
+ }, [createElementVNode("div", {
1658
+ class: normalizeClass(["relative w-36 h-36 rounded-lg border overflow-hidden bg-gray-50 flex items-center justify-center transition-colors", charDragOver[key] ? "border-blue-400 bg-blue-50" : "border-gray-200"]),
1659
+ onDragover: ($event) => onCharDragOver($event, key),
1660
+ onDragleave: ($event) => onCharDragLeave(key),
1661
+ onDrop: ($event) => onCharDrop($event, key)
1662
+ }, [
1663
+ charImages[key] ? (openBlock(), createElementBlock("img", {
1664
+ key: 0,
1665
+ src: charImages[key],
1666
+ class: "w-full h-full object-cover cursor-zoom-in",
1667
+ alt: key,
1668
+ onClick: ($event) => openCharacterLightbox(key)
1669
+ }, null, 8, _hoisted_32)) : charRenderState[key] === "rendering" ? (openBlock(), createElementBlock("svg", _hoisted_33, [..._cache[16] || (_cache[16] = [createElementVNode("circle", {
1670
+ class: "opacity-25",
1671
+ cx: "12",
1672
+ cy: "12",
1673
+ r: "10",
1674
+ stroke: "currentColor",
1675
+ "stroke-width": "4"
1676
+ }, null, -1), createElementVNode("path", {
1677
+ class: "opacity-75",
1678
+ fill: "currentColor",
1679
+ d: "M4 12a8 8 0 018-8v8H4z"
1680
+ }, null, -1)])])) : charRenderState[key] === "error" ? (openBlock(), createElementBlock("span", _hoisted_34, toDisplayString(charErrors[key]), 1)) : (openBlock(), createElementBlock("span", _hoisted_35, toDisplayString(characterPrompt(key)), 1)),
1681
+ !charDragOver[key] ? (openBlock(), createElementBlock("div", _hoisted_36, toDisplayString(unref(m).orDropImage), 1)) : createCommentVNode("", true),
1682
+ charDragOver[key] ? (openBlock(), createElementBlock("div", _hoisted_37, [createElementVNode("span", _hoisted_38, toDisplayString(unref(m).drop), 1)])) : createCommentVNode("", true),
1683
+ charImages[key] && charRenderState[key] !== "rendering" ? (openBlock(), createElementBlock("button", {
1684
+ key: 6,
1685
+ class: normalizeClass(["absolute top-0.5 right-0.5 px-1 py-0.5 text-xs rounded border bg-white", movieGenerating.value || anyBeatRendering.value ? "border-yellow-400 text-yellow-500 cursor-not-allowed" : "border-gray-400 text-gray-600 hover:bg-gray-50"]),
1686
+ disabled: movieGenerating.value || anyBeatRendering.value,
1687
+ onClick: withModifiers(($event) => renderCharacter(key, true), ["stop"])
1688
+ }, [movieGenerating.value || anyBeatRendering.value ? (openBlock(), createElementBlock("span", _hoisted_40, "↺")) : (openBlock(), createElementBlock("span", _hoisted_41, "↺"))], 10, _hoisted_39)) : !charImages[key] && charRenderState[key] !== "rendering" ? (openBlock(), createElementBlock("button", {
1689
+ key: 7,
1690
+ class: normalizeClass(["absolute top-0.5 right-0.5 px-1 py-0.5 text-xs rounded border bg-white", movieGenerating.value || anyBeatRendering.value ? "border-yellow-400 text-yellow-500 cursor-not-allowed" : "border-blue-400 text-blue-600 hover:bg-blue-50"]),
1691
+ disabled: movieGenerating.value || anyBeatRendering.value,
1692
+ onClick: withModifiers(($event) => renderCharacter(key, false), ["stop"])
1693
+ }, [movieGenerating.value || anyBeatRendering.value ? (openBlock(), createElementBlock("svg", _hoisted_43, [..._cache[17] || (_cache[17] = [createElementVNode("circle", {
1694
+ class: "opacity-25",
1695
+ cx: "12",
1696
+ cy: "12",
1697
+ r: "10",
1698
+ stroke: "currentColor",
1699
+ "stroke-width": "4"
1700
+ }, null, -1), createElementVNode("path", {
1701
+ class: "opacity-75",
1702
+ fill: "currentColor",
1703
+ d: "M4 12a8 8 0 018-8v8H4z"
1704
+ }, null, -1)])])) : (openBlock(), createElementBlock("span", _hoisted_44, toDisplayString(unref(m).gen), 1))], 10, _hoisted_42)) : createCommentVNode("", true)
1705
+ ], 42, _hoisted_31), createElementVNode("span", _hoisted_45, toDisplayString(key), 1)]);
1706
+ }), 128))])])) : createCommentVNode("", true),
1707
+ isDeck.value ? (openBlock(), createElementBlock("div", _hoisted_46, [createVNode(unref(MulmoScriptDeckEditor), {
1708
+ script: deckScriptInput.value,
1709
+ layout: "compact",
1710
+ "onUpdate:script": onDeckUpdate
1711
+ }, null, 8, ["script"])])) : (openBlock(), createElementBlock("div", {
1712
+ key: 3,
1713
+ ref_key: "beatListEl",
1714
+ ref: beatListEl,
1715
+ class: "flex-1 overflow-y-auto p-2 space-y-1.5"
1716
+ }, [(openBlock(true), createElementBlock(Fragment, null, renderList(beats.value, (beat, index) => {
1717
+ return openBlock(), createElementBlock("div", {
1718
+ key: index,
1719
+ class: "rounded-lg border border-gray-200 overflow-hidden"
1720
+ }, [createElementVNode("div", _hoisted_47, [createElementVNode("div", {
1721
+ class: normalizeClass(["relative shrink-0 w-[45%] overflow-hidden bg-gray-50 transition-colors", beatDragOver[index] ? "bg-blue-50" : ""]),
1722
+ onDragover: ($event) => onBeatDragOver($event, index),
1723
+ onDragleave: ($event) => onBeatDragLeave(index),
1724
+ onDrop: ($event) => onBeatDrop($event, index)
1725
+ }, [
1726
+ beatMovieOpen[index] && beatMovieUrls[index] ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [createElementVNode("video", {
1727
+ src: beatMovieUrls[index],
1728
+ class: "w-full object-contain",
1729
+ controls: "",
1730
+ autoplay: "",
1731
+ "data-testid": `mulmo-script-beat-movie-player-${index}`
1732
+ }, null, 8, _hoisted_49), createElementVNode("button", {
1733
+ 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",
1734
+ title: unref(m).close,
1735
+ "aria-label": unref(m).close,
1736
+ "data-testid": `mulmo-script-beat-movie-close-${index}`,
1737
+ onClick: withModifiers(($event) => closeBeatMovie(index), ["stop"])
1738
+ }, [..._cache[18] || (_cache[18] = [createElementVNode("span", { class: "material-icons text-sm" }, "close", -1)])], 8, _hoisted_50)], 64)) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [
1739
+ renderedImages[index] ? (openBlock(), createElementBlock("img", {
1740
+ key: 0,
1741
+ src: renderedImages[index],
1742
+ class: "w-full object-contain cursor-zoom-in",
1743
+ alt: `Beat ${index + 1}`,
1744
+ onClick: ($event) => openLightbox(index)
1745
+ }, null, 8, _hoisted_51)) : createCommentVNode("", true),
1746
+ renderedImages[index] && beatMovies[index] && canFetchMedia.value ? (openBlock(), createElementBlock("button", {
1747
+ key: 1,
1748
+ 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",
1749
+ title: unref(m).play,
1750
+ "aria-label": unref(m).play,
1751
+ "data-testid": `mulmo-script-beat-movie-play-${index}`,
1752
+ onClick: withModifiers(($event) => playBeatMovie(index), ["stop"])
1753
+ }, [beatMovieLoading[index] ? (openBlock(), createElementBlock("svg", _hoisted_53, [..._cache[19] || (_cache[19] = [createElementVNode("circle", {
1754
+ class: "opacity-25",
1755
+ cx: "12",
1756
+ cy: "12",
1757
+ r: "10",
1758
+ stroke: "currentColor",
1759
+ "stroke-width": "4"
1760
+ }, null, -1), createElementVNode("path", {
1761
+ class: "opacity-75",
1762
+ fill: "currentColor",
1763
+ d: "M4 12a8 8 0 018-8v8H4z"
1764
+ }, null, -1)])])) : (openBlock(), createElementBlock("span", _hoisted_54, "play_arrow"))], 8, _hoisted_52)) : createCommentVNode("", true),
1765
+ renderedImages[index] && renderState[index] !== "rendering" ? (openBlock(), createElementBlock("button", {
1766
+ key: 2,
1767
+ 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",
1768
+ disabled: movieGenerating.value,
1769
+ onClick: withModifiers(($event) => regenerateBeat(index), ["stop"])
1770
+ }, " ↺ ", 8, _hoisted_55)) : !renderedImages[index] ? (openBlock(), createElementBlock("div", _hoisted_56, [renderState[index] === "rendering" || movieGenerating.value && !renderedImages[index] && effectiveBeat(index).imagePrompt ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [_cache[20] || (_cache[20] = createElementVNode("svg", {
1771
+ class: "animate-spin w-4 h-4 text-green-400",
1772
+ viewBox: "0 0 24 24",
1773
+ fill: "none"
1774
+ }, [createElementVNode("circle", {
1775
+ class: "opacity-25",
1776
+ cx: "12",
1777
+ cy: "12",
1778
+ r: "10",
1779
+ stroke: "currentColor",
1780
+ "stroke-width": "4"
1781
+ }), createElementVNode("path", {
1782
+ class: "opacity-75",
1783
+ fill: "currentColor",
1784
+ d: "M4 12a8 8 0 018-8v8H4z"
1785
+ })], -1)), createElementVNode("span", _hoisted_57, toDisplayString(unref(m).rendering), 1)], 64)) : renderState[index] === "error" ? (openBlock(), createElementBlock("span", _hoisted_58, toDisplayString(renderErrors[index]), 1)) : (openBlock(), createElementBlock(Fragment, { key: 2 }, [effectiveBeat(index).imagePrompt ? (openBlock(), createElementBlock("span", _hoisted_59, toDisplayString(effectiveBeat(index).imagePrompt), 1)) : (openBlock(), createElementBlock("span", _hoisted_60, toDisplayString(beat.image?.type ?? "—"), 1))], 64))])) : createCommentVNode("", true)
1786
+ ], 64)),
1787
+ beatDragOver[index] ? (openBlock(), createElementBlock("div", _hoisted_61, [createElementVNode("span", _hoisted_62, toDisplayString(unref(m).drop), 1)])) : !renderedImages[index] && renderState[index] !== "rendering" ? (openBlock(), createElementBlock("div", _hoisted_63, toDisplayString(unref(m).orDropImage), 1)) : createCommentVNode("", true),
1788
+ !renderedImages[index] && renderState[index] !== "rendering" && !movieGenerating.value ? (openBlock(), createElementBlock("button", {
1789
+ key: 4,
1790
+ 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",
1791
+ onClick: ($event) => renderBeat(index)
1792
+ }, toDisplayString(unref(m).generate), 9, _hoisted_64)) : createCommentVNode("", true)
1793
+ ], 42, _hoisted_48), createElementVNode("div", _hoisted_65, [createElementVNode("span", _hoisted_66, toDisplayString(effectiveBeat(index).text), 1), createElementVNode("div", _hoisted_67, [createElementVNode("div", _hoisted_68, [audioState[index] === "generating" || movieGenerating.value && !beatAudios[index] && effectiveBeat(index).text ? (openBlock(), createElementBlock("svg", _hoisted_69, [..._cache[21] || (_cache[21] = [createElementVNode("circle", {
1794
+ class: "opacity-25",
1795
+ cx: "12",
1796
+ cy: "12",
1797
+ r: "10",
1798
+ stroke: "currentColor",
1799
+ "stroke-width": "4"
1800
+ }, null, -1), createElementVNode("path", {
1801
+ class: "opacity-75",
1802
+ fill: "currentColor",
1803
+ d: "M4 12a8 8 0 018-8v8H4z"
1804
+ }, null, -1)])])) : beatAudios[index] ? (openBlock(), createElementBlock("button", {
1805
+ key: 1,
1806
+ class: normalizeClass(["text-xs px-2 py-0.5 rounded border", playingAudio.value?.index === index ? "border-red-400 text-red-600 hover:bg-red-50" : "border-green-400 text-green-600 hover:bg-green-50"]),
1807
+ onClick: ($event) => playAudio(index)
1808
+ }, toDisplayString(playingAudio.value?.index === index ? unref(m).stop : unref(m).play), 11, _hoisted_70)) : audioErrors[index] ? (openBlock(), createElementBlock(Fragment, { key: 2 }, [createElementVNode("span", {
1809
+ class: "text-xs text-red-400 truncate min-w-0 max-w-[20rem]",
1810
+ title: audioErrors[index]
1811
+ }, toDisplayString(unref(m).errPrefix) + " " + toDisplayString(audioErrors[index]), 9, _hoisted_71), effectiveBeat(index).text ? (openBlock(), createElementBlock("button", {
1812
+ key: 0,
1813
+ class: "text-xs px-2 py-0.5 rounded border border-gray-300 text-gray-500 hover:bg-gray-50 disabled:opacity-50",
1814
+ disabled: movieGenerating.value,
1815
+ onClick: ($event) => generateAudio(index)
1816
+ }, " ↺ ", 8, _hoisted_72)) : createCommentVNode("", true)], 64)) : effectiveBeat(index).text ? (openBlock(), createElementBlock("button", {
1817
+ key: 3,
1818
+ class: "text-xs px-2 py-0.5 rounded border border-gray-300 text-gray-500 hover:bg-gray-50",
1819
+ onClick: ($event) => generateAudio(index)
1820
+ }, toDisplayString(unref(m).generateAudio), 9, _hoisted_73)) : createCommentVNode("", true)]), createElementVNode("button", {
1821
+ class: "text-gray-400 hover:text-gray-600",
1822
+ title: sourceOpen[index] ? "Hide source" : "Show source",
1823
+ "data-testid": `mulmo-script-beat-source-toggle-${index}`,
1824
+ onClick: ($event) => toggleSource(index)
1825
+ }, [..._cache[22] || (_cache[22] = [createElementVNode("svg", {
1826
+ xmlns: "http://www.w3.org/2000/svg",
1827
+ class: "w-3.5 h-3.5",
1828
+ viewBox: "0 0 24 24",
1829
+ fill: "none",
1830
+ stroke: "currentColor",
1831
+ "stroke-width": "2",
1832
+ "stroke-linecap": "round",
1833
+ "stroke-linejoin": "round"
1834
+ }, [createElementVNode("polyline", { points: "16 18 22 12 16 6" }), createElementVNode("polyline", { points: "8 6 2 12 8 18" })], -1)])], 8, _hoisted_74)])])]), sourceOpen[index] ? (openBlock(), createElementBlock("div", _hoisted_75, [withDirectives(createElementVNode("textarea", {
1835
+ "onUpdate:modelValue": ($event) => sourceText[index] = $event,
1836
+ class: normalizeClass(["w-full text-xs text-gray-600 bg-gray-50 p-2 font-mono resize-none", isValidBeat(index) ? "outline-none" : "outline outline-2 outline-red-400"]),
1837
+ rows: "8",
1838
+ spellcheck: "false",
1839
+ "data-testid": `mulmo-script-beat-source-textarea-${index}`
1840
+ }, null, 10, _hoisted_76), [[vModelText, sourceText[index]]]), createElementVNode("div", _hoisted_77, [beatSaveErrors[index] ? (openBlock(), createElementBlock("span", _hoisted_78, toDisplayString(beatSaveErrors[index].kind === "invalidJson" ? unref(m).saveErrorInvalidJson(beatSaveErrors[index].error) : unref(m).saveErrorSaveFailed(beatSaveErrors[index].error)), 1)) : createCommentVNode("", true), createElementVNode("button", {
1841
+ class: normalizeClass(["px-2 py-1 text-xs rounded border", isValidBeat(index) && !beatSaving[index] ? "border-blue-400 text-blue-600 hover:bg-blue-50 cursor-pointer" : "border-gray-200 text-gray-300 cursor-not-allowed"]),
1842
+ disabled: !isValidBeat(index) || !!beatSaving[index],
1843
+ "data-testid": `mulmo-script-beat-update-button-${index}`,
1844
+ onClick: ($event) => updateBeat(index)
1845
+ }, toDisplayString(beatSaving[index] ? unref(m).saving : unref(m).update), 11, _hoisted_79)])])) : createCommentVNode("", true)]);
1846
+ }), 128)), beats.value.length === 0 ? (openBlock(), createElementBlock("div", _hoisted_80, toDisplayString(unref(m).noBeats), 1)) : createCommentVNode("", true)], 512)),
1847
+ createElementVNode("div", _hoisted_81, [createElementVNode("details", {
1848
+ ref_key: "sourceDetails",
1849
+ ref: sourceDetails,
1850
+ class: "script-source",
1851
+ onToggle: _cache[1] || (_cache[1] = ($event) => onSourceToggle($event.target.open))
1852
+ }, [
1853
+ createElementVNode("summary", null, toDisplayString(unref(m).editSource), 1),
1854
+ withDirectives(createElementVNode("textarea", {
1855
+ "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => editableSource.value = $event),
1856
+ class: normalizeClass(["script-editor", { "script-editor-invalid": sourceChanged.value && !sourceValid.value }]),
1857
+ spellcheck: "false"
1858
+ }, null, 2), [[vModelText, editableSource.value]]),
1859
+ createElementVNode("div", _hoisted_82, [createElementVNode("button", {
1860
+ class: "apply-btn",
1861
+ disabled: !sourceChanged.value || !sourceValid.value,
1862
+ onClick: applySource
1863
+ }, toDisplayString(unref(m).applyChanges), 9, _hoisted_83), createElementVNode("button", {
1864
+ class: "cancel-btn",
1865
+ onClick: cancelSourceEdit
1866
+ }, toDisplayString(unref(m).cancel), 1)])
1867
+ ], 544), withDirectives(createElementVNode("button", {
1868
+ class: "copy-btn",
1869
+ title: unref(copied) ? "Copied!" : "Copy",
1870
+ onClick: copyText
1871
+ }, [createElementVNode("span", _hoisted_85, toDisplayString(unref(copied) ? "check" : "content_copy"), 1)], 8, _hoisted_84), [[vShow, !editing.value]])]),
1872
+ lightbox.value ? (openBlock(), createElementBlock("div", {
1873
+ key: 4,
1874
+ class: "fixed inset-0 z-50 bg-black/80 overflow-y-auto",
1875
+ onClick: closeLightbox
1876
+ }, [createElementVNode("button", {
1877
+ class: "fixed top-2 right-4 z-10 text-white/60 hover:text-white text-3xl leading-none",
1878
+ title: unref(m).close,
1879
+ onClick: withModifiers(closeLightbox, ["stop"])
1880
+ }, "✕", 8, _hoisted_86), createElementVNode("div", {
1881
+ class: "flex flex-col items-center gap-4 pt-4 pb-8",
1882
+ onClick: _cache[5] || (_cache[5] = withModifiers(() => {}, ["stop"]))
1883
+ }, [createElementVNode("div", _hoisted_87, [
1884
+ !lightbox.value.isCharacter ? (openBlock(), createElementBlock("button", {
1885
+ key: 0,
1886
+ class: "text-white/60 hover:text-white disabled:opacity-20 text-5xl leading-none",
1887
+ disabled: !hasPrev.value,
1888
+ onClick: _cache[2] || (_cache[2] = ($event) => lightboxMove(-1))
1889
+ }, " ‹ ", 8, _hoisted_88)) : createCommentVNode("", true),
1890
+ createElementVNode("div", _hoisted_89, [createElementVNode("img", {
1891
+ src: lightbox.value.src,
1892
+ class: "max-w-[80vw] max-h-[85vh] object-contain rounded shadow-2xl"
1893
+ }, null, 8, _hoisted_90), !lightbox.value.isCharacter && beats.value.length > 1 ? (openBlock(), createElementBlock("div", _hoisted_91, [createElementVNode("div", _hoisted_92, [(openBlock(true), createElementBlock(Fragment, null, renderList(beats.value.length, (i) => {
1894
+ return openBlock(), createElementBlock("div", {
1895
+ key: i - 1,
1896
+ class: normalizeClass(["group flex-1 cursor-pointer relative transition-colors", i - 1 === lightbox.value.index ? "bg-white/80 hover:bg-white" : i - 1 < lightbox.value.index ? "bg-white/40 hover:bg-white/60" : "bg-white/20 hover:bg-white/40"]),
1897
+ onClick: ($event) => jumpToBeat(i - 1)
1898
+ }, [_cache[23] || (_cache[23] = createElementVNode("span", { class: "absolute -inset-y-3 inset-x-0" }, null, -1)), beatTooltip(i - 1) ? (openBlock(), createElementBlock("div", _hoisted_94, toDisplayString(beatTooltip(i - 1)), 1)) : createCommentVNode("", true)], 10, _hoisted_93);
1899
+ }), 128))]), playingAudio.value && playingAudio.value.index === lightbox.value.index ? (openBlock(), createElementBlock("div", {
1900
+ key: 0,
1901
+ 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",
1902
+ style: normalizeStyle({ left: `${(lightbox.value.index + audioProgress.value) / beats.value.length * 100}%` })
1903
+ }, null, 4)) : createCommentVNode("", true)])) : createCommentVNode("", true)]),
1904
+ !lightbox.value.isCharacter ? (openBlock(), createElementBlock("button", {
1905
+ key: 1,
1906
+ class: "text-white/60 hover:text-white disabled:opacity-20 text-5xl leading-none",
1907
+ disabled: !hasNext.value,
1908
+ onClick: _cache[3] || (_cache[3] = ($event) => lightboxMove(1))
1909
+ }, " › ", 8, _hoisted_95)) : createCommentVNode("", true)
1910
+ ]), lightbox.value.text || beatAudios[lightbox.value.index] ? (openBlock(), createElementBlock("div", _hoisted_96, [lightbox.value.text ? (openBlock(), createElementBlock("p", _hoisted_97, toDisplayString(lightbox.value.text), 1)) : createCommentVNode("", true), beatAudios[lightbox.value.index] ? (openBlock(), createElementBlock("button", {
1911
+ key: 1,
1912
+ class: "absolute top-0 right-4 text-sm px-3 py-1 rounded border border-white/60 text-white/60 hover:bg-white/20",
1913
+ onClick: _cache[4] || (_cache[4] = ($event) => playAudio(lightbox.value.index))
1914
+ }, toDisplayString(playingAudio.value?.index === lightbox.value.index ? unref(m).stop : unref(m).play), 1)) : createCommentVNode("", true)])) : createCommentVNode("", true)])])) : createCommentVNode("", true)
1915
+ ]);
1916
+ };
1917
+ }
1918
+ });
1919
+ //#endregion
1920
+ //#region \0plugin-vue:export-helper
1921
+ var _plugin_vue_export_helper_default = (sfc, props) => {
1922
+ const target = sfc.__vccOpts || sfc;
1923
+ for (const [key, val] of props) target[key] = val;
1924
+ return target;
1925
+ };
1926
+ //#endregion
1927
+ //#region src/vue/View.vue
1928
+ var View_default = /*#__PURE__*/ _plugin_vue_export_helper_default(View_vue_vue_type_script_setup_true_lang_default, [["__scopeId", "data-v-485600b7"]]);
1929
+ //#endregion
1930
+ //#region src/vue/Preview.vue?vue&type=script&setup=true&lang.ts
1931
+ var _hoisted_1 = {
1932
+ class: "p-2 text-sm",
1933
+ "data-testid": "mulmo-script-preview"
1934
+ };
1935
+ var _hoisted_2 = {
1936
+ class: "font-medium text-gray-700 truncate mb-1",
1937
+ "data-testid": "mulmo-script-preview-title"
1938
+ };
1939
+ var _hoisted_3 = {
1940
+ key: 0,
1941
+ class: "text-xs text-gray-500 leading-relaxed",
1942
+ "data-testid": "mulmo-script-preview-description"
1943
+ };
1944
+ //#endregion
1945
+ //#region src/vue/Preview.vue
1946
+ var Preview_default = /* @__PURE__ */ defineComponent({
1947
+ __name: "Preview",
1948
+ props: { result: {} },
1949
+ setup(__props) {
1950
+ const props = __props;
1951
+ const data = computed(() => props.result.data);
1952
+ const script = computed(() => data.value?.script);
1953
+ const title = computed(() => script.value?.title || data.value?.filePath?.split("/").pop() || "MulmoScript");
1954
+ const description = computed(() => script.value?.description);
1955
+ return (_ctx, _cache) => {
1956
+ return openBlock(), createElementBlock("div", _hoisted_1, [createElementVNode("div", _hoisted_2, toDisplayString(title.value), 1), description.value ? (openBlock(), createElementBlock("div", _hoisted_3, toDisplayString(description.value), 1)) : createCommentVNode("", true)]);
1957
+ };
1958
+ }
1959
+ });
1960
+ //#endregion
1961
+ //#region src/vue/index.ts
1962
+ var plugin = {
1963
+ ...pluginCore,
1964
+ viewComponent: View_default,
1965
+ previewComponent: Preview_default
1966
+ };
1967
+ var vue_default = { plugin };
1968
+ //#endregion
1969
+ export { GENERATION_EVENT, MULMOSCRIPT_HOST_ADAPTER_KEY, Preview_default as Preview, TOOL_DEFINITION, TOOL_NAME, View_default as View, vue_default as default, plugin, useHostAdapter, useMulmoScriptTransport };
1970
+
1971
+ //# sourceMappingURL=vue.js.map