dsh-image-gen 0.6.9 → 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-20T15:13:07.204Z`);
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. */
@@ -122149,6 +122364,7 @@ img.ProseMirror-separator {
122149
122364
  confirmRegenerate: "确认生成",
122150
122365
  regenerating: "重新生成中…",
122151
122366
  regenerateFailed: "重新生成失败",
122367
+ regenerateSaveFailed: "已重新生成,但保存到图库失败,请重试",
122152
122368
  versionPrevious: "上一版本",
122153
122369
  versionNext: "下一版本",
122154
122370
  versionLabel: "图片版本 {current}/{total}"
@@ -122291,6 +122507,7 @@ img.ProseMirror-separator {
122291
122507
  confirmRegenerate: "Regenerate",
122292
122508
  regenerating: "Regenerating…",
122293
122509
  regenerateFailed: "Regeneration failed",
122510
+ regenerateSaveFailed: "Regenerated, but saving to the gallery failed. Please retry.",
122294
122511
  versionPrevious: "Previous version",
122295
122512
  versionNext: "Next version",
122296
122513
  versionLabel: "Image version {current}/{total}"
@@ -124602,7 +124819,7 @@ img.ProseMirror-separator {
124602
124819
  ratio: request.ratio,
124603
124820
  quality: request.quality
124604
124821
  };
124605
- await saveGalleryItem({
124822
+ const savedOk = await saveGalleryItem({
124606
124823
  id: String(revision.attachment.attachmentId),
124607
124824
  attachment: revision.attachment,
124608
124825
  prompt: revision.prompt,
@@ -124617,7 +124834,7 @@ img.ProseMirror-separator {
124617
124834
  });
124618
124835
  const next = appendConversationImageRevision(String(originId), revision);
124619
124836
  setRevisionChain(next);
124620
- setToast(t("regenerate"));
124837
+ setToast(savedOk ? t("regenerate") : t("regenerateSaveFailed"));
124621
124838
  setTimeout(() => {
124622
124839
  setToast(void 0);
124623
124840
  }, 2e3);