dsh-image-gen 0.6.8 → 0.6.10

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.
package/lib/client.js CHANGED
@@ -87,6 +87,8 @@ window.__ModuleLoader__.load({
87
87
  const TEST_CONNECTION_ROUTE = "/plugins/dsh-image-gen/test";
88
88
  /** Browser route the workbench infinite canvas pushes live state through. */
89
89
  const CANVAS_STATE_ROUTE = "/plugins/dsh-image-gen/canvas-state";
90
+ /** Upload a selected canvas original to the host attachment store. */
91
+ const CANVAS_ASSET_ROUTE = "/plugins/dsh-image-gen/canvas-asset";
90
92
  const SUBSCRIPTION_LOGIN_ROUTE = "/plugins/dsh-image-gen/subscription-login";
91
93
  const SUBSCRIPTION_STATUS_ROUTE = "/plugins/dsh-image-gen/subscription-status";
92
94
  /** Namespace persisted through DSH Settings. */
@@ -320,6 +322,7 @@ window.__ModuleLoader__.load({
320
322
  resolve(request.result);
321
323
  };
322
324
  request.onerror = () => {
325
+ dbPromise = null;
323
326
  reject(request.error);
324
327
  };
325
328
  });
@@ -370,11 +373,13 @@ window.__ModuleLoader__.load({
370
373
  * Save or update a gallery record by attachmentId.
371
374
  * Skipped if the item was previously deleted (tombstoned).
372
375
  * Preserves existing isFavorite, tags, and original createdAt on re-renders.
376
+ * Returns false when the record was not persisted (e.g. IndexedDB unavailable)
377
+ * so callers can surface the failure and let the user retry.
373
378
  */
374
379
  async function saveGalleryItem(item) {
375
380
  try {
376
381
  const db = await getDB();
377
- if ((await loadTombstones(db)).has(item.id)) return;
382
+ if ((await loadTombstones(db)).has(item.id)) return true;
378
383
  await new Promise((resolve, reject) => {
379
384
  const store = db.transaction(STORE_NAME, "readwrite").objectStore(STORE_NAME);
380
385
  const getReq = store.get(item.id);
@@ -398,8 +403,10 @@ window.__ModuleLoader__.load({
398
403
  getReq.onerror = () => reject(getReq.error);
399
404
  });
400
405
  notifyListeners();
406
+ return true;
401
407
  } catch (err) {
402
408
  console.warn("[dsh-image-gen] Failed to save gallery item to IndexedDB:", err);
409
+ return false;
403
410
  }
404
411
  }
405
412
  /**
@@ -115633,16 +115640,113 @@ img.ProseMirror-separator {
115633
115640
  });
115634
115641
  }
115635
115642
  //#endregion
115643
+ //#region lib/types/client/tl/canvas-selection.js
115644
+ /** Bound raster dimensions before exporting; large multi-image PNGs get a
115645
+ * JPEG preview instead of being silently discarded. Originals are separate. */
115646
+ async function exportSelectionPreview(editor, shapes, maxBytes, signal) {
115647
+ const bounds = editor.getSelectionPageBounds();
115648
+ const scale = Math.min(1, 2048 / Math.max(1, (bounds?.w ?? 2048) + 32, (bounds?.h ?? 2048) + 32));
115649
+ const attempts = [
115650
+ {
115651
+ format: "png",
115652
+ scale,
115653
+ quality: 1
115654
+ },
115655
+ {
115656
+ format: "jpeg",
115657
+ scale,
115658
+ quality: .85
115659
+ },
115660
+ {
115661
+ format: "jpeg",
115662
+ scale: scale * .75,
115663
+ quality: .7
115664
+ },
115665
+ {
115666
+ format: "jpeg",
115667
+ scale: scale * .5,
115668
+ quality: .6
115669
+ },
115670
+ {
115671
+ format: "jpeg",
115672
+ scale: scale * .25,
115673
+ quality: .5
115674
+ }
115675
+ ];
115676
+ const maxChars = Math.floor(Math.min(maxBytes, 4e6) / 3) * 4;
115677
+ for (const attempt of attempts) {
115678
+ signal.throwIfAborted();
115679
+ const { url } = await editor.toImageDataUrl(shapes, {
115680
+ background: true,
115681
+ padding: 16,
115682
+ pixelRatio: 1,
115683
+ ...attempt
115684
+ });
115685
+ signal.throwIfAborted();
115686
+ if (url.length - url.indexOf(",") - 1 <= maxChars) return url;
115687
+ }
115688
+ throw new Error("preview-too-large");
115689
+ }
115690
+ async function readCanvasImageLimit(signal) {
115691
+ const response = await fetch(CANVAS_ASSET_ROUTE, {
115692
+ credentials: "same-origin",
115693
+ signal
115694
+ });
115695
+ if (!response.ok) throw new Error(`host-unavailable (${response.status})`);
115696
+ const value = await response.json();
115697
+ if (typeof value.maxImageBytes !== "number" || !Number.isSafeInteger(value.maxImageBytes) || value.maxImageBytes <= 0) throw new Error("host-unavailable");
115698
+ return value.maxImageBytes;
115699
+ }
115700
+ /** Use tldraw's resolver for IndexedDB-backed imports, not just asset.props.src.
115701
+ * Upload each original once per mount/source, with individual byte limits. */
115702
+ async function syncCanvasOriginal(editor, shape, maxBytes, signal, cache) {
115703
+ if (shape.type !== "image" || shape.props.assetId === null) throw new Error("original-unavailable");
115704
+ const asset = editor.getAsset(shape.props.assetId);
115705
+ if (asset?.type !== "image") throw new Error("original-unavailable");
115706
+ const cached = cache.get(asset.id);
115707
+ if (cached?.src === asset.props.src) return cached.attachment;
115708
+ const url = await editor.resolveAssetUrl(asset.id, { shouldResolveToOriginal: true });
115709
+ signal.throwIfAborted();
115710
+ if (url === null) throw new Error("original-unavailable");
115711
+ const response = await fetch(url, { signal });
115712
+ if (!response.ok) throw new Error("original-unavailable");
115713
+ const blob = await response.blob();
115714
+ if (blob.size > maxBytes) throw new Error("original-too-large");
115715
+ const mediaType = blob.type || asset.props.mimeType || "";
115716
+ if (![
115717
+ "image/png",
115718
+ "image/jpeg",
115719
+ "image/webp",
115720
+ "image/gif"
115721
+ ].includes(mediaType)) throw new Error("original-format-unsupported");
115722
+ const uploaded = await fetch(CANVAS_ASSET_ROUTE, {
115723
+ method: "POST",
115724
+ credentials: "same-origin",
115725
+ headers: { "content-type": mediaType },
115726
+ body: blob,
115727
+ signal
115728
+ });
115729
+ if (!uploaded.ok) throw new Error(uploaded.status === 413 ? "original-too-large" : `original-upload-failed (${uploaded.status})`);
115730
+ const ref = (await uploaded.json()).attachment;
115731
+ if (!ref || typeof ref.attachmentId !== "string" || ![
115732
+ "image/png",
115733
+ "image/jpeg",
115734
+ "image/webp",
115735
+ "image/gif"
115736
+ ].includes(ref.mediaType) || !Number.isSafeInteger(ref.bytes) || ref.bytes <= 0 || !Number.isInteger(ref.width) || ref.width <= 0 || !Number.isInteger(ref.height) || ref.height <= 0) throw new Error("original-upload-failed");
115737
+ cache.set(asset.id, {
115738
+ src: asset.props.src,
115739
+ attachment: ref
115740
+ });
115741
+ return ref;
115742
+ }
115743
+ //#endregion
115636
115744
  //#region lib/types/client/tl/canvas-sync.js
115637
115745
  /** Debounce for ordinary document/session changes. */
115638
115746
  const STATE_DEBOUNCE_MS = 600;
115639
115747
  /** Extra settle time before an expensive selection screenshot export. */
115640
115748
  const SELECTION_EXPORT_DELAY_MS = 1200;
115641
- /** Minimum spacing between two HTTP pushes. */
115642
- const MIN_PUSH_INTERVAL_MS = 400;
115643
115749
  const MAX_TEXT_CHARS = 96;
115644
- /** Hard cap on the exported screenshot payload (base64 chars, ~5MB binary). */
115645
- const MAX_SCREENSHOT_DATAURL_CHARS = 7e6;
115646
115750
  /** Map a tldraw shape type onto the coarse model-facing node kind. */
115647
115751
  function kindOf(shapeType) {
115648
115752
  switch (shapeType) {
@@ -115749,69 +115853,66 @@ img.ProseMirror-separator {
115749
115853
  const ids = editor.getSelectedShapeIds().map(String).sort().join("|");
115750
115854
  return ids.length === 0 ? "none" : ids;
115751
115855
  }
115752
- /**
115753
- * Start mirroring one editor into the host canvas-state route.
115754
- * Returns a disposer that stops listening and marks the instance offline.
115755
- */
115756
- function startCanvasSync(editor) {
115757
- const clientInstance = String(editor.id);
115856
+ function startCanvasSync(editor, onStatus = () => {}) {
115857
+ const clientInstance = `${String(editor.id).slice(0, 36)}-${Math.random().toString(36).slice(2, 10)}`;
115758
115858
  let disposed = false;
115759
115859
  let stateTimer;
115760
115860
  let exportTimer;
115761
115861
  let lastPushKey = "";
115762
- let lastSelectionKey = "init";
115763
- let lastPostAt = 0;
115764
- let inFlight;
115765
- const stopListening = editor.store.listen(() => {
115862
+ let lastSelectionKey = selectionKeyOf(editor);
115863
+ let revision = 0;
115864
+ let sequence = 0;
115865
+ let imageLimit;
115866
+ let inFlight = new AbortController();
115867
+ let exporting = false;
115868
+ let exportAgain = false;
115869
+ let prepared = {};
115870
+ const originalCache = /* @__PURE__ */ new Map();
115871
+ let preparedItems;
115872
+ const stopListening = editor.store.listen((event) => {
115766
115873
  if (disposed) return;
115767
- const selectionKey = selectionKeyOf(editor);
115768
- if (selectionKey !== lastSelectionKey) {
115769
- lastSelectionKey = selectionKey;
115770
- if (exportTimer !== void 0) clearTimeout(exportTimer);
115771
- exportTimer = setTimeout(() => {
115772
- exportTimer = void 0;
115773
- flush(true);
115774
- }, SELECTION_EXPORT_DELAY_MS);
115874
+ const key = selectionKeyOf(editor);
115875
+ const changes = event.changes;
115876
+ const documentChanged = [
115877
+ ...Object.values(changes.added),
115878
+ ...Object.values(changes.removed),
115879
+ ...Object.values(changes.updated).map((pair) => pair[1])
115880
+ ].some((record) => record.typeName === "shape" || record.typeName === "asset" || record.typeName === "page");
115881
+ if (key !== lastSelectionKey || documentChanged) {
115882
+ lastSelectionKey = key;
115883
+ invalidate();
115775
115884
  }
115776
115885
  if (stateTimer === void 0) stateTimer = setTimeout(() => {
115777
115886
  stateTimer = void 0;
115778
- flush(false);
115887
+ pushState();
115779
115888
  }, STATE_DEBOUNCE_MS);
115780
115889
  }, {
115781
115890
  source: "all",
115782
115891
  scope: "all"
115783
115892
  });
115784
- async function flush(withScreenshot) {
115893
+ function invalidate() {
115894
+ revision++;
115895
+ inFlight.abort();
115896
+ inFlight = new AbortController();
115897
+ prepared = {};
115898
+ preparedItems = void 0;
115899
+ const count = editor.getSelectedShapeIds().length;
115900
+ onStatus({
115901
+ phase: count > 0 ? "preparing" : "idle",
115902
+ count
115903
+ });
115904
+ if (exportTimer !== void 0) clearTimeout(exportTimer);
115905
+ if (count > 0) exportTimer = setTimeout(() => {
115906
+ exportTimer = void 0;
115907
+ prepare();
115908
+ }, SELECTION_EXPORT_DELAY_MS);
115909
+ }
115910
+ async function pushState(force = false) {
115785
115911
  if (disposed) return;
115912
+ const currentRevision = revision;
115786
115913
  const summary = summarize(editor);
115787
- if (!withScreenshot && summary.stateKey === lastPushKey) return;
115788
- const elapsed = Date.now() - lastPostAt;
115789
- if (elapsed < MIN_PUSH_INTERVAL_MS) {
115790
- if (stateTimer !== void 0) clearTimeout(stateTimer);
115791
- stateTimer = setTimeout(() => {
115792
- stateTimer = void 0;
115793
- flush(withScreenshot);
115794
- }, MIN_PUSH_INTERVAL_MS - elapsed);
115795
- return;
115796
- }
115797
- let selectionImage;
115798
- if (withScreenshot && summary.selectionCount > 0) try {
115799
- const selectedBefore = editor.getSelectedShapes();
115800
- const { url } = await editor.toImageDataUrl(selectedBefore, {
115801
- background: true,
115802
- padding: 16,
115803
- scale: 1,
115804
- pixelRatio: 2
115805
- });
115806
- if (disposed) return;
115807
- const selectedAfter = editor.getSelectedShapes();
115808
- const idsBefore = new Set(selectedBefore.map((shape) => String(shape.id)));
115809
- if (!(selectedBefore.length === selectedAfter.length && selectedAfter.every((shape) => idsBefore.has(String(shape.id))))) return;
115810
- if (url.length > MAX_SCREENSHOT_DATAURL_CHARS) console.warn("[dsh-image-gen] canvas selection screenshot skipped: too large");
115811
- else selectionImage = url;
115812
- } catch (error) {
115813
- console.warn("[dsh-image-gen] canvas selection screenshot failed:", error);
115814
- }
115914
+ const key = `${revision}:${summary.stateKey}`;
115915
+ if (!force && key === lastPushKey) return;
115815
115916
  const payload = {
115816
115917
  clientInstance,
115817
115918
  connected: true,
@@ -115820,43 +115921,104 @@ img.ProseMirror-separator {
115820
115921
  selection: {
115821
115922
  count: summary.selectionCount,
115822
115923
  kinds: summary.selectionKinds,
115823
- items: summary.selectionItems
115924
+ items: preparedItems ?? summary.selectionItems
115824
115925
  },
115926
+ selectionRevision: String(revision),
115927
+ ...summary.selectionCount > 0 ? { selectionStatus: "preparing" } : {},
115928
+ ...prepared,
115929
+ sequence: ++sequence,
115825
115930
  updatedAt: Date.now()
115826
115931
  };
115827
- if (selectionImage !== void 0) payload.selectionImage = selectionImage;
115828
- lastPushKey = summary.stateKey;
115829
- lastPostAt = Date.now();
115830
- const controller = new AbortController();
115831
- inFlight = controller;
115932
+ const requestSequence = sequence;
115832
115933
  try {
115833
- await fetch(CANVAS_STATE_ROUTE, {
115934
+ const response = await fetch(CANVAS_STATE_ROUTE, {
115834
115935
  method: "POST",
115835
115936
  credentials: "same-origin",
115836
115937
  headers: { "content-type": "application/json" },
115837
115938
  body: JSON.stringify(payload),
115838
- signal: controller.signal
115939
+ signal: AbortSignal.any([inFlight.signal, AbortSignal.timeout(3e4)])
115940
+ });
115941
+ if (!response.ok) throw new Error(`state-sync-failed (${response.status})`);
115942
+ if (disposed || revision !== currentRevision || sequence !== requestSequence) return;
115943
+ lastPushKey = key;
115944
+ onStatus({
115945
+ phase: payload.selectionStatus ?? "idle",
115946
+ count: summary.selectionCount,
115947
+ ...payload.selectionError ? { error: payload.selectionError } : {}
115839
115948
  });
115840
115949
  } catch (error) {
115841
- if (!disposed) console.warn("[dsh-image-gen] canvas state push failed:", error);
115950
+ if (disposed || revision !== currentRevision || sequence !== requestSequence) return;
115842
115951
  lastPushKey = "";
115952
+ onStatus({
115953
+ phase: "error",
115954
+ count: summary.selectionCount,
115955
+ error: error instanceof Error ? error.message : "state-sync-failed"
115956
+ });
115957
+ }
115958
+ }
115959
+ async function prepare() {
115960
+ if (disposed) return;
115961
+ if (exporting) {
115962
+ exportAgain = true;
115963
+ return;
115964
+ }
115965
+ exporting = true;
115966
+ const currentRevision = revision;
115967
+ const signal = AbortSignal.any([inFlight.signal, AbortSignal.timeout(3e4)]);
115968
+ const shapes = editor.getSelectedShapes();
115969
+ const items = shapes.slice(0, 16).map((shape) => describeShape(editor, shape));
115970
+ let selectionImage;
115971
+ let failure;
115972
+ try {
115973
+ imageLimit ??= await readCanvasImageLimit(signal);
115974
+ try {
115975
+ selectionImage = await exportSelectionPreview(editor, shapes, imageLimit, signal);
115976
+ } catch (error) {
115977
+ signal.throwIfAborted();
115978
+ failure = error instanceof Error ? error.message : "preview-export-failed";
115979
+ }
115980
+ if (shapes.length > 16 && shapes.some((shape) => shape.type === "image")) throw new Error("too-many-selected-shapes");
115981
+ for (const [index, shape] of shapes.entries()) {
115982
+ signal.throwIfAborted();
115983
+ if (shape.type !== "image") continue;
115984
+ const item = items[index];
115985
+ if (item === void 0) continue;
115986
+ const attachment = await syncCanvasOriginal(editor, shape, imageLimit, signal, originalCache);
115987
+ item.attachment = attachment;
115988
+ item.attachmentId = attachment.attachmentId;
115989
+ }
115990
+ } catch (error) {
115991
+ failure = error instanceof Error ? error.message : "selection-sync-failed";
115992
+ } finally {
115993
+ exporting = false;
115994
+ }
115995
+ if (!disposed && revision === currentRevision) {
115996
+ preparedItems = items;
115997
+ prepared = {
115998
+ selectionStatus: failure === void 0 ? "ready" : "error",
115999
+ ...failure === void 0 ? {} : { selectionError: failure.slice(0, 300) },
116000
+ ...selectionImage === void 0 ? {} : { selectionImage }
116001
+ };
116002
+ await pushState(true);
116003
+ }
116004
+ if (exportAgain && !disposed) {
116005
+ exportAgain = false;
116006
+ prepare();
115843
116007
  }
115844
116008
  }
116009
+ invalidate();
115845
116010
  stateTimer = setTimeout(() => {
115846
116011
  stateTimer = void 0;
115847
- flush(false);
116012
+ pushState();
115848
116013
  }, STATE_DEBOUNCE_MS);
115849
- if (editor.getSelectedShapeIds().length > 0) exportTimer = setTimeout(() => {
115850
- exportTimer = void 0;
115851
- flush(true);
115852
- }, SELECTION_EXPORT_DELAY_MS);
115853
- return () => {
116014
+ const stop = () => {
115854
116015
  if (disposed) return;
115855
116016
  disposed = true;
115856
116017
  if (stateTimer !== void 0) clearTimeout(stateTimer);
115857
116018
  if (exportTimer !== void 0) clearTimeout(exportTimer);
115858
116019
  stopListening();
115859
- inFlight?.abort();
116020
+ inFlight.abort();
116021
+ originalCache.clear();
115860
116022
  fetch(CANVAS_STATE_ROUTE, {
115861
116023
  method: "POST",
115862
116024
  credentials: "same-origin",
@@ -115866,10 +116028,15 @@ img.ProseMirror-separator {
115866
116028
  clientInstance,
115867
116029
  connected: false,
115868
116030
  nodeCount: 0,
116031
+ sequence: ++sequence,
115869
116032
  updatedAt: Date.now()
115870
116033
  })
115871
116034
  }).catch(() => {});
115872
116035
  };
116036
+ return Object.assign(stop, { retry: () => {
116037
+ invalidate();
116038
+ pushState(true);
116039
+ } });
115873
116040
  }
115874
116041
  //#endregion
115875
116042
  //#region lib/types/client/tl/tl-canvas-bridge.js
@@ -116112,56 +116279,84 @@ img.ProseMirror-separator {
116112
116279
  }
116113
116280
  return true;
116114
116281
  }
116115
- const StudioTlCanvas = (0, react.memo)(function StudioTlCanvas() {
116282
+ const TL_COMPONENTS = { Toolbar: StudioToolbar };
116283
+ const StudioTlCanvas = (0, react.memo)(function StudioTlCanvas({ lang = "zh" }) {
116116
116284
  const sessionId = (0, react.useId)();
116117
- return (0, react_jsx_runtime.jsx)("div", {
116285
+ const [syncStatus, setSyncStatus] = (0, react.useState)({
116286
+ phase: "idle",
116287
+ count: 0
116288
+ });
116289
+ const retrySync = (0, react.useRef)(() => {});
116290
+ const onMount = (0, react.useCallback)((editor) => {
116291
+ let active = true;
116292
+ applyBrandTheme(editor);
116293
+ editor.updateInstanceState({ isGridMode: true });
116294
+ console.info(`[dsh-image-gen] tldraw mounted (instance ${editor.id})`);
116295
+ const stopLanding = registerTlLandingConsumer((items) => {
116296
+ const generation = getTlLandingGeneration();
116297
+ return landTlItems(editor, items, () => active && generation === getTlLandingGeneration());
116298
+ });
116299
+ let stopSync = startCanvasSync(editor, setSyncStatus);
116300
+ retrySync.current = () => stopSync.retry();
116301
+ const clear = () => {
116302
+ stopSync();
116303
+ editor.complete();
116304
+ editor.run(() => {
116305
+ editor.selectNone();
116306
+ const pages = editor.getPages();
116307
+ for (const page of pages) editor.deleteShapes([...editor.getPageShapeIds(page.id)]);
116308
+ for (const page of pages.slice(1)) editor.deletePage(page.id);
116309
+ editor.deleteAssets(editor.getAssets());
116310
+ editor.setCamera({
116311
+ x: 0,
116312
+ y: 0,
116313
+ z: 1
116314
+ });
116315
+ }, {
116316
+ history: "ignore",
116317
+ ignoreShapeLock: true
116318
+ });
116319
+ editor.clearHistory();
116320
+ stopSync = startCanvasSync(editor, setSyncStatus);
116321
+ };
116322
+ mountedCanvases.add(clear);
116323
+ return () => {
116324
+ active = false;
116325
+ mountedCanvases.delete(clear);
116326
+ stopLanding();
116327
+ stopSync();
116328
+ retrySync.current = () => {};
116329
+ };
116330
+ }, []);
116331
+ const statusText = lang === "en" ? syncStatus.phase === "ready" ? `Selection ready · ${syncStatus.count} shapes` : syncStatus.phase === "preparing" ? `Preparing selection · ${syncStatus.count} shapes…` : "Selection sync failed" : syncStatus.phase === "ready" ? `选区已就绪 · ${syncStatus.count} 个元素` : syncStatus.phase === "preparing" ? `正在准备选区 · ${syncStatus.count} 个元素…` : "选区同步失败";
116332
+ return (0, react_jsx_runtime.jsxs)("div", {
116118
116333
  className: "dsh-ig-tl-canvas",
116119
- children: (0, react_jsx_runtime.jsx)(Tldraw, {
116334
+ children: [(0, react_jsx_runtime.jsx)(Tldraw, {
116120
116335
  persistenceKey: "dsh-image-gen-workbench-v1",
116121
116336
  sessionId,
116122
- components: { Toolbar: StudioToolbar },
116123
- onMount: (editor) => {
116124
- let active = true;
116125
- applyBrandTheme(editor);
116126
- editor.updateInstanceState({ isGridMode: true });
116127
- console.info(`[dsh-image-gen] tldraw mounted (instance ${editor.id})`);
116128
- const stopLanding = registerTlLandingConsumer((items) => {
116129
- const generation = getTlLandingGeneration();
116130
- return landTlItems(editor, items, () => active && generation === getTlLandingGeneration());
116131
- });
116132
- let stopSync = startCanvasSync(editor);
116133
- const clear = () => {
116134
- stopSync();
116135
- editor.complete();
116136
- editor.run(() => {
116137
- editor.selectNone();
116138
- const pages = editor.getPages();
116139
- for (const page of pages) editor.deleteShapes([...editor.getPageShapeIds(page.id)]);
116140
- for (const page of pages.slice(1)) editor.deletePage(page.id);
116141
- editor.deleteAssets(editor.getAssets());
116142
- editor.setCamera({
116143
- x: 0,
116144
- y: 0,
116145
- z: 1
116146
- });
116147
- }, {
116148
- history: "ignore",
116149
- ignoreShapeLock: true
116150
- });
116151
- editor.clearHistory();
116152
- stopSync = startCanvasSync(editor);
116153
- };
116154
- mountedCanvases.add(clear);
116155
- return () => {
116156
- active = false;
116157
- mountedCanvases.delete(clear);
116158
- stopLanding();
116159
- stopSync();
116160
- };
116161
- }
116162
- })
116337
+ components: TL_COMPONENTS,
116338
+ onMount
116339
+ }), syncStatus.phase !== "idle" && (0, react_jsx_runtime.jsxs)("div", {
116340
+ className: "dsh-ig-canvas-sync-status",
116341
+ "data-phase": syncStatus.phase,
116342
+ role: "status",
116343
+ "aria-live": "polite",
116344
+ children: [(0, react_jsx_runtime.jsxs)("span", { children: [statusText, syncStatus.phase === "error" ? `:${canvasSyncError(syncStatus.error, lang)}` : ""] }), syncStatus.phase === "error" && (0, react_jsx_runtime.jsx)("button", {
116345
+ type: "button",
116346
+ onClick: () => retrySync.current(),
116347
+ children: lang === "en" ? "Retry" : "重试"
116348
+ })]
116349
+ })]
116163
116350
  });
116164
116351
  });
116352
+ function canvasSyncError(error, lang) {
116353
+ const en = lang === "en";
116354
+ if (error?.includes("too-many-selected")) return en ? "Select at most 16 shapes to edit with originals." : "使用原图编辑时,请一次选择不超过 16 个元素。";
116355
+ if (error?.includes("too-large")) return en ? "An image exceeds the size limit. Reduce its size and retry." : "图片超出大小限制,请缩小后重试。";
116356
+ if (error?.includes("unsupported")) return en ? "Use PNG, JPEG, WebP or GIF images." : "请使用 PNG、JPEG、WebP 或 GIF 图片。";
116357
+ if (error?.includes("host-unavailable")) return en ? "Restart DSH to load the updated plugin, then retry." : "请重启 DSH 加载新版插件后重试。";
116358
+ return en ? "Check the connection or re-import an unavailable image, then retry." : "请检查连接;若原图已失效,请重新导入后重试。";
116359
+ }
116165
116360
  //#endregion
116166
116361
  //#region lib/types/client/studio-view.js
116167
116362
  /** Below this workbench width the recent-generations rail folds automatically. */
@@ -116246,6 +116441,7 @@ img.ProseMirror-separator {
116246
116441
  favorited: "已收藏",
116247
116442
  favoriteAdded: "已添加到收藏",
116248
116443
  favoriteRemoved: "已取消收藏",
116444
+ gallerySaveFailed: "保存到图库失败,请重试",
116249
116445
  loadMore: "加载更多 ({n})",
116250
116446
  deleteModalTitle: "从图库删除",
116251
116447
  deleteModalDesc: "确定从图库中删除这张图片吗?(原聊天记录不会受影响)",
@@ -116354,6 +116550,7 @@ img.ProseMirror-separator {
116354
116550
  favorited: "Favorited",
116355
116551
  favoriteAdded: "Added to favorites",
116356
116552
  favoriteRemoved: "Removed from favorites",
116553
+ gallerySaveFailed: "Failed to save to the gallery, please retry",
116357
116554
  loadMore: "Load more ({n})",
116358
116555
  deleteModalTitle: "Delete from gallery",
116359
116556
  deleteModalDesc: "Remove this image from the local gallery? (Chat history remains unaffected)",
@@ -117018,7 +117215,7 @@ img.ProseMirror-separator {
117018
117215
  ...targetRoot ? { workspacePath: targetRoot } : {},
117019
117216
  ...workspace?.workspaceId ? { workspaceId: workspace.workspaceId } : {}
117020
117217
  };
117021
- await saveGalleryItem(updatedItem);
117218
+ if (!await saveGalleryItem(updatedItem)) throw new Error(t("gallerySaveFailed"));
117022
117219
  return updatedItem;
117023
117220
  };
117024
117221
  const handleSaveToGallery = async () => {
@@ -117112,16 +117309,29 @@ img.ProseMirror-separator {
117112
117309
  } : i) : null);
117113
117310
  flash(nextFav ? t("favoriteAdded") : t("favoriteRemoved"));
117114
117311
  } else {
117115
- const nextFav = !selected.isFavorite;
117116
- setSelected((curr) => curr !== null && curr.id === targetId ? {
117117
- ...curr,
117118
- isFavorite: nextFav
117119
- } : curr);
117120
- if (currentBatch) setCurrentBatch((prev) => prev ? prev.map((i) => i.id === targetId ? {
117121
- ...i,
117122
- isFavorite: nextFav
117123
- } : i) : null);
117124
- flash(nextFav ? t("favoriteAdded") : t("favoriteRemoved"));
117312
+ if (!!selected.isFavorite) {
117313
+ setSelected((curr) => curr !== null && curr.id === targetId ? {
117314
+ ...curr,
117315
+ isFavorite: false
117316
+ } : curr);
117317
+ if (currentBatch) setCurrentBatch((prev) => prev ? prev.map((i) => i.id === targetId ? {
117318
+ ...i,
117319
+ isFavorite: false
117320
+ } : i) : null);
117321
+ flash(t("favoriteRemoved"));
117322
+ return;
117323
+ }
117324
+ try {
117325
+ const updated = await saveGalleryEntry({
117326
+ ...selected,
117327
+ isFavorite: true
117328
+ });
117329
+ setSelected((curr) => curr !== null && curr.id === targetId ? updated : curr);
117330
+ if (currentBatch) setCurrentBatch((prev) => prev ? prev.map((i) => i.id === targetId ? updated : i) : prev);
117331
+ flash(t("favoriteAdded"));
117332
+ } catch (err) {
117333
+ setError(err instanceof Error ? err.message : t("gallerySaveFailed"));
117334
+ }
117125
117335
  }
117126
117336
  };
117127
117337
  const stepLightbox = (delta) => {
@@ -117353,7 +117563,7 @@ img.ProseMirror-separator {
117353
117563
  })
117354
117564
  ]
117355
117565
  }),
117356
- canvasSurface === "infinite" ? (0, react_jsx_runtime.jsx)(StudioTlCanvas, {}) : (0, react_jsx_runtime.jsx)("div", {
117566
+ canvasSurface === "infinite" ? (0, react_jsx_runtime.jsx)(StudioTlCanvas, { lang }) : (0, react_jsx_runtime.jsx)("div", {
117357
117567
  className: `dsh-ig-canvas ${isDragging ? "is-dragging" : ""}`,
117358
117568
  ref: canvasRef,
117359
117569
  onMouseDown: handleCanvasMouseDown,
@@ -119434,6 +119644,7 @@ img.ProseMirror-separator {
119434
119644
  confirmRegenerate: "开始生成",
119435
119645
  regenerating: "正在重新生成…",
119436
119646
  regenerateSuccess: "已生成新图片并收录到画廊",
119647
+ regenerateSaveFailed: "图片已生成,但保存到图库失败,请重试",
119437
119648
  regenerateFailed: "重新生成失败",
119438
119649
  delete: "从画廊删除",
119439
119650
  confirmDelete: "确定要从画廊中删除这张图片吗?(不会影响原聊天记录)",
@@ -119519,6 +119730,7 @@ img.ProseMirror-separator {
119519
119730
  confirmRegenerate: "Generate",
119520
119731
  regenerating: "Regenerating…",
119521
119732
  regenerateSuccess: "New image generated and added to gallery",
119733
+ regenerateSaveFailed: "Image generated, but saving to the gallery failed. Please retry.",
119522
119734
  regenerateFailed: "Regeneration failed",
119523
119735
  delete: "Delete from gallery",
119524
119736
  confirmDelete: "Are you sure you want to remove this image from the gallery? (Chat history will not be affected)",
@@ -119934,11 +120146,11 @@ img.ProseMirror-separator {
119934
120146
  ...activeWorkspace?.workspaceId ? { workspaceId: activeWorkspace.workspaceId } : {},
119935
120147
  ...currentSessionId ? { sessionId: currentSessionId } : {}
119936
120148
  };
119937
- await saveGalleryItem(newItem);
120149
+ const savedOk = await saveGalleryItem(newItem);
119938
120150
  if (activeTab !== "gallery") setActiveTab("gallery");
119939
120151
  if (search.trim().length > 0) setSearch("");
119940
120152
  openPreviewItem(newItem);
119941
- showToast(t("regenerateSuccess"));
120153
+ showToast(savedOk ? t("regenerateSuccess") : t("regenerateSaveFailed"));
119942
120154
  } catch (err) {
119943
120155
  if (controller.signal.aborted) return;
119944
120156
  showToast(err instanceof Error ? err.message : String(err));
@@ -121284,6 +121496,9 @@ img.ProseMirror-separator {
121284
121496
 
121285
121497
  /* tldraw infinite canvas surface (P0 spike) */
121286
121498
  .dsh-ig-tl-canvas{position:relative;flex:1 1 0;height:0;min-height:0;overflow:hidden;isolation:isolate}
121499
+ .dsh-ig-canvas-sync-status{position:absolute;z-index:500;bottom:48px;left:60px;right:16px;display:flex;align-items:center;justify-content:center;gap:8px;max-width:560px;margin:auto;padding:7px 12px;border:1px solid var(--dsh-ig-border);border-radius:10px;background:var(--dsh-ig-surface,#fff);color:var(--dsh-ig-text,#475569);box-shadow:0 2px 8px #0001;font-size:12px;pointer-events:none}
121500
+ .dsh-ig-canvas-sync-status[data-phase='error']{color:#b42318;border-color:#fecdca}
121501
+ .dsh-ig-canvas-sync-status button{pointer-events:auto;flex-shrink:0;border:1px solid currentColor;border-radius:6px;padding:3px 9px;background:transparent;color:inherit;cursor:pointer}
121287
121502
 
121288
121503
  /* ---- Right-sidebar seat variant (DSH 0.1.5 'sidebar.right.pane.tab') ----
121289
121504
  The seat is a narrow panel beside the conversation column, not a full
@@ -122005,7 +122220,7 @@ img.ProseMirror-separator {
122005
122220
  `;
122006
122221
  //#endregion
122007
122222
  //#region lib/types/client/index.js
122008
- console.info(`[dsh-image-gen] canvas bundle 2026-09-14T15:26:26.831Z`);
122223
+ console.info(`[dsh-image-gen] canvas bundle 2026-09-20T16:35:15.504Z`);
122009
122224
  /** Right-sidebar tab-type registration id (DSH 0.1.5 `sidebarRightTabs`). */
122010
122225
  const SIDEBAR_STUDIO_TAB_ID = "dsh-image-gen/studio";
122011
122226
  /** The tab kind: a page type opened by kind, recognizing no resource address. */
@@ -122066,6 +122281,7 @@ img.ProseMirror-separator {
122066
122281
  keyCleared: "已清除 Key",
122067
122282
  clearKeyFailed: "清除 Key 失败",
122068
122283
  saveKeyFailed: "保存 Key 失败",
122284
+ saveKeyFirst: "请先保存 API Key,再拉取模型或测试连接。",
122069
122285
  clearKeyUnsupported: "当前版本 DSH 不支持在此清除 Key,请到凭据管理中删除。",
122070
122286
  endpoint: "接口地址",
122071
122287
  reset: "重置",
@@ -122080,6 +122296,18 @@ img.ProseMirror-separator {
122080
122296
  editExtra: "附加 JSON 字段",
122081
122297
  editExtraPlaceholder: "{\"watermark\": false, \"prompt_extend\": true}",
122082
122298
  editExtraHint: "仅 JSON 形态生效;会合并到请求体末尾,可覆盖默认字段。留空表示不附加。",
122299
+ arkOutputFormat: "输出格式",
122300
+ arkOutputFormatPng: "PNG(无损,带透明通道)",
122301
+ arkOutputFormatJpeg: "JPEG(方舟默认,有损)",
122302
+ arkOutputFormatHint: "JPEG 会在主体边缘留下压缩色斑,抠图时变成难去掉的彩色毛边。要后期加工就选 PNG。",
122303
+ arkWatermark: "AI 生成水印",
122304
+ arkWatermarkOn: "添加(方舟默认)",
122305
+ arkWatermarkOff: "不添加",
122306
+ arkWatermarkHint: "方舟默认在右下角烙上“AI 生成”标识。做游戏素材或需要后期处理时关掉。",
122307
+ arkBackground: "背景",
122308
+ arkBackgroundOpaque: "不透明(方舟默认)",
122309
+ arkBackgroundTransparent: "透明",
122310
+ arkBackgroundHint: "仅图生图有效,且要求参考图本身带透明通道;文生图会被方舟拒绝,因此这一项只在图生图时发出。",
122083
122311
  editExtraInvalid: "附加 JSON 字段必须是合法的 JSON 对象。",
122084
122312
  endpointHintSeedream: "火山方舟兼容的 /api/v3 地址。",
122085
122313
  endpointHintDashScope: "阿里云百炼 DashScope 官方接口地址。",
@@ -122136,6 +122364,7 @@ img.ProseMirror-separator {
122136
122364
  confirmRegenerate: "确认生成",
122137
122365
  regenerating: "重新生成中…",
122138
122366
  regenerateFailed: "重新生成失败",
122367
+ regenerateSaveFailed: "已重新生成,但保存到图库失败,请重试",
122139
122368
  versionPrevious: "上一版本",
122140
122369
  versionNext: "下一版本",
122141
122370
  versionLabel: "图片版本 {current}/{total}"
@@ -122195,6 +122424,7 @@ img.ProseMirror-separator {
122195
122424
  keyCleared: "Key cleared",
122196
122425
  clearKeyFailed: "Failed to clear key",
122197
122426
  saveKeyFailed: "Failed to save the key",
122427
+ saveKeyFirst: "Save the API key first, then fetch models or test the connection.",
122198
122428
  clearKeyUnsupported: "This DSH build cannot clear keys here; remove it from credential management instead.",
122199
122429
  endpoint: "Endpoint / Base URL",
122200
122430
  reset: "Reset",
@@ -122209,6 +122439,18 @@ img.ProseMirror-separator {
122209
122439
  editExtra: "Extra JSON fields",
122210
122440
  editExtraPlaceholder: "{\"watermark\": false, \"prompt_extend\": true}",
122211
122441
  editExtraHint: "Only used with the JSON format; merged into the request body last and may override defaults. Leave empty for none.",
122442
+ arkOutputFormat: "Output format",
122443
+ arkOutputFormatPng: "PNG (lossless, keeps an alpha channel)",
122444
+ arkOutputFormatJpeg: "JPEG (Ark default, lossy)",
122445
+ arkOutputFormatHint: "JPEG ringing around the subject turns into coloured fringing that is hard to remove when cutting the background out. Pick PNG if you plan to edit the result.",
122446
+ arkWatermark: "AI generated watermark",
122447
+ arkWatermarkOn: "Add (Ark default)",
122448
+ arkWatermarkOff: "Do not add",
122449
+ arkWatermarkHint: "Ark stamps an \"AI generated\" mark into the bottom-right corner by default. Turn it off for game assets or any post-processing.",
122450
+ arkBackground: "Background",
122451
+ arkBackgroundOpaque: "Opaque (Ark default)",
122452
+ arkBackgroundTransparent: "Transparent",
122453
+ arkBackgroundHint: "Image-to-image only, and every reference image must already carry an alpha channel; Ark rejects it for text-to-image, so it is sent on edits alone.",
122212
122454
  editExtraInvalid: "Extra JSON fields must be a valid JSON object.",
122213
122455
  endpointHintSeedream: "Volcengine Ark compatible /api/v3 base URL.",
122214
122456
  endpointHintDashScope: "Official Aliyun DashScope endpoint.",
@@ -122265,6 +122507,7 @@ img.ProseMirror-separator {
122265
122507
  confirmRegenerate: "Regenerate",
122266
122508
  regenerating: "Regenerating…",
122267
122509
  regenerateFailed: "Regeneration failed",
122510
+ regenerateSaveFailed: "Regenerated, but saving to the gallery failed. Please retry.",
122268
122511
  versionPrevious: "Previous version",
122269
122512
  versionNext: "Next version",
122270
122513
  versionLabel: "Image version {current}/{total}"
@@ -122621,16 +122864,25 @@ img.ProseMirror-separator {
122621
122864
  } };
122622
122865
  const injectSettingsItem = (owner) => {
122623
122866
  const ownerRegister = owner.slots.register.bind(owner.slots);
122867
+ const injectSettingsFace = () => ({
122868
+ scope,
122869
+ credentials: credentialsProxy,
122870
+ credentialsAvailable,
122871
+ locale,
122872
+ credentialEvents
122873
+ });
122874
+ owner.slots.inject("settings.plugins.tab", () => ownerRegister({
122875
+ name: "settings.plugins.tab",
122876
+ id: IMAGE_GENERATION_NAMESPACE,
122877
+ order: 30,
122878
+ label: () => locale?.getSnapshot?.()?.active?.startsWith("en") ? "Image generation" : "图像生成",
122879
+ locale,
122880
+ inject: injectSettingsFace
122881
+ }, ImageGenerationSettingsCard));
122624
122882
  owner.slots.inject("settings.plugin.item", () => ownerRegister({
122625
122883
  name: "settings.plugin.item",
122626
122884
  key: IMAGE_GENERATION_NAMESPACE,
122627
- inject: () => ({
122628
- scope,
122629
- credentials: credentialsProxy,
122630
- credentialsAvailable,
122631
- locale,
122632
- credentialEvents
122633
- })
122885
+ inject: injectSettingsFace
122634
122886
  }, ImageGenerationSettingsCard));
122635
122887
  };
122636
122888
  const injectComposerPill = (owner) => {
@@ -122793,7 +123045,10 @@ img.ProseMirror-separator {
122793
123045
  activeWorkflow: "",
122794
123046
  timeoutSeconds: DEFAULT_COMFYUI_TIMEOUT_MS / 1e3,
122795
123047
  editFormat: "multipart",
122796
- editExtraText: ""
123048
+ editExtraText: "",
123049
+ outputFormat: "jpeg",
123050
+ watermark: true,
123051
+ background: "opaque"
122797
123052
  };
122798
123053
  }
122799
123054
  /** Settings field each cloud provider persists its model under. */
@@ -122862,6 +123117,11 @@ img.ProseMirror-separator {
122862
123117
  ...provider === "openai-compat" ? {
122863
123118
  editFormat: value?.openaiCompatEditFormat === "jsonImageUrlArray" ? "jsonImageUrlArray" : "multipart",
122864
123119
  editExtraText: editExtraTextOf(value?.openaiCompatEditExtra)
123120
+ } : {},
123121
+ ...provider === "seedream" ? {
123122
+ outputFormat: value?.seedreamOutputFormat === "png" ? "png" : "jpeg",
123123
+ watermark: value?.seedreamWatermark !== false,
123124
+ background: value?.seedreamBackground === "transparent" ? "transparent" : "opaque"
122865
123125
  } : {}
122866
123126
  };
122867
123127
  return rows;
@@ -123074,6 +123334,11 @@ img.ProseMirror-separator {
123074
123334
  workflows: resolveComfyUIWorkflows(value ?? {}),
123075
123335
  activeWorkflow: activeComfyUIWorkflow(value ?? {})?.name ?? "",
123076
123336
  timeoutSeconds: Math.max(1, Math.round((value?.comfyuiTimeoutMs ?? 3e5) / 1e3))
123337
+ } : {},
123338
+ ...provider === "seedream" ? {
123339
+ outputFormat: value?.seedreamOutputFormat === "png" ? "png" : "jpeg",
123340
+ watermark: value?.seedreamWatermark !== false,
123341
+ background: value?.seedreamBackground === "transparent" ? "transparent" : "opaque"
123077
123342
  } : {}
123078
123343
  };
123079
123344
  return next;
@@ -123205,6 +123470,11 @@ img.ProseMirror-separator {
123205
123470
  await props.scope.set("openaiCompatEditFormat", row.editFormat);
123206
123471
  await props.scope.set("openaiCompatEditExtra", editExtra);
123207
123472
  }
123473
+ if (provider === "seedream") {
123474
+ await props.scope.set("seedreamOutputFormat", row.outputFormat);
123475
+ await props.scope.set("seedreamWatermark", row.watermark);
123476
+ await props.scope.set("seedreamBackground", row.background);
123477
+ }
123208
123478
  if (row.keyInput.trim().length > 0) {
123209
123479
  const keyRef = cloudCredentialRef(provider);
123210
123480
  if (keyRef === void 0) throw new Error(t("comfyuiNoKey"));
@@ -123219,7 +123489,10 @@ img.ProseMirror-separator {
123219
123489
  }
123220
123490
  updateRow(provider, {
123221
123491
  message: t("saved"),
123222
- messageIsError: false
123492
+ messageIsError: false,
123493
+ testResult: void 0,
123494
+ modelFetchMessage: "",
123495
+ modelFetchIsError: false
123223
123496
  });
123224
123497
  } catch (cause) {
123225
123498
  updateRow(provider, {
@@ -123232,6 +123505,14 @@ img.ProseMirror-separator {
123232
123505
  };
123233
123506
  /** Probe through the host route so the browser side never touches credential values. */
123234
123507
  const testConnection = async (provider) => {
123508
+ if (rows[provider].keyInput.trim().length > 0) {
123509
+ updateRow(provider, {
123510
+ message: t("saveKeyFirst"),
123511
+ messageIsError: false,
123512
+ testResult: void 0
123513
+ });
123514
+ return;
123515
+ }
123235
123516
  updateRow(provider, {
123236
123517
  testing: true,
123237
123518
  testResult: void 0,
@@ -123258,6 +123539,13 @@ img.ProseMirror-separator {
123258
123539
  };
123259
123540
  /** Pull the provider's image-capable model ids through the host route (Google and the OpenAI family). */
123260
123541
  const fetchProviderModels = async (provider) => {
123542
+ if (rows[provider].keyInput.trim().length > 0) {
123543
+ updateRow(provider, {
123544
+ modelFetchMessage: t("saveKeyFirst"),
123545
+ modelFetchIsError: false
123546
+ });
123547
+ return;
123548
+ }
123261
123549
  updateRow(provider, {
123262
123550
  fetchingModels: true,
123263
123551
  modelFetchMessage: "",
@@ -123606,7 +123894,7 @@ img.ProseMirror-separator {
123606
123894
  (0, react_jsx_runtime.jsx)("button", {
123607
123895
  type: "button",
123608
123896
  className: "dsh-ig-btn-secondary",
123609
- disabled: row.fetchingModels || !snapshot.writable,
123897
+ disabled: row.fetchingModels || row.saving || !snapshot.writable,
123610
123898
  onClick: () => {
123611
123899
  fetchProviderModels(provider);
123612
123900
  },
@@ -123660,6 +123948,92 @@ img.ProseMirror-separator {
123660
123948
  })
123661
123949
  ]
123662
123950
  }) : null,
123951
+ provider === "seedream" ? (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
123952
+ (0, react_jsx_runtime.jsxs)("label", {
123953
+ className: "dsh-ig-field",
123954
+ children: [
123955
+ (0, react_jsx_runtime.jsx)("span", {
123956
+ className: "dsh-ig-label",
123957
+ children: t("arkOutputFormat")
123958
+ }),
123959
+ (0, react_jsx_runtime.jsxs)("select", {
123960
+ className: "dsh-ig-input",
123961
+ value: row.outputFormat,
123962
+ onChange: (event) => {
123963
+ updateRow(provider, { outputFormat: event.target.value === "png" ? "png" : "jpeg" });
123964
+ },
123965
+ disabled: !snapshot.writable,
123966
+ children: [(0, react_jsx_runtime.jsx)("option", {
123967
+ value: "jpeg",
123968
+ children: t("arkOutputFormatJpeg")
123969
+ }), (0, react_jsx_runtime.jsx)("option", {
123970
+ value: "png",
123971
+ children: t("arkOutputFormatPng")
123972
+ })]
123973
+ }),
123974
+ (0, react_jsx_runtime.jsx)("span", {
123975
+ className: "dsh-ig-hint",
123976
+ children: t("arkOutputFormatHint")
123977
+ })
123978
+ ]
123979
+ }),
123980
+ (0, react_jsx_runtime.jsxs)("label", {
123981
+ className: "dsh-ig-field",
123982
+ children: [
123983
+ (0, react_jsx_runtime.jsx)("span", {
123984
+ className: "dsh-ig-label",
123985
+ children: t("arkWatermark")
123986
+ }),
123987
+ (0, react_jsx_runtime.jsxs)("select", {
123988
+ className: "dsh-ig-input",
123989
+ value: row.watermark ? "on" : "off",
123990
+ onChange: (event) => {
123991
+ updateRow(provider, { watermark: event.target.value === "on" });
123992
+ },
123993
+ disabled: !snapshot.writable,
123994
+ children: [(0, react_jsx_runtime.jsx)("option", {
123995
+ value: "on",
123996
+ children: t("arkWatermarkOn")
123997
+ }), (0, react_jsx_runtime.jsx)("option", {
123998
+ value: "off",
123999
+ children: t("arkWatermarkOff")
124000
+ })]
124001
+ }),
124002
+ (0, react_jsx_runtime.jsx)("span", {
124003
+ className: "dsh-ig-hint",
124004
+ children: t("arkWatermarkHint")
124005
+ })
124006
+ ]
124007
+ }),
124008
+ (0, react_jsx_runtime.jsxs)("label", {
124009
+ className: "dsh-ig-field",
124010
+ children: [
124011
+ (0, react_jsx_runtime.jsx)("span", {
124012
+ className: "dsh-ig-label",
124013
+ children: t("arkBackground")
124014
+ }),
124015
+ (0, react_jsx_runtime.jsxs)("select", {
124016
+ className: "dsh-ig-input",
124017
+ value: row.background,
124018
+ onChange: (event) => {
124019
+ updateRow(provider, { background: event.target.value === "transparent" ? "transparent" : "opaque" });
124020
+ },
124021
+ disabled: !snapshot.writable,
124022
+ children: [(0, react_jsx_runtime.jsx)("option", {
124023
+ value: "opaque",
124024
+ children: t("arkBackgroundOpaque")
124025
+ }), (0, react_jsx_runtime.jsx)("option", {
124026
+ value: "transparent",
124027
+ children: t("arkBackgroundTransparent")
124028
+ })]
124029
+ }),
124030
+ (0, react_jsx_runtime.jsx)("span", {
124031
+ className: "dsh-ig-hint",
124032
+ children: t("arkBackgroundHint")
124033
+ })
124034
+ ]
124035
+ })
124036
+ ] }) : null,
123663
124037
  provider === "openai-compat" && row.editFormat === "jsonImageUrlArray" ? (0, react_jsx_runtime.jsxs)("label", {
123664
124038
  className: "dsh-ig-field",
123665
124039
  children: [
@@ -123695,7 +124069,7 @@ img.ProseMirror-separator {
123695
124069
  (0, react_jsx_runtime.jsx)("button", {
123696
124070
  type: "button",
123697
124071
  className: "dsh-ig-btn-secondary",
123698
- disabled: row.testing,
124072
+ disabled: row.testing || row.saving,
123699
124073
  onClick: () => {
123700
124074
  testConnection(provider);
123701
124075
  },
@@ -123713,7 +124087,7 @@ img.ProseMirror-separator {
123713
124087
  (0, react_jsx_runtime.jsx)("button", {
123714
124088
  className: "dsh-ig-save",
123715
124089
  type: "submit",
123716
- disabled: row.saving || !snapshot.writable,
124090
+ disabled: row.saving || row.testing || row.fetchingModels || !snapshot.writable,
123717
124091
  children: row.saving ? t("saving") : t("save")
123718
124092
  })
123719
124093
  ]
@@ -124445,7 +124819,7 @@ img.ProseMirror-separator {
124445
124819
  ratio: request.ratio,
124446
124820
  quality: request.quality
124447
124821
  };
124448
- await saveGalleryItem({
124822
+ const savedOk = await saveGalleryItem({
124449
124823
  id: String(revision.attachment.attachmentId),
124450
124824
  attachment: revision.attachment,
124451
124825
  prompt: revision.prompt,
@@ -124460,7 +124834,7 @@ img.ProseMirror-separator {
124460
124834
  });
124461
124835
  const next = appendConversationImageRevision(String(originId), revision);
124462
124836
  setRevisionChain(next);
124463
- setToast(t("regenerate"));
124837
+ setToast(savedOk ? t("regenerate") : t("regenerateSaveFailed"));
124464
124838
  setTimeout(() => {
124465
124839
  setToast(void 0);
124466
124840
  }, 2e3);