@ai-game-assets/phaser 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"designer.d.ts","sourceRoot":"","sources":["../src/designer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAGV,iBAAiB,EAOjB,eAAe,EACf,cAAc,EACd,sBAAsB,EAEvB,MAAM,sBAAsB,CAAC;AAQ9B,OAAO,EACL,kBAAkB,EAElB,KAAK,oBAAoB,EAE1B,MAAM,mBAAmB,CAAC;AA0B3B,KAAK,yBAAyB,GAAG;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG;IACrC,QAAQ,EAAE;QACR,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;QAC7B,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;QAC7B,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC;QACxD,cAAc,CAAC,CACb,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,gBAAgB,EACvB,MAAM,EAAE,yBAAyB,GAChC,OAAO,CAAC;KACZ,CAAC;IACF,KAAK,CAAC,EAAE;QACN,QAAQ,CAAC,EAAE;YACT,OAAO,EAAE,OAAO,CAAC;SAClB,GAAG,IAAI,CAAC;KACV,CAAC;CACH,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,KAAK,EAAE,wBAAwB,CAAC;IAChC,QAAQ,EAAE,eAAe,CAAC;IAC1B,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,yBAAyB,CAAC,EAAE,OAAO,CAAC;IACpC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,kBAAkB,CAAC,EACf,MAAM,CAAC,MAAM,EAAE,yBAAyB,CAAC,GACzC,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,iBAAiB,KAAK,yBAAyB,GAAG,SAAS,CAAC,CAAC;IAC3F,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,iBAAiB,GAAG,IAAI,CAAC;IAC/E,yBAAyB,CAAC,CACxB,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,MAAM,EAAE,EACrB,KAAK,EAAE,iBAAiB,GACvB,IAAI,CAAC;IACR,YAAY,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,iBAAiB,GAAG,IAAI,CAAC;IACnF,iBAAiB,CAAC,CAAC,QAAQ,EAAE,eAAe,GAAG,IAAI,CAAC;CACrD,CAAC;AAEF,MAAM,MAAM,yBAAyB,GAAG;IACtC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,IAAI,EAAE,cAAc,CAAC;IACrB,IAAI,IAAI,IAAI,CAAC;IACb,KAAK,IAAI,IAAI,CAAC;IACd,OAAO,IAAI,IAAI,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,yBAAyB,GAAG;IACtC,SAAS,EAAE,MAAM,CAAC;IAClB,iBAAiB,CAAC,EAAE,sBAAsB,EAAE,CAAC;CAC9C,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,gCAAgC,CAAC,OAAO,EAAE;IACxD,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,oBAAoB,CAAC;IACrC,aAAa,CAAC,EAAE,cAAc,CAAC;IAC/B,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;CACvC,GAAG,yBAAyB,CAoB5B;AAoDD,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,sBAAsB,GAC9B,eAAe,CA44FjB"}
1
+ {"version":3,"file":"designer.d.ts","sourceRoot":"","sources":["../src/designer.ts"],"names":[],"mappings":"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"}
package/dist/designer.js CHANGED
@@ -4,6 +4,7 @@ import { ensureMissingAiAssetFirstDrafts } from "./first-drafts.js";
4
4
  import { createLocalDerivedOption, openDeriveDialog, referenceImageForCandidate } from "./derive-dialog.js";
5
5
  import { createMixedTilesetOption, isDesignerTilesetAsset, openTilesetBaseMixerDialog, openTilesetAnimationEditor, openTilesetAnimationMixerDialog, openTilesetEditor, tilesetAnimationForKey, resolveTilesetBaseMixCurrent, tilesetMetadataForAsset, tilesetTileGenerationOverride } from "./tileset-dialog.js";
6
6
  import { aiTilesetAnimationTextureKey } from "./keys.js";
