@ai-game-assets/phaser 0.7.0 → 0.8.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.
@@ -1 +1 @@
1
- {"version":3,"file":"designer.d.ts","sourceRoot":"","sources":["../src/designer.ts"],"names":[],"mappings":"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"}
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
@@ -33,7 +34,7 @@ export function resolveTilesetEditorInitialState(options) {
33
34
  function cloneTilesetTransforms(transforms) {
34
35
  return transforms?.map((transform) => ({ ...transform }));
35
36
  }
36
- import { assetWithGeneratedGeometry, audioGenerationOverridesFromInputs, bindKeyboardCapture, canEditGenerationFormat, createDesignerElements, effectiveGenerationFormat, ensureDesignerStyles, errorMessage, generationOverridesFromInputs, hasStyleGuide, imageSourceToDataUrl, installPromotedImageTexture, isAbortError, isAudioAsset, isSvgSource, isVoiceAsset, loadImageElement, mimeTypeFromDataUrl, normalizeAssetFormat, normalizeAudioFormat, openAnimationEditor, openAssetVersionsDialog, openAudioEditor, openFrameTouchUpEditor, openStyleGuideEditor, pickUploadFile, positiveIntegerInput, previewCurrentAsset, previewImageSource, previewOption, readableAssetName, renderAssetFolderBrowser, renderAudioPlayer, renderOptions, resolvePreviewDisplaySize, setStatus, stopStatusAnimation, styleGuideDraftFromManifest, styleGuideRequest, uploadedOptionFromFile, voiceGenerationOverridesFromInputs } from "./designer-support.js";
37
+ import { assetWithGeneratedGeometry, audioGenerationOverridesFromInputs, bindDesignerInputBoundary, bindKeyboardCapture, canEditGenerationFormat, createDesignerElements, effectiveGenerationFormat, ensureDesignerStyles, errorMessage, generationOverridesFromInputs, hasStyleGuide, imageSourceToDataUrl, installPromotedImageTexture, isAbortError, isAudioAsset, isSvgSource, isVoiceAsset, loadImageElement, mimeTypeFromDataUrl, normalizeAssetFormat, normalizeAudioFormat, openAnimationEditor, openAssetVersionsDialog, openAudioEditor, openFrameTouchUpEditor, openStyleGuideEditor, pickUploadFile, positiveIntegerInput, previewCurrentAsset, previewImageSource, previewOption, readableAssetName, renderAssetFolderBrowser, renderAudioPlayer, renderOptions, resolvePreviewDisplaySize, setStatus, stopStatusAnimation, styleGuideDraftFromManifest, styleGuideRequest, uploadedOptionFromFile, voiceGenerationOverridesFromInputs } from "./designer-support.js";
37
38
  export function installAiAssetDesigner(options) {
38
39
  ensureDesignerStyles();
39
40
  const client = options.client ?? new AiAssetDebugClient();
@@ -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);
@@ -99,12 +107,89 @@ export function installAiAssetDesigner(options) {
99
107
  }
100
108
  const mount = options.mount ?? document.body;
101
109
  mount.append(elements.root);
110
+ const unbindInputBoundary = bindDesignerInputBoundary(elements.root, elements.toggle);
102
111
  bindKeyboardCapture(elements.root, options.scene);
112
+ const setVoiceLineBatchControlsLocked = (locked) => {
113
+ if (locked) {
114
+ if (voiceLineBatchControlStates)
115
+ return;
116
+ const controls = Array.from(elements.panel.querySelectorAll("button, input, select, textarea")).filter((control) => control !== elements.regenerateAllLinesButton);
117
+ voiceLineBatchControlStates = controls.map((control) => [control, control.disabled]);
118
+ for (const control of controls)
119
+ control.disabled = true;
120
+ elements.assetBrowser.setAttribute("inert", "");
121
+ elements.currentPreview.setAttribute("inert", "");
122
+ elements.options.setAttribute("inert", "");
123
+ return;
124
+ }
125
+ for (const [control, wasDisabled] of voiceLineBatchControlStates ?? []) {
126
+ control.disabled = wasDisabled;
127
+ }
128
+ voiceLineBatchControlStates = undefined;
129
+ elements.assetBrowser.removeAttribute("inert");
130
+ elements.currentPreview.removeAttribute("inert");
131
+ elements.options.removeAttribute("inert");
132
+ };
133
+ const syncRegenerateAllLinesButton = () => {
134
+ if (activeVoiceLineRegeneration) {
135
+ const stopping = activeVoiceLineRegeneration.controller.signal.aborted;
136
+ elements.regenerateAllLinesButton.hidden = false;
137
+ elements.regenerateAllLinesButton.disabled = stopping;
138
+ elements.regenerateAllLinesButton.textContent = stopping
139
+ ? "Stopping after current line..."
140
+ : "Stop after current line";
141
+ elements.regenerateAllLinesButton.title = stopping
142
+ ? "The batch will stop after the current line is promoted."
143
+ : "Finish and promote the current line, then stop before generating another.";
144
+ return;
145
+ }
146
+ const selectedAsset = manifest.assets[selectedAssetId];
147
+ const plan = voiceLineRegenerationPlan(manifest, selectedAssetId, selectedTargetId);
148
+ const { baseVoiceAssetId, lineAssetIds, missingTargetLineAssetIds } = plan;
149
+ const isBaseVoiceView = selectedAsset?.kind === "voice" &&
150
+ selectedTargetAssetId === baseVoiceAssetId &&
151
+ !selectedTilesetAnimationKey;
152
+ const hasPromotedVoice = Boolean(promotedVoiceId(manifest, baseVoiceAssetId));
153
+ const hasPendingBaseVoice = pendingOptions.has(baseVoiceAssetId);
154
+ const pendingLineCount = lineAssetIds.filter((assetId) => pendingOptions.has(assetId)).length;
155
+ const hasAnyPendingOption = pendingOptions.size > 0;
156
+ elements.regenerateAllLinesButton.textContent = "Regenerate all lines";
157
+ elements.regenerateAllLinesButton.hidden = !isBaseVoiceView;
158
+ elements.regenerateAllLinesButton.disabled =
159
+ !isBaseVoiceView ||
160
+ !hasPromotedVoice ||
161
+ hasPendingBaseVoice ||
162
+ pendingLineCount > 0 ||
163
+ hasAnyPendingOption ||
164
+ missingTargetLineAssetIds.length > 0 ||
165
+ lineAssetIds.length === 0 ||
166
+ activeFirstDrafts ||
167
+ Boolean(activeGeneration) ||
168
+ activePromotionId !== undefined;
169
+ elements.regenerateAllLinesButton.title = !isBaseVoiceView
170
+ ? ""
171
+ : hasPendingBaseVoice
172
+ ? "Promote the selected base voice before regenerating its lines."
173
+ : missingTargetLineAssetIds.length > 0
174
+ ? `Create target variants for ${missingTargetLineAssetIds.length} linked line${missingTargetLineAssetIds.length === 1 ? "" : "s"} before regenerating this target voice.`
175
+ : pendingLineCount > 0
176
+ ? `Promote or revert the ${pendingLineCount} pending linked line${pendingLineCount === 1 ? "" : "s"} before regenerating all lines.`
177
+ : hasAnyPendingOption
178
+ ? "Promote or revert all pending asset options before regenerating voice lines."
179
+ : activeFirstDrafts
180
+ ? "Wait for automatic first drafts to finish before regenerating voice lines."
181
+ : !hasPromotedVoice
182
+ ? "Promote a base voice before regenerating its lines."
183
+ : lineAssetIds.length === 0
184
+ ? "This voice has no linked lines to regenerate."
185
+ : `Generate and promote one new option for ${lineAssetIds.length} linked line${lineAssetIds.length === 1 ? "" : "s"}.`;
186
+ };
103
187
  const syncPromoteAllButton = () => {
104
188
  elements.promoteAllButton.disabled =
105
189
  pendingOptions.size === 0 ||
106
190
  Boolean(activeGeneration) ||
107
191
  activePromotionId !== undefined;
192
+ syncRegenerateAllLinesButton();
108
193
  };
109
194
  const rememberPendingOption = (assetId, option, pending = {}) => {
110
195
  pendingOptions.set(assetId, {
@@ -116,8 +201,53 @@ export function installAiAssetDesigner(options) {
116
201
  });
117
202
  syncPromoteAllButton();
118
203
  };
204
+ const flushDeferredVoiceLineManifest = () => {
205
+ if (!deferredVoiceLineManifestSync)
206
+ return Promise.resolve(true);
207
+ if ([...deferredVoiceLinePendingAssetIds].some((assetId) => pendingOptions.has(assetId))) {
208
+ return Promise.resolve(false);
209
+ }
210
+ if (deferredVoiceLineManifestSyncPromise) {
211
+ return deferredVoiceLineManifestSyncPromise;
212
+ }
213
+ deferredVoiceLineManifestSyncPromise = (async () => {
214
+ try {
215
+ const syncedManifest = await client.syncManifestModule();
216
+ manifest = syncedManifest;
217
+ deferredVoiceLineManifestSync = false;
218
+ deferredVoiceLinePendingAssetIds.clear();
219
+ if (!destroyed) {
220
+ try {
221
+ options.onManifestUpdated?.(manifest);
222
+ }
223
+ catch (error) {
224
+ setStatus(elements, `Manifest refreshed, but the live game could not adopt it. ${errorMessage(error)}`, "error");
225
+ }
226
+ }
227
+ return true;
228
+ }
229
+ catch (error) {
230
+ if (!destroyed) {
231
+ setStatus(elements, `Could not refresh the generated manifest module. ${errorMessage(error)}`, "error");
232
+ }
233
+ return false;
234
+ }
235
+ finally {
236
+ deferredVoiceLineManifestSyncPromise = undefined;
237
+ }
238
+ })();
239
+ return deferredVoiceLineManifestSyncPromise;
240
+ };
241
+ const resolveDeferredVoiceLinePendingOption = (assetId) => {
242
+ if (!deferredVoiceLinePendingAssetIds.delete(assetId))
243
+ return;
244
+ if (deferredVoiceLinePendingAssetIds.size === 0) {
245
+ void flushDeferredVoiceLineManifest();
246
+ }
247
+ };
119
248
  const forgetPendingOption = (assetId) => {
120
249
  pendingOptions.delete(assetId);
250
+ resolveDeferredVoiceLinePendingOption(assetId);
121
251
  syncPromoteAllButton();
122
252
  };
123
253
  const hasEditableTilesetAnimationFrames = (assetId, animationKey) => {
@@ -1417,6 +1547,189 @@ export function installAiAssetDesigner(options) {
1417
1547
  finishGeneration(currentGenerationId);
1418
1548
  }
1419
1549
  });
1550
+ elements.regenerateAllLinesButton.addEventListener("click", async () => {
1551
+ if (activeVoiceLineRegeneration) {
1552
+ if (!activeVoiceLineRegeneration.controller.signal.aborted) {
1553
+ activeVoiceLineRegeneration.controller.abort();
1554
+ syncRegenerateAllLinesButton();
1555
+ setStatus(elements, "Stopping after the current line is promoted...", "busy");
1556
+ }
1557
+ return;
1558
+ }
1559
+ const voiceAssetId = selectedAssetId;
1560
+ const voice = manifest.assets[voiceAssetId];
1561
+ const plan = voiceLineRegenerationPlan(manifest, voiceAssetId, selectedTargetId);
1562
+ const { baseVoiceAssetId, lineAssetIds, missingTargetLineAssetIds } = plan;
1563
+ if (voice?.kind !== "voice" ||
1564
+ selectedTargetAssetId !== baseVoiceAssetId ||
1565
+ selectedTilesetAnimationKey ||
1566
+ activeFirstDrafts ||
1567
+ activeGeneration ||
1568
+ activePromotionId !== undefined) {
1569
+ return;
1570
+ }
1571
+ if (pendingOptions.has(baseVoiceAssetId)) {
1572
+ setStatus(elements, "Promote the selected base voice before regenerating its lines.", "error");
1573
+ return;
1574
+ }
1575
+ if (!promotedVoiceId(manifest, baseVoiceAssetId)) {
1576
+ setStatus(elements, "Promote a base voice before regenerating its lines.", "error");
1577
+ return;
1578
+ }
1579
+ if (missingTargetLineAssetIds.length > 0) {
1580
+ setStatus(elements, `Create target variants for ${missingTargetLineAssetIds.map(readableAssetName).join(", ")} before regenerating this target voice.`, "error");
1581
+ return;
1582
+ }
1583
+ if (lineAssetIds.length === 0) {
1584
+ setStatus(elements, "This voice has no linked lines to regenerate.", "error");
1585
+ return;
1586
+ }
1587
+ const pendingLineAssetIds = lineAssetIds.filter((assetId) => pendingOptions.has(assetId));
1588
+ if (pendingLineAssetIds.length > 0) {
1589
+ setStatus(elements, `Promote or revert pending options for ${pendingLineAssetIds.map(readableAssetName).join(", ")} before regenerating all lines.`, "error");
1590
+ return;
1591
+ }
1592
+ if (pendingOptions.size > 0) {
1593
+ setStatus(elements, "Promote or revert all pending asset options before regenerating voice lines.", "error");
1594
+ return;
1595
+ }
1596
+ const targetLabel = selectedTargetId
1597
+ ? manifest.targets?.[selectedTargetId]?.label ?? readableAssetName(selectedTargetId)
1598
+ : "Default";
1599
+ 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.`);
1600
+ if (!confirmed)
1601
+ return;
1602
+ const currentPromotionId = promotionId + 1;
1603
+ const controller = new AbortController();
1604
+ promotionId = currentPromotionId;
1605
+ activePromotionId = currentPromotionId;
1606
+ activeVoiceLineRegeneration = { controller, id: currentPromotionId };
1607
+ elements.panel.setAttribute("aria-busy", "true");
1608
+ setVoiceLineBatchControlsLocked(true);
1609
+ syncPromoteAllButton();
1610
+ syncMixTilesetButton();
1611
+ let promotedCount = 0;
1612
+ let liveRefreshError;
1613
+ let batchCompletedCleanly = false;
1614
+ try {
1615
+ const result = await regenerateAndPromoteVoiceLines({
1616
+ manifest,
1617
+ voiceAssetId,
1618
+ targetId: selectedTargetId,
1619
+ client,
1620
+ signal: controller.signal,
1621
+ onProgress(progress) {
1622
+ if (activeVoiceLineRegeneration?.id !== currentPromotionId)
1623
+ return;
1624
+ const action = controller.signal.aborted
1625
+ ? "Finishing"
1626
+ : progress.phase === "generation" ? "Generating" : "Promoting";
1627
+ setStatus(elements, `${action} ${readableAssetName(progress.assetId)} (${progress.index + 1}/${progress.total})...`, "busy");
1628
+ }
1629
+ });
1630
+ manifest = result.manifest;
1631
+ promotedCount = result.promoted.length;
1632
+ if (destroyed || activeVoiceLineRegeneration?.id !== currentPromotionId) {
1633
+ return;
1634
+ }
1635
+ if (result.manifestModuleSyncDeferred || result.manifestModuleSyncError) {
1636
+ deferredVoiceLineManifestSync = true;
1637
+ }
1638
+ if (result.manifestModuleSyncDeferred) {
1639
+ for (const failure of result.failures) {
1640
+ if (failure.option)
1641
+ deferredVoiceLinePendingAssetIds.add(failure.assetId);
1642
+ }
1643
+ if (result.cancelled?.option) {
1644
+ deferredVoiceLinePendingAssetIds.add(result.cancelled.assetId);
1645
+ }
1646
+ }
1647
+ for (const saved of result.promoted) {
1648
+ pendingOptions.delete(saved.asset.id);
1649
+ resolveDeferredVoiceLinePendingOption(saved.asset.id);
1650
+ formatDrafts.delete(saved.asset.id);
1651
+ try {
1652
+ (options.onAssetReady ?? options.onPreview)(saved.asset.id, resolveAssetUrl(saved.file), saved.asset);
1653
+ }
1654
+ catch (error) {
1655
+ liveRefreshError ??= { assetId: saved.asset.id, error };
1656
+ }
1657
+ }
1658
+ for (const failure of result.failures) {
1659
+ if (failure.option) {
1660
+ rememberPendingOption(failure.assetId, failure.option);
1661
+ }
1662
+ }
1663
+ if (result.cancelled?.option) {
1664
+ rememberPendingOption(result.cancelled.assetId, result.cancelled.option);
1665
+ }
1666
+ batchCompletedCleanly =
1667
+ promotedCount > 0 &&
1668
+ result.failures.length === 0 &&
1669
+ !result.cancelled &&
1670
+ !result.manifestModuleSyncError &&
1671
+ !result.manifestModuleSyncDeferred &&
1672
+ pendingOptions.size === 0;
1673
+ if (promotedCount > 0) {
1674
+ try {
1675
+ options.onManifestUpdated?.(manifest);
1676
+ }
1677
+ catch (error) {
1678
+ liveRefreshError ??= { assetId: baseVoiceAssetId, error };
1679
+ }
1680
+ }
1681
+ syncTargetAsset(baseVoiceAssetId);
1682
+ renderAssetBrowser();
1683
+ const firstFailure = result.failures[0];
1684
+ const processedCount = result.promoted.length + result.failures.length;
1685
+ if (result.cancelled) {
1686
+ setStatus(elements, `Stopped after ${processedCount}/${lineAssetIds.length} lines; ${promotedCount} promoted` +
1687
+ `${result.failures.length > 0 ? ` and ${result.failures.length} failed` : ""}.`, result.failures.length > 0 ? "error" : "success");
1688
+ }
1689
+ else if (firstFailure) {
1690
+ const unattemptedCount = lineAssetIds.length - processedCount;
1691
+ setStatus(elements, `Regenerated and promoted ${promotedCount}/${lineAssetIds.length} lines. ` +
1692
+ `${result.failures.length} failed; first failure was ${readableAssetName(firstFailure.assetId)} ` +
1693
+ `during ${firstFailure.phase}: ${errorMessage(firstFailure.error)}` +
1694
+ `${firstFailure.phase === "promotion" && unattemptedCount > 0
1695
+ ? ` ${unattemptedCount} remaining line${unattemptedCount === 1 ? " was" : "s were"} not generated to avoid further paid requests while promotion is unavailable.`
1696
+ : ""}` +
1697
+ `${result.manifestModuleSyncDeferred
1698
+ ? " The generated option remains selected; promote or revert it before restarting."
1699
+ : ""}`, "error");
1700
+ }
1701
+ else if (result.manifestModuleSyncError) {
1702
+ setStatus(elements, `Regenerated and promoted all ${promotedCount} lines, but the generated manifest module could not be refreshed. ${errorMessage(result.manifestModuleSyncError)}`, "error");
1703
+ }
1704
+ else if (liveRefreshError) {
1705
+ setStatus(elements, `Regenerated all ${promotedCount} lines, but ${readableAssetName(liveRefreshError.assetId)} ` +
1706
+ `could not refresh live. Restart to load it. ${errorMessage(liveRefreshError.error)}`, "error");
1707
+ }
1708
+ else {
1709
+ setStatus(elements, `Regenerated and promoted all ${promotedCount} lines.`, "success");
1710
+ }
1711
+ }
1712
+ catch (error) {
1713
+ setStatus(elements, `Could not regenerate voice lines. ${errorMessage(error)}`, "error");
1714
+ }
1715
+ finally {
1716
+ if (activeVoiceLineRegeneration?.id === currentPromotionId) {
1717
+ activeVoiceLineRegeneration = undefined;
1718
+ }
1719
+ if (activePromotionId === currentPromotionId) {
1720
+ activePromotionId = undefined;
1721
+ }
1722
+ if (!destroyed) {
1723
+ elements.panel.removeAttribute("aria-busy");
1724
+ setVoiceLineBatchControlsLocked(false);
1725
+ syncPromoteAllButton();
1726
+ syncMixTilesetButton();
1727
+ }
1728
+ }
1729
+ if (!destroyed && options.restartOnPromote && batchCompletedCleanly) {
1730
+ window.location.reload();
1731
+ }
1732
+ });
1420
1733
  elements.uploadButton.addEventListener("click", async () => {
1421
1734
  const asset = manifest.assets[selectedTargetAssetId];
1422
1735
  try {
@@ -1865,6 +2178,7 @@ export function installAiAssetDesigner(options) {
1865
2178
  liveRefreshErrors.push({ assetId, error });
1866
2179
  }
1867
2180
  pendingOptions.delete(assetId);
2181
+ resolveDeferredVoiceLinePendingOption(assetId);
1868
2182
  formatDrafts.delete(assetId);
1869
2183
  promotedCount += 1;
1870
2184
  }
@@ -1904,8 +2218,19 @@ export function installAiAssetDesigner(options) {
1904
2218
  window.location.reload();
1905
2219
  }
1906
2220
  });
1907
- elements.restartButton.addEventListener("click", () => {
1908
- window.location.reload();
2221
+ elements.restartButton.addEventListener("click", async () => {
2222
+ if (deferredVoiceLineManifestSync &&
2223
+ [...deferredVoiceLinePendingAssetIds].some((assetId) => pendingOptions.has(assetId))) {
2224
+ setStatus(elements, "Promote or revert the pending generated voice line before restarting.", "error");
2225
+ return;
2226
+ }
2227
+ if (deferredVoiceLineManifestSync) {
2228
+ setStatus(elements, "Refreshing the generated manifest module...", "busy");
2229
+ if (!await flushDeferredVoiceLineManifest())
2230
+ return;
2231
+ }
2232
+ if (!destroyed)
2233
+ window.location.reload();
1909
2234
  });
1910
2235
  elements.versionsButton.addEventListener("click", () => {
1911
2236
  const asset = manifest.assets[selectedTargetAssetId];
@@ -2419,22 +2744,38 @@ export function installAiAssetDesigner(options) {
2419
2744
  continueOnError: true,
2420
2745
  onManifestUpdated: (updatedManifest) => {
2421
2746
  manifest = updatedManifest;
2422
- options.onManifestUpdated?.(updatedManifest);
2423
- syncAsset(selectedAssetId);
2747
+ if (!destroyed) {
2748
+ options.onManifestUpdated?.(updatedManifest);
2749
+ syncAsset(selectedAssetId);
2750
+ }
2424
2751
  },
2425
2752
  onAssetReady: (assetId, textureKey, asset) => {
2426
- (options.onAssetReady ?? options.onPreview)(assetId, textureKey, asset);
2753
+ if (!destroyed) {
2754
+ (options.onAssetReady ?? options.onPreview)(assetId, textureKey, asset);
2755
+ }
2756
+ },
2757
+ onProgress: (progress) => {
2758
+ if (!destroyed)
2759
+ handleFirstDraftProgress(progress);
2427
2760
  },
2428
- onProgress: handleFirstDraftProgress,
2429
2761
  onError: (error, assetId) => {
2430
- setStatus(elements, `First draft failed for ${readableAssetName(assetId)}: ${errorMessage(error)}`, "error");
2762
+ if (!destroyed) {
2763
+ setStatus(elements, `First draft failed for ${readableAssetName(assetId)}: ${errorMessage(error)}`, "error");
2764
+ }
2431
2765
  }
2432
2766
  }).then((result) => {
2433
- if (result.generatedAssetIds.length > 0 && result.errors.length === 0) {
2767
+ if (!destroyed && result.generatedAssetIds.length > 0 && result.errors.length === 0) {
2434
2768
  setStatus(elements, "First drafts generated.", "success");
2435
2769
  }
2436
2770
  }).catch((error) => {
2437
- setStatus(elements, `First draft failed: ${errorMessage(error)}`, "error");
2771
+ if (!destroyed) {
2772
+ setStatus(elements, `First draft failed: ${errorMessage(error)}`, "error");
2773
+ }
2774
+ }).finally(() => {
2775
+ activeFirstDrafts = false;
2776
+ if (!destroyed) {
2777
+ syncPromoteAllButton();
2778
+ }
2438
2779
  });
2439
2780
  }
2440
2781
  return {
@@ -2442,8 +2783,11 @@ export function installAiAssetDesigner(options) {
2442
2783
  open: () => setOpen(true),
2443
2784
  close: () => setOpen(false),
2444
2785
  destroy: () => {
2786
+ destroyed = true;
2445
2787
  activeGeneration?.controller.abort();
2788
+ activeVoiceLineRegeneration?.controller.abort();
2446
2789
  stopStatusAnimation(elements.status);
2790
+ unbindInputBoundary();
2447
2791
  dockPanel.destroy();
2448
2792
  elements.root.remove();
2449
2793
  }