@mulmoclaude/mulmoscript-plugin 0.2.3 → 1.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 (53) hide show
  1. package/dist/core/paths.d.ts +2 -5
  2. package/dist/core/paths.d.ts.map +1 -1
  3. package/dist/core/plugin.d.ts.map +1 -1
  4. package/dist/core/validate.d.ts.map +1 -1
  5. package/dist/index.cjs +1 -1
  6. package/dist/index.js +1 -1
  7. package/dist/lang/index.d.ts +1 -2
  8. package/dist/lang/index.d.ts.map +1 -1
  9. package/dist/{plugin-Bi986Wga.cjs → plugin-BLp8tSYt.cjs} +86 -46
  10. package/dist/plugin-BLp8tSYt.cjs.map +1 -0
  11. package/dist/{plugin-BQyzxkui.js → plugin-CXhOP_xU.js} +81 -47
  12. package/dist/plugin-CXhOP_xU.js.map +1 -0
  13. package/dist/server/mulmoErrorCapture.d.ts.map +1 -1
  14. package/dist/server/ops.d.ts.map +1 -1
  15. package/dist/server/support.d.ts +0 -5
  16. package/dist/server/support.d.ts.map +1 -1
  17. package/dist/server/types.d.ts +3 -7
  18. package/dist/server/types.d.ts.map +1 -1
  19. package/dist/server.cjs +34 -58
  20. package/dist/server.cjs.map +1 -1
  21. package/dist/server.js +33 -57
  22. package/dist/server.js.map +1 -1
  23. package/dist/style.css +36 -18
  24. package/dist/vue/View.vue.d.ts.map +1 -1
  25. package/dist/vue/components/BeatLightbox.vue.d.ts +25 -0
  26. package/dist/vue/components/BeatLightbox.vue.d.ts.map +1 -0
  27. package/dist/vue/components/CharacterStrip.vue.d.ts +29 -0
  28. package/dist/vue/components/CharacterStrip.vue.d.ts.map +1 -0
  29. package/dist/vue/components/MulmoScriptToolbar.vue.d.ts +26 -0
  30. package/dist/vue/components/MulmoScriptToolbar.vue.d.ts.map +1 -0
  31. package/dist/vue/composables/useBeatMovie.d.ts +20 -0
  32. package/dist/vue/composables/useBeatMovie.d.ts.map +1 -0
  33. package/dist/vue/composables/useCharacterImages.d.ts +31 -0
  34. package/dist/vue/composables/useCharacterImages.d.ts.map +1 -0
  35. package/dist/vue/composables/useDeckEditor.d.ts +18 -0
  36. package/dist/vue/composables/useDeckEditor.d.ts.map +1 -0
  37. package/dist/vue/composables/useMediaExport.d.ts +26 -0
  38. package/dist/vue/composables/useMediaExport.d.ts.map +1 -0
  39. package/dist/vue/helpers.d.ts +78 -0
  40. package/dist/vue/helpers.d.ts.map +1 -1
  41. package/dist/vue/index.d.ts +1 -1
  42. package/dist/vue/support.d.ts +6 -4
  43. package/dist/vue/support.d.ts.map +1 -1
  44. package/dist/vue/transport.d.ts.map +1 -1
  45. package/dist/vue/viewTypes.d.ts +49 -0
  46. package/dist/vue/viewTypes.d.ts.map +1 -0
  47. package/dist/vue.cjs +1021 -729
  48. package/dist/vue.cjs.map +1 -1
  49. package/dist/vue.js +1019 -727
  50. package/dist/vue.js.map +1 -1
  51. package/package.json +11 -7
  52. package/dist/plugin-BQyzxkui.js.map +0 -1
  53. package/dist/plugin-Bi986Wga.cjs.map +0 -1
package/dist/vue.js CHANGED
@@ -1,24 +1,28 @@
1
- import { a as pluginCore, d as TOOL_DEFINITION, f as TOOL_NAME } from "./plugin-BQyzxkui.js";
1
+ import { a as pluginCore, d as errorMessage, f as TOOL_DEFINITION, p as TOOL_NAME } from "./plugin-CXhOP_xU.js";
2
2
  import { t as GENERATION_EVENT } from "./contract-vY0niHkh.js";
3
3
  import { mulmoBeatSchema, mulmoScriptSchema } from "@mulmocast/types";
4
- 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";
5
- import { PLUGIN_RUNTIME_KEY, useRuntime } from "gui-chat-protocol/vue";
4
+ import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createVNode, defineAsyncComponent, defineComponent, inject, normalizeClass, normalizeStyle, onBeforeUnmount, onMounted, openBlock, reactive, ref, renderList, toDisplayString, unref, vModelText, vShow, watch, withDirectives, withModifiers } from "vue";
5
+ import { createUseT, useRuntime } from "gui-chat-protocol/vue";
6
6
  //#region src/vue/support.ts
7
7
  function isRecord(value) {
8
8
  return typeof value === "object" && value !== null && !Array.isArray(value);
9
9
  }