7
+ import { promotedVoiceId, regenerateAndPromoteVoiceLines, voiceLineRegenerationPlan } from "./voice-line-regeneration.js";
7
8
  /**
8
9
  * Resolves the canonical, untransformed source used by Edit Tileset. A pending
9
10
  * option is its own editing lineage: when it has no saved source metadata, its
@@ -63,6 +64,13 @@ export function installAiAssetDesigner(options) {
63
64
  let panelRevision = 0;
64
65
  let promotionId = 0;
65
66
  let activePromotionId;
67
+ let activeVoiceLineRegeneration;
68
+ let activeFirstDrafts = options.autoFirstDrafts !== false;
69
+ let destroyed = false;
70
+ let deferredVoiceLineManifestSync = false;
71
+ const deferredVoiceLinePendingAssetIds = new Set();
72
+ let deferredVoiceLineManifestSyncPromise;
73
+ let voiceLineBatchControlStates;
66
74
  let mixingTileset = false;
67
75
  const regenerateTilesetTile = async (assetId, tile, tileset, currentTileSrc) => {
68
76
  const guide = await styleGuideRequest(styleGuideDraft);
@@ -100,11 +108,87 @@ export function installAiAssetDesigner(options) {
100
108
  const mount = options.mount ?? document.body;
101
109
  mount.append(elements.root);
102
110
  bindKeyboardCapture(elements.root, options.scene);
111
+ const setVoiceLineBatchControlsLocked = (locked) => {
112
+ if (locked) {
113
+ if (voiceLineBatchControlStates)
114
+ return;
115
+ const controls = Array.from(elements.panel.querySelectorAll("button, input, select, textarea")).filter((control) => control !== elements.regenerateAllLinesButton);
116
+ voiceLineBatchControlStates = controls.map((control) => [control, control.disabled]);
117
+ for (const control of controls)
118
+ control.disabled = true;
119
+ elements.assetBrowser.setAttribute("inert", "");
120
+ elements.currentPreview.setAttribute("inert", "");
121
+ elements.options.setAttribute("inert", "");
122
+ return;
123
+ }
124
+ for (const [control, wasDisabled] of voiceLineBatchControlStates ?? []) {
125
+ control.disabled = wasDisabled;
126
+ }
127
+ voiceLineBatchControlStates = undefined;
128
+ elements.assetBrowser.removeAttribute("inert");
129
+ elements.currentPreview.removeAttribute("inert");
130
+ elements.options.removeAttribute("inert");
131
+ };
132
+ const syncRegenerateAllLinesButton = () => {
133
+ if (activeVoiceLineRegeneration) {
134
+ const stopping = activeVoiceLineRegeneration.controller.signal.aborted;
135
+ elements.regenerateAllLinesButton.hidden = false;
136
+ elements.regenerateAllLinesButton.disabled = stopping;
137
+ elements.regenerateAllLinesButton.textContent = stopping
138
+ ? "Stopping after current line..."
139
+ : "Stop after current line";
140
+ elements.regenerateAllLinesButton.title = stopping
141
+ ? "The batch will stop after the current line is promoted."
142
+ : "Finish and promote the current line, then stop before generating another.";
143
+ return;
144
+ }
145
+ const selectedAsset = manifest.assets[selectedAssetId];
146
+ const plan = voiceLineRegenerationPlan(manifest, selectedAssetId, selectedTargetId);
147
+ const { baseVoiceAssetId, lineAssetIds, missingTargetLineAssetIds } = plan;
148
+ const isBaseVoiceView = selectedAsset?.kind === "voice" &&
149
+ selectedTargetAssetId === baseVoiceAssetId &&
150
+ !selectedTilesetAnimationKey;
151
+ const hasPromotedVoice = Boolean(promotedVoiceId(manifest, baseVoiceAssetId));
152
+ const hasPendingBaseVoice = pendingOptions.has(baseVoiceAssetId);
153
+ const pendingLineCount = lineAssetIds.filter((assetId) => pendingOptions.has(assetId)).length;
154
+ const hasAnyPendingOption = pendingOptions.size > 0;
155
+ elements.regenerateAllLinesButton.textContent = "Regenerate all lines";
156
+ elements.regenerateAllLinesButton.hidden = !isBaseVoiceView;
157
+ elements.regenerateAllLinesButton.disabled =
158
+ !isBaseVoiceView ||
159
+ !hasPromotedVoice ||
160
+ hasPendingBaseVoice ||
161
+ pendingLineCount > 0 ||
162
+ hasAnyPendingOption ||
163
+ missingTargetLineAssetIds.length > 0 ||
164
+ lineAssetIds.length === 0 ||
165
+ activeFirstDrafts ||
166
+ Boolean(activeGeneration) ||
167
+ activePromotionId !== undefined;
168
+ elements.regenerateAllLinesButton.title = !isBaseVoiceView
169
+ ? ""
170
+ : hasPendingBaseVoice
171
+ ? "Promote the selected base voice before regenerating its lines."
172
+ : missingTargetLineAssetIds.length > 0
173
+ ? `Create target variants for ${missingTargetLineAssetIds.length} linked line${missingTargetLineAssetIds.length === 1 ? "" : "s"} before regenerating this target voice.`
174
+ : pendingLineCount > 0
175
+ ? `Promote or revert the ${pendingLineCount} pending linked line${pendingLineCount === 1 ? "" : "s"} before regenerating all lines.`
176
+ : hasAnyPendingOption
177
+ ? "Promote or revert all pending asset options before regenerating voice lines."
178
+ : activeFirstDrafts
179
+ ? "Wait for automatic first drafts to finish before regenerating voice lines."
180
+ : !hasPromotedVoice
181
+ ? "Promote a base voice before regenerating its lines."
182
+ : lineAssetIds.length === 0
183
+ ? "This voice has no linked lines to regenerate."
184
+ : `Generate and promote one new option for ${lineAssetIds.length} linked line${lineAssetIds.length === 1 ? "" : "s"}.`;
185
+ };
103
186
  const syncPromoteAllButton = () => {
104
187
  elements.promoteAllButton.disabled =
105
188
  pendingOptions.size === 0 ||
106
189
  Boolean(activeGeneration) ||
107
190
  activePromotionId !== undefined;
191
+ syncRegenerateAllLinesButton();
108
192
  };
109
193
  const rememberPendingOption = (assetId, option, pending = {}) => {
110
194
  pendingOptions.set(assetId, {
@@ -116,8 +200,53 @@ export function installAiAssetDesigner(options) {
116
200
  });
117
201
  syncPromoteAllButton();
118
202
  };
203
+ const flushDeferredVoiceLineManifest = () => {
204
+ if (!deferredVoiceLineManifestSync)
205
+ return Promise.resolve(true);
206
+ if ([...deferredVoiceLinePendingAssetIds].some((assetId) => pendingOptions.has(assetId))) {
207
+ return Promise.resolve(false);
208
+ }
209
+ if (deferredVoiceLineManifestSyncPromise) {
210
+ return deferredVoiceLineManifestSyncPromise;
211
+ }
212
+ deferredVoiceLineManifestSyncPromise = (async () => {
213
+ try {
214
+ const syncedManifest = await client.syncManifestModule();
215
+ manifest = syncedManifest;
216
+ deferredVoiceLineManifestSync = false;
217
+ deferredVoiceLinePendingAssetIds.clear();
218
+ if (!destroyed) {
219
+ try {
220
+ options.onManifestUpdated?.(manifest);
221
+ }
222
+ catch (error) {
223
+ setStatus(elements, `Manifest refreshed, but the live game could not adopt it. ${errorMessage(error)}`, "error");
224
+ }
225
+ }
226
+ return true;
227
+ }
228
+ catch (error) {
229
+ if (!destroyed) {
230
+ setStatus(elements, `Could not refresh the generated manifest module. ${errorMessage(error)}`, "error");
231
+ }
232
+ return false;
233
+ }
234
+ finally {
235
+ deferredVoiceLineManifestSyncPromise = undefined;
236
+ }
237
+ })();
238
+ return deferredVoiceLineManifestSyncPromise;
239
+ };
240
+ const resolveDeferredVoiceLinePendingOption = (assetId) => {
241
+ if (!deferredVoiceLinePendingAssetIds.delete(assetId))
242
+ return;
243
+ if (deferredVoiceLinePendingAssetIds.size === 0) {
244
+ void flushDeferredVoiceLineManifest();
245
+ }
246
+ };
119
247
  const forgetPendingOption = (assetId) => {
120
248
  pendingOptions.delete(assetId);
249
+ resolveDeferredVoiceLinePendingOption(assetId);
121
250
  syncPromoteAllButton();
122
251
  };
123
252
  const hasEditableTilesetAnimationFrames = (assetId, animationKey) => {
@@ -1417,6 +1546,189 @@ export function installAiAssetDesigner(options) {
1417
1546
  finishGeneration(currentGenerationId);
1418
1547
  }
1419
1548
  });
1549
+ elements.regenerateAllLinesButton.addEventListener("click", async () => {
1550
+ if (activeVoiceLineRegeneration) {
1551
+ if (!activeVoiceLineRegeneration.controller.signal.aborted) {
1552
+ activeVoiceLineRegeneration.controller.abort();
1553
+ syncRegenerateAllLinesButton();
1554
+ setStatus(elements, "Stopping after the current line is promoted...", "busy");
1555
+ }
1556
+ return;
1557
+ }
1558
+ const voiceAssetId = selectedAssetId;
1559
+ const voice = manifest.assets[voiceAssetId];
1560
+ const plan = voiceLineRegenerationPlan(manifest, voiceAssetId, selectedTargetId);
1561
+ const { baseVoiceAssetId, lineAssetIds, missingTargetLineAssetIds } = plan;
1562
+ if (voice?.kind !== "voice" ||
1563
+ selectedTargetAssetId !== baseVoiceAssetId ||
1564
+ selectedTilesetAnimationKey ||
1565
+ activeFirstDrafts ||
1566
+ activeGeneration ||
1567
+ activePromotionId !== undefined) {
1568
+ return;
1569
+ }
1570
+ if (pendingOptions.has(baseVoiceAssetId)) {
1571
+ setStatus(elements, "Promote the selected base voice before regenerating its lines.", "error");
1572
+ return;
1573
+ }
1574
+ if (!promotedVoiceId(manifest, baseVoiceAssetId)) {
1575
+ setStatus(elements, "Promote a base voice before regenerating its lines.", "error");
1576
+ return;
1577
+ }
1578
+ if (missingTargetLineAssetIds.length > 0) {
1579
+ setStatus(elements, `Create target variants for ${missingTargetLineAssetIds.map(readableAssetName).join(", ")} before regenerating this target voice.`, "error");
1580
+ return;
1581
+ }
1582
+ if (lineAssetIds.length === 0) {
1583
+ setStatus(elements, "This voice has no linked lines to regenerate.", "error");
1584
+ return;
1585
+ }
1586
+ const pendingLineAssetIds = lineAssetIds.filter((assetId) => pendingOptions.has(assetId));
1587
+ if (pendingLineAssetIds.length > 0) {
1588
+ setStatus(elements, `Promote or revert pending options for ${pendingLineAssetIds.map(readableAssetName).join(", ")} before regenerating all lines.`, "error");
1589
+ return;
1590
+ }
1591
+ if (pendingOptions.size > 0) {
1592
+ setStatus(elements, "Promote or revert all pending asset options before regenerating voice lines.", "error");
1593
+ return;
1594
+ }
1595
+ const targetLabel = selectedTargetId
1596
+ ? manifest.targets?.[selectedTargetId]?.label ?? readableAssetName(selectedTargetId)
1597
+ : "Default";
1598
+ const confirmed = window.confirm(`Regenerate and promote one new option for all ${lineAssetIds.length} linked line${lineAssetIds.length === 1 ? "" : "s"} of ${readableAssetName(baseVoiceAssetId)} (${targetLabel})?\n\nThis uses paid generation and replaces each line's active version as it completes.`);
1599
+ if (!confirmed)
1600
+ return;
1601
+ const currentPromotionId = promotionId + 1;
1602
+ const controller = new AbortController();
1603
+ promotionId = currentPromotionId;
1604
+ activePromotionId = currentPromotionId;
1605
+ activeVoiceLineRegeneration = { controller, id: currentPromotionId };
1606
+ elements.panel.setAttribute("aria-busy", "true");
1607
+ setVoiceLineBatchControlsLocked(true);
1608
+ syncPromoteAllButton();
1609
+ syncMixTilesetButton();
1610
+ let promotedCount = 0;
1611
+ let liveRefreshError;
1612
+ let batchCompletedCleanly = false;
1613
+ try {
1614
+ const result = await regenerateAndPromoteVoiceLines({
1615
+ manifest,
1616
+ voiceAssetId,
1617
+ targetId: selectedTargetId,
1618
+ client,
1619
+ signal: controller.signal,
1620
+ onProgress(progress) {
1621
+ if (activeVoiceLineRegeneration?.id !== currentPromotionId)
1622
+ return;
1623
+ const action = controller.signal.aborted
1624
+ ? "Finishing"
1625
+ : progress.phase === "generation" ? "Generating" : "Promoting";
1626
+ setStatus(elements, `${action} ${readableAssetName(progress.assetId)} (${progress.index + 1}/${progress.total})...`, "busy");
1627
+ }
1628
+ });
1629
+ manifest = result.manifest;
1630
+ promotedCount = result.promoted.length;
1631
+ if (destroyed || activeVoiceLineRegeneration?.id !== currentPromotionId) {
1632
+ return;
1633
+ }
1634
+ if (result.manifestModuleSyncDeferred || result.manifestModuleSyncError) {
1635
+ deferredVoiceLineManifestSync = true;
1636
+ }
1637
+ if (result.manifestModuleSyncDeferred) {
1638
+ for (const failure of result.failures) {
1639
+ if (failure.option)
1640
+ deferredVoiceLinePendingAssetIds.add(failure.assetId);
1641
+ }
1642
+ if (result.cancelled?.option) {
1643
+ deferredVoiceLinePendingAssetIds.add(result.cancelled.assetId);
1644
+ }
1645
+ }
1646
+ for (const saved of result.promoted) {
1647
+ pendingOptions.delete(saved.asset.id);
1648
+ resolveDeferredVoiceLinePendingOption(saved.asset.id);
1649
+ formatDrafts.delete(saved.asset.id);
1650
+ try {
1651
+ (options.onAssetReady ?? options.onPreview)(saved.asset.id, resolveAssetUrl(saved.file), saved.asset);
1652
+ }
1653
+ catch (error) {
1654
+ liveRefreshError ??= { assetId: saved.asset.id, error };
1655
+ }
1656
+ }
1657
+ for (const failure of result.failures) {
1658
+ if (failure.option) {
1659
+ rememberPendingOption(failure.assetId, failure.option);
1660
+ }
1661
+ }
1662
+ if (result.cancelled?.option) {
1663
+ rememberPendingOption(result.cancelled.assetId, result.cancelled.option);
1664
+ }
1665
+ batchCompletedCleanly =
1666
+ promotedCount > 0 &&
1667
+ result.failures.length === 0 &&
1668
+ !result.cancelled &&
1669
+ !result.manifestModuleSyncError &&
1670
+ !result.manifestModuleSyncDeferred &&
1671
+ pendingOptions.size === 0;
1672
+ if (promotedCount > 0) {
1673
+ try {
1674
+ options.onManifestUpdated?.(manifest);
1675
+ }
1676
+ catch (error) {
1677
+ liveRefreshError ??= { assetId: baseVoiceAssetId, error };
1678
+ }
1679
+ }
1680
+ syncTargetAsset(baseVoiceAssetId);
1681
+ renderAssetBrowser();
1682
+ const firstFailure = result.failures[0];
1683
+ const processedCount = result.promoted.length + result.failures.length;
1684
+ if (result.cancelled) {
1685
+ setStatus(elements, `Stopped after ${processedCount}/${lineAssetIds.length} lines; ${promotedCount} promoted` +
1686
+ `${result.failures.length > 0 ? ` and ${result.failures.length} failed` : ""}.`, result.failures.length > 0 ? "error" : "success");
1687
+ }
1688
+ else if (firstFailure) {
1689
+ const unattemptedCount = lineAssetIds.length - processedCount;
1690
+ setStatus(elements, `Regenerated and promoted ${promotedCount}/${lineAssetIds.length} lines. ` +
1691
+ `${result.failures.length} failed; first failure was ${readableAssetName(firstFailure.assetId)} ` +
1692
+ `during ${firstFailure.phase}: ${errorMessage(firstFailure.error)}` +
1693
+ `${firstFailure.phase === "promotion" && unattemptedCount > 0
1694
+ ? ` ${unattemptedCount} remaining line${unattemptedCount === 1 ? " was" : "s were"} not generated to avoid further paid requests while promotion is unavailable.`
1695
+ : ""}` +
1696
+ `${result.manifestModuleSyncDeferred
1697
+ ? " The generated option remains selected; promote or revert it before restarting."
1698
+ : ""}`, "error");
1699
+ }
1700
+ else if (result.manifestModuleSyncError) {
1701
+ setStatus(elements, `Regenerated and promoted all ${promotedCount} lines, but the generated manifest module could not be refreshed. ${errorMessage(result.manifestModuleSyncError)}`, "error");
1702
+ }
1703
+ else if (liveRefreshError) {
1704
+ setStatus(elements, `Regenerated all ${promotedCount} lines, but ${readableAssetName(liveRefreshError.assetId)} ` +
1705
+ `could not refresh live. Restart to load it. ${errorMessage(liveRefreshError.error)}`, "error");
1706
+ }
1707
+ else {
1708
+ setStatus(elements, `Regenerated and promoted all ${promotedCount} lines.`, "success");
1709
+ }
1710
+ }
1711
+ catch (error) {
1712
+ setStatus(elements, `Could not regenerate voice lines. ${errorMessage(error)}`, "error");
1713
+ }
1714
+ finally {
1715
+ if (activeVoiceLineRegeneration?.id === currentPromotionId) {
1716
+ activeVoiceLineRegeneration = undefined;
1717
+ }
1718
+ if (activePromotionId === currentPromotionId) {
1719
+ activePromotionId = undefined;
1720
+ }
1721
+ if (!destroyed) {
1722
+ elements.panel.removeAttribute("aria-busy");
1723
+ setVoiceLineBatchControlsLocked(false);
1724
+ syncPromoteAllButton();
1725
+ syncMixTilesetButton();
1726
+ }
1727
+ }
1728
+ if (!destroyed && options.restartOnPromote && batchCompletedCleanly) {
1729
+ window.location.reload();
1730
+ }
1731
+ });
1420
1732
  elements.uploadButton.addEventListener("click", async () => {
1421
1733
  const asset = manifest.assets[selectedTargetAssetId];
1422
1734
  try {
@@ -1865,6 +2177,7 @@ export function installAiAssetDesigner(options) {
1865
2177
  liveRefreshErrors.push({ assetId, error });
1866
2178
  }
1867
2179
  pendingOptions.delete(assetId);
2180
+ resolveDeferredVoiceLinePendingOption(assetId);
1868
2181
  formatDrafts.delete(assetId);
1869
2182
  promotedCount += 1;
1870
2183
  }
@@ -1904,8 +2217,19 @@ export function installAiAssetDesigner(options) {
1904
2217
  window.location.reload();
1905
2218
  }
1906
2219
  });
1907
- elements.restartButton.addEventListener("click", () => {
1908
- window.location.reload();
2220
+ elements.restartButton.addEventListener("click", async () => {
2221
+ if (deferredVoiceLineManifestSync &&
2222
+ [...deferredVoiceLinePendingAssetIds].some((assetId) => pendingOptions.has(assetId))) {
2223
+ setStatus(elements, "Promote or revert the pending generated voice line before restarting.", "error");
2224
+ return;
2225
+ }
2226
+ if (deferredVoiceLineManifestSync) {
2227
+ setStatus(elements, "Refreshing the generated manifest module...", "busy");
2228
+ if (!await flushDeferredVoiceLineManifest())
2229
+ return;
2230
+ }
2231
+ if (!destroyed)
2232
+ window.location.reload();
1909
2233
  });
1910
2234
  elements.versionsButton.addEventListener("click", () => {
1911
2235
  const asset = manifest.assets[selectedTargetAssetId];
@@ -2419,22 +2743,38 @@ export function installAiAssetDesigner(options) {
2419
2743
  continueOnError: true,
2420
2744
  onManifestUpdated: (updatedManifest) => {
2421
2745
  manifest = updatedManifest;
2422
- options.onManifestUpdated?.(updatedManifest);
2423
- syncAsset(selectedAssetId);
2746
+ if (!destroyed) {
2747
+ options.onManifestUpdated?.(updatedManifest);
2748
+ syncAsset(selectedAssetId);
2749
+ }
2424
2750
  },
2425
2751
  onAssetReady: (assetId, textureKey, asset) => {
2426
- (options.onAssetReady ?? options.onPreview)(assetId, textureKey, asset);
2752
+ if (!destroyed) {
2753
+ (options.onAssetReady ?? options.onPreview)(assetId, textureKey, asset);
2754
+ }
2755
+ },
2756
+ onProgress: (progress) => {
2757
+ if (!destroyed)
2758
+ handleFirstDraftProgress(progress);
2427
2759
  },
2428
- onProgress: handleFirstDraftProgress,
2429
2760
  onError: (error, assetId) => {
2430
- setStatus(elements, `First draft failed for ${readableAssetName(assetId)}: ${errorMessage(error)}`, "error");
2761
+ if (!destroyed) {
2762
+ setStatus(elements, `First draft failed for ${readableAssetName(assetId)}: ${errorMessage(error)}`, "error");
2763
+ }
2431
2764
  }
2432
2765
  }).then((result) => {
2433
- if (result.generatedAssetIds.length > 0 && result.errors.length === 0) {
2766
+ if (!destroyed && result.generatedAssetIds.length > 0 && result.errors.length === 0) {
2434
2767
  setStatus(elements, "First drafts generated.", "success");
2435
2768
  }
2436
2769
  }).catch((error) => {
2437
- setStatus(elements, `First draft failed: ${errorMessage(error)}`, "error");
2770
+ if (!destroyed) {
2771
+ setStatus(elements, `First draft failed: ${errorMessage(error)}`, "error");
2772
+ }
2773
+ }).finally(() => {
2774
+ activeFirstDrafts = false;
2775
+ if (!destroyed) {
2776
+ syncPromoteAllButton();
2777
+ }
2438
2778
  });
2439
2779
  }
2440
2780
  return {
@@ -2442,7 +2782,9 @@ export function installAiAssetDesigner(options) {
2442
2782
  open: () => setOpen(true),
2443
2783
  close: () => setOpen(false),
2444
2784
  destroy: () => {
2785
+ destroyed = true;
2445
2786
  activeGeneration?.controller.abort();
2787
+ activeVoiceLineRegeneration?.controller.abort();
2446
2788
  stopStatusAnimation(elements.status);
2447
2789
  dockPanel.destroy();
2448
2790
  elements.root.remove();