@mulmoclaude/mulmoscript-plugin 0.2.3 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/core/paths.d.ts +2 -5
  2. package/dist/core/paths.d.ts.map +1 -1
  3. package/dist/core/plugin.d.ts.map +1 -1
  4. package/dist/core/validate.d.ts.map +1 -1
  5. package/dist/index.cjs +1 -1
  6. package/dist/index.js +1 -1
  7. package/dist/lang/index.d.ts +1 -2
  8. package/dist/lang/index.d.ts.map +1 -1
  9. package/dist/{plugin-Bi986Wga.cjs → plugin-BLp8tSYt.cjs} +86 -46
  10. package/dist/plugin-BLp8tSYt.cjs.map +1 -0
  11. package/dist/{plugin-BQyzxkui.js → plugin-CXhOP_xU.js} +81 -47
  12. package/dist/plugin-CXhOP_xU.js.map +1 -0
  13. package/dist/server/mulmoErrorCapture.d.ts.map +1 -1
  14. package/dist/server/ops.d.ts.map +1 -1
  15. package/dist/server/support.d.ts +0 -5
  16. package/dist/server/support.d.ts.map +1 -1
  17. package/dist/server/types.d.ts +3 -7
  18. package/dist/server/types.d.ts.map +1 -1
  19. package/dist/server.cjs +34 -58
  20. package/dist/server.cjs.map +1 -1
  21. package/dist/server.js +33 -57
  22. package/dist/server.js.map +1 -1
  23. package/dist/style.css +36 -18
  24. package/dist/vue/View.vue.d.ts.map +1 -1
  25. package/dist/vue/components/BeatLightbox.vue.d.ts +25 -0
  26. package/dist/vue/components/BeatLightbox.vue.d.ts.map +1 -0
  27. package/dist/vue/components/CharacterStrip.vue.d.ts +29 -0
  28. package/dist/vue/components/CharacterStrip.vue.d.ts.map +1 -0
  29. package/dist/vue/components/MulmoScriptToolbar.vue.d.ts +26 -0
  30. package/dist/vue/components/MulmoScriptToolbar.vue.d.ts.map +1 -0
  31. package/dist/vue/composables/useBeatMovie.d.ts +20 -0
  32. package/dist/vue/composables/useBeatMovie.d.ts.map +1 -0
  33. package/dist/vue/composables/useCharacterImages.d.ts +31 -0
  34. package/dist/vue/composables/useCharacterImages.d.ts.map +1 -0
  35. package/dist/vue/composables/useDeckEditor.d.ts +18 -0
  36. package/dist/vue/composables/useDeckEditor.d.ts.map +1 -0
  37. package/dist/vue/composables/useMediaExport.d.ts +26 -0
  38. package/dist/vue/composables/useMediaExport.d.ts.map +1 -0
  39. package/dist/vue/helpers.d.ts +78 -0
  40. package/dist/vue/helpers.d.ts.map +1 -1
  41. package/dist/vue/index.d.ts +1 -1
  42. package/dist/vue/support.d.ts +6 -4
  43. package/dist/vue/support.d.ts.map +1 -1
  44. package/dist/vue/transport.d.ts.map +1 -1
  45. package/dist/vue/viewTypes.d.ts +49 -0
  46. package/dist/vue/viewTypes.d.ts.map +1 -0
  47. package/dist/vue.cjs +1021 -729
  48. package/dist/vue.cjs.map +1 -1
  49. package/dist/vue.js +1019 -727
  50. package/dist/vue.js.map +1 -1
  51. package/package.json +11 -7
  52. package/dist/plugin-BQyzxkui.js.map +0 -1
  53. package/dist/plugin-Bi986Wga.cjs.map +0 -1