10
- /** Canonical unknown-caught-value human-readable string. Non-Error
11
- * objects with a `details` (gRPC convention) or `message` string field
12
- * have that field surfaced. */
13
- function errorMessage(err, fallback) {
14
- if (err instanceof Error) return err.message;
15
- if (err !== null && typeof err === "object") {
16
- const obj = err;
17
- if (typeof obj.details === "string" && obj.details) return obj.details;
18
- if (typeof obj.message === "string" && obj.message) return obj.message;
19
- }
20
- if (fallback !== void 0) return fallback;
21
- return String(err);
10
+ /** Read a dropped image File as a base64 data URL, the form the upload
11
+ * dispatches expect. Shared by the beat and character drop handlers.
12
+ * `readAsDataURL` always yields a string on load; the non-string reject
13
+ * is an unreachable guard kept so the resolve type stays `string` without
14
+ * a cast. */
15
+ function readFileAsDataUrl(file) {
16
+ return new Promise((resolve, reject) => {
17
+ const reader = new FileReader();
18
+ reader.onload = () => {
19
+ const { result } = reader;
20
+ if (typeof result === "string") resolve(result);
21
+ else reject(/* @__PURE__ */ new Error("FileReader did not return a data URL string"));
22
+ };
23
+ reader.onerror = reject;
24
+ reader.readAsDataURL(file);
25
+ });
22
26
  }
23
27
  /** Clipboard failures (permissions, insecure context) are swallowed on
24
28
  * purpose: the UI just leaves the "Copied!" hint off, which is what
@@ -100,6 +104,16 @@ function beatMayHaveMovie(beat) {
100
104
  if (beat.moviePrompt) return true;
101
105
  return beat.image?.type === "html_tailwind" && Boolean(beat.image.animation);
102
106
  }
107
+ /**
108
+ * True for a beat whose image merely REFERENCES another beat's image
109
+ * (`image: { type: "beat", id }` — mulmoBeatReferenceMediaSchema). Such a
110
+ * beat owns no asset of its own, so there is nothing to generate for it:
111
+ * the View hides the Generate button (offering it produced a render that
112
+ * could never succeed on its own terms).
113
+ */
114
+ function isBeatImageReference(beat) {
115
+ return beat.image?.type === "beat";
116
+ }
103
117
  /** Pure check: is every beat in the script a `slide`-typed beat?
104
118
  * When true, the View mounts `@mulmocast/deck-web`'s
105
119
  * `MulmoScriptDeckEditor` instead of the per-beat list UI (#1575).
@@ -117,6 +131,74 @@ function isAllSlideDeck(script) {
117
131
  return isRecord(image) && image.type === "slide";
118
132
  });
119
133
  }
134
+ /** Resolve the beat the View should render at `index`: the user's
135
+ * in-place edit (`overrides`) wins over the on-disk beat, and an
136
+ * out-of-range index yields an empty beat so callers can read
137
+ * `.text` / `.image` without a guard. */
138
+ function effectiveBeat(overrides, beats, index) {
139
+ return overrides[index] ?? beats[index] ?? {};
140
+ }
141
+ var BEAT_TOOLTIP_MAX_CHARS = 80;
142
+ /** Beat-strip hover tooltip: the beat text, truncated with an ellipsis
143
+ * past the cap. Missing text yields an empty string. Text of exactly
144
+ * the cap length is returned whole (only a longer string is cut). */
145
+ function beatTooltip(text) {
146
+ const value = text ?? "";
147
+ return value.length > BEAT_TOOLTIP_MAX_CHARS ? `${value.slice(0, BEAT_TOOLTIP_MAX_CHARS)}…` : value;
148
+ }
149
+ /** The prompt for a character image, or "" when the key or its prompt
150
+ * is absent — the character strip renders the empty string as no
151
+ * caption rather than `undefined`. */
152
+ function characterPrompt(images, key) {
153
+ return images?.[key]?.prompt ?? "";
154
+ }
155
+ /** Is the in-editor JSON for a beat currently valid? A missing entry
156
+ * (source editor never opened) validates the empty string, which is
157
+ * not parseable JSON, so it reports invalid rather than throwing. */
158
+ function isValidBeat(source, schema) {
159
+ return validateBeatJSON(source ?? "", schema);
160
+ }
161
+ /** Stale-response guard: a per-beat / per-character response is stale
162
+ * once the View has navigated to a different result, i.e. the current
163
+ * file path no longer matches the one captured when the call was made.
164
+ * Keeping the direction pinned matters — an inverted check would let
165
+ * script A's late responses write into script B's state. */
166
+ function staleSince(currentFilePath, requestedFilePath) {
167
+ return currentFilePath !== requestedFilePath;
168
+ }
169
+ var JSON_INDENT = 2;
170
+ /** Pretty-print a script (or any value) as the source-editor / clipboard
171
+ * text — two-space indent, matching what the beat and disk views emit. */
172
+ function scriptSourceText(value) {
173
+ return JSON.stringify(value, null, JSON_INDENT);
174
+ }
175
+ /** Basename for a download `<a download>` attribute, falling back when
176
+ * the path has no basename. Mirrors the exact existing behaviour, and
177
+ * it has a sharp edge: `.pop()` returns "" (not undefined) for a
178
+ * trailing slash or empty path, and `??` does NOT replace "", so those
179
+ * yield an empty filename rather than the fallback. Server paths always
180
+ * carry a basename, so this never bites in practice — pinned so a later
181
+ * reader doesn't "simplify" `??` to `||` and change behaviour. */
182
+ function downloadFilename(path, fallback) {
183
+ return path.split("/").pop() ?? fallback;
184
+ }
185
+ /** Narrow a script-supplied silent-beat duration to a safe positive number.
186
+ * Zero / negative / NaN / Infinity / non-number collapse the auto-advance
187
+ * timer to an immediate fire, which races the Play loop through every silent
188
+ * beat in a single tick (#1365) — fall back to the default so a run of silent
189
+ * beats stays watchable. The script's own valid `duration` always wins. */
190
+ function resolveSilentAdvanceSeconds(raw, defaultSec) {
191
+ return typeof raw === "number" && Number.isFinite(raw) && raw > 0 ? raw : defaultSec;
192
+ }
193
+ /** Delete every own enumerable key of each record, in place. Used to reset the
194
+ * View's per-beat / per-character reactive maps between scripts — passing the
195
+ * reactive proxies mutates them so the template re-renders empty. Replaces a
196
+ * wall of hand-rolled `Object.keys(map).forEach(delete)` loops. */
197
+ function clearReactiveRecords(...records) {
198
+ records.forEach((record) => {
199
+ Object.keys(record).forEach((key) => Reflect.deleteProperty(record, key));
200
+ });
201
+ }
120
202
  //#endregion
121
203
  //#region src/vue/transport.ts
122
204
  var GENERATION_EVENT_KINDS = /* @__PURE__ */ new Set([
@@ -185,8 +267,337 @@ function useHostAdapter() {
185
267
  return inject(MULMOSCRIPT_HOST_ADAPTER_KEY, EMPTY_ADAPTER);
186
268
  }
187
269
  //#endregion
188
- //#region src/lang/index.ts
189
- var MESSAGES = {
270
+ //#region src/vue/composables/useMediaExport.ts
271
+ function useMediaExport({ api, adapter, filePath, chatSessionId }) {
272
+ const movieGenerating = ref(false);
273
+ const movieDownloading = ref(false);
274
+ const moviePath = ref(null);
275
+ const movieError = ref(null);
276
+ const pdfGenerating = ref(false);
277
+ const pdfDownloading = ref(false);
278
+ const pdfPath = ref(null);
279
+ async function generateMovie() {
280
+ const requestedFilePath = filePath.value;
281
+ movieGenerating.value = true;
282
+ movieError.value = null;
283
+ const response = await api.call("generateMovie", {
284
+ filePath: requestedFilePath,
285
+ chatSessionId: chatSessionId.value
286
+ });
287
+ if (filePath.value !== requestedFilePath) return;
288
+ movieGenerating.value = false;
289
+ if (!response.ok) {
290
+ movieError.value = response.error;
291
+ return;
292
+ }
293
+ moviePath.value = response.data.moviePath;
294
+ }
295
+ async function generatePdf() {
296
+ const requestedFilePath = filePath.value;
297
+ pdfGenerating.value = true;
298
+ const response = await api.call("generatePdf", {
299
+ filePath: requestedFilePath,
300
+ chatSessionId: chatSessionId.value
301
+ });
302
+ if (filePath.value !== requestedFilePath) return;
303
+ pdfGenerating.value = false;
304
+ if (!response.ok) {
305
+ alert(response.error);
306
+ return;
307
+ }
308
+ pdfPath.value = response.data.pdfPath;
309
+ }
310
+ async function refreshMoviePath() {
311
+ const requestedFilePath = filePath.value;
312
+ if (!requestedFilePath) return;
313
+ const response = await api.call("movieStatus", { filePath: requestedFilePath });
314
+ if (filePath.value !== requestedFilePath) return;
315
+ if (response.ok && response.data.moviePath) moviePath.value = response.data.moviePath;
316
+ }
317
+ async function refreshPdfPath() {
318
+ const requestedFilePath = filePath.value;
319
+ if (!requestedFilePath) return;
320
+ const response = await api.call("pdfStatus", { filePath: requestedFilePath });
321
+ if (filePath.value !== requestedFilePath) return;
322
+ if (response.ok && response.data.pdfPath) pdfPath.value = response.data.pdfPath;
323
+ }
324
+ async function downloadMedia(kind, sourcePath, fallbackName, downloading) {
325
+ const fetchMediaBlob = adapter.fetchMediaBlob;
326
+ if (!fetchMediaBlob || !sourcePath || downloading.value) return;
327
+ downloading.value = true;
328
+ let objectUrl = null;
329
+ try {
330
+ const blob = await fetchMediaBlob(kind === "movie" ? { moviePath: sourcePath } : { pdfPath: sourcePath });
331
+ objectUrl = URL.createObjectURL(blob);
332
+ clickDownloadAnchor(objectUrl, downloadFilename(sourcePath, fallbackName));
333
+ } catch (err) {
334
+ alert(errorMessage(err));
335
+ } finally {
336
+ if (objectUrl) URL.revokeObjectURL(objectUrl);
337
+ downloading.value = false;
338
+ }
339
+ }
340
+ function downloadMovie() {
341
+ return downloadMedia("movie", moviePath.value, "movie.mp4", movieDownloading);
342
+ }
343
+ function downloadPdf() {
344
+ return downloadMedia("pdf", pdfPath.value, "deck.pdf", pdfDownloading);
345
+ }
346
+ function resetMedia() {
347
+ moviePath.value = null;
348
+ pdfPath.value = null;
349
+ movieGenerating.value = false;
350
+ pdfGenerating.value = false;
351
+ movieError.value = null;
352
+ }
353
+ return {
354
+ moviePath,
355
+ movieGenerating,
356
+ movieDownloading,
357
+ movieError,
358
+ pdfPath,
359
+ pdfGenerating,
360
+ pdfDownloading,
361
+ generateMovie,
362
+ downloadMovie,
363
+ refreshMoviePath,
364
+ generatePdf,
365
+ downloadPdf,
366
+ refreshPdfPath,
367
+ resetMedia
368
+ };
369
+ }
370
+ function clickDownloadAnchor(href, filename) {
371
+ const anchor = document.createElement("a");
372
+ anchor.href = href;
373
+ anchor.download = filename;
374
+ document.body.appendChild(anchor);
375
+ anchor.click();
376
+ anchor.remove();
377
+ }
378
+ //#endregion
379
+ //#region src/vue/composables/useBeatMovie.ts
380
+ function useBeatMovie({ api, adapter, filePath }) {
381
+ const beatMovies = reactive({});
382
+ const beatMovieUrls = reactive({});
383
+ const beatMovieOpen = reactive({});
384
+ const beatMovieLoading = reactive({});
385
+ const staleSince$3 = (requestedFilePath) => staleSince(filePath.value, requestedFilePath);
386
+ async function loadExistingBeatMovie(index) {
387
+ const requestedFilePath = filePath.value;
388
+ const response = await api.call("beatMovie", {
389
+ filePath: requestedFilePath,
390
+ beatIndex: index
391
+ });
392
+ if (staleSince$3(requestedFilePath)) return;
393
+ if (response.ok && response.data.moviePath) beatMovies[index] = response.data.moviePath;
394
+ }
395
+ async function playBeatMovie(index) {
396
+ const fetchMediaBlob = adapter.fetchMediaBlob;
397
+ if (!fetchMediaBlob || !beatMovies[index] || beatMovieLoading[index]) return;
398
+ if (beatMovieUrls[index]) {
399
+ beatMovieOpen[index] = true;
400
+ return;
401
+ }
402
+ beatMovieLoading[index] = true;
403
+ try {
404
+ const blob = new Blob([await fetchMediaBlob({ moviePath: beatMovies[index] })], { type: "video/mp4" });
405
+ beatMovieUrls[index] = URL.createObjectURL(blob);
406
+ beatMovieOpen[index] = true;
407
+ } catch (err) {
408
+ alert(errorMessage(err));
409
+ } finally {
410
+ Reflect.deleteProperty(beatMovieLoading, index);
411
+ }
412
+ }
413
+ function closeBeatMovie(index) {
414
+ Reflect.deleteProperty(beatMovieOpen, index);
415
+ }
416
+ function invalidateBeatMovie(index) {
417
+ if (beatMovieUrls[index]) URL.revokeObjectURL(beatMovieUrls[index]);
418
+ [
419
+ beatMovies,
420
+ beatMovieUrls,
421
+ beatMovieOpen
422
+ ].forEach((map) => Reflect.deleteProperty(map, index));
423
+ }
424
+ function resetBeatMovies() {
425
+ Object.values(beatMovieUrls).forEach((url) => URL.revokeObjectURL(url));
426
+ clearReactiveRecords(beatMovies, beatMovieUrls, beatMovieOpen, beatMovieLoading);
427
+ }
428
+ return {
429
+ beatMovies,
430
+ beatMovieUrls,
431
+ beatMovieOpen,
432
+ beatMovieLoading,
433
+ loadExistingBeatMovie,
434
+ playBeatMovie,
435
+ closeBeatMovie,
436
+ invalidateBeatMovie,
437
+ resetBeatMovies
438
+ };
439
+ }
440
+ //#endregion
441
+ //#region src/vue/composables/useCharacterImages.ts
442
+ function useCharacterImages({ api, filePath, chatSessionId, getImages }) {
443
+ const charRenderState = reactive({});
444
+ const charImages = reactive({});
445
+ const charErrors = reactive({});
446
+ const charDragOver = reactive({});
447
+ const staleSince$2 = (requestedFilePath) => staleSince(filePath.value, requestedFilePath);
448
+ const characterKeys = computed(() => {
449
+ const imgs = getImages() ?? {};
450
+ return Object.keys(imgs).filter((key) => imgs[key]?.type === "imagePrompt");
451
+ });
452
+ function characterPrompt$1(key) {
453
+ return characterPrompt(getImages(), key);
454
+ }
455
+ function onCharDragOver(event, key) {
456
+ if (!event.dataTransfer?.types.includes("Files")) return;
457
+ event.preventDefault();
458
+ charDragOver[key] = true;
459
+ }
460
+ function onCharDragLeave(key) {
461
+ charDragOver[key] = false;
462
+ }
463
+ async function onCharDrop(event, key) {
464
+ event.preventDefault();
465
+ charDragOver[key] = false;
466
+ const file = event.dataTransfer?.files[0];
467
+ if (!file || !file.type.startsWith("image/")) return;
468
+ charRenderState[key] = "rendering";
469
+ Reflect.deleteProperty(charErrors, key);
470
+ let imageData;
471
+ try {
472
+ imageData = await readFileAsDataUrl(file);
473
+ } catch (err) {
474
+ charErrors[key] = errorMessage(err);
475
+ charRenderState[key] = "error";
476
+ return;
477
+ }
478
+ const requestedFilePath = filePath.value;
479
+ const response = await api.call("uploadCharacterImage", {
480
+ filePath: requestedFilePath,
481
+ key,
482
+ imageData
483
+ });
484
+ if (staleSince$2(requestedFilePath)) return;
485
+ if (!response.ok) {
486
+ charErrors[key] = response.error || "Upload failed";
487
+ charRenderState[key] = "error";
488
+ return;
489
+ }
490
+ charImages[key] = response.data.image ?? "";
491
+ charRenderState[key] = "done";
492
+ }
493
+ async function loadExistingCharacterImage(key) {
494
+ const requestedFilePath = filePath.value;
495
+ const response = await api.call("characterImage", {
496
+ filePath: requestedFilePath,
497
+ key
498
+ });
499
+ if (staleSince$2(requestedFilePath)) return;
500
+ if (response.ok && response.data.image) {
501
+ charImages[key] = response.data.image;
502
+ charRenderState[key] = "done";
503
+ }
504
+ }
505
+ function refreshMissingCharacterImages() {
506
+ getMissingCharacterKeys(characterKeys.value, charImages, charRenderState).forEach((key) => loadExistingCharacterImage(key));
507
+ }
508
+ async function renderCharacter(key, force) {
509
+ const requestedFilePath = filePath.value;
510
+ charRenderState[key] = "rendering";
511
+ Reflect.deleteProperty(charErrors, key);
512
+ const response = await api.call("renderCharacter", {
513
+ filePath: requestedFilePath,
514
+ key,
515
+ force,
516
+ chatSessionId: chatSessionId.value
517
+ });
518
+ if (staleSince$2(requestedFilePath)) return;
519
+ if (!response.ok) {
520
+ charErrors[key] = response.error || "Render failed";
521
+ charRenderState[key] = "error";
522
+ return;
523
+ }
524
+ charImages[key] = response.data.image ?? "";
525
+ charRenderState[key] = "done";
526
+ }
527
+ async function generateAllCharacters() {
528
+ await Promise.all(characterKeys.value.filter((key) => charRenderState[key] !== "rendering").map((key) => renderCharacter(key, false)));
529
+ }
530
+ function resetCharacters() {
531
+ clearReactiveRecords(charRenderState, charImages, charErrors, charDragOver);
532
+ }
533
+ return {
534
+ charRenderState,
535
+ charImages,
536
+ charErrors,
537
+ charDragOver,
538
+ characterKeys,
539
+ characterPrompt: characterPrompt$1,
540
+ onCharDragOver,
541
+ onCharDragLeave,
542
+ onCharDrop,
543
+ loadExistingCharacterImage,
544
+ refreshMissingCharacterImages,
545
+ renderCharacter,
546
+ generateAllCharacters,
547
+ resetCharacters
548
+ };
549
+ }
550
+ //#endregion
551
+ //#region src/vue/composables/useDeckEditor.ts
552
+ var DECK_SAVE_DEBOUNCE_MS = 300;
553
+ function useDeckEditor({ api, filePath, effectiveScript, commitScript }) {
554
+ const isDeck = computed(() => isAllSlideDeck(effectiveScript.value));
555
+ const deckScriptInput = computed(() => effectiveScript.value);
556
+ let deckSaveTimer = null;
557
+ let pendingDeckScript = null;
558
+ function scheduleDeckSave(next) {
559
+ pendingDeckScript = next;
560
+ if (deckSaveTimer) clearTimeout(deckSaveTimer);
561
+ deckSaveTimer = setTimeout(() => {
562
+ flushDeckSave();
563
+ }, DECK_SAVE_DEBOUNCE_MS);
564
+ }
565
+ async function flushDeckSave() {
566
+ deckSaveTimer = null;
567
+ const next = pendingDeckScript;
568
+ pendingDeckScript = null;
569
+ if (!next || !filePath.value) return;
570
+ const response = await api.call("updateScript", {
571
+ filePath: filePath.value,
572
+ script: next
573
+ });
574
+ if (!response.ok) {
575
+ console.error("[presentMulmoScript] deck save failed:", response.error);
576
+ return;
577
+ }
578
+ commitScript(next);
579
+ }
580
+ function onDeckUpdate(next) {
581
+ scheduleDeckSave(next);
582
+ }
583
+ function flushPendingDeckSave() {
584
+ if (deckSaveTimer) {
585
+ clearTimeout(deckSaveTimer);
586
+ flushDeckSave();
587
+ }
588
+ }
589
+ return {
590
+ isDeck,
591
+ deckScriptInput,
592
+ onDeckUpdate,
593
+ flushPendingDeckSave
594
+ };
595
+ }
596
+ /** Reactive message bundle for the active host locale. The plugin carries its
597
+ * own translations (no host i18n dependency); it reads the locale off the
598
+ * injected `BrowserPluginRuntime.locale` ref and falls back to English.
599
+ * Same pattern as @mulmoclaude/html-plugin. */
600
+ var useT = createUseT({
190
601
  de: {
191
602
  beatCount: (count) => count === 1 ? `${count} Beat` : `${count} Beats`,
192
603
  movie: "Video",
@@ -443,267 +854,519 @@ var MESSAGES = {
443
854
  close: "关闭",
444
855
  cancel: "取消"
445
856
  }
446
- };
447
- function isSupportedLocale(value) {
448
- return Object.hasOwn(MESSAGES, value);
449
- }
450
- /** Reactive message bundle for the active host locale. The plugin carries its
451
- * own translations (no host i18n dependency); it reads the locale off the
452
- * injected `BrowserPluginRuntime.locale` ref and falls back to English.
453
- * Same pattern as @mulmoclaude/html-plugin. */
454
- function useT() {
455
- const locale = inject(PLUGIN_RUNTIME_KEY, void 0)?.locale ?? ref("en");
456
- return computed(() => isSupportedLocale(locale.value) ? MESSAGES[locale.value] : MESSAGES.en);
457
- }
857
+ });
458
858
  //#endregion
459
- //#region src/vue/View.vue?vue&type=script&setup=true&lang.ts
460
- var _hoisted_1$1 = { class: "h-full bg-white flex flex-col overflow-hidden" };
461
- var _hoisted_2$1 = { class: "flex items-start justify-between px-6 py-4 border-b border-gray-100 shrink-0" };
462
- var _hoisted_3$1 = { class: "min-w-0 flex-1" };
463
- var _hoisted_4 = {
464
- class: "text-lg font-semibold text-gray-800 truncate",
465
- "data-testid": "mulmo-script-title"
466
- };
467
- var _hoisted_5 = {
859
+ //#region src/vue/components/BeatLightbox.vue?vue&type=script&setup=true&lang.ts
860
+ var _hoisted_1$4 = ["title"];
861
+ var _hoisted_2$4 = { class: "flex items-center gap-4" };
862
+ var _hoisted_3$4 = ["disabled"];
863
+ var _hoisted_4$3 = { class: "flex flex-col items-center" };
864
+ var _hoisted_5$3 = ["src"];
865
+ var _hoisted_6$3 = {
468
866
  key: 0,
469
- class: "text-sm text-gray-500 mt-0.5 truncate",
470
- "data-testid": "mulmo-script-description"
471
- };
472
- var _hoisted_6 = { class: "flex items-center gap-3 mt-1 text-xs text-gray-400" };
473
- var _hoisted_7 = { key: 0 };
474
- var _hoisted_8 = {
475
- key: 1,
476
- class: "truncate"
867
+ class: "relative w-full h-1"
477
868
  };
478
- var _hoisted_9 = { class: "ml-4 shrink-0 flex items-center gap-2" };
479
- var _hoisted_10 = [
480
- "disabled",
481
- "title",
482
- "aria-label"
483
- ];
484
- var _hoisted_11 = ["disabled"];
485
- var _hoisted_12 = ["title", "aria-label"];
486
- var _hoisted_13 = ["disabled"];
487
- var _hoisted_14 = {
869
+ var _hoisted_7$3 = { class: "flex gap-1 h-full" };
870
+ var _hoisted_8$3 = ["onClick"];
871
+ var _hoisted_9$3 = {
488
872
  key: 0,
489
- class: "animate-spin w-4 h-4 shrink-0",
490
- viewBox: "0 0 24 24",
491
- fill: "none"
873
+ 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"
492
874
  };
493
- var _hoisted_15 = { key: 1 };
494
- var _hoisted_16 = ["disabled"];
495
- var _hoisted_17 = ["title", "aria-label"];
496
- var _hoisted_18 = ["disabled"];
497
- var _hoisted_19 = {
875
+ var _hoisted_10$3 = ["disabled"];
876
+ var _hoisted_11$3 = {
498
877
  key: 0,
499
- class: "animate-spin w-4 h-4 shrink-0",
500
- viewBox: "0 0 24 24",
501
- fill: "none"
878
+ class: "relative w-screen flex justify-center px-16"
502
879
  };
503
- var _hoisted_20 = { key: 1 };
504
- var _hoisted_21 = {
880
+ var _hoisted_12$3 = {
505
881
  key: 0,
506
- "data-testid": "mulmo-script-movie-error-chip",
507
- 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"
508
- };
509
- var _hoisted_22 = { class: "flex-1 min-w-0" };
510
- var _hoisted_23 = { class: "font-medium" };
511
- var _hoisted_24 = { class: "break-words whitespace-pre-wrap mt-0.5" };
512
- var _hoisted_25 = ["disabled"];
513
- var _hoisted_26 = {
514
- key: 1,
515
- class: "border-b border-gray-100 shrink-0 px-4 py-3"
882
+ class: "max-w-[80vw] text-center text-white leading-relaxed text-[clamp(0.8rem,1.76vw,1.6rem)]"
516
883
  };
517
- var _hoisted_27 = { class: "flex items-center justify-between mb-2" };
518
- var _hoisted_28 = { class: "text-xs font-semibold text-gray-500 uppercase tracking-wide" };
519
- var _hoisted_29 = ["disabled"];
520
- var _hoisted_30 = { class: "flex gap-3 flex-wrap" };
521
- var _hoisted_31 = [
884
+ //#endregion
885
+ //#region src/vue/components/BeatLightbox.vue
886
+ var BeatLightbox_default = /* @__PURE__ */ defineComponent({
887
+ __name: "BeatLightbox",
888
+ props: {
889
+ lightbox: {},
890
+ beatCount: {},
891
+ beatTexts: {},
892
+ hasPrev: { type: Boolean },
893
+ hasNext: { type: Boolean },
894
+ playingAudioIndex: {},
895
+ audioProgress: {},
896
+ hasCurrentAudio: { type: Boolean }
897
+ },
898
+ emits: [
899
+ "close",
900
+ "move",
901
+ "jump",
902
+ "playAudio"
903
+ ],
904
+ setup(__props, { emit: __emit }) {
905
+ const m = useT();
906
+ const emit = __emit;
907
+ return (_ctx, _cache) => {
908
+ return openBlock(), createElementBlock("div", {
909
+ class: "fixed inset-0 z-50 bg-black/80 overflow-y-auto",
910
+ onClick: _cache[5] || (_cache[5] = ($event) => emit("close"))
911
+ }, [createElementVNode("button", {
912
+ class: "fixed top-2 right-4 z-10 text-white/60 hover:text-white text-3xl leading-none",
913
+ title: unref(m).close,
914
+ onClick: _cache[0] || (_cache[0] = withModifiers(($event) => emit("close"), ["stop"]))
915
+ }, "✕", 8, _hoisted_1$4), createElementVNode("div", {
916
+ class: "flex flex-col items-center gap-4 pt-4 pb-8",
917
+ onClick: _cache[4] || (_cache[4] = withModifiers(() => {}, ["stop"]))
918
+ }, [createElementVNode("div", _hoisted_2$4, [
919
+ !__props.lightbox.isCharacter ? (openBlock(), createElementBlock("button", {
920
+ key: 0,
921
+ class: "text-white/60 hover:text-white disabled:opacity-20 text-5xl leading-none",
922
+ disabled: !__props.hasPrev,
923
+ onClick: _cache[1] || (_cache[1] = ($event) => emit("move", -1))
924
+ }, " ‹ ", 8, _hoisted_3$4)) : createCommentVNode("", true),
925
+ createElementVNode("div", _hoisted_4$3, [createElementVNode("img", {
926
+ src: __props.lightbox.src,
927
+ class: "max-w-[80vw] max-h-[85vh] object-contain rounded shadow-2xl"
928
+ }, null, 8, _hoisted_5$3), !__props.lightbox.isCharacter && __props.beatCount > 1 ? (openBlock(), createElementBlock("div", _hoisted_6$3, [createElementVNode("div", _hoisted_7$3, [(openBlock(true), createElementBlock(Fragment, null, renderList(__props.beatCount, (i) => {
929
+ return openBlock(), createElementBlock("div", {
930
+ key: i - 1,
931
+ class: normalizeClass(["group flex-1 cursor-pointer relative transition-colors", i - 1 === __props.lightbox.index ? "bg-white/80 hover:bg-white" : i - 1 < __props.lightbox.index ? "bg-white/40 hover:bg-white/60" : "bg-white/20 hover:bg-white/40"]),
932
+ onClick: ($event) => emit("jump", i - 1)
933
+ }, [_cache[6] || (_cache[6] = createElementVNode("span", { class: "absolute -inset-y-3 inset-x-0" }, null, -1)), unref(beatTooltip)(__props.beatTexts[i - 1]) ? (openBlock(), createElementBlock("div", _hoisted_9$3, toDisplayString(unref(beatTooltip)(__props.beatTexts[i - 1])), 1)) : createCommentVNode("", true)], 10, _hoisted_8$3);
934
+ }), 128))]), __props.playingAudioIndex !== null && __props.playingAudioIndex === __props.lightbox.index ? (openBlock(), createElementBlock("div", {
935
+ key: 0,
936
+ 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",
937
+ style: normalizeStyle({ left: `${(__props.lightbox.index + __props.audioProgress) / __props.beatCount * 100}%` })
938
+ }, null, 4)) : createCommentVNode("", true)])) : createCommentVNode("", true)]),
939
+ !__props.lightbox.isCharacter ? (openBlock(), createElementBlock("button", {
940
+ key: 1,
941
+ class: "text-white/60 hover:text-white disabled:opacity-20 text-5xl leading-none",
942
+ disabled: !__props.hasNext,
943
+ onClick: _cache[2] || (_cache[2] = ($event) => emit("move", 1))
944
+ }, " › ", 8, _hoisted_10$3)) : createCommentVNode("", true)
945
+ ]), __props.lightbox.text || __props.hasCurrentAudio ? (openBlock(), createElementBlock("div", _hoisted_11$3, [__props.lightbox.text ? (openBlock(), createElementBlock("p", _hoisted_12$3, toDisplayString(__props.lightbox.text), 1)) : createCommentVNode("", true), __props.hasCurrentAudio ? (openBlock(), createElementBlock("button", {
946
+ key: 1,
947
+ class: "absolute top-0 right-4 text-sm px-3 py-1 rounded border border-white/60 text-white/60 hover:bg-white/20",
948
+ onClick: _cache[3] || (_cache[3] = ($event) => emit("playAudio", __props.lightbox.index))
949
+ }, toDisplayString(__props.playingAudioIndex === __props.lightbox.index ? unref(m).stop : unref(m).play), 1)) : createCommentVNode("", true)])) : createCommentVNode("", true)])]);
950
+ };
951
+ }
952
+ });
953
+ //#endregion
954
+ //#region src/vue/components/CharacterStrip.vue?vue&type=script&setup=true&lang.ts
955
+ var _hoisted_1$3 = { class: "border-b border-gray-100 shrink-0 px-4 py-3" };
956
+ var _hoisted_2$3 = { class: "flex items-center justify-between mb-2" };
957
+ var _hoisted_3$3 = { class: "text-xs font-semibold text-gray-500 uppercase tracking-wide" };
958
+ var _hoisted_4$2 = ["disabled"];
959
+ var _hoisted_5$2 = { class: "flex gap-3 flex-wrap" };
960
+ var _hoisted_6$2 = [
522
961
  "onDragover",
523
962
  "onDragleave",
524
963
  "onDrop"
525
964
  ];
526
- var _hoisted_32 = [
965
+ var _hoisted_7$2 = [
527
966
  "src",
528
967
  "alt",
529
968
  "onClick"
530
969
  ];
531
- var _hoisted_33 = {
970
+ var _hoisted_8$2 = {
532
971
  key: 1,
533
972
  class: "animate-spin w-4 h-4 text-green-400",
534
973
  viewBox: "0 0 24 24",
535
974
  fill: "none"
536
975
  };
537
- var _hoisted_34 = {
976
+ var _hoisted_9$2 = {
538
977
  key: 2,
539
978
  class: "text-xs text-red-400 text-center px-1"
540
979
  };
541
- var _hoisted_35 = {
980
+ var _hoisted_10$2 = {
542
981
  key: 3,
543
982
  class: "text-xs text-gray-300 text-center px-1 leading-tight"
544
983
  };
545
- var _hoisted_36 = {
984
+ var _hoisted_11$2 = {
546
985
  key: 4,
547
986
  class: "absolute bottom-0 inset-x-0 text-center text-xs text-gray-400 bg-white/70 py-0.5 pointer-events-none"
548
987
  };
549
- var _hoisted_37 = {
988
+ var _hoisted_12$2 = {
550
989
  key: 5,
551
990
  class: "absolute inset-0 flex items-center justify-center bg-blue-50/80 pointer-events-none"
552
991
  };
553
- var _hoisted_38 = { class: "text-xs text-blue-500 font-medium" };
554
- var _hoisted_39 = ["disabled", "onClick"];
555
- var _hoisted_40 = {
992
+ var _hoisted_13$1 = { class: "text-xs text-blue-500 font-medium" };
993
+ var _hoisted_14$1 = ["disabled", "onClick"];
994
+ var _hoisted_15$1 = {
556
995
  key: 0,
557
996
  class: "inline-block animate-spin"
558
997
  };
559
- var _hoisted_41 = { key: 1 };
560
- var _hoisted_42 = ["disabled", "onClick"];
561
- var _hoisted_43 = {
998
+ var _hoisted_16$1 = { key: 1 };
999
+ var _hoisted_17$1 = ["disabled", "onClick"];
1000
+ var _hoisted_18$1 = {
562
1001
  key: 0,
563
1002
  class: "animate-spin w-3 h-3",
564
1003
  viewBox: "0 0 24 24",
565
1004
  fill: "none"
566
1005
  };
567
- var _hoisted_44 = { key: 1 };
568
- var _hoisted_45 = { class: "text-xs text-gray-600 text-center truncate w-full" };
569
- var _hoisted_46 = {
1006
+ var _hoisted_19$1 = { key: 1 };
1007
+ var _hoisted_20$1 = { class: "text-xs text-gray-600 text-center truncate w-full" };
1008
+ //#endregion
1009
+ //#region src/vue/components/CharacterStrip.vue
1010
+ var CharacterStrip_default = /* @__PURE__ */ defineComponent({
1011
+ __name: "CharacterStrip",
1012
+ props: {
1013
+ characterKeys: {},
1014
+ images: {},
1015
+ thumbnails: {},
1016
+ renderState: {},
1017
+ errors: {},
1018
+ dragOver: {},
1019
+ movieGenerating: { type: Boolean },
1020
+ anyBeatRendering: { type: Boolean }
1021
+ },
1022
+ emits: [
1023
+ "generateAll",
1024
+ "charDragOver",
1025
+ "charDragLeave",
1026
+ "charDrop",
1027
+ "openLightbox",
1028
+ "renderCharacter"
1029
+ ],
1030
+ setup(__props, { emit: __emit }) {
1031
+ const m = useT();
1032
+ const props = __props;
1033
+ const emit = __emit;
1034
+ const busy = computed(() => props.movieGenerating || props.anyBeatRendering);
1035
+ return (_ctx, _cache) => {
1036
+ return openBlock(), createElementBlock("div", _hoisted_1$3, [createElementVNode("div", _hoisted_2$3, [createElementVNode("span", _hoisted_3$3, toDisplayString(unref(m).characters), 1), createElementVNode("button", {
1037
+ class: "px-2 py-0.5 text-xs rounded border border-gray-300 text-gray-500 hover:bg-gray-50 disabled:opacity-50",
1038
+ disabled: busy.value || __props.characterKeys.every((key) => __props.renderState[key] === "rendering"),
1039
+ onClick: _cache[0] || (_cache[0] = ($event) => emit("generateAll"))
1040
+ }, toDisplayString(unref(m).generateAll), 9, _hoisted_4$2)]), createElementVNode("div", _hoisted_5$2, [(openBlock(true), createElementBlock(Fragment, null, renderList(__props.characterKeys, (key) => {
1041
+ return openBlock(), createElementBlock("div", {
1042
+ key,
1043
+ class: "flex flex-col items-center gap-1 w-36"
1044
+ }, [createElementVNode("div", {
1045
+ class: normalizeClass(["relative w-36 h-36 rounded-lg border overflow-hidden bg-gray-50 flex items-center justify-center transition-colors", __props.dragOver[key] ? "border-blue-400 bg-blue-50" : "border-gray-200"]),
1046
+ onDragover: ($event) => emit("charDragOver", $event, key),
1047
+ onDragleave: ($event) => emit("charDragLeave", key),
1048
+ onDrop: ($event) => emit("charDrop", $event, key)
1049
+ }, [
1050
+ __props.thumbnails[key] ? (openBlock(), createElementBlock("img", {
1051
+ key: 0,
1052
+ src: __props.thumbnails[key],
1053
+ class: "w-full h-full object-cover cursor-zoom-in",
1054
+ alt: key,
1055
+ onClick: ($event) => emit("openLightbox", key)
1056
+ }, null, 8, _hoisted_7$2)) : __props.renderState[key] === "rendering" ? (openBlock(), createElementBlock("svg", _hoisted_8$2, [..._cache[1] || (_cache[1] = [createElementVNode("circle", {
1057
+ class: "opacity-25",
1058
+ cx: "12",
1059
+ cy: "12",
1060
+ r: "10",
1061
+ stroke: "currentColor",
1062
+ "stroke-width": "4"
1063
+ }, null, -1), createElementVNode("path", {
1064
+ class: "opacity-75",
1065
+ fill: "currentColor",
1066
+ d: "M4 12a8 8 0 018-8v8H4z"
1067
+ }, null, -1)])])) : __props.renderState[key] === "error" ? (openBlock(), createElementBlock("span", _hoisted_9$2, toDisplayString(__props.errors[key]), 1)) : (openBlock(), createElementBlock("span", _hoisted_10$2, toDisplayString(unref(characterPrompt)(__props.images, key)), 1)),
1068
+ !__props.dragOver[key] ? (openBlock(), createElementBlock("div", _hoisted_11$2, toDisplayString(unref(m).orDropImage), 1)) : createCommentVNode("", true),
1069
+ __props.dragOver[key] ? (openBlock(), createElementBlock("div", _hoisted_12$2, [createElementVNode("span", _hoisted_13$1, toDisplayString(unref(m).drop), 1)])) : createCommentVNode("", true),
1070
+ __props.thumbnails[key] && __props.renderState[key] !== "rendering" ? (openBlock(), createElementBlock("button", {
1071
+ key: 6,
1072
+ class: normalizeClass(["absolute top-0.5 right-0.5 px-1 py-0.5 text-xs rounded border bg-white", busy.value ? "border-yellow-400 text-yellow-500 cursor-not-allowed" : "border-gray-400 text-gray-600 hover:bg-gray-50"]),
1073
+ disabled: busy.value,
1074
+ onClick: withModifiers(($event) => emit("renderCharacter", key, true), ["stop"])
1075
+ }, [busy.value ? (openBlock(), createElementBlock("span", _hoisted_15$1, "↺")) : (openBlock(), createElementBlock("span", _hoisted_16$1, "↺"))], 10, _hoisted_14$1)) : !__props.thumbnails[key] && __props.renderState[key] !== "rendering" ? (openBlock(), createElementBlock("button", {
1076
+ key: 7,
1077
+ class: normalizeClass(["absolute top-0.5 right-0.5 px-1 py-0.5 text-xs rounded border bg-white", busy.value ? "border-yellow-400 text-yellow-500 cursor-not-allowed" : "border-blue-400 text-blue-600 hover:bg-blue-50"]),
1078
+ disabled: busy.value,
1079
+ onClick: withModifiers(($event) => emit("renderCharacter", key, false), ["stop"])
1080
+ }, [busy.value ? (openBlock(), createElementBlock("svg", _hoisted_18$1, [..._cache[2] || (_cache[2] = [createElementVNode("circle", {
1081
+ class: "opacity-25",
1082
+ cx: "12",
1083
+ cy: "12",
1084
+ r: "10",
1085
+ stroke: "currentColor",
1086
+ "stroke-width": "4"
1087
+ }, null, -1), createElementVNode("path", {
1088
+ class: "opacity-75",
1089
+ fill: "currentColor",
1090
+ d: "M4 12a8 8 0 018-8v8H4z"
1091
+ }, null, -1)])])) : (openBlock(), createElementBlock("span", _hoisted_19$1, toDisplayString(unref(m).gen), 1))], 10, _hoisted_17$1)) : createCommentVNode("", true)
1092
+ ], 42, _hoisted_6$2), createElementVNode("span", _hoisted_20$1, toDisplayString(key), 1)]);
1093
+ }), 128))])]);
1094
+ };
1095
+ }
1096
+ });
1097
+ //#endregion
1098
+ //#region src/vue/components/MulmoScriptToolbar.vue?vue&type=script&setup=true&lang.ts
1099
+ var _hoisted_1$2 = { class: "ml-4 shrink-0 flex items-center gap-2" };
1100
+ var _hoisted_2$2 = [
1101
+ "disabled",
1102
+ "title",
1103
+ "aria-label"
1104
+ ];
1105
+ var _hoisted_3$2 = ["disabled"];
1106
+ var _hoisted_4$1 = ["title", "aria-label"];
1107
+ var _hoisted_5$1 = ["disabled"];
1108
+ var _hoisted_6$1 = {
1109
+ key: 0,
1110
+ class: "animate-spin w-4 h-4 shrink-0",
1111
+ viewBox: "0 0 24 24",
1112
+ fill: "none"
1113
+ };
1114
+ var _hoisted_7$1 = { key: 1 };
1115
+ var _hoisted_8$1 = ["disabled"];
1116
+ var _hoisted_9$1 = ["title", "aria-label"];
1117
+ var _hoisted_10$1 = ["disabled"];
1118
+ var _hoisted_11$1 = {
1119
+ key: 0,
1120
+ class: "animate-spin w-4 h-4 shrink-0",
1121
+ viewBox: "0 0 24 24",
1122
+ fill: "none"
1123
+ };
1124
+ var _hoisted_12$1 = { key: 1 };
1125
+ //#endregion
1126
+ //#region src/vue/components/MulmoScriptToolbar.vue
1127
+ var MulmoScriptToolbar_default = /* @__PURE__ */ defineComponent({
1128
+ __name: "MulmoScriptToolbar",
1129
+ props: {
1130
+ moviePath: {},
1131
+ movieGenerating: { type: Boolean },
1132
+ movieDownloading: { type: Boolean },
1133
+ isPlayReady: { type: Boolean },
1134
+ canFetchMedia: { type: Boolean },
1135
+ pdfPath: {},
1136
+ pdfGenerating: { type: Boolean },
1137
+ pdfDownloading: { type: Boolean }
1138
+ },
1139
+ emits: [
1140
+ "play",
1141
+ "generateMovie",
1142
+ "downloadMovie",
1143
+ "generatePdf",
1144
+ "downloadPdf"
1145
+ ],
1146
+ setup(__props, { emit: __emit }) {
1147
+ const m = useT();
1148
+ const emit = __emit;
1149
+ return (_ctx, _cache) => {
1150
+ return openBlock(), createElementBlock("div", _hoisted_1$2, [
1151
+ __props.moviePath && !__props.movieGenerating ? (openBlock(), createElementBlock("button", {
1152
+ key: 0,
1153
+ 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",
1154
+ disabled: !__props.isPlayReady,
1155
+ title: unref(m).playPresentation,
1156
+ "aria-label": unref(m).playPresentation,
1157
+ onClick: _cache[0] || (_cache[0] = ($event) => emit("play"))
1158
+ }, [..._cache[7] || (_cache[7] = [createElementVNode("span", { class: "material-icons text-base" }, "play_arrow", -1)])], 8, _hoisted_2$2)) : createCommentVNode("", true),
1159
+ __props.moviePath && !__props.movieGenerating && __props.canFetchMedia ? (openBlock(), createElementBlock("button", {
1160
+ key: 1,
1161
+ 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",
1162
+ disabled: __props.movieDownloading,
1163
+ "data-testid": "mulmo-script-download-movie-button",
1164
+ onClick: _cache[1] || (_cache[1] = ($event) => emit("downloadMovie"))
1165
+ }, [_cache[8] || (_cache[8] = createElementVNode("span", { class: "material-icons text-base" }, "download", -1)), createElementVNode("span", null, toDisplayString(unref(m).movie), 1)], 8, _hoisted_3$2)) : createCommentVNode("", true),
1166
+ __props.moviePath && !__props.movieGenerating ? (openBlock(), createElementBlock("button", {
1167
+ key: 2,
1168
+ class: "h-8 w-8 flex items-center justify-center rounded border border-gray-200 text-gray-600 hover:bg-gray-100 transition-colors",
1169
+ title: unref(m).regenerateMovie,
1170
+ "aria-label": unref(m).regenerateMovie,
1171
+ "data-testid": "mulmo-script-regenerate-movie-button",
1172
+ onClick: _cache[2] || (_cache[2] = ($event) => emit("generateMovie"))
1173
+ }, [..._cache[9] || (_cache[9] = [createElementVNode("span", { class: "material-icons text-base" }, "refresh", -1)])], 8, _hoisted_4$1)) : (openBlock(), createElementBlock("button", {
1174
+ key: 3,
1175
+ 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",
1176
+ disabled: __props.movieGenerating,
1177
+ "data-testid": "mulmo-script-generate-movie-button",
1178
+ onClick: _cache[3] || (_cache[3] = ($event) => emit("generateMovie"))
1179
+ }, [__props.movieGenerating ? (openBlock(), createElementBlock("svg", _hoisted_6$1, [..._cache[10] || (_cache[10] = [createElementVNode("circle", {
1180
+ class: "opacity-25",
1181
+ cx: "12",
1182
+ cy: "12",
1183
+ r: "10",
1184
+ stroke: "currentColor",
1185
+ "stroke-width": "4"
1186
+ }, null, -1), createElementVNode("path", {
1187
+ class: "opacity-75",
1188
+ fill: "currentColor",
1189
+ d: "M4 12a8 8 0 018-8v8H4z"
1190
+ }, null, -1)])])) : createCommentVNode("", true), __props.movieGenerating ? (openBlock(), createElementBlock("span", _hoisted_7$1, toDisplayString(unref(m).generating), 1)) : (openBlock(), createElementBlock(Fragment, { key: 2 }, [_cache[11] || (_cache[11] = createElementVNode("span", { class: "material-icons text-sm" }, "refresh", -1)), createElementVNode("span", null, toDisplayString(unref(m).movie), 1)], 64))], 8, _hoisted_5$1)),
1191
+ __props.pdfPath && !__props.pdfGenerating && __props.canFetchMedia ? (openBlock(), createElementBlock("button", {
1192
+ key: 4,
1193
+ 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",
1194
+ disabled: __props.pdfDownloading,
1195
+ "data-testid": "mulmo-script-download-pdf-button",
1196
+ onClick: _cache[4] || (_cache[4] = ($event) => emit("downloadPdf"))
1197
+ }, [_cache[12] || (_cache[12] = createElementVNode("span", { class: "material-icons text-base" }, "download", -1)), createElementVNode("span", null, toDisplayString(unref(m).pdf), 1)], 8, _hoisted_8$1)) : createCommentVNode("", true),
1198
+ __props.pdfPath && !__props.pdfGenerating ? (openBlock(), createElementBlock("button", {
1199
+ key: 5,
1200
+ class: "h-8 w-8 flex items-center justify-center rounded border border-gray-200 text-gray-600 hover:bg-gray-100 transition-colors",
1201
+ title: unref(m).regeneratePdf,
1202
+ "aria-label": unref(m).regeneratePdf,
1203
+ "data-testid": "mulmo-script-regenerate-pdf-button",
1204
+ onClick: _cache[5] || (_cache[5] = ($event) => emit("generatePdf"))
1205
+ }, [..._cache[13] || (_cache[13] = [createElementVNode("span", { class: "material-icons text-base" }, "refresh", -1)])], 8, _hoisted_9$1)) : (openBlock(), createElementBlock("button", {
1206
+ key: 6,
1207
+ 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",
1208
+ disabled: __props.pdfGenerating,
1209
+ "data-testid": "mulmo-script-generate-pdf-button",
1210
+ onClick: _cache[6] || (_cache[6] = ($event) => emit("generatePdf"))
1211
+ }, [__props.pdfGenerating ? (openBlock(), createElementBlock("svg", _hoisted_11$1, [..._cache[14] || (_cache[14] = [createElementVNode("circle", {
1212
+ class: "opacity-25",
1213
+ cx: "12",
1214
+ cy: "12",
1215
+ r: "10",
1216
+ stroke: "currentColor",
1217
+ "stroke-width": "4"
1218
+ }, null, -1), createElementVNode("path", {
1219
+ class: "opacity-75",
1220
+ fill: "currentColor",
1221
+ d: "M4 12a8 8 0 018-8v8H4z"
1222
+ }, null, -1)])])) : createCommentVNode("", true), __props.pdfGenerating ? (openBlock(), createElementBlock("span", _hoisted_12$1, toDisplayString(unref(m).generatingPdf), 1)) : (openBlock(), createElementBlock(Fragment, { key: 2 }, [_cache[15] || (_cache[15] = createElementVNode("span", { class: "material-icons text-sm" }, "picture_as_pdf", -1)), createElementVNode("span", null, toDisplayString(unref(m).pdf), 1)], 64))], 8, _hoisted_10$1))
1223
+ ]);
1224
+ };
1225
+ }
1226
+ });
1227
+ //#endregion
1228
+ //#region src/vue/View.vue?vue&type=script&setup=true&lang.ts
1229
+ var _hoisted_1$1 = { class: "h-full bg-white flex flex-col overflow-hidden" };
1230
+ var _hoisted_2$1 = { class: "flex items-start justify-between px-6 py-4 border-b border-gray-100 shrink-0" };
1231
+ var _hoisted_3$1 = { class: "min-w-0 flex-1" };
1232
+ var _hoisted_4 = {
1233
+ class: "text-lg font-semibold text-gray-800 truncate",
1234
+ "data-testid": "mulmo-script-title"
1235
+ };
1236
+ var _hoisted_5 = {
1237
+ key: 0,
1238
+ class: "text-sm text-gray-500 mt-0.5 truncate",
1239
+ "data-testid": "mulmo-script-description"
1240
+ };
1241
+ var _hoisted_6 = { class: "flex items-center gap-3 mt-1 text-xs text-gray-400" };
1242
+ var _hoisted_7 = { key: 0 };
1243
+ var _hoisted_8 = {
1244
+ key: 1,
1245
+ class: "truncate"
1246
+ };
1247
+ var _hoisted_9 = {
1248
+ key: 0,
1249
+ "data-testid": "mulmo-script-movie-error-chip",
1250
+ 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"
1251
+ };
1252
+ var _hoisted_10 = { class: "flex-1 min-w-0" };
1253
+ var _hoisted_11 = { class: "font-medium" };
1254
+ var _hoisted_12 = { class: "break-words whitespace-pre-wrap mt-0.5" };
1255
+ var _hoisted_13 = ["disabled"];
1256
+ var _hoisted_14 = {
570
1257
  key: 2,
571
1258
  class: "flex-1 overflow-hidden",
572
1259
  "data-testid": "mulmo-script-deck-editor"
573
1260
  };
574
- var _hoisted_47 = { class: "flex gap-3 items-stretch" };
575
- var _hoisted_48 = [
1261
+ var _hoisted_15 = { class: "flex gap-3 items-stretch" };
1262
+ var _hoisted_16 = [
576
1263
  "onDragover",
577
1264
  "onDragleave",
578
1265
  "onDrop"
579
1266
  ];
580
- var _hoisted_49 = ["src", "data-testid"];
581
- var _hoisted_50 = [
1267
+ var _hoisted_17 = ["data-testid"];
1268
+ var _hoisted_18 = ["src", "data-testid"];
1269
+ var _hoisted_19 = [
582
1270
  "title",
583
1271
  "aria-label",
584
1272
  "data-testid",
585
1273
  "onClick"
586
1274
  ];
587
- var _hoisted_51 = [
1275
+ var _hoisted_20 = [
588
1276
  "src",
589
1277
  "alt",
590
1278
  "onClick"
591
1279
  ];
592
- var _hoisted_52 = [
1280
+ var _hoisted_21 = [
593
1281
  "title",
594
1282
  "aria-label",
595
1283
  "data-testid",
596
1284
  "onClick"
597
1285
  ];
598
- var _hoisted_53 = {
1286
+ var _hoisted_22 = {
599
1287
  key: 0,
600
1288
  class: "animate-spin w-5 h-5",
601
1289
  viewBox: "0 0 24 24",
602
1290
  fill: "none"
603
1291
  };
604
- var _hoisted_54 = {
1292
+ var _hoisted_23 = {
605
1293
  key: 1,
606
1294
  class: "material-icons text-3xl"
607
1295
  };
608
- var _hoisted_55 = ["disabled", "onClick"];
609
- var _hoisted_56 = {
1296
+ var _hoisted_24 = ["disabled", "onClick"];
1297
+ var _hoisted_25 = {
610
1298
  key: 3,
611
1299
  class: "w-full aspect-video flex flex-col items-center justify-center gap-1 p-2"
612
1300
  };
613
- var _hoisted_57 = { class: "text-xs text-green-500" };
614
- var _hoisted_58 = {
1301
+ var _hoisted_26 = { class: "text-xs text-green-500" };
1302
+ var _hoisted_27 = {
615
1303
  key: 1,
616
1304
  class: "text-xs text-red-400 text-center"
617
1305
  };
618
- var _hoisted_59 = {
1306
+ var _hoisted_28 = {
619
1307
  key: 0,
620
1308
  class: "text-xs text-gray-400 text-center italic leading-relaxed px-1"
621
1309
  };
622
- var _hoisted_60 = {
1310
+ var _hoisted_29 = {
623
1311
  key: 1,
624
1312
  class: "text-xs text-gray-300"
625
1313
  };
626
- var _hoisted_61 = {
1314
+ var _hoisted_30 = {
627
1315
  key: 2,
628
1316
  class: "absolute inset-0 flex items-center justify-center bg-blue-50/80 pointer-events-none"
629
1317
  };
630
- var _hoisted_62 = { class: "text-xs text-blue-500 font-medium" };
631
- var _hoisted_63 = {
1318
+ var _hoisted_31 = { class: "text-xs text-blue-500 font-medium" };
1319
+ var _hoisted_32 = {
632
1320
  key: 3,
633
1321
  class: "absolute bottom-0 inset-x-0 text-center text-xs text-gray-400 bg-white/70 py-0.5 pointer-events-none"
634
1322
  };
635
- var _hoisted_64 = ["onClick"];
636
- var _hoisted_65 = { class: "flex flex-col flex-1 min-w-0 px-2 py-1.5" };
637
- var _hoisted_66 = { class: "text-sm text-gray-800 leading-relaxed" };
638
- var _hoisted_67 = { class: "flex justify-between mt-auto pt-1" };
639
- var _hoisted_68 = { class: "flex items-center gap-1" };
640
- var _hoisted_69 = {
1323
+ var _hoisted_33 = ["onClick"];
1324
+ var _hoisted_34 = { class: "flex flex-col flex-1 min-w-0 px-2 py-1.5" };
1325
+ var _hoisted_35 = { class: "text-sm text-gray-800 leading-relaxed" };
1326
+ var _hoisted_36 = { class: "flex justify-between mt-auto pt-1" };
1327
+ var _hoisted_37 = { class: "flex items-center gap-1" };
1328
+ var _hoisted_38 = {
641
1329
  key: 0,
642
1330
  class: "animate-spin w-3 h-3 text-green-400",
643
1331
  viewBox: "0 0 24 24",
644
1332
  fill: "none"
645
1333
  };
646
- var _hoisted_70 = ["onClick"];
647
- var _hoisted_71 = ["title"];
648
- var _hoisted_72 = ["disabled", "onClick"];
649
- var _hoisted_73 = ["onClick"];
650
- var _hoisted_74 = [
1334
+ var _hoisted_39 = ["onClick"];
1335
+ var _hoisted_40 = ["title"];
1336
+ var _hoisted_41 = ["disabled", "onClick"];
1337
+ var _hoisted_42 = ["onClick"];
1338
+ var _hoisted_43 = [
651
1339
  "title",
652
1340
  "data-testid",
653
1341
  "onClick"
654
1342
  ];
655
- var _hoisted_75 = {
1343
+ var _hoisted_44 = {
656
1344
  key: 0,
657
1345
  class: "border-t border-gray-100"
658
1346
  };
659
- var _hoisted_76 = ["onUpdate:modelValue", "data-testid"];
660
- var _hoisted_77 = { class: "flex items-center justify-end gap-2 px-2 pb-2" };
661
- var _hoisted_78 = {
1347
+ var _hoisted_45 = ["onUpdate:modelValue", "data-testid"];
1348
+ var _hoisted_46 = { class: "flex items-center justify-end gap-2 px-2 pb-2" };
1349
+ var _hoisted_47 = {
662
1350
  key: 0,
663
1351
  class: "text-xs text-red-600",
664
1352
  role: "alert"
665
1353
  };
666
- var _hoisted_79 = [
1354
+ var _hoisted_48 = [
667
1355
  "disabled",
668
1356
  "data-testid",
669
1357
  "onClick"
670
1358
  ];
671
- var _hoisted_80 = {
1359
+ var _hoisted_49 = {
672
1360
  key: 0,
673
1361
  class: "flex items-center justify-center h-32 text-gray-400 text-sm"
674
1362
  };
675
- var _hoisted_81 = { class: "bottom-bar-wrapper" };
676
- var _hoisted_82 = { class: "editor-actions" };
677
- var _hoisted_83 = ["disabled"];
678
- var _hoisted_84 = ["title"];
679
- var _hoisted_85 = { class: "material-icons" };
680
- var _hoisted_86 = ["title"];
681
- var _hoisted_87 = { class: "flex items-center gap-4" };
682
- var _hoisted_88 = ["disabled"];
683
- var _hoisted_89 = { class: "flex flex-col items-center" };
684
- var _hoisted_90 = ["src"];
685
- var _hoisted_91 = {
686
- key: 0,
687
- class: "relative w-full h-1"
688
- };
689
- var _hoisted_92 = { class: "flex gap-1 h-full" };
690
- var _hoisted_93 = ["onClick"];
691
- var _hoisted_94 = {
692
- key: 0,
693
- 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"
694
- };
695
- var _hoisted_95 = ["disabled"];
696
- var _hoisted_96 = {
697
- key: 0,
698
- class: "relative w-screen flex justify-center px-16"
699
- };
700
- var _hoisted_97 = {
701
- key: 0,
702
- class: "max-w-[80vw] text-center text-white leading-relaxed text-[clamp(0.8rem,1.76vw,1.6rem)]"
703
- };
1363
+ var _hoisted_50 = { class: "bottom-bar-wrapper" };
1364
+ var _hoisted_51 = { class: "editor-actions" };
1365
+ var _hoisted_52 = ["disabled"];
1366
+ var _hoisted_53 = ["title"];
1367
+ var _hoisted_54 = { class: "material-icons" };
704
1368
  var SILENT_BEAT_DEFAULT_SEC = 3;
705
1369
  var MS_PER_SECOND = 1e3;
706
- var DECK_SAVE_DEBOUNCE_MS = 300;
707
1370
  var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineComponent({
708
1371
  __name: "View",
709
1372
  props: { selectedResult: {} },
@@ -728,39 +1391,34 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
728
1391
  const beatSaveErrors = reactive({});
729
1392
  const beatSaving = reactive({});
730
1393
  const localOverrides = reactive({});
731
- const movieGenerating = ref(false);
732
- const movieDownloading = ref(false);
733
- const moviePath = ref(null);
734
- const movieError = ref(null);
735
- const pdfGenerating = ref(false);
736
- const pdfDownloading = ref(false);
737
- const pdfPath = ref(null);
738
1394
  const beatAudios = reactive({});
739
1395
  const audioState = reactive({});
740
1396
  const audioErrors = reactive({});
741
- const beatMovies = reactive({});
742
- const beatMovieUrls = reactive({});
743
- const beatMovieOpen = reactive({});
744
- const beatMovieLoading = reactive({});
745
1397
  const playingAudio = ref(null);
746
1398
  const silentPlaybackTimer = ref(null);
747
1399
  const audioProgress = ref(0);
748
1400
  const beatListEl = ref(null);
749
1401
  const lightbox = ref(null);
750
- const charRenderState = reactive({});
751
- const charImages = reactive({});
752
- const charErrors = reactive({});
753
- const charDragOver = reactive({});
754
1402
  const beatDragOver = reactive({});
755
1403
  const anyBeatRendering = computed(() => Object.values(renderState).some((state) => state === "rendering"));
756
- const characterKeys = computed(() => {
757
- const imgs = script.value.imageParams?.images ?? {};
758
- return Object.keys(imgs).filter((key) => imgs[key]?.type === "imagePrompt");
759
- });
760
1404
  const chatSessionId = computed(() => adapter.chatSessionId?.value);
761
- function characterPrompt(key) {
762
- return script.value.imageParams?.images?.[key]?.prompt ?? "";
763
- }
1405
+ const { moviePath, movieGenerating, movieDownloading, movieError, pdfPath, pdfGenerating, pdfDownloading, generateMovie, downloadMovie, refreshMoviePath, generatePdf, downloadPdf, refreshPdfPath, resetMedia } = useMediaExport({
1406
+ api,
1407
+ adapter,
1408
+ filePath,
1409
+ chatSessionId
1410
+ });
1411
+ const { beatMovies, beatMovieUrls, beatMovieOpen, beatMovieLoading, loadExistingBeatMovie, playBeatMovie, closeBeatMovie, invalidateBeatMovie, resetBeatMovies } = useBeatMovie({
1412
+ api,
1413
+ adapter,
1414
+ filePath
1415
+ });
1416
+ const { charRenderState, charImages, charErrors, charDragOver, characterKeys, onCharDragOver, onCharDragLeave, onCharDrop, loadExistingCharacterImage, refreshMissingCharacterImages, renderCharacter, generateAllCharacters, resetCharacters } = useCharacterImages({
1417
+ api,
1418
+ filePath,
1419
+ chatSessionId,
1420
+ getImages: () => script.value.imageParams?.images
1421
+ });
764
1422
  function stopPlayingAudio() {
765
1423
  stopAllPlayback();
766
1424
  }
@@ -768,7 +1426,7 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
768
1426
  stopPlayingAudio();
769
1427
  lightbox.value = {
770
1428
  src: renderedImages[index],
771
- text: effectiveBeat(index).text,
1429
+ text: effectiveBeat$1(index).text,
772
1430
  index
773
1431
  };
774
1432
  }
@@ -779,7 +1437,7 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
779
1437
  const isPlayReady = computed(() => {
780
1438
  if (beats.value.length === 0) return false;
781
1439
  if (!renderedImages[0]) return false;
782
- if (effectiveBeat(0).text && !beatAudios[0]) return false;
1440
+ if (effectiveBeat$1(0).text && !beatAudios[0]) return false;
783
1441
  return true;
784
1442
  });
785
1443
  function playPresentation() {
@@ -800,19 +1458,19 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
800
1458
  }
801
1459
  function playBeat(index) {
802
1460
  stopAllPlayback();
803
- if (!Boolean(effectiveBeat(index).text)) {
1461
+ if (!Boolean(effectiveBeat$1(index).text)) {
804
1462
  scheduleSilentAdvance(index);
805
1463
  return;
806
1464
  }
807
1465
  if (beatAudios[index]) playAudio(index);
808
1466
  }
809
1467
  function scheduleSilentAdvance(index) {
810
- const raw = effectiveBeat(index).duration;
1468
+ const seconds = resolveSilentAdvanceSeconds(effectiveBeat$1(index).duration, SILENT_BEAT_DEFAULT_SEC);
811
1469
  const timer = setTimeout(() => {
812
1470
  if (silentPlaybackTimer.value?.index !== index) return;
813
1471
  silentPlaybackTimer.value = null;
814
1472
  if (lightbox.value?.index === index) advanceFromBeat(index);
815
- }, (typeof raw === "number" && Number.isFinite(raw) && raw > 0 ? raw : SILENT_BEAT_DEFAULT_SEC) * MS_PER_SECOND);
1473
+ }, seconds * MS_PER_SECOND);
816
1474
  silentPlaybackTimer.value = {
817
1475
  index,
818
1476
  timer
@@ -834,6 +1492,7 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
834
1492
  for (let i = lightbox.value.index + 1; i < beats.value.length; i++) if (renderedImages[i]) return true;
835
1493
  return false;
836
1494
  });
1495
+ const beatTexts = computed(() => beats.value.map((_, index) => effectiveBeat$1(index).text));
837
1496
  function jumpToBeat(index) {
838
1497
  if (!lightbox.value) return;
839
1498
  if (index === lightbox.value.index) return;
@@ -842,10 +1501,6 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
842
1501
  openLightbox(index);
843
1502
  if (wasPlaying) playBeat(index);
844
1503
  }
845
- function beatTooltip(index) {
846
- const text = effectiveBeat(index).text ?? "";
847
- return text.length > 80 ? `${text.slice(0, 80)}…` : text;
848
- }
849
1504
  function lightboxMove(delta) {
850
1505
  if (!lightbox.value) return;
851
1506
  const total = beats.value.length;
@@ -868,31 +1523,8 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
868
1523
  ...script.value,
869
1524
  beats: beats.value.map((beat, i) => localOverrides[i] ?? beat)
870
1525
  }));
871
- const scriptSourceText = computed(() => JSON.stringify(effectiveScript.value, null, 2));
872
- const isDeck = computed(() => isAllSlideDeck(effectiveScript.value));
873
- const deckScriptInput = computed(() => effectiveScript.value);
874
- let deckSaveTimer = null;
875
- let pendingDeckScript = null;
876
- function scheduleDeckSave(next) {
877
- pendingDeckScript = next;
878
- if (deckSaveTimer) clearTimeout(deckSaveTimer);
879
- deckSaveTimer = setTimeout(() => {
880
- flushDeckSave();
881
- }, DECK_SAVE_DEBOUNCE_MS);
882
- }
883
- async function flushDeckSave() {
884
- deckSaveTimer = null;
885
- const next = pendingDeckScript;
886
- pendingDeckScript = null;
887
- if (!next || !filePath.value) return;
888
- const response = await api.call("updateScript", {
889
- filePath: filePath.value,
890
- script: next
891
- });
892
- if (!response.ok) {
893
- console.error("[presentMulmoScript] deck save failed:", response.error);
894
- return;
895
- }
1526
+ const scriptSourceText$1 = computed(() => scriptSourceText(effectiveScript.value));
1527
+ function commitScript(next) {
896
1528
  emit("updateResult", {
897
1529
  ...props.selectedResult,
898
1530
  data: {
@@ -901,14 +1533,14 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
901
1533
  }
902
1534
  });
903
1535
  }
904
- function onDeckUpdate(next) {
905
- scheduleDeckSave(next);
906
- }
1536
+ const { isDeck, deckScriptInput, onDeckUpdate, flushPendingDeckSave } = useDeckEditor({
1537
+ api,
1538
+ filePath,
1539
+ effectiveScript,
1540
+ commitScript
1541
+ });
907
1542
  onBeforeUnmount(() => {
908
- if (deckSaveTimer) {
909
- clearTimeout(deckSaveTimer);
910
- flushDeckSave();
911
- }
1543
+ flushPendingDeckSave();
912
1544
  resetBeatMovies();
913
1545
  unsubscribeGenerationEvents();
914
1546
  });
@@ -925,11 +1557,11 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
925
1557
  async function onSourceToggle(open) {
926
1558
  editing.value = open;
927
1559
  if (open) {
928
- let text = scriptSourceText.value;
1560
+ let text = scriptSourceText$1.value;
929
1561
  if (filePath.value) {
930
1562
  const response = await api.call("save", { filePath: filePath.value });
931
1563
  const diskScript = response.ok ? response.data.script : void 0;
932
- if (diskScript) text = JSON.stringify(diskScript, null, 2);
1564
+ if (diskScript) text = scriptSourceText(diskScript);
933
1565
  }
934
1566
  editableSource.value = text;
935
1567
  loadedSource.value = text;
@@ -954,31 +1586,25 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
954
1586
  alert(response.error || "Update failed");
955
1587
  return;
956
1588
  }
957
- emit("updateResult", {
958
- ...props.selectedResult,
959
- data: {
960
- ...props.selectedResult.data,
961
- script: parsed
962
- }
963
- });
1589
+ commitScript(parsed);
964
1590
  if (sourceDetails.value) sourceDetails.value.open = false;
965
1591
  await initializeScript();
966
1592
  }
967
1593
  async function copyText() {
968
- await copy(scriptSourceText.value);
1594
+ await copy(scriptSourceText$1.value);
969
1595
  }
970
- function effectiveBeat(index) {
971
- return localOverrides[index] ?? beats.value[index] ?? {};
1596
+ function effectiveBeat$1(index) {
1597
+ return effectiveBeat(localOverrides, beats.value, index);
972
1598
  }
973
1599
  function toggleSource(index) {
974
1600
  if (!sourceOpen[index]) {
975
- sourceText[index] = JSON.stringify(effectiveBeat(index), null, 2);
1601
+ sourceText[index] = scriptSourceText(effectiveBeat$1(index));
976
1602
  Reflect.deleteProperty(beatSaveErrors, index);
977
1603
  }
978
1604
  sourceOpen[index] = !sourceOpen[index];
979
1605
  }
980
- function isValidBeat(index) {
981
- return validateBeatJSON(sourceText[index] ?? "", mulmoBeatSchema);
1606
+ function isValidBeat$1(index) {
1607
+ return isValidBeat(sourceText[index], mulmoBeatSchema);
982
1608
  }
983
1609
  async function updateBeat(index) {
984
1610
  let beat;
@@ -991,8 +1617,8 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
991
1617
  };
992
1618
  return;
993
1619
  }
994
- const prevImage = JSON.stringify(effectiveBeat(index).image);
995
- const prevText = effectiveBeat(index).text;
1620
+ const prevImage = JSON.stringify(effectiveBeat$1(index).image);
1621
+ const prevText = effectiveBeat$1(index).text;
996
1622
  const requestedFilePath = filePath.value;
997
1623
  Reflect.deleteProperty(beatSaveErrors, index);
998
1624
  beatSaving[index] = true;
@@ -1001,7 +1627,7 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
1001
1627
  beatIndex: index,
1002
1628
  beat
1003
1629
  });
1004
- if (staleSince(requestedFilePath)) return;
1630
+ if (staleSince$1(requestedFilePath)) return;
1005
1631
  Reflect.deleteProperty(beatSaving, index);
1006
1632
  if (!response.ok) {
1007
1633
  beatSaveErrors[index] = {
@@ -1032,7 +1658,7 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
1032
1658
  beatIndex: index,
1033
1659
  chatSessionId: chatSessionId.value
1034
1660
  });
1035
- if (staleSince(requestedFilePath)) return;
1661
+ if (staleSince$1(requestedFilePath)) return;
1036
1662
  if (!response.ok) {
1037
1663
  renderErrors[index] = response.error || "Render failed";
1038
1664
  renderState[index] = "error";
@@ -1041,7 +1667,7 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
1041
1667
  renderedImages[index] = response.data.image ?? "";
1042
1668
  renderState[index] = "done";
1043
1669
  refreshMissingCharacterImages();
1044
- if (beatMayHaveMovie(effectiveBeat(index))) loadExistingBeatMovie(index);
1670
+ if (beatMayHaveMovie(effectiveBeat$1(index))) loadExistingBeatMovie(index);
1045
1671
  }
1046
1672
  async function regenerateBeat(index) {
1047
1673
  const requestedFilePath = filePath.value;
@@ -1054,7 +1680,7 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
1054
1680
  force: true,
1055
1681
  chatSessionId: chatSessionId.value
1056
1682
  });
1057
- if (staleSince(requestedFilePath)) return;
1683
+ if (staleSince$1(requestedFilePath)) return;
1058
1684
  if (!response.ok) {
1059
1685
  renderErrors[index] = response.error || "Render failed";
1060
1686
  renderState[index] = "error";
@@ -1062,10 +1688,10 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
1062
1688
  }
1063
1689
  renderedImages[index] = response.data.image ?? "";
1064
1690
  renderState[index] = "done";
1065
- if (beatMayHaveMovie(effectiveBeat(index))) loadExistingBeatMovie(index);
1691
+ if (beatMayHaveMovie(effectiveBeat$1(index))) loadExistingBeatMovie(index);
1066
1692
  }
1067
- function staleSince(requestedFilePath) {
1068
- return filePath.value !== requestedFilePath;
1693
+ function staleSince$1(requestedFilePath) {
1694
+ return staleSince(filePath.value, requestedFilePath);
1069
1695
  }
1070
1696
  async function loadExistingBeatImage(index) {
1071
1697
  const requestedFilePath = filePath.value;
@@ -1073,7 +1699,7 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
1073
1699
  filePath: requestedFilePath,
1074
1700
  beatIndex: index
1075
1701
  });
1076
- if (staleSince(requestedFilePath)) return;
1702
+ if (staleSince$1(requestedFilePath)) return;
1077
1703
  if (response.ok && response.data.image) {
1078
1704
  renderedImages[index] = response.data.image;
1079
1705
  renderState[index] = "done";
@@ -1085,61 +1711,12 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
1085
1711
  filePath: requestedFilePath,
1086
1712
  beatIndex: index
1087
1713
  });
1088
- if (staleSince(requestedFilePath)) return;
1714
+ if (staleSince$1(requestedFilePath)) return;
1089
1715
  if (response.ok && response.data.audio) {
1090
1716
  beatAudios[index] = response.data.audio;
1091
1717
  audioState[index] = "done";
1092
1718
  }
1093
1719
  }
1094
- async function loadExistingBeatMovie(index) {
1095
- const requestedFilePath = filePath.value;
1096
- const response = await api.call("beatMovie", {
1097
- filePath: requestedFilePath,
1098
- beatIndex: index
1099
- });
1100
- if (staleSince(requestedFilePath)) return;
1101
- if (response.ok && response.data.moviePath) beatMovies[index] = response.data.moviePath;
1102
- }
1103
- async function playBeatMovie(index) {
1104
- const fetchMediaBlob = adapter.fetchMediaBlob;
1105
- if (!fetchMediaBlob || !beatMovies[index] || beatMovieLoading[index]) return;
1106
- if (beatMovieUrls[index]) {
1107
- beatMovieOpen[index] = true;
1108
- return;
1109
- }
1110
- beatMovieLoading[index] = true;
1111
- try {
1112
- const blob = new Blob([await fetchMediaBlob({ moviePath: beatMovies[index] })], { type: "video/mp4" });
1113
- beatMovieUrls[index] = URL.createObjectURL(blob);
1114
- beatMovieOpen[index] = true;
1115
- } catch (err) {
1116
- alert(errorMessage(err));
1117
- } finally {
1118
- Reflect.deleteProperty(beatMovieLoading, index);
1119
- }
1120
- }
1121
- function closeBeatMovie(index) {
1122
- Reflect.deleteProperty(beatMovieOpen, index);
1123
- }
1124
- function invalidateBeatMovie(index) {
1125
- if (beatMovieUrls[index]) URL.revokeObjectURL(beatMovieUrls[index]);
1126
- [
1127
- beatMovies,
1128
- beatMovieUrls,
1129
- beatMovieOpen
1130
- ].forEach((map) => Reflect.deleteProperty(map, index));
1131
- }
1132
- function resetBeatMovies() {
1133
- Object.values(beatMovieUrls).forEach((url) => URL.revokeObjectURL(url));
1134
- [
1135
- beatMovies,
1136
- beatMovieUrls,
1137
- beatMovieOpen,
1138
- beatMovieLoading
1139
- ].forEach((map) => {
1140
- Object.keys(map).forEach((key) => Reflect.deleteProperty(map, key));
1141
- });
1142
- }
1143
1720
  async function generateAudio(index) {
1144
1721
  const requestedFilePath = filePath.value;
1145
1722
  audioState[index] = "generating";
@@ -1149,7 +1726,7 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
1149
1726
  beatIndex: index,
1150
1727
  chatSessionId: chatSessionId.value
1151
1728
  });
1152
- if (staleSince(requestedFilePath)) return;
1729
+ if (staleSince$1(requestedFilePath)) return;
1153
1730
  if (!response.ok) {
1154
1731
  audioErrors[index] = response.error || "Audio generation failed";
1155
1732
  audioState[index] = "error";
@@ -1202,12 +1779,7 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
1202
1779
  Reflect.deleteProperty(renderErrors, index);
1203
1780
  let imageData;
1204
1781
  try {
1205
- imageData = await new Promise((resolve, reject) => {
1206
- const reader = new FileReader();
1207
- reader.onload = () => resolve(reader.result);
1208
- reader.onerror = reject;
1209
- reader.readAsDataURL(file);
1210
- });
1782
+ imageData = await readFileAsDataUrl(file);
1211
1783
  } catch (err) {
1212
1784
  renderErrors[index] = errorMessage(err);
1213
1785
  renderState[index] = "error";
@@ -1219,7 +1791,7 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
1219
1791
  beatIndex: index,
1220
1792
  imageData
1221
1793
  });
1222
- if (staleSince(requestedFilePath)) return;
1794
+ if (staleSince$1(requestedFilePath)) return;
1223
1795
  if (!response.ok) {
1224
1796
  renderErrors[index] = response.error || "Upload failed";
1225
1797
  renderState[index] = "error";
@@ -1228,49 +1800,6 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
1228
1800
  renderedImages[index] = response.data.image ?? "";
1229
1801
  renderState[index] = "done";
1230
1802
  }
1231
- function onCharDragOver(event, key) {
1232
- if (!event.dataTransfer?.types.includes("Files")) return;
1233
- event.preventDefault();
1234
- charDragOver[key] = true;
1235
- }
1236
- function onCharDragLeave(key) {
1237
- charDragOver[key] = false;
1238
- }
1239
- async function onCharDrop(event, key) {
1240
- event.preventDefault();
1241
- charDragOver[key] = false;
1242
- const file = event.dataTransfer?.files[0];
1243
- if (!file || !file.type.startsWith("image/")) return;
1244
- charRenderState[key] = "rendering";
1245
- Reflect.deleteProperty(charErrors, key);
1246
- let imageData;
1247
- try {
1248
- imageData = await new Promise((resolve, reject) => {
1249
- const reader = new FileReader();
1250
- reader.onload = () => resolve(reader.result);
1251
- reader.onerror = reject;
1252
- reader.readAsDataURL(file);
1253
- });
1254
- } catch (err) {
1255
- charErrors[key] = errorMessage(err);
1256
- charRenderState[key] = "error";
1257
- return;
1258
- }
1259
- const requestedFilePath = filePath.value;
1260
- const response = await api.call("uploadCharacterImage", {
1261
- filePath: requestedFilePath,
1262
- key,
1263
- imageData
1264
- });
1265
- if (staleSince(requestedFilePath)) return;
1266
- if (!response.ok) {
1267
- charErrors[key] = response.error || "Upload failed";
1268
- charRenderState[key] = "error";
1269
- return;
1270
- }
1271
- charImages[key] = response.data.image ?? "";
1272
- charRenderState[key] = "done";
1273
- }
1274
1803
  function openCharacterLightbox(key) {
1275
1804
  stopAllPlayback();
1276
1805
  lightbox.value = {
@@ -1280,43 +1809,6 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
1280
1809
  isCharacter: true
1281
1810
  };
1282
1811
  }
1283
- async function loadExistingCharacterImage(key) {
1284
- const requestedFilePath = filePath.value;
1285
- const response = await api.call("characterImage", {
1286
- filePath: requestedFilePath,
1287
- key
1288
- });
1289
- if (staleSince(requestedFilePath)) return;
1290
- if (response.ok && response.data.image) {
1291
- charImages[key] = response.data.image;
1292
- charRenderState[key] = "done";
1293
- }
1294
- }
1295
- function refreshMissingCharacterImages() {
1296
- getMissingCharacterKeys(characterKeys.value, charImages, charRenderState).forEach((key) => loadExistingCharacterImage(key));
1297
- }
1298
- async function renderCharacter(key, force) {
1299
- const requestedFilePath = filePath.value;
1300
- charRenderState[key] = "rendering";
1301
- Reflect.deleteProperty(charErrors, key);
1302
- const response = await api.call("renderCharacter", {
1303
- filePath: requestedFilePath,
1304
- key,
1305
- force,
1306
- chatSessionId: chatSessionId.value
1307
- });
1308
- if (staleSince(requestedFilePath)) return;
1309
- if (!response.ok) {
1310
- charErrors[key] = response.error || "Render failed";
1311
- charRenderState[key] = "error";
1312
- return;
1313
- }
1314
- charImages[key] = response.data.image ?? "";
1315
- charRenderState[key] = "done";
1316
- }
1317
- async function generateAllCharacters() {
1318
- await Promise.all(characterKeys.value.filter((key) => charRenderState[key] !== "rendering").map((key) => renderCharacter(key, false)));
1319
- }
1320
1812
  async function hydrateBeatImage(beat, index, hasCharacters, autoRenderTypes) {
1321
1813
  await loadExistingBeatImage(index);
1322
1814
  if (renderedImages[index]) return;
@@ -1358,39 +1850,16 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
1358
1850
  const diskScript = response.data.script;
1359
1851
  if (!diskScript) return;
1360
1852
  if (isSameScript(diskScript, script.value)) return;
1361
- emit("updateResult", {
1362
- ...props.selectedResult,
1363
- data: {
1364
- ...props.selectedResult.data,
1365
- script: diskScript
1366
- }
1367
- });
1853
+ commitScript(diskScript);
1368
1854
  }
1369
1855
  async function initializeScript() {
1370
1856
  stopAllPlayback();
1371
1857
  lightbox.value = null;
1372
1858
  if (beatListEl.value) beatListEl.value.scrollTop = 0;
1373
- Object.keys(renderState).forEach((key) => Reflect.deleteProperty(renderState, key));
1374
- Object.keys(renderedImages).forEach((key) => Reflect.deleteProperty(renderedImages, key));
1375
- Object.keys(renderErrors).forEach((key) => Reflect.deleteProperty(renderErrors, key));
1376
- Object.keys(sourceOpen).forEach((key) => Reflect.deleteProperty(sourceOpen, key));
1377
- Object.keys(sourceText).forEach((key) => Reflect.deleteProperty(sourceText, key));
1378
- Object.keys(beatSaveErrors).forEach((key) => Reflect.deleteProperty(beatSaveErrors, key));
1379
- Object.keys(beatSaving).forEach((key) => Reflect.deleteProperty(beatSaving, key));
1380
- Object.keys(localOverrides).forEach((key) => Reflect.deleteProperty(localOverrides, key));
1381
- Object.keys(beatAudios).forEach((key) => Reflect.deleteProperty(beatAudios, key));
1382
- Object.keys(audioState).forEach((key) => Reflect.deleteProperty(audioState, key));
1383
- Object.keys(audioErrors).forEach((key) => Reflect.deleteProperty(audioErrors, key));
1384
- Object.keys(charRenderState).forEach((key) => Reflect.deleteProperty(charRenderState, key));
1385
- Object.keys(charImages).forEach((key) => Reflect.deleteProperty(charImages, key));
1386
- Object.keys(charErrors).forEach((key) => Reflect.deleteProperty(charErrors, key));
1387
- Object.keys(beatDragOver).forEach((key) => Reflect.deleteProperty(beatDragOver, key));
1859
+ clearReactiveRecords(renderState, renderedImages, renderErrors, sourceOpen, sourceText, beatSaveErrors, beatSaving, localOverrides, beatAudios, audioState, audioErrors, beatDragOver);
1860
+ resetCharacters();
1388
1861
  resetBeatMovies();
1389
- moviePath.value = null;
1390
- pdfPath.value = null;
1391
- movieGenerating.value = false;
1392
- pdfGenerating.value = false;
1393
- movieError.value = null;
1862
+ resetMedia();
1394
1863
  if (sourceDetails.value) sourceDetails.value.open = false;
1395
1864
  await refreshScriptFromDisk();
1396
1865
  const AUTO_RENDER_TYPES = [
@@ -1449,7 +1918,7 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
1449
1918
  if (entry.kind === "beatImage") {
1450
1919
  const idx = Number(entry.key);
1451
1920
  await loadExistingBeatImage(idx);
1452
- if (beatMayHaveMovie(effectiveBeat(idx))) await loadExistingBeatMovie(idx);
1921
+ if (beatMayHaveMovie(effectiveBeat$1(idx))) await loadExistingBeatMovie(idx);
1453
1922
  if (renderState[idx] === "rendering") Reflect.deleteProperty(renderState, idx);
1454
1923
  refreshMissingCharacterImages();
1455
1924
  } else if (entry.kind === "beatAudio") {
@@ -1467,95 +1936,6 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
1467
1936
  await refreshPdfPath();
1468
1937
  }
1469
1938
  }
1470
- async function refreshMoviePath() {
1471
- const requestedFilePath = filePath.value;
1472
- if (!requestedFilePath) return;
1473
- const response = await api.call("movieStatus", { filePath: requestedFilePath });
1474
- if (filePath.value !== requestedFilePath) return;
1475
- if (response.ok && response.data.moviePath) moviePath.value = response.data.moviePath;
1476
- }
1477
- async function generateMovie() {
1478
- const requestedFilePath = filePath.value;
1479
- movieGenerating.value = true;
1480
- movieError.value = null;
1481
- const response = await api.call("generateMovie", {
1482
- filePath: requestedFilePath,
1483
- chatSessionId: chatSessionId.value
1484
- });
1485
- if (filePath.value !== requestedFilePath) return;
1486
- movieGenerating.value = false;
1487
- if (!response.ok) {
1488
- movieError.value = response.error;
1489
- return;
1490
- }
1491
- moviePath.value = response.data.moviePath;
1492
- }
1493
- async function downloadMovie() {
1494
- const fetchMediaBlob = adapter.fetchMediaBlob;
1495
- if (!fetchMediaBlob || !moviePath.value || movieDownloading.value) return;
1496
- movieDownloading.value = true;
1497
- let objectUrl = null;
1498
- try {
1499
- const blob = await fetchMediaBlob({ moviePath: moviePath.value });
1500
- objectUrl = URL.createObjectURL(blob);
1501
- const filename = moviePath.value.split("/").pop() ?? "movie.mp4";
1502
- const anchor = document.createElement("a");
1503
- anchor.href = objectUrl;
1504
- anchor.download = filename;
1505
- document.body.appendChild(anchor);
1506
- anchor.click();
1507
- anchor.remove();
1508
- } catch (err) {
1509
- alert(errorMessage(err));
1510
- } finally {
1511
- if (objectUrl) URL.revokeObjectURL(objectUrl);
1512
- movieDownloading.value = false;
1513
- }
1514
- }
1515
- async function refreshPdfPath() {
1516
- const requestedFilePath = filePath.value;
1517
- if (!requestedFilePath) return;
1518
- const response = await api.call("pdfStatus", { filePath: requestedFilePath });
1519
- if (filePath.value !== requestedFilePath) return;
1520
- if (response.ok && response.data.pdfPath) pdfPath.value = response.data.pdfPath;
1521
- }
1522
- async function generatePdf() {
1523
- const requestedFilePath = filePath.value;
1524
- pdfGenerating.value = true;
1525
- const response = await api.call("generatePdf", {
1526
- filePath: requestedFilePath,
1527
- chatSessionId: chatSessionId.value
1528
- });
1529
- if (filePath.value !== requestedFilePath) return;
1530
- pdfGenerating.value = false;
1531
- if (!response.ok) {
1532
- alert(response.error);
1533
- return;
1534
- }
1535
- pdfPath.value = response.data.pdfPath;
1536
- }
1537
- async function downloadPdf() {
1538
- const fetchMediaBlob = adapter.fetchMediaBlob;
1539
- if (!fetchMediaBlob || !pdfPath.value || pdfDownloading.value) return;
1540
- pdfDownloading.value = true;
1541
- let objectUrl = null;
1542
- try {
1543
- const blob = await fetchMediaBlob({ pdfPath: pdfPath.value });
1544
- objectUrl = URL.createObjectURL(blob);
1545
- const filename = pdfPath.value.split("/").pop() ?? "deck.pdf";
1546
- const anchor = document.createElement("a");
1547
- anchor.href = objectUrl;
1548
- anchor.download = filename;
1549
- document.body.appendChild(anchor);
1550
- anchor.click();
1551
- anchor.remove();
1552
- } catch (err) {
1553
- alert(errorMessage(err));
1554
- } finally {
1555
- if (objectUrl) URL.revokeObjectURL(objectUrl);
1556
- pdfDownloading.value = false;
1557
- }
1558
- }
1559
1939
  return (_ctx, _cache) => {
1560
1940
  return openBlock(), createElementBlock("div", _hoisted_1$1, [
1561
1941
  createElementVNode("div", _hoisted_2$1, [createElementVNode("div", _hoisted_3$1, [
@@ -1566,153 +1946,80 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
1566
1946
  script.value.lang ? (openBlock(), createElementBlock("span", _hoisted_7, toDisplayString(script.value.lang), 1)) : createCommentVNode("", true),
1567
1947
  filePath.value ? (openBlock(), createElementBlock("span", _hoisted_8, toDisplayString(filePath.value), 1)) : createCommentVNode("", true)
1568
1948
  ])
1569
- ]), createElementVNode("div", _hoisted_9, [
1570
- moviePath.value && !movieGenerating.value ? (openBlock(), createElementBlock("button", {
1571
- key: 0,
1572
- 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",
1573
- disabled: !isPlayReady.value,
1574
- title: unref(m).playPresentation,
1575
- "aria-label": unref(m).playPresentation,
1576
- onClick: playPresentation
1577
- }, [..._cache[6] || (_cache[6] = [createElementVNode("span", { class: "material-icons text-base" }, "play_arrow", -1)])], 8, _hoisted_10)) : createCommentVNode("", true),
1578
- moviePath.value && !movieGenerating.value && canFetchMedia.value ? (openBlock(), createElementBlock("button", {
1579
- key: 1,
1580
- 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",
1581
- disabled: movieDownloading.value,
1582
- "data-testid": "mulmo-script-download-movie-button",
1583
- onClick: downloadMovie
1584
- }, [_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),
1585
- moviePath.value && !movieGenerating.value ? (openBlock(), createElementBlock("button", {
1586
- key: 2,
1587
- class: "h-8 w-8 flex items-center justify-center rounded border border-gray-200 text-gray-600 hover:bg-gray-100 transition-colors",
1588
- title: unref(m).regenerateMovie,
1589
- "aria-label": unref(m).regenerateMovie,
1590
- "data-testid": "mulmo-script-regenerate-movie-button",
1591
- onClick: generateMovie
1592
- }, [..._cache[8] || (_cache[8] = [createElementVNode("span", { class: "material-icons text-base" }, "refresh", -1)])], 8, _hoisted_12)) : (openBlock(), createElementBlock("button", {
1593
- key: 3,
1594
- 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",
1595
- disabled: movieGenerating.value,
1596
- "data-testid": "mulmo-script-generate-movie-button",
1597
- onClick: generateMovie
1598
- }, [movieGenerating.value ? (openBlock(), createElementBlock("svg", _hoisted_14, [..._cache[9] || (_cache[9] = [createElementVNode("circle", {
1599
- class: "opacity-25",
1600
- cx: "12",
1601
- cy: "12",
1602
- r: "10",
1603
- stroke: "currentColor",
1604
- "stroke-width": "4"
1605
- }, null, -1), createElementVNode("path", {
1606
- class: "opacity-75",
1607
- fill: "currentColor",
1608
- d: "M4 12a8 8 0 018-8v8H4z"
1609
- }, 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)),
1610
- pdfPath.value && !pdfGenerating.value && canFetchMedia.value ? (openBlock(), createElementBlock("button", {
1611
- key: 4,
1612
- 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",
1613
- disabled: pdfDownloading.value,
1614
- "data-testid": "mulmo-script-download-pdf-button",
1615
- onClick: downloadPdf
1616
- }, [_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),
1617
- pdfPath.value && !pdfGenerating.value ? (openBlock(), createElementBlock("button", {
1618
- key: 5,
1619
- class: "h-8 w-8 flex items-center justify-center rounded border border-gray-200 text-gray-600 hover:bg-gray-100 transition-colors",
1620
- title: unref(m).regeneratePdf,
1621
- "aria-label": unref(m).regeneratePdf,
1622
- "data-testid": "mulmo-script-regenerate-pdf-button",
1623
- onClick: generatePdf
1624
- }, [..._cache[12] || (_cache[12] = [createElementVNode("span", { class: "material-icons text-base" }, "refresh", -1)])], 8, _hoisted_17)) : (openBlock(), createElementBlock("button", {
1625
- key: 6,
1626
- 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",
1627
- disabled: pdfGenerating.value,
1628
- "data-testid": "mulmo-script-generate-pdf-button",
1629
- onClick: generatePdf
1630
- }, [pdfGenerating.value ? (openBlock(), createElementBlock("svg", _hoisted_19, [..._cache[13] || (_cache[13] = [createElementVNode("circle", {
1631
- class: "opacity-25",
1632
- cx: "12",
1633
- cy: "12",
1634
- r: "10",
1635
- stroke: "currentColor",
1636
- "stroke-width": "4"
1637
- }, null, -1), createElementVNode("path", {
1638
- class: "opacity-75",
1639
- fill: "currentColor",
1640
- d: "M4 12a8 8 0 018-8v8H4z"
1641
- }, 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))
1949
+ ]), createVNode(MulmoScriptToolbar_default, {
1950
+ "movie-path": unref(moviePath),
1951
+ "movie-generating": unref(movieGenerating),
1952
+ "movie-downloading": unref(movieDownloading),
1953
+ "is-play-ready": isPlayReady.value,
1954
+ "can-fetch-media": canFetchMedia.value,
1955
+ "pdf-path": unref(pdfPath),
1956
+ "pdf-generating": unref(pdfGenerating),
1957
+ "pdf-downloading": unref(pdfDownloading),
1958
+ onPlay: playPresentation,
1959
+ onGenerateMovie: unref(generateMovie),
1960
+ onDownloadMovie: unref(downloadMovie),
1961
+ onGeneratePdf: unref(generatePdf),
1962
+ onDownloadPdf: unref(downloadPdf)
1963
+ }, null, 8, [
1964
+ "movie-path",
1965
+ "movie-generating",
1966
+ "movie-downloading",
1967
+ "is-play-ready",
1968
+ "can-fetch-media",
1969
+ "pdf-path",
1970
+ "pdf-generating",
1971
+ "pdf-downloading",
1972
+ "onGenerateMovie",
1973
+ "onDownloadMovie",
1974
+ "onGeneratePdf",
1975
+ "onDownloadPdf"
1642
1976
  ])]),
1643
- movieError.value ? (openBlock(), createElementBlock("div", _hoisted_21, [
1644
- _cache[15] || (_cache[15] = createElementVNode("span", { class: "material-icons text-base shrink-0 mt-px" }, "error_outline", -1)),
1645
- createElementVNode("div", _hoisted_22, [createElementVNode("div", _hoisted_23, toDisplayString(unref(m).movieGenerationFailed), 1), createElementVNode("div", _hoisted_24, toDisplayString(movieError.value), 1)]),
1977
+ unref(movieError) ? (openBlock(), createElementBlock("div", _hoisted_9, [
1978
+ _cache[3] || (_cache[3] = createElementVNode("span", { class: "material-icons text-base shrink-0 mt-px" }, "error_outline", -1)),
1979
+ createElementVNode("div", _hoisted_10, [createElementVNode("div", _hoisted_11, toDisplayString(unref(m).movieGenerationFailed), 1), createElementVNode("div", _hoisted_12, toDisplayString(unref(movieError)), 1)]),
1646
1980
  createElementVNode("button", {
1647
1981
  class: "shrink-0 h-7 px-2 text-xs rounded border border-red-300 text-red-700 hover:bg-red-100 disabled:opacity-50",
1648
- disabled: movieGenerating.value,
1982
+ disabled: unref(movieGenerating),
1649
1983
  "data-testid": "mulmo-script-movie-retry-button",
1650
- onClick: generateMovie
1651
- }, toDisplayString(unref(m).retry), 9, _hoisted_25)
1984
+ onClick: _cache[0] || (_cache[0] = (...args) => unref(generateMovie) && unref(generateMovie)(...args))
1985
+ }, toDisplayString(unref(m).retry), 9, _hoisted_13)
1652
1986
  ])) : createCommentVNode("", true),
1653
- characterKeys.value.length > 0 ? (openBlock(), createElementBlock("div", _hoisted_26, [createElementVNode("div", _hoisted_27, [createElementVNode("span", _hoisted_28, toDisplayString(unref(m).characters), 1), createElementVNode("button", {
1654
- class: "px-2 py-0.5 text-xs rounded border border-gray-300 text-gray-500 hover:bg-gray-50 disabled:opacity-50",
1655
- disabled: movieGenerating.value || anyBeatRendering.value || characterKeys.value.every((key) => charRenderState[key] === "rendering"),
1656
- onClick: generateAllCharacters
1657
- }, toDisplayString(unref(m).generateAll), 9, _hoisted_29)]), createElementVNode("div", _hoisted_30, [(openBlock(true), createElementBlock(Fragment, null, renderList(characterKeys.value, (key) => {
1658
- return openBlock(), createElementBlock("div", {
1659
- key,
1660
- class: "flex flex-col items-center gap-1 w-36"
1661
- }, [createElementVNode("div", {
1662
- 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"]),
1663
- onDragover: ($event) => onCharDragOver($event, key),
1664
- onDragleave: ($event) => onCharDragLeave(key),
1665
- onDrop: ($event) => onCharDrop($event, key)
1666
- }, [
1667
- charImages[key] ? (openBlock(), createElementBlock("img", {
1668
- key: 0,
1669
- src: charImages[key],
1670
- class: "w-full h-full object-cover cursor-zoom-in",
1671
- alt: key,
1672
- onClick: ($event) => openCharacterLightbox(key)
1673
- }, null, 8, _hoisted_32)) : charRenderState[key] === "rendering" ? (openBlock(), createElementBlock("svg", _hoisted_33, [..._cache[16] || (_cache[16] = [createElementVNode("circle", {
1674
- class: "opacity-25",
1675
- cx: "12",
1676
- cy: "12",
1677
- r: "10",
1678
- stroke: "currentColor",
1679
- "stroke-width": "4"
1680
- }, null, -1), createElementVNode("path", {
1681
- class: "opacity-75",
1682
- fill: "currentColor",
1683
- d: "M4 12a8 8 0 018-8v8H4z"
1684
- }, null, -1)])])) : charRenderState[key] === "error" ? (openBlock(), createElementBlock("span", _hoisted_34, toDisplayString(charErrors[key]), 1)) : (openBlock(), createElementBlock("span", _hoisted_35, toDisplayString(characterPrompt(key)), 1)),
1685
- !charDragOver[key] ? (openBlock(), createElementBlock("div", _hoisted_36, toDisplayString(unref(m).orDropImage), 1)) : createCommentVNode("", true),
1686
- charDragOver[key] ? (openBlock(), createElementBlock("div", _hoisted_37, [createElementVNode("span", _hoisted_38, toDisplayString(unref(m).drop), 1)])) : createCommentVNode("", true),
1687
- charImages[key] && charRenderState[key] !== "rendering" ? (openBlock(), createElementBlock("button", {
1688
- key: 6,
1689
- 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"]),
1690
- disabled: movieGenerating.value || anyBeatRendering.value,
1691
- onClick: withModifiers(($event) => renderCharacter(key, true), ["stop"])
1692
- }, [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", {
1693
- key: 7,
1694
- 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"]),
1695
- disabled: movieGenerating.value || anyBeatRendering.value,
1696
- onClick: withModifiers(($event) => renderCharacter(key, false), ["stop"])
1697
- }, [movieGenerating.value || anyBeatRendering.value ? (openBlock(), createElementBlock("svg", _hoisted_43, [..._cache[17] || (_cache[17] = [createElementVNode("circle", {
1698
- class: "opacity-25",
1699
- cx: "12",
1700
- cy: "12",
1701
- r: "10",
1702
- stroke: "currentColor",
1703
- "stroke-width": "4"
1704
- }, null, -1), createElementVNode("path", {
1705
- class: "opacity-75",
1706
- fill: "currentColor",
1707
- d: "M4 12a8 8 0 018-8v8H4z"
1708
- }, null, -1)])])) : (openBlock(), createElementBlock("span", _hoisted_44, toDisplayString(unref(m).gen), 1))], 10, _hoisted_42)) : createCommentVNode("", true)
1709
- ], 42, _hoisted_31), createElementVNode("span", _hoisted_45, toDisplayString(key), 1)]);
1710
- }), 128))])])) : createCommentVNode("", true),
1711
- isDeck.value ? (openBlock(), createElementBlock("div", _hoisted_46, [createVNode(unref(MulmoScriptDeckEditor), {
1712
- script: deckScriptInput.value,
1987
+ unref(characterKeys).length > 0 ? (openBlock(), createBlock(CharacterStrip_default, {
1988
+ key: 1,
1989
+ "character-keys": unref(characterKeys),
1990
+ images: script.value.imageParams?.images,
1991
+ thumbnails: unref(charImages),
1992
+ "render-state": unref(charRenderState),
1993
+ errors: unref(charErrors),
1994
+ "drag-over": unref(charDragOver),
1995
+ "movie-generating": unref(movieGenerating),
1996
+ "any-beat-rendering": anyBeatRendering.value,
1997
+ onGenerateAll: unref(generateAllCharacters),
1998
+ onCharDragOver: unref(onCharDragOver),
1999
+ onCharDragLeave: unref(onCharDragLeave),
2000
+ onCharDrop: unref(onCharDrop),
2001
+ onOpenLightbox: openCharacterLightbox,
2002
+ onRenderCharacter: unref(renderCharacter)
2003
+ }, null, 8, [
2004
+ "character-keys",
2005
+ "images",
2006
+ "thumbnails",
2007
+ "render-state",
2008
+ "errors",
2009
+ "drag-over",
2010
+ "movie-generating",
2011
+ "any-beat-rendering",
2012
+ "onGenerateAll",
2013
+ "onCharDragOver",
2014
+ "onCharDragLeave",
2015
+ "onCharDrop",
2016
+ "onRenderCharacter"
2017
+ ])) : createCommentVNode("", true),
2018
+ unref(isDeck) ? (openBlock(), createElementBlock("div", _hoisted_14, [createVNode(unref(MulmoScriptDeckEditor), {
2019
+ script: unref(deckScriptInput),
1713
2020
  layout: "compact",
1714
- "onUpdate:script": onDeckUpdate
1715
- }, null, 8, ["script"])])) : (openBlock(), createElementBlock("div", {
2021
+ "onUpdate:script": unref(onDeckUpdate)
2022
+ }, null, 8, ["script", "onUpdate:script"])])) : (openBlock(), createElementBlock("div", {
1716
2023
  key: 3,
1717
2024
  ref_key: "beatListEl",
1718
2025
  ref: beatListEl,
@@ -1721,40 +2028,44 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
1721
2028
  return openBlock(), createElementBlock("div", {
1722
2029
  key: index,
1723
2030
  class: "rounded-lg border border-gray-200 overflow-hidden"
1724
- }, [createElementVNode("div", _hoisted_47, [createElementVNode("div", {
2031
+ }, [createElementVNode("div", _hoisted_15, [createElementVNode("div", {
1725
2032
  class: normalizeClass(["relative shrink-0 w-[45%] overflow-hidden bg-gray-50 transition-colors", beatDragOver[index] ? "bg-blue-50" : ""]),
1726
2033
  onDragover: ($event) => onBeatDragOver($event, index),
1727
2034
  onDragleave: ($event) => onBeatDragLeave(index),
1728
2035
  onDrop: ($event) => onBeatDrop($event, index)
1729
2036
  }, [
1730
- beatMovieOpen[index] && beatMovieUrls[index] ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [createElementVNode("video", {
1731
- src: beatMovieUrls[index],
2037
+ createElementVNode("div", {
2038
+ 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",
2039
+ "data-testid": `mulmo-script-beat-number-${index}`
2040
+ }, toDisplayString(index + 1), 9, _hoisted_17),
2041
+ unref(beatMovieOpen)[index] && unref(beatMovieUrls)[index] ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [createElementVNode("video", {
2042
+ src: unref(beatMovieUrls)[index],
1732
2043
  class: "w-full object-contain",
1733
2044
  controls: "",
1734
2045
  autoplay: "",
1735
2046
  "data-testid": `mulmo-script-beat-movie-player-${index}`
1736
- }, null, 8, _hoisted_49), createElementVNode("button", {
2047
+ }, null, 8, _hoisted_18), createElementVNode("button", {
1737
2048
  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",
1738
2049
  title: unref(m).close,
1739
2050
  "aria-label": unref(m).close,
1740
2051
  "data-testid": `mulmo-script-beat-movie-close-${index}`,
1741
- onClick: withModifiers(($event) => closeBeatMovie(index), ["stop"])
1742
- }, [..._cache[18] || (_cache[18] = [createElementVNode("span", { class: "material-icons text-sm" }, "close", -1)])], 8, _hoisted_50)], 64)) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [
2052
+ onClick: withModifiers(($event) => unref(closeBeatMovie)(index), ["stop"])
2053
+ }, [..._cache[4] || (_cache[4] = [createElementVNode("span", { class: "material-icons text-sm" }, "close", -1)])], 8, _hoisted_19)], 64)) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [
1743
2054
  renderedImages[index] ? (openBlock(), createElementBlock("img", {
1744
2055
  key: 0,
1745
2056
  src: renderedImages[index],
1746
2057
  class: "w-full object-contain cursor-zoom-in",
1747
2058
  alt: `Beat ${index + 1}`,
1748
2059
  onClick: ($event) => openLightbox(index)
1749
- }, null, 8, _hoisted_51)) : createCommentVNode("", true),
1750
- renderedImages[index] && beatMovies[index] && canFetchMedia.value ? (openBlock(), createElementBlock("button", {
2060
+ }, null, 8, _hoisted_20)) : createCommentVNode("", true),
2061
+ renderedImages[index] && unref(beatMovies)[index] && canFetchMedia.value ? (openBlock(), createElementBlock("button", {
1751
2062
  key: 1,
1752
2063
  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",
1753
2064
  title: unref(m).play,
1754
2065
  "aria-label": unref(m).play,
1755
2066
  "data-testid": `mulmo-script-beat-movie-play-${index}`,
1756
- onClick: withModifiers(($event) => playBeatMovie(index), ["stop"])
1757
- }, [beatMovieLoading[index] ? (openBlock(), createElementBlock("svg", _hoisted_53, [..._cache[19] || (_cache[19] = [createElementVNode("circle", {
2067
+ onClick: withModifiers(($event) => unref(playBeatMovie)(index), ["stop"])
2068
+ }, [unref(beatMovieLoading)[index] ? (openBlock(), createElementBlock("svg", _hoisted_22, [..._cache[5] || (_cache[5] = [createElementVNode("circle", {
1758
2069
  class: "opacity-25",
1759
2070
  cx: "12",
1760
2071
  cy: "12",
@@ -1765,13 +2076,13 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
1765
2076
  class: "opacity-75",
1766
2077
  fill: "currentColor",
1767
2078
  d: "M4 12a8 8 0 018-8v8H4z"
1768
- }, null, -1)])])) : (openBlock(), createElementBlock("span", _hoisted_54, "play_arrow"))], 8, _hoisted_52)) : createCommentVNode("", true),
2079
+ }, null, -1)])])) : (openBlock(), createElementBlock("span", _hoisted_23, "play_arrow"))], 8, _hoisted_21)) : createCommentVNode("", true),
1769
2080
  renderedImages[index] && renderState[index] !== "rendering" ? (openBlock(), createElementBlock("button", {
1770
2081
  key: 2,
1771
2082
  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",
1772
- disabled: movieGenerating.value,
2083
+ disabled: unref(movieGenerating),
1773
2084
  onClick: withModifiers(($event) => regenerateBeat(index), ["stop"])
1774
- }, " ↺ ", 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", {
2085
+ }, " ↺ ", 8, _hoisted_24)) : !renderedImages[index] ? (openBlock(), createElementBlock("div", _hoisted_25, [renderState[index] === "rendering" || unref(movieGenerating) && !renderedImages[index] && effectiveBeat$1(index).imagePrompt ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [_cache[6] || (_cache[6] = createElementVNode("svg", {
1775
2086
  class: "animate-spin w-4 h-4 text-green-400",
1776
2087
  viewBox: "0 0 24 24",
1777
2088
  fill: "none"
@@ -1786,15 +2097,15 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
1786
2097
  class: "opacity-75",
1787
2098
  fill: "currentColor",
1788
2099
  d: "M4 12a8 8 0 018-8v8H4z"
1789
- })], -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)
2100
+ })], -1)), createElementVNode("span", _hoisted_26, toDisplayString(unref(m).rendering), 1)], 64)) : renderState[index] === "error" ? (openBlock(), createElementBlock("span", _hoisted_27, toDisplayString(renderErrors[index]), 1)) : (openBlock(), createElementBlock(Fragment, { key: 2 }, [effectiveBeat$1(index).imagePrompt ? (openBlock(), createElementBlock("span", _hoisted_28, toDisplayString(effectiveBeat$1(index).imagePrompt), 1)) : (openBlock(), createElementBlock("span", _hoisted_29, toDisplayString(beat.image?.type ?? "—"), 1))], 64))])) : createCommentVNode("", true)
1790
2101
  ], 64)),
1791
- 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),
1792
- !renderedImages[index] && renderState[index] !== "rendering" && !movieGenerating.value ? (openBlock(), createElementBlock("button", {
2102
+ beatDragOver[index] ? (openBlock(), createElementBlock("div", _hoisted_30, [createElementVNode("span", _hoisted_31, toDisplayString(unref(m).drop), 1)])) : !renderedImages[index] && renderState[index] !== "rendering" ? (openBlock(), createElementBlock("div", _hoisted_32, toDisplayString(unref(m).orDropImage), 1)) : createCommentVNode("", true),
2103
+ !renderedImages[index] && renderState[index] !== "rendering" && !unref(movieGenerating) && !unref(isBeatImageReference)(effectiveBeat$1(index)) ? (openBlock(), createElementBlock("button", {
1793
2104
  key: 4,
1794
2105
  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",
1795
2106
  onClick: ($event) => renderBeat(index)
1796
- }, toDisplayString(unref(m).generate), 9, _hoisted_64)) : createCommentVNode("", true)
1797
- ], 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", {
2107
+ }, toDisplayString(unref(m).generate), 9, _hoisted_33)) : createCommentVNode("", true)
2108
+ ], 42, _hoisted_16), createElementVNode("div", _hoisted_34, [createElementVNode("span", _hoisted_35, toDisplayString(effectiveBeat$1(index).text), 1), createElementVNode("div", _hoisted_36, [createElementVNode("div", _hoisted_37, [audioState[index] === "generating" || unref(movieGenerating) && !beatAudios[index] && effectiveBeat$1(index).text ? (openBlock(), createElementBlock("svg", _hoisted_38, [..._cache[7] || (_cache[7] = [createElementVNode("circle", {
1798
2109
  class: "opacity-25",
1799
2110
  cx: "12",
1800
2111
  cy: "12",
@@ -1809,24 +2120,24 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
1809
2120
  key: 1,
1810
2121
  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"]),
1811
2122
  onClick: ($event) => playAudio(index)
1812
- }, toDisplayString(playingAudio.value?.index === index ? unref(m).stop : unref(m).play), 11, _hoisted_70)) : audioErrors[index] ? (openBlock(), createElementBlock(Fragment, { key: 2 }, [createElementVNode("span", {
2123
+ }, toDisplayString(playingAudio.value?.index === index ? unref(m).stop : unref(m).play), 11, _hoisted_39)) : audioErrors[index] ? (openBlock(), createElementBlock(Fragment, { key: 2 }, [createElementVNode("span", {
1813
2124
  class: "text-xs text-red-400 truncate min-w-0 max-w-[20rem]",
1814
2125
  title: audioErrors[index]
1815
- }, toDisplayString(unref(m).errPrefix) + " " + toDisplayString(audioErrors[index]), 9, _hoisted_71), effectiveBeat(index).text ? (openBlock(), createElementBlock("button", {
2126
+ }, toDisplayString(unref(m).errPrefix) + " " + toDisplayString(audioErrors[index]), 9, _hoisted_40), effectiveBeat$1(index).text ? (openBlock(), createElementBlock("button", {
1816
2127
  key: 0,
1817
2128
  class: "text-xs px-2 py-0.5 rounded border border-gray-300 text-gray-500 hover:bg-gray-50 disabled:opacity-50",
1818
- disabled: movieGenerating.value,
2129
+ disabled: unref(movieGenerating),
1819
2130
  onClick: ($event) => generateAudio(index)
1820
- }, " ↺ ", 8, _hoisted_72)) : createCommentVNode("", true)], 64)) : effectiveBeat(index).text ? (openBlock(), createElementBlock("button", {
2131
+ }, " ↺ ", 8, _hoisted_41)) : createCommentVNode("", true)], 64)) : effectiveBeat$1(index).text ? (openBlock(), createElementBlock("button", {
1821
2132
  key: 3,
1822
2133
  class: "text-xs px-2 py-0.5 rounded border border-gray-300 text-gray-500 hover:bg-gray-50",
1823
2134
  onClick: ($event) => generateAudio(index)
1824
- }, toDisplayString(unref(m).generateAudio), 9, _hoisted_73)) : createCommentVNode("", true)]), createElementVNode("button", {
2135
+ }, toDisplayString(unref(m).generateAudio), 9, _hoisted_42)) : createCommentVNode("", true)]), createElementVNode("button", {
1825
2136
  class: "text-gray-400 hover:text-gray-600",
1826
2137
  title: sourceOpen[index] ? "Hide source" : "Show source",
1827
2138
  "data-testid": `mulmo-script-beat-source-toggle-${index}`,
1828
2139
  onClick: ($event) => toggleSource(index)
1829
- }, [..._cache[22] || (_cache[22] = [createElementVNode("svg", {
2140
+ }, [..._cache[8] || (_cache[8] = [createElementVNode("svg", {
1830
2141
  xmlns: "http://www.w3.org/2000/svg",
1831
2142
  class: "w-3.5 h-3.5",
1832
2143
  viewBox: "0 0 24 24",
@@ -1835,36 +2146,36 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
1835
2146
  "stroke-width": "2",
1836
2147
  "stroke-linecap": "round",
1837
2148
  "stroke-linejoin": "round"
1838
- }, [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", {
2149
+ }, [createElementVNode("polyline", { points: "16 18 22 12 16 6" }), createElementVNode("polyline", { points: "8 6 2 12 8 18" })], -1)])], 8, _hoisted_43)])])]), sourceOpen[index] ? (openBlock(), createElementBlock("div", _hoisted_44, [withDirectives(createElementVNode("textarea", {
1839
2150
  "onUpdate:modelValue": ($event) => sourceText[index] = $event,
1840
- 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"]),
2151
+ class: normalizeClass(["w-full text-xs text-gray-600 bg-gray-50 p-2 font-mono resize-none", isValidBeat$1(index) ? "outline-none" : "outline outline-2 outline-red-400"]),
1841
2152
  rows: "8",
1842
2153
  spellcheck: "false",
1843
2154
  "data-testid": `mulmo-script-beat-source-textarea-${index}`
1844
- }, 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", {
1845
- 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"]),
1846
- disabled: !isValidBeat(index) || !!beatSaving[index],
2155
+ }, null, 10, _hoisted_45), [[vModelText, sourceText[index]]]), createElementVNode("div", _hoisted_46, [beatSaveErrors[index] ? (openBlock(), createElementBlock("span", _hoisted_47, toDisplayString(beatSaveErrors[index].kind === "invalidJson" ? unref(m).saveErrorInvalidJson(beatSaveErrors[index].error) : unref(m).saveErrorSaveFailed(beatSaveErrors[index].error)), 1)) : createCommentVNode("", true), createElementVNode("button", {
2156
+ class: normalizeClass(["px-2 py-1 text-xs rounded border", isValidBeat$1(index) && !beatSaving[index] ? "border-blue-400 text-blue-600 hover:bg-blue-50 cursor-pointer" : "border-gray-200 text-gray-300 cursor-not-allowed"]),
2157
+ disabled: !isValidBeat$1(index) || !!beatSaving[index],
1847
2158
  "data-testid": `mulmo-script-beat-update-button-${index}`,
1848
2159
  onClick: ($event) => updateBeat(index)
1849
- }, toDisplayString(beatSaving[index] ? unref(m).saving : unref(m).update), 11, _hoisted_79)])])) : createCommentVNode("", true)]);
1850
- }), 128)), beats.value.length === 0 ? (openBlock(), createElementBlock("div", _hoisted_80, toDisplayString(unref(m).noBeats), 1)) : createCommentVNode("", true)], 512)),
1851
- createElementVNode("div", _hoisted_81, [createElementVNode("details", {
2160
+ }, toDisplayString(beatSaving[index] ? unref(m).saving : unref(m).update), 11, _hoisted_48)])])) : createCommentVNode("", true)]);
2161
+ }), 128)), beats.value.length === 0 ? (openBlock(), createElementBlock("div", _hoisted_49, toDisplayString(unref(m).noBeats), 1)) : createCommentVNode("", true)], 512)),
2162
+ createElementVNode("div", _hoisted_50, [createElementVNode("details", {
1852
2163
  ref_key: "sourceDetails",
1853
2164
  ref: sourceDetails,
1854
2165
  class: "script-source",
1855
- onToggle: _cache[1] || (_cache[1] = ($event) => onSourceToggle($event.target.open))
2166
+ onToggle: _cache[2] || (_cache[2] = ($event) => onSourceToggle($event.target.open))
1856
2167
  }, [
1857
2168
  createElementVNode("summary", null, toDisplayString(unref(m).editSource), 1),
1858
2169
  withDirectives(createElementVNode("textarea", {
1859
- "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => editableSource.value = $event),
2170
+ "onUpdate:modelValue": _cache[1] || (_cache[1] = ($event) => editableSource.value = $event),
1860
2171
  class: normalizeClass(["script-editor", { "script-editor-invalid": sourceChanged.value && !sourceValid.value }]),
1861
2172
  spellcheck: "false"
1862
2173
  }, null, 2), [[vModelText, editableSource.value]]),
1863
- createElementVNode("div", _hoisted_82, [createElementVNode("button", {
2174
+ createElementVNode("div", _hoisted_51, [createElementVNode("button", {
1864
2175
  class: "apply-btn",
1865
2176
  disabled: !sourceChanged.value || !sourceValid.value,
1866
2177
  onClick: applySource
1867
- }, toDisplayString(unref(m).applyChanges), 9, _hoisted_83), createElementVNode("button", {
2178
+ }, toDisplayString(unref(m).applyChanges), 9, _hoisted_52), createElementVNode("button", {
1868
2179
  class: "cancel-btn",
1869
2180
  onClick: cancelSourceEdit
1870
2181
  }, toDisplayString(unref(m).cancel), 1)])
@@ -1872,50 +2183,31 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineCompo
1872
2183
  class: "copy-btn",
1873
2184
  title: unref(copied) ? "Copied!" : "Copy",
1874
2185
  onClick: copyText
1875
- }, [createElementVNode("span", _hoisted_85, toDisplayString(unref(copied) ? "check" : "content_copy"), 1)], 8, _hoisted_84), [[vShow, !editing.value]])]),
1876
- lightbox.value ? (openBlock(), createElementBlock("div", {
2186
+ }, [createElementVNode("span", _hoisted_54, toDisplayString(unref(copied) ? "check" : "content_copy"), 1)], 8, _hoisted_53), [[vShow, !editing.value]])]),
2187
+ lightbox.value ? (openBlock(), createBlock(BeatLightbox_default, {
1877
2188
  key: 4,
1878
- class: "fixed inset-0 z-50 bg-black/80 overflow-y-auto",
1879
- onClick: closeLightbox
1880
- }, [createElementVNode("button", {
1881
- class: "fixed top-2 right-4 z-10 text-white/60 hover:text-white text-3xl leading-none",
1882
- title: unref(m).close,
1883
- onClick: withModifiers(closeLightbox, ["stop"])
1884
- }, "", 8, _hoisted_86), createElementVNode("div", {
1885
- class: "flex flex-col items-center gap-4 pt-4 pb-8",
1886
- onClick: _cache[5] || (_cache[5] = withModifiers(() => {}, ["stop"]))
1887
- }, [createElementVNode("div", _hoisted_87, [
1888
- !lightbox.value.isCharacter ? (openBlock(), createElementBlock("button", {
1889
- key: 0,
1890
- class: "text-white/60 hover:text-white disabled:opacity-20 text-5xl leading-none",
1891
- disabled: !hasPrev.value,
1892
- onClick: _cache[2] || (_cache[2] = ($event) => lightboxMove(-1))
1893
- }, "", 8, _hoisted_88)) : createCommentVNode("", true),
1894
- createElementVNode("div", _hoisted_89, [createElementVNode("img", {
1895
- src: lightbox.value.src,
1896
- class: "max-w-[80vw] max-h-[85vh] object-contain rounded shadow-2xl"
1897
- }, 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) => {
1898
- return openBlock(), createElementBlock("div", {
1899
- key: i - 1,
1900
- 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"]),
1901
- onClick: ($event) => jumpToBeat(i - 1)
1902
- }, [_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);
1903
- }), 128))]), playingAudio.value && playingAudio.value.index === lightbox.value.index ? (openBlock(), createElementBlock("div", {
1904
- key: 0,
1905
- 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",
1906
- style: normalizeStyle({ left: `${(lightbox.value.index + audioProgress.value) / beats.value.length * 100}%` })
1907
- }, null, 4)) : createCommentVNode("", true)])) : createCommentVNode("", true)]),
1908
- !lightbox.value.isCharacter ? (openBlock(), createElementBlock("button", {
1909
- key: 1,
1910
- class: "text-white/60 hover:text-white disabled:opacity-20 text-5xl leading-none",
1911
- disabled: !hasNext.value,
1912
- onClick: _cache[3] || (_cache[3] = ($event) => lightboxMove(1))
1913
- }, " › ", 8, _hoisted_95)) : createCommentVNode("", true)
1914
- ]), 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", {
1915
- key: 1,
1916
- class: "absolute top-0 right-4 text-sm px-3 py-1 rounded border border-white/60 text-white/60 hover:bg-white/20",
1917
- onClick: _cache[4] || (_cache[4] = ($event) => playAudio(lightbox.value.index))
1918
- }, toDisplayString(playingAudio.value?.index === lightbox.value.index ? unref(m).stop : unref(m).play), 1)) : createCommentVNode("", true)])) : createCommentVNode("", true)])])) : createCommentVNode("", true)
2189
+ lightbox: lightbox.value,
2190
+ "beat-count": beats.value.length,
2191
+ "beat-texts": beatTexts.value,
2192
+ "has-prev": hasPrev.value,
2193
+ "has-next": hasNext.value,
2194
+ "playing-audio-index": playingAudio.value?.index ?? null,
2195
+ "audio-progress": audioProgress.value,
2196
+ "has-current-audio": Boolean(beatAudios[lightbox.value.index]),
2197
+ onClose: closeLightbox,
2198
+ onMove: lightboxMove,
2199
+ onJump: jumpToBeat,
2200
+ onPlayAudio: playAudio
2201
+ }, null, 8, [
2202
+ "lightbox",
2203
+ "beat-count",
2204
+ "beat-texts",
2205
+ "has-prev",
2206
+ "has-next",
2207
+ "playing-audio-index",
2208
+ "audio-progress",
2209
+ "has-current-audio"
2210
+ ])) : createCommentVNode("", true)
1919
2211
  ]);
1920
2212
  };
1921
2213
  }
@@ -1929,7 +2221,7 @@ var _plugin_vue_export_helper_default = (sfc, props) => {
1929
2221
  };
1930
2222
  //#endregion
1931
2223
  //#region src/vue/View.vue
1932
- var View_default = /*#__PURE__*/ _plugin_vue_export_helper_default(View_vue_vue_type_script_setup_true_lang_default, [["__scopeId", "data-v-c28ee15a"]]);
2224
+ var View_default = /*#__PURE__*/ _plugin_vue_export_helper_default(View_vue_vue_type_script_setup_true_lang_default, [["__scopeId", "data-v-3776a73b"]]);
1933
2225
  //#endregion
1934
2226
  //#region src/vue/Preview.vue?vue&type=script&setup=true&lang.ts
1935
2227
  var _hoisted_1 = {