@mulmoclaude/mulmoscript-plugin 1.0.0 → 1.1.1

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