package/dist/vue.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 {
@@ -189,8 +271,337 @@ function useHostAdapter() {
189
271
  return (0, vue.inject)(MULMOSCRIPT_HOST_ADAPTER_KEY, EMPTY_ADAPTER);
190
272
  }
191
273
  //#endregion
192
- //#region src/lang/index.ts
193
- var MESSAGES = {
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
+ }
600
+ /** Reactive message bundle for the active host locale. The plugin carries its
601
+ * own translations (no host i18n dependency); it reads the locale off the
602
+ * injected `BrowserPluginRuntime.locale` ref and falls back to English.
603
+ * Same pattern as @mulmoclaude/html-plugin. */
604
+ var useT = (0, gui_chat_protocol_vue.createUseT)({
194
605
  de: {
195
606
  beatCount: (count) => count === 1 ? `${count} Beat` : `${count} Beats`,
196
607
  movie: "Video",
@@ -447,267 +858,519 @@ var MESSAGES = {
447
858
  close: "关闭",
448
859
  cancel: "取消"
449
860
  }
450
- };
451
- function isSupportedLocale(value) {
452
- return Object.hasOwn(MESSAGES, value);
453
- }
454
- /** Reactive message bundle for the active host locale. The plugin carries its
455
- * own translations (no host i18n dependency); it reads the locale off the
456
- * injected `BrowserPluginRuntime.locale` ref and falls back to English.
457
- * Same pattern as @mulmoclaude/html-plugin. */
458
- function useT() {
459
- const locale = (0, vue.inject)(gui_chat_protocol_vue.PLUGIN_RUNTIME_KEY, void 0)?.locale ?? (0, vue.ref)("en");
460
- return (0, vue.computed)(() => isSupportedLocale(locale.value) ? MESSAGES[locale.value] : MESSAGES.en);
461
- }
861
+ });
462
862
  //#endregion
463
- //#region src/vue/View.vue?vue&type=script&setup=true&lang.ts
464
- var _hoisted_1$1 = { class: "h-full bg-white flex flex-col overflow-hidden" };
465
- var _hoisted_2$1 = { class: "flex items-start justify-between px-6 py-4 border-b border-gray-100 shrink-0" };
466
- var _hoisted_3$1 = { class: "min-w-0 flex-1" };
467
- var _hoisted_4 = {
468
- class: "text-lg font-semibold text-gray-800 truncate",
469
- "data-testid": "mulmo-script-title"
470
- };
471
- var _hoisted_5 = {
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 = {
472
870
  key: 0,
473
- class: "text-sm text-gray-500 mt-0.5 truncate",
474
- "data-testid": "mulmo-script-description"
475
- };
476
- var _hoisted_6 = { class: "flex items-center gap-3 mt-1 text-xs text-gray-400" };
477
- var _hoisted_7 = { key: 0 };
478
- var _hoisted_8 = {
479
- key: 1,
480
- class: "truncate"
871
+ class: "relative w-full h-1"
481
872
  };
482
- var _hoisted_9 = { class: "ml-4 shrink-0 flex items-center gap-2" };
483
- var _hoisted_10 = [
484
- "disabled",
485
- "title",
486
- "aria-label"
487
- ];
488
- var _hoisted_11 = ["disabled"];
489
- var _hoisted_12 = ["title", "aria-label"];
490
- var _hoisted_13 = ["disabled"];
491
- var _hoisted_14 = {
873
+ var _hoisted_7$3 = { class: "flex gap-1 h-full" };
874
+ var _hoisted_8$3 = ["onClick"];
875
+ var _hoisted_9$3 = {
492
876
  key: 0,
493
- class: "animate-spin w-4 h-4 shrink-0",
494
- viewBox: "0 0 24 24",
495
- 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"
496
878
  };
497
- var _hoisted_15 = { key: 1 };
498
- var _hoisted_16 = ["disabled"];
499
- var _hoisted_17 = ["title", "aria-label"];
500
- var _hoisted_18 = ["disabled"];
501
- var _hoisted_19 = {
879
+ var _hoisted_10$3 = ["disabled"];
880
+ var _hoisted_11$3 = {
502
881
  key: 0,
503
- class: "animate-spin w-4 h-4 shrink-0",
504
- viewBox: "0 0 24 24",
505
- fill: "none"
882
+ class: "relative w-screen flex justify-center px-16"
506
883
  };
507
- var _hoisted_20 = { key: 1 };
508
- var _hoisted_21 = {
884
+ var _hoisted_12$3 = {
509
885
  key: 0,
510
- "data-testid": "mulmo-script-movie-error-chip",
511
- class: "bg-red-50 border border-red-200 text-red-800 text-xs px-3 py-2 mx-4 mt-3 mb-1 rounded flex items-start gap-2"
512
- };
513
- var _hoisted_22 = { class: "flex-1 min-w-0" };
514
- var _hoisted_23 = { class: "font-medium" };
515
- var _hoisted_24 = { class: "break-words whitespace-pre-wrap mt-0.5" };
516
- var _hoisted_25 = ["disabled"];
517
- var _hoisted_26 = {
518
- key: 1,
519
- class: "border-b border-gray-100 shrink-0 px-4 py-3"
886
+ class: "max-w-[80vw] text-center text-white leading-relaxed text-[clamp(0.8rem,1.76vw,1.6rem)]"
520
887
  };
521
- var _hoisted_27 = { class: "flex items-center justify-between mb-2" };
522
- var _hoisted_28 = { class: "text-xs font-semibold text-gray-500 uppercase tracking-wide" };
523
- var _hoisted_29 = ["disabled"];
524
- var _hoisted_30 = { class: "flex gap-3 flex-wrap" };
525
- var _hoisted_31 = [
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 = [
526
965
  "onDragover",
527
966
  "onDragleave",
528
967
  "onDrop"
529
968
  ];
530
- var _hoisted_32 = [
969
+ var _hoisted_7$2 = [
531
970
  "src",
532
971
  "alt",
533
972
  "onClick"
534
973
  ];
535
- var _hoisted_33 = {
974
+ var _hoisted_8$2 = {
536
975
  key: 1,
537
976
  class: "animate-spin w-4 h-4 text-green-400",
538
977
  viewBox: "0 0 24 24",
539
978
  fill: "none"
540
979
  };
541
- var _hoisted_34 = {
980
+ var _hoisted_9$2 = {
542
981
  key: 2,
543
982
  class: "text-xs text-red-400 text-center px-1"
544
983
  };
545
- var _hoisted_35 = {
984
+ var _hoisted_10$2 = {
546
985
  key: 3,
547
986
  class: "text-xs text-gray-300 text-center px-1 leading-tight"
548
987
  };
549
- var _hoisted_36 = {
988
+ var _hoisted_11$2 = {
550
989
  key: 4,
551
990
  class: "absolute bottom-0 inset-x-0 text-center text-xs text-gray-400 bg-white/70 py-0.5 pointer-events-none"
552
991
  };
553
- var _hoisted_37 = {
992
+ var _hoisted_12$2 = {
554
993
  key: 5,
555
994
  class: "absolute inset-0 flex items-center justify-center bg-blue-50/80 pointer-events-none"
556
995
  };
557
- var _hoisted_38 = { class: "text-xs text-blue-500 font-medium" };
558
- var _hoisted_39 = ["disabled", "onClick"];
559
- 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 = {
560
999
  key: 0,
561
1000
  class: "inline-block animate-spin"
562
1001
  };
563
- var _hoisted_41 = { key: 1 };
564
- var _hoisted_42 = ["disabled", "onClick"];
565
- var _hoisted_43 = {
1002
+ var _hoisted_16$1 = { key: 1 };
1003
+ var _hoisted_17$1 = ["disabled", "onClick"];
1004
+ var _hoisted_18$1 = {
566
1005
  key: 0,
567
1006
  class: "animate-spin w-3 h-3",
568
1007
  viewBox: "0 0 24 24",
569
1008
  fill: "none"
570
1009
  };
571
- var _hoisted_44 = { key: 1 };
572
- var _hoisted_45 = { class: "text-xs text-gray-600 text-center truncate w-full" };
573
- var _hoisted_46 = {
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 = {
574
1261
  key: 2,
575
1262
  class: "flex-1 overflow-hidden",
576
1263
  "data-testid": "mulmo-script-deck-editor"
577
1264
  };
578
- var _hoisted_47 = { class: "flex gap-3 items-stretch" };
579
- var _hoisted_48 = [
1265
+ var _hoisted_15 = { class: "flex gap-3 items-stretch" };
1266
+ var _hoisted_16 = [
580
1267
  "onDragover",
581
1268
  "onDragleave",
582
1269
  "onDrop"
583
1270
  ];
584
- var _hoisted_49 = ["src", "data-testid"];
585
- var _hoisted_50 = [
1271
+ var _hoisted_17 = ["data-testid"];
1272
+ var _hoisted_18 = ["src", "data-testid"];
1273
+ var _hoisted_19 = [
586
1274
  "title",
587
1275
  "aria-label",
588
1276
  "data-testid",
589
1277
  "onClick"
590
1278
  ];
591
- var _hoisted_51 = [
1279
+ var _hoisted_20 = [
592
1280
  "src",
593
1281
  "alt",
594
1282
  "onClick"
595
1283
  ];
596
- var _hoisted_52 = [
1284
+ var _hoisted_21 = [
597
1285
  "title",
598
1286
  "aria-label",
599
1287
  "data-testid",
600
1288
  "onClick"
601
1289
  ];
602
- var _hoisted_53 = {
1290
+ var _hoisted_22 = {
603
1291
  key: 0,
604
1292
  class: "animate-spin w-5 h-5",
605
1293
  viewBox: "0 0 24 24",
606
1294
  fill: "none"
607
1295
  };
608
- var _hoisted_54 = {
1296
+ var _hoisted_23 = {
609
1297
  key: 1,
610
1298
  class: "material-icons text-3xl"
611
1299
  };
612
- var _hoisted_55 = ["disabled", "onClick"];
613
- var _hoisted_56 = {
1300
+ var _hoisted_24 = ["disabled", "onClick"];
1301
+ var _hoisted_25 = {
614
1302
  key: 3,
615
1303
  class: "w-full aspect-video flex flex-col items-center justify-center gap-1 p-2"
616
1304
  };
617
- var _hoisted_57 = { class: "text-xs text-green-500" };
618
- var _hoisted_58 = {
1305
+ var _hoisted_26 = { class: "text-xs text-green-500" };
1306
+ var _hoisted_27 = {
619
1307
  key: 1,
620
1308
  class: "text-xs text-red-400 text-center"
621
1309
  };
622
- var _hoisted_59 = {
1310
+ var _hoisted_28 = {
623
1311
  key: 0,
624
1312
  class: "text-xs text-gray-400 text-center italic leading-relaxed px-1"
625
1313
  };
626
- var _hoisted_60 = {
1314
+ var _hoisted_29 = {
627
1315
  key: 1,
628
1316
  class: "text-xs text-gray-300"
629
1317
  };
630
- var _hoisted_61 = {
1318
+ var _hoisted_30 = {
631
1319
  key: 2,
632
1320
  class: "absolute inset-0 flex items-center justify-center bg-blue-50/80 pointer-events-none"
633
1321
  };
634
- var _hoisted_62 = { class: "text-xs text-blue-500 font-medium" };
635
- var _hoisted_63 = {
1322
+ var _hoisted_31 = { class: "text-xs text-blue-500 font-medium" };
1323
+ var _hoisted_32 = {
636
1324
  key: 3,
637
1325
  class: "absolute bottom-0 inset-x-0 text-center text-xs text-gray-400 bg-white/70 py-0.5 pointer-events-none"
638
1326
  };
639
- var _hoisted_64 = ["onClick"];
640
- var _hoisted_65 = { class: "flex flex-col flex-1 min-w-0 px-2 py-1.5" };
641
- var _hoisted_66 = { class: "text-sm text-gray-800 leading-relaxed" };
642
- var _hoisted_67 = { class: "flex justify-between mt-auto pt-1" };
643
- var _hoisted_68 = { class: "flex items-center gap-1" };
644
- var _hoisted_69 = {
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 = {
645
1333
  key: 0,
646
1334
  class: "animate-spin w-3 h-3 text-green-400",
647
1335
  viewBox: "0 0 24 24",
648
1336
  fill: "none"
649
1337
  };
650
- var _hoisted_70 = ["onClick"];
651
- var _hoisted_71 = ["title"];
652
- var _hoisted_72 = ["disabled", "onClick"];
653
- var _hoisted_73 = ["onClick"];
654
- var _hoisted_74 = [
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 = [
655
1343
  "title",
656
1344
  "data-testid",
657
1345
  "onClick"
658
1346
  ];
659
- var _hoisted_75 = {
1347
+ var _hoisted_44 = {
660
1348
  key: 0,
661
1349
  class: "border-t border-gray-100"
662
1350
  };
663
- var _hoisted_76 = ["onUpdate:modelValue", "data-testid"];
664
- var _hoisted_77 = { class: "flex items-center justify-end gap-2 px-2 pb-2" };
665
- var _hoisted_78 = {
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 = {
666
1354
  key: 0,
667
1355
  class: "text-xs text-red-600",
668
1356
  role: "alert"
669
1357
  };
670
- var _hoisted_79 = [
1358
+ var _hoisted_48 = [
671
1359
  "disabled",
672
1360
  "data-testid",
673
1361
  "onClick"
674
1362
  ];
675
- var _hoisted_80 = {
1363
+ var _hoisted_49 = {
676
1364
  key: 0,
677
1365
  class: "flex items-center justify-center h-32 text-gray-400 text-sm"
678
1366
  };
679
- var _hoisted_81 = { class: "bottom-bar-wrapper" };
680
- var _hoisted_82 = { class: "editor-actions" };
681
- var _hoisted_83 = ["disabled"];
682
- var _hoisted_84 = ["title"];
683
- var _hoisted_85 = { class: "material-icons" };
684
- var _hoisted_86 = ["title"];
685
- var _hoisted_87 = { class: "flex items-center gap-4" };
686
- var _hoisted_88 = ["disabled"];
687
- var _hoisted_89 = { class: "flex flex-col items-center" };
688
- var _hoisted_90 = ["src"];
689
- var _hoisted_91 = {
690
- key: 0,
691
- class: "relative w-full h-1"
692
- };
693
- var _hoisted_92 = { class: "flex gap-1 h-full" };
694
- var _hoisted_93 = ["onClick"];
695
- var _hoisted_94 = {
696
- key: 0,
697
- class: "absolute bottom-full mb-2 left-1/2 -translate-x-1/2 z-20 px-2 py-1 rounded bg-black/90 text-white text-xs leading-tight w-48 max-h-[53px] overflow-hidden opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity"
698
- };
699
- var _hoisted_95 = ["disabled"];
700
- var _hoisted_96 = {
701
- key: 0,
702
- class: "relative w-screen flex justify-center px-16"
703
- };
704
- var _hoisted_97 = {
705
- key: 0,
706
- class: "max-w-[80vw] text-center text-white leading-relaxed text-[clamp(0.8rem,1.76vw,1.6rem)]"
707
- };
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" };
708
1372
  var SILENT_BEAT_DEFAULT_SEC = 3;
709
1373
  var MS_PER_SECOND = 1e3;
710
- var DECK_SAVE_DEBOUNCE_MS = 300;
711
1374
  var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ (0, vue.defineComponent)({
712
1375
  __name: "View",
713
1376
  props: { selectedResult: {} },
@@ -732,39 +1395,34 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ (0, vue.def
732
1395
  const beatSaveErrors = (0, vue.reactive)({});
733
1396
  const beatSaving = (0, vue.reactive)({});
734
1397
  const localOverrides = (0, vue.reactive)({});
735
- const movieGenerating = (0, vue.ref)(false);
736
- const movieDownloading = (0, vue.ref)(false);
737
- const moviePath = (0, vue.ref)(null);
738
- const movieError = (0, vue.ref)(null);
739
- const pdfGenerating = (0, vue.ref)(false);
740
- const pdfDownloading = (0, vue.ref)(false);
741
- const pdfPath = (0, vue.ref)(null);
742
1398
  const beatAudios = (0, vue.reactive)({});
743
1399
  const audioState = (0, vue.reactive)({});
744
1400
  const audioErrors = (0, vue.reactive)({});
745
- const beatMovies = (0, vue.reactive)({});
746
- const beatMovieUrls = (0, vue.reactive)({});
747
- const beatMovieOpen = (0, vue.reactive)({});
748
- const beatMovieLoading = (0, vue.reactive)({});
749
1401
  const playingAudio = (0, vue.ref)(null);
750
1402
  const silentPlaybackTimer = (0, vue.ref)(null);
751
1403
  const audioProgress = (0, vue.ref)(0);
752
1404
  const beatListEl = (0, vue.ref)(null);
753
1405
  const lightbox = (0, vue.ref)(null);
754
- const charRenderState = (0, vue.reactive)({});
755
- const charImages = (0, vue.reactive)({});
756
- const charErrors = (0, vue.reactive)({});
757
- const charDragOver = (0, vue.reactive)({});
758
1406
  const beatDragOver = (0, vue.reactive)({});
759
1407
  const anyBeatRendering = (0, vue.computed)(() => Object.values(renderState).some((state) => state === "rendering"));
760
- const characterKeys = (0, vue.computed)(() => {
761
- const imgs = script.value.imageParams?.images ?? {};
762
- return Object.keys(imgs).filter((key) => imgs[key]?.type === "imagePrompt");
763
- });
764
1408
  const chatSessionId = (0, vue.computed)(() => adapter.chatSessionId?.value);
765
- function characterPrompt(key) {
766
- return script.value.imageParams?.images?.[key]?.prompt ?? "";
767
- }
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
+ });
768
1426
  function stopPlayingAudio() {
769
1427
  stopAllPlayback();
770
1428
  }
@@ -772,7 +1430,7 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ (0, vue.def
772
1430
  stopPlayingAudio();
773
1431
  lightbox.value = {
774
1432
  src: renderedImages[index],
775
- text: effectiveBeat(index).text,
1433
+ text: effectiveBeat$1(index).text,
776
1434
  index
777
1435
  };
778
1436
  }
@@ -783,7 +1441,7 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ (0, vue.def
783
1441
  const isPlayReady = (0, vue.computed)(() => {
784
1442
  if (beats.value.length === 0) return false;
785
1443
  if (!renderedImages[0]) return false;
786
- if (effectiveBeat(0).text && !beatAudios[0]) return false;
1444
+ if (effectiveBeat$1(0).text && !beatAudios[0]) return false;
787
1445
  return true;
788
1446
  });
789
1447
  function playPresentation() {
@@ -804,19 +1462,19 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ (0, vue.def
804
1462
  }
805
1463
  function playBeat(index) {
806
1464
  stopAllPlayback();
807
- if (!Boolean(effectiveBeat(index).text)) {
1465
+ if (!Boolean(effectiveBeat$1(index).text)) {
808
1466
  scheduleSilentAdvance(index);
809
1467
  return;
810
1468
  }
811
1469
  if (beatAudios[index]) playAudio(index);
812
1470
  }
813
1471
  function scheduleSilentAdvance(index) {
814
- const raw = effectiveBeat(index).duration;
1472
+ const seconds = resolveSilentAdvanceSeconds(effectiveBeat$1(index).duration, SILENT_BEAT_DEFAULT_SEC);
815
1473
  const timer = setTimeout(() => {
816
1474
  if (silentPlaybackTimer.value?.index !== index) return;
817
1475
  silentPlaybackTimer.value = null;
818
1476
  if (lightbox.value?.index === index) advanceFromBeat(index);
819
- }, (typeof raw === "number" && Number.isFinite(raw) && raw > 0 ? raw : SILENT_BEAT_DEFAULT_SEC) * MS_PER_SECOND);
1477
+ }, seconds * MS_PER_SECOND);
820
1478
  silentPlaybackTimer.value = {
821
1479
  index,
822
1480
  timer
@@ -838,6 +1496,7 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ (0, vue.def
838
1496
  for (let i = lightbox.value.index + 1; i < beats.value.length; i++) if (renderedImages[i]) return true;
839
1497
  return false;
840
1498
  });
1499
+ const beatTexts = (0, vue.computed)(() => beats.value.map((_, index) => effectiveBeat$1(index).text));
841
1500
  function jumpToBeat(index) {
842
1501
  if (!lightbox.value) return;
843
1502
  if (index === lightbox.value.index) return;
@@ -846,10 +1505,6 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ (0, vue.def
846
1505
  openLightbox(index);
847
1506
  if (wasPlaying) playBeat(index);
848
1507
  }
849
- function beatTooltip(index) {
850
- const text = effectiveBeat(index).text ?? "";
851
- return text.length > 80 ? `${text.slice(0, 80)}…` : text;
852
- }
853
1508
  function lightboxMove(delta) {
854
1509
  if (!lightbox.value) return;
855
1510
  const total = beats.value.length;
@@ -872,31 +1527,8 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ (0, vue.def
872
1527
  ...script.value,
873
1528
  beats: beats.value.map((beat, i) => localOverrides[i] ?? beat)
874
1529
  }));
875
- const scriptSourceText = (0, vue.computed)(() => JSON.stringify(effectiveScript.value, null, 2));
876
- const isDeck = (0, vue.computed)(() => isAllSlideDeck(effectiveScript.value));
877
- const deckScriptInput = (0, vue.computed)(() => effectiveScript.value);
878
- let deckSaveTimer = null;
879
- let pendingDeckScript = null;
880
- function scheduleDeckSave(next) {
881
- pendingDeckScript = next;
882
- if (deckSaveTimer) clearTimeout(deckSaveTimer);
883
- deckSaveTimer = setTimeout(() => {
884
- flushDeckSave();
885
- }, DECK_SAVE_DEBOUNCE_MS);
886
- }
887
- async function flushDeckSave() {
888
- deckSaveTimer = null;
889
- const next = pendingDeckScript;
890
- pendingDeckScript = null;
891
- if (!next || !filePath.value) return;
892
- const response = await api.call("updateScript", {
893
- filePath: filePath.value,
894
- script: next
895
- });
896
- if (!response.ok) {
897
- console.error("[presentMulmoScript] deck save failed:", response.error);
898
- return;
899
- }
1530
+ const scriptSourceText$1 = (0, vue.computed)(() => scriptSourceText(effectiveScript.value));
1531
+ function commitScript(next) {
900
1532
  emit("updateResult", {
901
1533
  ...props.selectedResult,
902
1534
  data: {
@@ -905,14 +1537,14 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ (0, vue.def
905
1537
  }
906
1538
  });
907
1539
  }
908
- function onDeckUpdate(next) {
909
- scheduleDeckSave(next);
910
- }
1540
+ const { isDeck, deckScriptInput, onDeckUpdate, flushPendingDeckSave } = useDeckEditor({
1541
+ api,
1542
+ filePath,
1543
+ effectiveScript,
1544
+ commitScript
1545
+ });
911
1546
  (0, vue.onBeforeUnmount)(() => {
912
- if (deckSaveTimer) {
913
- clearTimeout(deckSaveTimer);
914
- flushDeckSave();
915
- }
1547
+ flushPendingDeckSave();
916
1548
  resetBeatMovies();
917
1549
  unsubscribeGenerationEvents();
918
1550
  });
@@ -929,11 +1561,11 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ (0, vue.def
929
1561
  async function onSourceToggle(open) {
930
1562
  editing.value = open;
931
1563
  if (open) {
932
- let text = scriptSourceText.value;
1564
+ let text = scriptSourceText$1.value;
933
1565
  if (filePath.value) {
934
1566
  const response = await api.call("save", { filePath: filePath.value });
935
1567
  const diskScript = response.ok ? response.data.script : void 0;
936
- if (diskScript) text = JSON.stringify(diskScript, null, 2);
1568
+ if (diskScript) text = scriptSourceText(diskScript);
937
1569
  }
938
1570
  editableSource.value = text;
939
1571
  loadedSource.value = text;
@@ -947,7 +1579,7 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ (0, vue.def
947
1579
  try {
948
1580
  parsed = JSON.parse(editableSource.value);
949
1581
  } catch (err) {
950
- alert(errorMessage(err));
1582
+ alert(require_plugin.errorMessage(err));
951
1583
  return;
952
1584
  }
953
1585
  const response = await api.call("updateScript", {
@@ -958,31 +1590,25 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ (0, vue.def
958
1590
  alert(response.error || "Update failed");
959
1591
  return;
960
1592
  }
961
- emit("updateResult", {
962
- ...props.selectedResult,
963
- data: {
964
- ...props.selectedResult.data,
965
- script: parsed
966
- }
967
- });
1593
+ commitScript(parsed);
968
1594
  if (sourceDetails.value) sourceDetails.value.open = false;
969
1595
  await initializeScript();
970
1596
  }
971
1597
  async function copyText() {
972
- await copy(scriptSourceText.value);
1598
+ await copy(scriptSourceText$1.value);
973
1599
  }
974
- function effectiveBeat(index) {
975
- return localOverrides[index] ?? beats.value[index] ?? {};
1600
+ function effectiveBeat$1(index) {
1601
+ return effectiveBeat(localOverrides, beats.value, index);
976
1602
  }
977
1603
  function toggleSource(index) {
978
1604
  if (!sourceOpen[index]) {
979
- sourceText[index] = JSON.stringify(effectiveBeat(index), null, 2);
1605
+ sourceText[index] = scriptSourceText(effectiveBeat$1(index));
980
1606
  Reflect.deleteProperty(beatSaveErrors, index);
981
1607
  }
982
1608
  sourceOpen[index] = !sourceOpen[index];
983
1609
  }
984
- function isValidBeat(index) {
985
- return validateBeatJSON(sourceText[index] ?? "", _mulmocast_types.mulmoBeatSchema);
1610
+ function isValidBeat$1(index) {
1611
+ return isValidBeat(sourceText[index], _mulmocast_types.mulmoBeatSchema);
986
1612
  }
987
1613
  async function updateBeat(index) {
988
1614
  let beat;
@@ -991,12 +1617,12 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ (0, vue.def
991
1617
  } catch (err) {
992
1618
  beatSaveErrors[index] = {
993
1619
  kind: "invalidJson",
994
- error: errorMessage(err)
1620
+ error: require_plugin.errorMessage(err)
995
1621
  };
996
1622
  return;
997
1623
  }
998
- const prevImage = JSON.stringify(effectiveBeat(index).image);
999
- const prevText = effectiveBeat(index).text;
1624
+ const prevImage = JSON.stringify(effectiveBeat$1(index).image);
1625
+ const prevText = effectiveBeat$1(index).text;
1000
1626
  const requestedFilePath = filePath.value;
1001
1627
  Reflect.deleteProperty(beatSaveErrors, index);
1002
1628
  beatSaving[index] = true;
@@ -1005,7 +1631,7 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ (0, vue.def
1005
1631
  beatIndex: index,
1006
1632
  beat
1007
1633
  });
1008
- if (staleSince(requestedFilePath)) return;
1634
+ if (staleSince$1(requestedFilePath)) return;
1009
1635
  Reflect.deleteProperty(beatSaving, index);
1010
1636
  if (!response.ok) {
1011
1637
  beatSaveErrors[index] = {
@@ -1036,7 +1662,7 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ (0, vue.def
1036
1662
  beatIndex: index,
1037
1663
  chatSessionId: chatSessionId.value
1038
1664
  });
1039
- if (staleSince(requestedFilePath)) return;
1665
+ if (staleSince$1(requestedFilePath)) return;
1040
1666
  if (!response.ok) {
1041
1667
  renderErrors[index] = response.error || "Render failed";
1042
1668
  renderState[index] = "error";
@@ -1045,7 +1671,7 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ (0, vue.def
1045
1671
  renderedImages[index] = response.data.image ?? "";
1046
1672
  renderState[index] = "done";
1047
1673
  refreshMissingCharacterImages();
1048
- if (beatMayHaveMovie(effectiveBeat(index))) loadExistingBeatMovie(index);
1674
+ if (beatMayHaveMovie(effectiveBeat$1(index))) loadExistingBeatMovie(index);
1049
1675
  }
1050
1676
  async function regenerateBeat(index) {
1051
1677
  const requestedFilePath = filePath.value;
@@ -1058,7 +1684,7 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ (0, vue.def
1058
1684
  force: true,
1059
1685
  chatSessionId: chatSessionId.value
1060
1686
  });
1061
- if (staleSince(requestedFilePath)) return;
1687
+ if (staleSince$1(requestedFilePath)) return;
1062
1688
  if (!response.ok) {
1063
1689
  renderErrors[index] = response.error || "Render failed";
1064
1690
  renderState[index] = "error";
@@ -1066,10 +1692,10 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ (0, vue.def
1066
1692
  }
1067
1693
  renderedImages[index] = response.data.image ?? "";
1068
1694
  renderState[index] = "done";
1069
- if (beatMayHaveMovie(effectiveBeat(index))) loadExistingBeatMovie(index);
1695
+ if (beatMayHaveMovie(effectiveBeat$1(index))) loadExistingBeatMovie(index);
1070
1696
  }
1071
- function staleSince(requestedFilePath) {
1072
- return filePath.value !== requestedFilePath;
1697
+ function staleSince$1(requestedFilePath) {
1698
+ return staleSince(filePath.value, requestedFilePath);
1073
1699
  }
1074
1700
  async function loadExistingBeatImage(index) {
1075
1701
  const requestedFilePath = filePath.value;
@@ -1077,7 +1703,7 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ (0, vue.def
1077
1703
  filePath: requestedFilePath,
1078
1704
  beatIndex: index
1079
1705
  });
1080
- if (staleSince(requestedFilePath)) return;
1706
+ if (staleSince$1(requestedFilePath)) return;
1081
1707
  if (response.ok && response.data.image) {
1082
1708
  renderedImages[index] = response.data.image;
1083
1709
  renderState[index] = "done";
@@ -1089,61 +1715,12 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ (0, vue.def
1089
1715
  filePath: requestedFilePath,
1090
1716
  beatIndex: index
1091
1717
  });
1092
- if (staleSince(requestedFilePath)) return;
1718
+ if (staleSince$1(requestedFilePath)) return;
1093
1719
  if (response.ok && response.data.audio) {
1094
1720
  beatAudios[index] = response.data.audio;
1095
1721
  audioState[index] = "done";
1096
1722
  }
1097
1723
  }
1098
- async function loadExistingBeatMovie(index) {
1099
- const requestedFilePath = filePath.value;
1100
- const response = await api.call("beatMovie", {
1101
- filePath: requestedFilePath,
1102
- beatIndex: index
1103
- });
1104
- if (staleSince(requestedFilePath)) return;
1105
- if (response.ok && response.data.moviePath) beatMovies[index] = response.data.moviePath;
1106
- }
1107
- async function playBeatMovie(index) {
1108
- const fetchMediaBlob = adapter.fetchMediaBlob;
1109
- if (!fetchMediaBlob || !beatMovies[index] || beatMovieLoading[index]) return;
1110
- if (beatMovieUrls[index]) {
1111
- beatMovieOpen[index] = true;
1112
- return;
1113
- }
1114
- beatMovieLoading[index] = true;
1115
- try {
1116
- const blob = new Blob([await fetchMediaBlob({ moviePath: beatMovies[index] })], { type: "video/mp4" });
1117
- beatMovieUrls[index] = URL.createObjectURL(blob);
1118
- beatMovieOpen[index] = true;
1119
- } catch (err) {
1120
- alert(errorMessage(err));
1121
- } finally {
1122
- Reflect.deleteProperty(beatMovieLoading, index);
1123
- }
1124
- }
1125
- function closeBeatMovie(index) {
1126
- Reflect.deleteProperty(beatMovieOpen, index);
1127
- }
1128
- function invalidateBeatMovie(index) {
1129
- if (beatMovieUrls[index]) URL.revokeObjectURL(beatMovieUrls[index]);
1130
- [
1131
- beatMovies,
1132
- beatMovieUrls,
1133
- beatMovieOpen
1134
- ].forEach((map) => Reflect.deleteProperty(map, index));
1135
- }
1136
- function resetBeatMovies() {
1137
- Object.values(beatMovieUrls).forEach((url) => URL.revokeObjectURL(url));
1138
- [
1139
- beatMovies,
1140
- beatMovieUrls,
1141
- beatMovieOpen,
1142
- beatMovieLoading
1143
- ].forEach((map) => {
1144
- Object.keys(map).forEach((key) => Reflect.deleteProperty(map, key));
1145
- });
1146
- }
1147
1724
  async function generateAudio(index) {
1148
1725
  const requestedFilePath = filePath.value;
1149
1726
  audioState[index] = "generating";
@@ -1153,7 +1730,7 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ (0, vue.def
1153
1730
  beatIndex: index,
1154
1731
  chatSessionId: chatSessionId.value
1155
1732
  });
1156
- if (staleSince(requestedFilePath)) return;
1733
+ if (staleSince$1(requestedFilePath)) return;
1157
1734
  if (!response.ok) {
1158
1735
  audioErrors[index] = response.error || "Audio generation failed";
1159
1736
  audioState[index] = "error";
@@ -1206,14 +1783,9 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ (0, vue.def
1206
1783
  Reflect.deleteProperty(renderErrors, index);
1207
1784
  let imageData;
1208
1785
  try {
1209
- imageData = await new Promise((resolve, reject) => {
1210
- const reader = new FileReader();
1211
- reader.onload = () => resolve(reader.result);
1212
- reader.onerror = reject;
1213
- reader.readAsDataURL(file);
1214
- });
1786
+ imageData = await readFileAsDataUrl(file);
1215
1787
  } catch (err) {
1216
- renderErrors[index] = errorMessage(err);
1788
+ renderErrors[index] = require_plugin.errorMessage(err);
1217
1789
  renderState[index] = "error";
1218
1790
  return;
1219
1791
  }
@@ -1223,7 +1795,7 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ (0, vue.def
1223
1795
  beatIndex: index,
1224
1796
  imageData
1225
1797
  });
1226
- if (staleSince(requestedFilePath)) return;
1798
+ if (staleSince$1(requestedFilePath)) return;
1227
1799
  if (!response.ok) {
1228
1800
  renderErrors[index] = response.error || "Upload failed";
1229
1801
  renderState[index] = "error";
@@ -1232,49 +1804,6 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ (0, vue.def
1232
1804
  renderedImages[index] = response.data.image ?? "";
1233
1805
  renderState[index] = "done";
1234
1806
  }
1235
- function onCharDragOver(event, key) {
1236
- if (!event.dataTransfer?.types.includes("Files")) return;
1237
- event.preventDefault();
1238
- charDragOver[key] = true;
1239
- }
1240
- function onCharDragLeave(key) {
1241
- charDragOver[key] = false;
1242
- }
1243
- async function onCharDrop(event, key) {
1244
- event.preventDefault();
1245
- charDragOver[key] = false;
1246
- const file = event.dataTransfer?.files[0];
1247
- if (!file || !file.type.startsWith("image/")) return;
1248
- charRenderState[key] = "rendering";
1249
- Reflect.deleteProperty(charErrors, key);
1250
- let imageData;
1251
- try {
1252
- imageData = await new Promise((resolve, reject) => {
1253
- const reader = new FileReader();
1254
- reader.onload = () => resolve(reader.result);
1255
- reader.onerror = reject;
1256
- reader.readAsDataURL(file);
1257
- });
1258
- } catch (err) {
1259
- charErrors[key] = errorMessage(err);
1260
- charRenderState[key] = "error";
1261
- return;
1262
- }
1263
- const requestedFilePath = filePath.value;
1264
- const response = await api.call("uploadCharacterImage", {
1265
- filePath: requestedFilePath,
1266
- key,
1267
- imageData
1268
- });
1269
- if (staleSince(requestedFilePath)) return;
1270
- if (!response.ok) {
1271
- charErrors[key] = response.error || "Upload failed";
1272
- charRenderState[key] = "error";
1273
- return;
1274
- }
1275
- charImages[key] = response.data.image ?? "";
1276
- charRenderState[key] = "done";
1277
- }
1278
1807
  function openCharacterLightbox(key) {
1279
1808
  stopAllPlayback();
1280
1809
  lightbox.value = {
@@ -1284,43 +1813,6 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ (0, vue.def
1284
1813
  isCharacter: true
1285
1814
  };
1286
1815
  }
1287
- async function loadExistingCharacterImage(key) {
1288
- const requestedFilePath = filePath.value;
1289
- const response = await api.call("characterImage", {
1290
- filePath: requestedFilePath,
1291
- key
1292
- });
1293
- if (staleSince(requestedFilePath)) return;
1294
- if (response.ok && response.data.image) {
1295
- charImages[key] = response.data.image;
1296
- charRenderState[key] = "done";
1297
- }
1298
- }
1299
- function refreshMissingCharacterImages() {
1300
- getMissingCharacterKeys(characterKeys.value, charImages, charRenderState).forEach((key) => loadExistingCharacterImage(key));
1301
- }
1302
- async function renderCharacter(key, force) {
1303
- const requestedFilePath = filePath.value;
1304
- charRenderState[key] = "rendering";
1305
- Reflect.deleteProperty(charErrors, key);
1306
- const response = await api.call("renderCharacter", {
1307
- filePath: requestedFilePath,
1308
- key,
1309
- force,
1310
- chatSessionId: chatSessionId.value
1311
- });
1312
- if (staleSince(requestedFilePath)) return;
1313
- if (!response.ok) {
1314
- charErrors[key] = response.error || "Render failed";
1315
- charRenderState[key] = "error";
1316
- return;
1317
- }
1318
- charImages[key] = response.data.image ?? "";
1319
- charRenderState[key] = "done";
1320
- }
1321
- async function generateAllCharacters() {
1322
- await Promise.all(characterKeys.value.filter((key) => charRenderState[key] !== "rendering").map((key) => renderCharacter(key, false)));
1323
- }
1324
1816
  async function hydrateBeatImage(beat, index, hasCharacters, autoRenderTypes) {
1325
1817
  await loadExistingBeatImage(index);
1326
1818
  if (renderedImages[index]) return;
@@ -1362,39 +1854,16 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ (0, vue.def
1362
1854
  const diskScript = response.data.script;
1363
1855
  if (!diskScript) return;
1364
1856
  if (isSameScript(diskScript, script.value)) return;
1365
- emit("updateResult", {
1366
- ...props.selectedResult,
1367
- data: {
1368
- ...props.selectedResult.data,
1369
- script: diskScript
1370
- }
1371
- });
1857
+ commitScript(diskScript);
1372
1858
  }
1373
1859
  async function initializeScript() {
1374
1860
  stopAllPlayback();
1375
1861
  lightbox.value = null;
1376
1862
  if (beatListEl.value) beatListEl.value.scrollTop = 0;
1377
- Object.keys(renderState).forEach((key) => Reflect.deleteProperty(renderState, key));
1378
- Object.keys(renderedImages).forEach((key) => Reflect.deleteProperty(renderedImages, key));
1379
- Object.keys(renderErrors).forEach((key) => Reflect.deleteProperty(renderErrors, key));
1380
- Object.keys(sourceOpen).forEach((key) => Reflect.deleteProperty(sourceOpen, key));
1381
- Object.keys(sourceText).forEach((key) => Reflect.deleteProperty(sourceText, key));
1382
- Object.keys(beatSaveErrors).forEach((key) => Reflect.deleteProperty(beatSaveErrors, key));
1383
- Object.keys(beatSaving).forEach((key) => Reflect.deleteProperty(beatSaving, key));
1384
- Object.keys(localOverrides).forEach((key) => Reflect.deleteProperty(localOverrides, key));
1385
- Object.keys(beatAudios).forEach((key) => Reflect.deleteProperty(beatAudios, key));
1386
- Object.keys(audioState).forEach((key) => Reflect.deleteProperty(audioState, key));
1387
- Object.keys(audioErrors).forEach((key) => Reflect.deleteProperty(audioErrors, key));
1388
- Object.keys(charRenderState).forEach((key) => Reflect.deleteProperty(charRenderState, key));
1389
- Object.keys(charImages).forEach((key) => Reflect.deleteProperty(charImages, key));
1390
- Object.keys(charErrors).forEach((key) => Reflect.deleteProperty(charErrors, key));
1391
- 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();
1392
1865
  resetBeatMovies();
1393
- moviePath.value = null;
1394
- pdfPath.value = null;
1395
- movieGenerating.value = false;
1396
- pdfGenerating.value = false;
1397
- movieError.value = null;
1866
+ resetMedia();
1398
1867
  if (sourceDetails.value) sourceDetails.value.open = false;
1399
1868
  await refreshScriptFromDisk();
1400
1869
  const AUTO_RENDER_TYPES = [
@@ -1453,7 +1922,7 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ (0, vue.def
1453
1922
  if (entry.kind === "beatImage") {
1454
1923
  const idx = Number(entry.key);
1455
1924
  await loadExistingBeatImage(idx);
1456
- if (beatMayHaveMovie(effectiveBeat(idx))) await loadExistingBeatMovie(idx);
1925
+ if (beatMayHaveMovie(effectiveBeat$1(idx))) await loadExistingBeatMovie(idx);
1457
1926
  if (renderState[idx] === "rendering") Reflect.deleteProperty(renderState, idx);
1458
1927
  refreshMissingCharacterImages();
1459
1928
  } else if (entry.kind === "beatAudio") {
@@ -1471,95 +1940,6 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ (0, vue.def
1471
1940
  await refreshPdfPath();
1472
1941
  }
1473
1942
  }
1474
- async function refreshMoviePath() {
1475
- const requestedFilePath = filePath.value;
1476
- if (!requestedFilePath) return;
1477
- const response = await api.call("movieStatus", { filePath: requestedFilePath });
1478
- if (filePath.value !== requestedFilePath) return;
1479
- if (response.ok && response.data.moviePath) moviePath.value = response.data.moviePath;
1480
- }
1481
- async function generateMovie() {
1482
- const requestedFilePath = filePath.value;
1483
- movieGenerating.value = true;
1484
- movieError.value = null;
1485
- const response = await api.call("generateMovie", {
1486
- filePath: requestedFilePath,
1487
- chatSessionId: chatSessionId.value
1488
- });
1489
- if (filePath.value !== requestedFilePath) return;
1490
- movieGenerating.value = false;
1491
- if (!response.ok) {
1492
- movieError.value = response.error;
1493
- return;
1494
- }
1495
- moviePath.value = response.data.moviePath;
1496
- }
1497
- async function downloadMovie() {
1498
- const fetchMediaBlob = adapter.fetchMediaBlob;
1499
- if (!fetchMediaBlob || !moviePath.value || movieDownloading.value) return;
1500
- movieDownloading.value = true;
1501
- let objectUrl = null;
1502
- try {
1503
- const blob = await fetchMediaBlob({ moviePath: moviePath.value });
1504
- objectUrl = URL.createObjectURL(blob);
1505
- const filename = moviePath.value.split("/").pop() ?? "movie.mp4";
1506
- const anchor = document.createElement("a");
1507
- anchor.href = objectUrl;
1508
- anchor.download = filename;
1509
- document.body.appendChild(anchor);
1510
- anchor.click();
1511
- anchor.remove();
1512
- } catch (err) {
1513
- alert(errorMessage(err));
1514
- } finally {
1515
- if (objectUrl) URL.revokeObjectURL(objectUrl);
1516
- movieDownloading.value = false;
1517
- }
1518
- }
1519
- async function refreshPdfPath() {
1520
- const requestedFilePath = filePath.value;
1521
- if (!requestedFilePath) return;
1522
- const response = await api.call("pdfStatus", { filePath: requestedFilePath });
1523
- if (filePath.value !== requestedFilePath) return;
1524
- if (response.ok && response.data.pdfPath) pdfPath.value = response.data.pdfPath;
1525
- }
1526
- async function generatePdf() {
1527
- const requestedFilePath = filePath.value;
1528
- pdfGenerating.value = true;
1529
- const response = await api.call("generatePdf", {
1530
- filePath: requestedFilePath,
1531
- chatSessionId: chatSessionId.value
1532
- });
1533
- if (filePath.value !== requestedFilePath) return;
1534
- pdfGenerating.value = false;
1535
- if (!response.ok) {
1536
- alert(response.error);
1537
- return;
1538
- }
1539
- pdfPath.value = response.data.pdfPath;
1540
- }
1541
- async function downloadPdf() {
1542
- const fetchMediaBlob = adapter.fetchMediaBlob;
1543
- if (!fetchMediaBlob || !pdfPath.value || pdfDownloading.value) return;
1544
- pdfDownloading.value = true;
1545
- let objectUrl = null;
1546
- try {
1547
- const blob = await fetchMediaBlob({ pdfPath: pdfPath.value });
1548
- objectUrl = URL.createObjectURL(blob);
1549
- const filename = pdfPath.value.split("/").pop() ?? "deck.pdf";
1550
- const anchor = document.createElement("a");
1551
- anchor.href = objectUrl;
1552
- anchor.download = filename;
1553
- document.body.appendChild(anchor);
1554
- anchor.click();
1555
- anchor.remove();
1556
- } catch (err) {
1557
- alert(errorMessage(err));
1558
- } finally {
1559
- if (objectUrl) URL.revokeObjectURL(objectUrl);
1560
- pdfDownloading.value = false;
1561
- }
1562
- }
1563
1943
  return (_ctx, _cache) => {
1564
1944
  return (0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_1$1, [
1565
1945
  (0, vue.createElementVNode)("div", _hoisted_2$1, [(0, vue.createElementVNode)("div", _hoisted_3$1, [
@@ -1570,153 +1950,80 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ (0, vue.def
1570
1950
  script.value.lang ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", _hoisted_7, (0, vue.toDisplayString)(script.value.lang), 1)) : (0, vue.createCommentVNode)("", true),
1571
1951
  filePath.value ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", _hoisted_8, (0, vue.toDisplayString)(filePath.value), 1)) : (0, vue.createCommentVNode)("", true)
1572
1952
  ])
1573
- ]), (0, vue.createElementVNode)("div", _hoisted_9, [
1574
- moviePath.value && !movieGenerating.value ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("button", {
1575
- key: 0,
1576
- 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",
1577
- disabled: !isPlayReady.value,
1578
- title: (0, vue.unref)(m).playPresentation,
1579
- "aria-label": (0, vue.unref)(m).playPresentation,
1580
- onClick: playPresentation
1581
- }, [..._cache[6] || (_cache[6] = [(0, vue.createElementVNode)("span", { class: "material-icons text-base" }, "play_arrow", -1)])], 8, _hoisted_10)) : (0, vue.createCommentVNode)("", true),
1582
- moviePath.value && !movieGenerating.value && canFetchMedia.value ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("button", {
1583
- key: 1,
1584
- 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",
1585
- disabled: movieDownloading.value,
1586
- "data-testid": "mulmo-script-download-movie-button",
1587
- onClick: downloadMovie
1588
- }, [_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),
1589
- moviePath.value && !movieGenerating.value ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("button", {
1590
- key: 2,
1591
- class: "h-8 w-8 flex items-center justify-center rounded border border-gray-200 text-gray-600 hover:bg-gray-100 transition-colors",
1592
- title: (0, vue.unref)(m).regenerateMovie,
1593
- "aria-label": (0, vue.unref)(m).regenerateMovie,
1594
- "data-testid": "mulmo-script-regenerate-movie-button",
1595
- onClick: generateMovie
1596
- }, [..._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", {
1597
- key: 3,
1598
- 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",
1599
- disabled: movieGenerating.value,
1600
- "data-testid": "mulmo-script-generate-movie-button",
1601
- onClick: generateMovie
1602
- }, [movieGenerating.value ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("svg", _hoisted_14, [..._cache[9] || (_cache[9] = [(0, vue.createElementVNode)("circle", {
1603
- class: "opacity-25",
1604
- cx: "12",
1605
- cy: "12",
1606
- r: "10",
1607
- stroke: "currentColor",
1608
- "stroke-width": "4"
1609
- }, null, -1), (0, vue.createElementVNode)("path", {
1610
- class: "opacity-75",
1611
- fill: "currentColor",
1612
- d: "M4 12a8 8 0 018-8v8H4z"
1613
- }, 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)),
1614
- pdfPath.value && !pdfGenerating.value && canFetchMedia.value ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("button", {
1615
- key: 4,
1616
- 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",
1617
- disabled: pdfDownloading.value,
1618
- "data-testid": "mulmo-script-download-pdf-button",
1619
- onClick: downloadPdf
1620
- }, [_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),
1621
- pdfPath.value && !pdfGenerating.value ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("button", {
1622
- key: 5,
1623
- class: "h-8 w-8 flex items-center justify-center rounded border border-gray-200 text-gray-600 hover:bg-gray-100 transition-colors",
1624
- title: (0, vue.unref)(m).regeneratePdf,
1625
- "aria-label": (0, vue.unref)(m).regeneratePdf,
1626
- "data-testid": "mulmo-script-regenerate-pdf-button",
1627
- onClick: generatePdf
1628
- }, [..._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", {
1629
- key: 6,
1630
- 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",
1631
- disabled: pdfGenerating.value,
1632
- "data-testid": "mulmo-script-generate-pdf-button",
1633
- onClick: generatePdf
1634
- }, [pdfGenerating.value ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("svg", _hoisted_19, [..._cache[13] || (_cache[13] = [(0, vue.createElementVNode)("circle", {
1635
- class: "opacity-25",
1636
- cx: "12",
1637
- cy: "12",
1638
- r: "10",
1639
- stroke: "currentColor",
1640
- "stroke-width": "4"
1641
- }, null, -1), (0, vue.createElementVNode)("path", {
1642
- class: "opacity-75",
1643
- fill: "currentColor",
1644
- d: "M4 12a8 8 0 018-8v8H4z"
1645
- }, 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"
1646
1980
  ])]),
1647
- movieError.value ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_21, [
1648
- _cache[15] || (_cache[15] = (0, vue.createElementVNode)("span", { class: "material-icons text-base shrink-0 mt-px" }, "error_outline", -1)),
1649
- (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)]),
1650
1984
  (0, vue.createElementVNode)("button", {
1651
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",
1652
- disabled: movieGenerating.value,
1986
+ disabled: (0, vue.unref)(movieGenerating),
1653
1987
  "data-testid": "mulmo-script-movie-retry-button",
1654
- onClick: generateMovie
1655
- }, (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)
1656
1990
  ])) : (0, vue.createCommentVNode)("", true),
1657
- 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", {
1658
- class: "px-2 py-0.5 text-xs rounded border border-gray-300 text-gray-500 hover:bg-gray-50 disabled:opacity-50",
1659
- disabled: movieGenerating.value || anyBeatRendering.value || characterKeys.value.every((key) => charRenderState[key] === "rendering"),
1660
- onClick: generateAllCharacters
1661
- }, (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) => {
1662
- return (0, vue.openBlock)(), (0, vue.createElementBlock)("div", {
1663
- key,
1664
- class: "flex flex-col items-center gap-1 w-36"
1665
- }, [(0, vue.createElementVNode)("div", {
1666
- 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"]),
1667
- onDragover: ($event) => onCharDragOver($event, key),
1668
- onDragleave: ($event) => onCharDragLeave(key),
1669
- onDrop: ($event) => onCharDrop($event, key)
1670
- }, [
1671
- charImages[key] ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("img", {
1672
- key: 0,
1673
- src: charImages[key],
1674
- class: "w-full h-full object-cover cursor-zoom-in",
1675
- alt: key,
1676
- onClick: ($event) => openCharacterLightbox(key)
1677
- }, null, 8, _hoisted_32)) : charRenderState[key] === "rendering" ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("svg", _hoisted_33, [..._cache[16] || (_cache[16] = [(0, vue.createElementVNode)("circle", {
1678
- class: "opacity-25",
1679
- cx: "12",
1680
- cy: "12",
1681
- r: "10",
1682
- stroke: "currentColor",
1683
- "stroke-width": "4"
1684
- }, null, -1), (0, vue.createElementVNode)("path", {
1685
- class: "opacity-75",
1686
- fill: "currentColor",
1687
- d: "M4 12a8 8 0 018-8v8H4z"
1688
- }, 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)),
1689
- !charDragOver[key] ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_36, (0, vue.toDisplayString)((0, vue.unref)(m).orDropImage), 1)) : (0, vue.createCommentVNode)("", true),
1690
- 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),
1691
- charImages[key] && charRenderState[key] !== "rendering" ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("button", {
1692
- key: 6,
1693
- 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"]),
1694
- disabled: movieGenerating.value || anyBeatRendering.value,
1695
- onClick: (0, vue.withModifiers)(($event) => renderCharacter(key, true), ["stop"])
1696
- }, [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", {
1697
- key: 7,
1698
- 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"]),
1699
- disabled: movieGenerating.value || anyBeatRendering.value,
1700
- onClick: (0, vue.withModifiers)(($event) => renderCharacter(key, false), ["stop"])
1701
- }, [movieGenerating.value || anyBeatRendering.value ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("svg", _hoisted_43, [..._cache[17] || (_cache[17] = [(0, vue.createElementVNode)("circle", {
1702
- class: "opacity-25",
1703
- cx: "12",
1704
- cy: "12",
1705
- r: "10",
1706
- stroke: "currentColor",
1707
- "stroke-width": "4"
1708
- }, null, -1), (0, vue.createElementVNode)("path", {
1709
- class: "opacity-75",
1710
- fill: "currentColor",
1711
- d: "M4 12a8 8 0 018-8v8H4z"
1712
- }, 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)
1713
- ], 42, _hoisted_31), (0, vue.createElementVNode)("span", _hoisted_45, (0, vue.toDisplayString)(key), 1)]);
1714
- }), 128))])])) : (0, vue.createCommentVNode)("", true),
1715
- isDeck.value ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_46, [(0, vue.createVNode)((0, vue.unref)(MulmoScriptDeckEditor), {
1716
- 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),
1717
2024
  layout: "compact",
1718
- "onUpdate:script": onDeckUpdate
1719
- }, 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", {
1720
2027
  key: 3,
1721
2028
  ref_key: "beatListEl",
1722
2029
  ref: beatListEl,
@@ -1725,40 +2032,44 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ (0, vue.def
1725
2032
  return (0, vue.openBlock)(), (0, vue.createElementBlock)("div", {
1726
2033
  key: index,
1727
2034
  class: "rounded-lg border border-gray-200 overflow-hidden"
1728
- }, [(0, vue.createElementVNode)("div", _hoisted_47, [(0, vue.createElementVNode)("div", {
2035
+ }, [(0, vue.createElementVNode)("div", _hoisted_15, [(0, vue.createElementVNode)("div", {
1729
2036
  class: (0, vue.normalizeClass)(["relative shrink-0 w-[45%] overflow-hidden bg-gray-50 transition-colors", beatDragOver[index] ? "bg-blue-50" : ""]),
1730
2037
  onDragover: ($event) => onBeatDragOver($event, index),
1731
2038
  onDragleave: ($event) => onBeatDragLeave(index),
1732
2039
  onDrop: ($event) => onBeatDrop($event, index)
1733
2040
  }, [
1734
- beatMovieOpen[index] && beatMovieUrls[index] ? ((0, vue.openBlock)(), (0, vue.createElementBlock)(vue.Fragment, { key: 0 }, [(0, vue.createElementVNode)("video", {
1735
- 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],
1736
2047
  class: "w-full object-contain",
1737
2048
  controls: "",
1738
2049
  autoplay: "",
1739
2050
  "data-testid": `mulmo-script-beat-movie-player-${index}`
1740
- }, null, 8, _hoisted_49), (0, vue.createElementVNode)("button", {
2051
+ }, null, 8, _hoisted_18), (0, vue.createElementVNode)("button", {
1741
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",
1742
2053
  title: (0, vue.unref)(m).close,
1743
2054
  "aria-label": (0, vue.unref)(m).close,
1744
2055
  "data-testid": `mulmo-script-beat-movie-close-${index}`,
1745
- onClick: (0, vue.withModifiers)(($event) => closeBeatMovie(index), ["stop"])
1746
- }, [..._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 }, [
1747
2058
  renderedImages[index] ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("img", {
1748
2059
  key: 0,
1749
2060
  src: renderedImages[index],
1750
2061
  class: "w-full object-contain cursor-zoom-in",
1751
2062
  alt: `Beat ${index + 1}`,
1752
2063
  onClick: ($event) => openLightbox(index)
1753
- }, null, 8, _hoisted_51)) : (0, vue.createCommentVNode)("", true),
1754
- 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", {
1755
2066
  key: 1,
1756
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",
1757
2068
  title: (0, vue.unref)(m).play,
1758
2069
  "aria-label": (0, vue.unref)(m).play,
1759
2070
  "data-testid": `mulmo-script-beat-movie-play-${index}`,
1760
- onClick: (0, vue.withModifiers)(($event) => playBeatMovie(index), ["stop"])
1761
- }, [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", {
1762
2073
  class: "opacity-25",
1763
2074
  cx: "12",
1764
2075
  cy: "12",
@@ -1769,13 +2080,13 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ (0, vue.def
1769
2080
  class: "opacity-75",
1770
2081
  fill: "currentColor",
1771
2082
  d: "M4 12a8 8 0 018-8v8H4z"
1772
- }, 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),
1773
2084
  renderedImages[index] && renderState[index] !== "rendering" ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("button", {
1774
2085
  key: 2,
1775
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",
1776
- disabled: movieGenerating.value,
2087
+ disabled: (0, vue.unref)(movieGenerating),
1777
2088
  onClick: (0, vue.withModifiers)(($event) => regenerateBeat(index), ["stop"])
1778
- }, " ↺ ", 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", {
1779
2090
  class: "animate-spin w-4 h-4 text-green-400",
1780
2091
  viewBox: "0 0 24 24",
1781
2092
  fill: "none"
@@ -1790,15 +2101,15 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ (0, vue.def
1790
2101
  class: "opacity-75",
1791
2102
  fill: "currentColor",
1792
2103
  d: "M4 12a8 8 0 018-8v8H4z"
1793
- })], -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)
1794
2105
  ], 64)),
1795
- 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),
1796
- !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", {
1797
2108
  key: 4,
1798
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",
1799
2110
  onClick: ($event) => renderBeat(index)
1800
- }, (0, vue.toDisplayString)((0, vue.unref)(m).generate), 9, _hoisted_64)) : (0, vue.createCommentVNode)("", true)
1801
- ], 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", {
1802
2113
  class: "opacity-25",
1803
2114
  cx: "12",
1804
2115
  cy: "12",
@@ -1813,24 +2124,24 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ (0, vue.def
1813
2124
  key: 1,
1814
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"]),
1815
2126
  onClick: ($event) => playAudio(index)
1816
- }, (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", {
1817
2128
  class: "text-xs text-red-400 truncate min-w-0 max-w-[20rem]",
1818
2129
  title: audioErrors[index]
1819
- }, (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", {
1820
2131
  key: 0,
1821
2132
  class: "text-xs px-2 py-0.5 rounded border border-gray-300 text-gray-500 hover:bg-gray-50 disabled:opacity-50",
1822
- disabled: movieGenerating.value,
2133
+ disabled: (0, vue.unref)(movieGenerating),
1823
2134
  onClick: ($event) => generateAudio(index)
1824
- }, " ↺ ", 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", {
1825
2136
  key: 3,
1826
2137
  class: "text-xs px-2 py-0.5 rounded border border-gray-300 text-gray-500 hover:bg-gray-50",
1827
2138
  onClick: ($event) => generateAudio(index)
1828
- }, (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", {
1829
2140
  class: "text-gray-400 hover:text-gray-600",
1830
2141
  title: sourceOpen[index] ? "Hide source" : "Show source",
1831
2142
  "data-testid": `mulmo-script-beat-source-toggle-${index}`,
1832
2143
  onClick: ($event) => toggleSource(index)
1833
- }, [..._cache[22] || (_cache[22] = [(0, vue.createElementVNode)("svg", {
2144
+ }, [..._cache[8] || (_cache[8] = [(0, vue.createElementVNode)("svg", {
1834
2145
  xmlns: "http://www.w3.org/2000/svg",
1835
2146
  class: "w-3.5 h-3.5",
1836
2147
  viewBox: "0 0 24 24",
@@ -1839,36 +2150,36 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ (0, vue.def
1839
2150
  "stroke-width": "2",
1840
2151
  "stroke-linecap": "round",
1841
2152
  "stroke-linejoin": "round"
1842
- }, [(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", {
1843
2154
  "onUpdate:modelValue": ($event) => sourceText[index] = $event,
1844
- 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"]),
1845
2156
  rows: "8",
1846
2157
  spellcheck: "false",
1847
2158
  "data-testid": `mulmo-script-beat-source-textarea-${index}`
1848
- }, 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", {
1849
- 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"]),
1850
- 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],
1851
2162
  "data-testid": `mulmo-script-beat-update-button-${index}`,
1852
2163
  onClick: ($event) => updateBeat(index)
1853
- }, (0, vue.toDisplayString)(beatSaving[index] ? (0, vue.unref)(m).saving : (0, vue.unref)(m).update), 11, _hoisted_79)])])) : (0, vue.createCommentVNode)("", true)]);
1854
- }), 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)),
1855
- (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", {
1856
2167
  ref_key: "sourceDetails",
1857
2168
  ref: sourceDetails,
1858
2169
  class: "script-source",
1859
- onToggle: _cache[1] || (_cache[1] = ($event) => onSourceToggle($event.target.open))
2170
+ onToggle: _cache[2] || (_cache[2] = ($event) => onSourceToggle($event.target.open))
1860
2171
  }, [
1861
2172
  (0, vue.createElementVNode)("summary", null, (0, vue.toDisplayString)((0, vue.unref)(m).editSource), 1),
1862
2173
  (0, vue.withDirectives)((0, vue.createElementVNode)("textarea", {
1863
- "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => editableSource.value = $event),
2174
+ "onUpdate:modelValue": _cache[1] || (_cache[1] = ($event) => editableSource.value = $event),
1864
2175
  class: (0, vue.normalizeClass)(["script-editor", { "script-editor-invalid": sourceChanged.value && !sourceValid.value }]),
1865
2176
  spellcheck: "false"
1866
2177
  }, null, 2), [[vue.vModelText, editableSource.value]]),
1867
- (0, vue.createElementVNode)("div", _hoisted_82, [(0, vue.createElementVNode)("button", {
2178
+ (0, vue.createElementVNode)("div", _hoisted_51, [(0, vue.createElementVNode)("button", {
1868
2179
  class: "apply-btn",
1869
2180
  disabled: !sourceChanged.value || !sourceValid.value,
1870
2181
  onClick: applySource
1871
- }, (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", {
1872
2183
  class: "cancel-btn",
1873
2184
  onClick: cancelSourceEdit
1874
2185
  }, (0, vue.toDisplayString)((0, vue.unref)(m).cancel), 1)])
@@ -1876,50 +2187,31 @@ var View_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ (0, vue.def
1876
2187
  class: "copy-btn",
1877
2188
  title: (0, vue.unref)(copied) ? "Copied!" : "Copy",
1878
2189
  onClick: copyText
1879
- }, [(0, vue.createElementVNode)("span", _hoisted_85, (0, vue.toDisplayString)((0, vue.unref)(copied) ? "check" : "content_copy"), 1)], 8, _hoisted_84), [[vue.vShow, !editing.value]])]),
1880
- 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, {
1881
2192
  key: 4,
1882
- class: "fixed inset-0 z-50 bg-black/80 overflow-y-auto",
1883
- onClick: closeLightbox
1884
- }, [(0, vue.createElementVNode)("button", {
1885
- class: "fixed top-2 right-4 z-10 text-white/60 hover:text-white text-3xl leading-none",
1886
- title: (0, vue.unref)(m).close,
1887
- onClick: (0, vue.withModifiers)(closeLightbox, ["stop"])
1888
- }, "", 8, _hoisted_86), (0, vue.createElementVNode)("div", {
1889
- class: "flex flex-col items-center gap-4 pt-4 pb-8",
1890
- onClick: _cache[5] || (_cache[5] = (0, vue.withModifiers)(() => {}, ["stop"]))
1891
- }, [(0, vue.createElementVNode)("div", _hoisted_87, [
1892
- !lightbox.value.isCharacter ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("button", {
1893
- key: 0,
1894
- class: "text-white/60 hover:text-white disabled:opacity-20 text-5xl leading-none",
1895
- disabled: !hasPrev.value,
1896
- onClick: _cache[2] || (_cache[2] = ($event) => lightboxMove(-1))
1897
- }, "", 8, _hoisted_88)) : (0, vue.createCommentVNode)("", true),
1898
- (0, vue.createElementVNode)("div", _hoisted_89, [(0, vue.createElementVNode)("img", {
1899
- src: lightbox.value.src,
1900
- class: "max-w-[80vw] max-h-[85vh] object-contain rounded shadow-2xl"
1901
- }, 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) => {
1902
- return (0, vue.openBlock)(), (0, vue.createElementBlock)("div", {
1903
- key: i - 1,
1904
- 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"]),
1905
- onClick: ($event) => jumpToBeat(i - 1)
1906
- }, [_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);
1907
- }), 128))]), playingAudio.value && playingAudio.value.index === lightbox.value.index ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", {
1908
- key: 0,
1909
- 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",
1910
- style: (0, vue.normalizeStyle)({ left: `${(lightbox.value.index + audioProgress.value) / beats.value.length * 100}%` })
1911
- }, null, 4)) : (0, vue.createCommentVNode)("", true)])) : (0, vue.createCommentVNode)("", true)]),
1912
- !lightbox.value.isCharacter ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("button", {
1913
- key: 1,
1914
- class: "text-white/60 hover:text-white disabled:opacity-20 text-5xl leading-none",
1915
- disabled: !hasNext.value,
1916
- onClick: _cache[3] || (_cache[3] = ($event) => lightboxMove(1))
1917
- }, " › ", 8, _hoisted_95)) : (0, vue.createCommentVNode)("", true)
1918
- ]), 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", {
1919
- key: 1,
1920
- class: "absolute top-0 right-4 text-sm px-3 py-1 rounded border border-white/60 text-white/60 hover:bg-white/20",
1921
- onClick: _cache[4] || (_cache[4] = ($event) => playAudio(lightbox.value.index))
1922
- }, (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)
1923
2215
  ]);
1924
2216
  };
1925
2217
  }
@@ -1933,7 +2225,7 @@ var _plugin_vue_export_helper_default = (sfc, props) => {
1933
2225
  };
1934
2226
  //#endregion
1935
2227
  //#region src/vue/View.vue
1936
- 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"]]);
1937
2229
  //#endregion
1938
2230
  //#region src/vue/Preview.vue?vue&type=script&setup=true&lang.ts
1939
2231
  var _hoisted_1 = {