@mulmoclaude/mulmoscript-plugin 1.0.0 → 1.1.1

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