dsh-image-gen 0.3.0 → 0.3.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.
package/lib/client.js CHANGED
@@ -13,6 +13,8 @@ window.__ModuleLoader__.load({
13
13
  const DELETE_ROUTE = "/plugins/dsh-image-gen/delete";
14
14
  /** Same-origin route used by the browser image workbench. */
15
15
  const STUDIO_ROUTE = "/plugins/dsh-image-gen/studio";
16
+ /** Browser route used for saving generated images to workspace on demand. */
17
+ const SAVE_WORKSPACE_ROUTE = "/plugins/dsh-image-gen/save-workspace";
16
18
  /** Namespace persisted through DSH Settings. */
17
19
  const IMAGE_GENERATION_NAMESPACE = "image-generation";
18
20
  /** Providers supported by the first browser workbench release. */
@@ -511,6 +513,26 @@ window.__ModuleLoader__.load({
511
513
  * This source code is licensed under the ISC license.
512
514
  * See the LICENSE file in the root directory of this source tree.
513
515
  */
516
+ const BookmarkPlus = createLucideIcon("bookmark-plus", [
517
+ ["path", {
518
+ d: "M12 7v6",
519
+ key: "lw1j43"
520
+ }],
521
+ ["path", {
522
+ d: "M15 10H9",
523
+ key: "o6yqo3"
524
+ }],
525
+ ["path", {
526
+ d: "M17 3a2 2 0 0 1 2 2v15a1 1 0 0 1-1.496.868l-4.512-2.578a2 2 0 0 0-1.984 0l-4.512 2.578A1 1 0 0 1 5 20V5a2 2 0 0 1 2-2z",
527
+ key: "oz39mx"
528
+ }]
529
+ ]);
530
+ /**
531
+ * @license lucide-react v1.38.0 - ISC
532
+ *
533
+ * This source code is licensed under the ISC license.
534
+ * See the LICENSE file in the root directory of this source tree.
535
+ */
514
536
  const Check = createLucideIcon("check", [["path", {
515
537
  d: "M20 6 9 17l-5-5",
516
538
  key: "1gmf2c"
@@ -1222,17 +1244,17 @@ window.__ModuleLoader__.load({
1222
1244
  //#region lib/types/client/studio-view.js
1223
1245
  const COPY = {
1224
1246
  zh: {
1225
- title: "云端图像工作台",
1247
+ title: "云端生图工作台",
1226
1248
  configured: "API 已配置",
1227
1249
  unconfigured: "未配置",
1228
1250
  recent: "最近生成",
1229
- empty: "还没有生成记录",
1251
+ empty: "暂无生成历史",
1230
1252
  generate: "文生图",
1231
1253
  edit: "图生图",
1232
1254
  details: "图片详情",
1233
1255
  reference: "参考图",
1234
- optional: "可选",
1235
- upload: "点击上传或拖拽图片到此处",
1256
+ optional: "选填",
1257
+ upload: "点击或拖拽图片到此处",
1236
1258
  uploadHint: "支持 JPG / PNG / WebP / GIF,最大 10MB(最多 5 张)",
1237
1259
  prompt: "提示词 Prompt",
1238
1260
  clear: "清空",
@@ -1244,6 +1266,14 @@ window.__ModuleLoader__.load({
1244
1266
  start: "开始生成",
1245
1267
  generating: "正在生成…",
1246
1268
  cancelGenerate: "取消生成",
1269
+ count: "生成数量",
1270
+ countUnit: "{n} 张",
1271
+ partialSuccess: "已生成 {success} 张图片,{failed} 张失败",
1272
+ generatingCount: "正在生成(共 {count} 张)…",
1273
+ batchResult: "本次生成(共 {count} 张)",
1274
+ saveToGallery: "收藏进画廊",
1275
+ savedToGallery: "已收藏进画廊",
1276
+ inGallery: "已在画廊",
1247
1277
  retry: "重新加载",
1248
1278
  configLoadFailed: "工作台配置加载失败,请检查服务后重试。",
1249
1279
  noProvider: "请先在设置中配置至少一个云端图像 Provider 的 API Key。",
@@ -1319,6 +1349,14 @@ window.__ModuleLoader__.load({
1319
1349
  start: "Generate",
1320
1350
  generating: "Generating…",
1321
1351
  cancelGenerate: "Cancel",
1352
+ count: "Number of images",
1353
+ countUnit: "{n}",
1354
+ partialSuccess: "Generated {success} images, {failed} failed",
1355
+ generatingCount: "Generating ({count} images)…",
1356
+ batchResult: "Generated {count} images",
1357
+ saveToGallery: "Save to Gallery",
1358
+ savedToGallery: "Saved to Gallery",
1359
+ inGallery: "In Gallery",
1322
1360
  retry: "Retry",
1323
1361
  configLoadFailed: "Failed to load studio configuration.",
1324
1362
  noProvider: "Configure an API key for at least one cloud image provider in Settings.",
@@ -1389,6 +1427,8 @@ window.__ModuleLoader__.load({
1389
1427
  const [quality, setQuality] = (0, react.useState)("1K");
1390
1428
  const [prompt, setPrompt] = (0, react.useState)("");
1391
1429
  const [references, setReferences] = (0, react.useState)([]);
1430
+ const [count, setCount] = (0, react.useState)(1);
1431
+ const [currentBatch, setCurrentBatch] = (0, react.useState)(null);
1392
1432
  const [isGenerating, setIsGenerating] = (0, react.useState)(false);
1393
1433
  const [error, setError] = (0, react.useState)(null);
1394
1434
  const [notice, setNotice] = (0, react.useState)(null);
@@ -1476,7 +1516,7 @@ window.__ModuleLoader__.load({
1476
1516
  const canvasEl = canvasRef.current;
1477
1517
  if (!canvasEl) return;
1478
1518
  const onWheel = (e) => {
1479
- if (selected === null || !image.url) return;
1519
+ if (selected === null && (!currentBatch || currentBatch.length === 0)) return;
1480
1520
  e.preventDefault();
1481
1521
  const delta = e.deltaY < 0 ? 15 : -15;
1482
1522
  setFit(false);
@@ -1484,7 +1524,7 @@ window.__ModuleLoader__.load({
1484
1524
  };
1485
1525
  canvasEl.addEventListener("wheel", onWheel, { passive: false });
1486
1526
  return () => canvasEl.removeEventListener("wheel", onWheel);
1487
- }, [selected, image.url]);
1527
+ }, [selected, currentBatch]);
1488
1528
  (0, react.useEffect)(() => {
1489
1529
  const handleMouseMove = (e) => {
1490
1530
  if (!dragStartRef.current) return;
@@ -1544,18 +1584,20 @@ window.__ModuleLoader__.load({
1544
1584
  });
1545
1585
  };
1546
1586
  const selectItem = (item) => {
1587
+ setCurrentBatch(null);
1547
1588
  setSelected(item);
1548
1589
  setPanelTab("details");
1549
1590
  resetFit();
1550
1591
  };
1551
1592
  const startNew = () => {
1593
+ setCurrentBatch(null);
1552
1594
  setSelected(null);
1553
1595
  setPanelTab("generate");
1554
1596
  resetFit();
1555
1597
  };
1556
1598
  const handleCanvasMouseDown = (e) => {
1557
1599
  if (e.button !== 0 && e.button !== 1) return;
1558
- if (selected === null || !image.url) return;
1600
+ if (selected === null && (!currentBatch || currentBatch.length === 0)) return;
1559
1601
  setIsDragging(true);
1560
1602
  hasDraggedRef.current = false;
1561
1603
  dragStartRef.current = {
@@ -1565,9 +1607,7 @@ window.__ModuleLoader__.load({
1565
1607
  startY: offset.y
1566
1608
  };
1567
1609
  };
1568
- const handleImageClick = () => {
1569
- if (!hasDraggedRef.current) setLightboxOpen(true);
1570
- };
1610
+ const handleImageClick = () => {};
1571
1611
  const addReferenceFiles = (fileList) => {
1572
1612
  if (!fileList || fileList.length === 0) return;
1573
1613
  const files = Array.from(fileList);
@@ -1692,30 +1732,45 @@ window.__ModuleLoader__.load({
1692
1732
  prompt: prompt.trim(),
1693
1733
  ratio,
1694
1734
  quality,
1735
+ ...count > 1 ? { count } : {},
1695
1736
  ...workspace?.path ? { workspaceRoot: workspace.path } : {},
1696
1737
  ...referencesPayload === void 0 ? {} : { references: referencesPayload }
1697
1738
  })
1698
1739
  });
1699
1740
  const payload = await response.json();
1700
1741
  if (!response.ok || !("attachment" in payload)) throw new Error("error" in payload && payload.error ? payload.error : t("generationFailed"));
1701
- const item = {
1702
- id: String(payload.attachment.attachmentId),
1742
+ const generatedList = Array.isArray(payload.items) && payload.items.length > 0 ? payload.items : [{
1703
1743
  attachment: payload.attachment,
1744
+ output: payload.output,
1745
+ ...payload.savedTo ? { savedTo: payload.savedTo } : {}
1746
+ }];
1747
+ const galleryEntries = generatedList.map((gen, idx) => ({
1748
+ id: String(gen.attachment.attachmentId || `${payload.createdAt}-${idx}`),
1749
+ attachment: gen.attachment,
1704
1750
  prompt: payload.prompt,
1705
1751
  provider: payload.provider,
1706
1752
  model: payload.model,
1707
- createdAt: payload.createdAt,
1753
+ createdAt: payload.createdAt + idx,
1708
1754
  aspectRatio: ratio,
1709
1755
  imageSize: quality,
1710
- output: payload.output,
1711
- ...payload.savedTo ? { savedTo: payload.savedTo } : {},
1756
+ output: gen.output,
1757
+ ...gen.savedTo ? { savedTo: gen.savedTo } : {},
1712
1758
  ...workspace?.path ? { workspacePath: workspace.path } : {},
1713
1759
  ...workspace?.workspaceId ? { workspaceId: workspace.workspaceId } : {}
1714
- };
1715
- await saveGalleryItem(item);
1716
- setSelected(item);
1760
+ }));
1761
+ if (galleryEntries.length > 1) {
1762
+ setCurrentBatch(galleryEntries);
1763
+ setSelected(galleryEntries[0]);
1764
+ } else if (galleryEntries.length === 1) {
1765
+ setCurrentBatch(null);
1766
+ setSelected(galleryEntries[0]);
1767
+ }
1717
1768
  setPanelTab("details");
1718
1769
  resetFit();
1770
+ if (payload.failedCount && payload.failedCount > 0) flash(t("partialSuccess", {
1771
+ success: String(generatedList.length),
1772
+ failed: String(payload.failedCount)
1773
+ }));
1719
1774
  } catch (submitError) {
1720
1775
  if (controller.signal.aborted) return;
1721
1776
  setError(messageOf(submitError));
@@ -1726,6 +1781,42 @@ window.__ModuleLoader__.load({
1726
1781
  }
1727
1782
  }
1728
1783
  };
1784
+ const isSelectedInGallery = (0, react.useMemo)(() => {
1785
+ if (selected === null) return false;
1786
+ return items.some((item) => item.id === selected.id);
1787
+ }, [items, selected]);
1788
+ const handleSaveToGallery = async () => {
1789
+ if (selected === null || isSelectedInGallery) return;
1790
+ let savedTo = selected.savedTo;
1791
+ const targetRoot = workspace?.path || config?.workspaceRoot;
1792
+ if (!savedTo) try {
1793
+ const res = await fetch(SAVE_WORKSPACE_ROUTE, {
1794
+ method: "POST",
1795
+ credentials: "same-origin",
1796
+ headers: { "content-type": "application/json" },
1797
+ body: JSON.stringify({
1798
+ attachment: selected.attachment,
1799
+ ...targetRoot ? { workspaceRoot: targetRoot } : {}
1800
+ })
1801
+ });
1802
+ if (res.ok) {
1803
+ const data = await res.json();
1804
+ if (data.savedTo) savedTo = data.savedTo;
1805
+ }
1806
+ } catch (e) {
1807
+ console.warn("Failed to save to workspace:", e);
1808
+ }
1809
+ const updatedItem = {
1810
+ ...selected,
1811
+ ...savedTo ? { savedTo } : {},
1812
+ ...targetRoot ? { workspacePath: targetRoot } : {},
1813
+ ...workspace?.workspaceId ? { workspaceId: workspace.workspaceId } : {}
1814
+ };
1815
+ await saveGalleryItem(updatedItem);
1816
+ setSelected(updatedItem);
1817
+ if (currentBatch) setCurrentBatch((prev) => prev ? prev.map((i) => i.id === updatedItem.id ? updatedItem : i) : prev);
1818
+ flash(t("savedToGallery"));
1819
+ };
1729
1820
  const handleConfirmDelete = async () => {
1730
1821
  if (selected === null) return;
1731
1822
  const idToDelete = selected.id;
@@ -1747,8 +1838,21 @@ window.__ModuleLoader__.load({
1747
1838
  throw new Error(reason);
1748
1839
  }
1749
1840
  }
1750
- await deleteGalleryItem(idToDelete);
1841
+ if (isSelectedInGallery) await deleteGalleryItem(idToDelete);
1751
1842
  evictAttachmentCache(attachmentId);
1843
+ if (currentBatch) {
1844
+ const remaining = currentBatch.filter((i) => i.id !== idToDelete);
1845
+ if (remaining.length > 1) {
1846
+ setCurrentBatch(remaining);
1847
+ setSelected(remaining[0]);
1848
+ } else if (remaining.length === 1) {
1849
+ setCurrentBatch(null);
1850
+ setSelected(remaining[0]);
1851
+ } else {
1852
+ setCurrentBatch(null);
1853
+ setSelected(null);
1854
+ }
1855
+ } else setSelected(null);
1752
1856
  flash(t("deletedToast"));
1753
1857
  } catch (delError) {
1754
1858
  setError(messageOf(delError));
@@ -1766,13 +1870,50 @@ window.__ModuleLoader__.load({
1766
1870
  const toggleFavorite = async () => {
1767
1871
  if (selected === null) return;
1768
1872
  const targetId = selected.id;
1769
- const nextFav = await toggleFavoriteGalleryItem(targetId);
1770
- setSelected((curr) => curr !== null && curr.id === targetId ? {
1771
- ...curr,
1772
- isFavorite: nextFav
1773
- } : curr);
1774
- flash(nextFav ? t("favoriteAdded") : t("favoriteRemoved"));
1873
+ if (isSelectedInGallery) {
1874
+ const nextFav = await toggleFavoriteGalleryItem(targetId);
1875
+ setSelected((curr) => curr !== null && curr.id === targetId ? {
1876
+ ...curr,
1877
+ isFavorite: nextFav
1878
+ } : curr);
1879
+ if (currentBatch) setCurrentBatch((prev) => prev ? prev.map((i) => i.id === targetId ? {
1880
+ ...i,
1881
+ isFavorite: nextFav
1882
+ } : i) : null);
1883
+ flash(nextFav ? t("favoriteAdded") : t("favoriteRemoved"));
1884
+ } else {
1885
+ const nextFav = !selected.isFavorite;
1886
+ setSelected((curr) => curr !== null && curr.id === targetId ? {
1887
+ ...curr,
1888
+ isFavorite: nextFav
1889
+ } : curr);
1890
+ if (currentBatch) setCurrentBatch((prev) => prev ? prev.map((i) => i.id === targetId ? {
1891
+ ...i,
1892
+ isFavorite: nextFav
1893
+ } : i) : null);
1894
+ flash(nextFav ? t("favoriteAdded") : t("favoriteRemoved"));
1895
+ }
1896
+ };
1897
+ const stepLightbox = (delta) => {
1898
+ if (!currentBatch || currentBatch.length <= 1 || selected === null) return;
1899
+ const currIdx = currentBatch.findIndex((i) => i.id === selected.id);
1900
+ if (currIdx === -1) return;
1901
+ const nextIdx = (currIdx + delta + currentBatch.length) % currentBatch.length;
1902
+ setSelected(currentBatch[nextIdx]);
1775
1903
  };
1904
+ (0, react.useEffect)(() => {
1905
+ if (!lightboxOpen || !currentBatch || currentBatch.length <= 1) return;
1906
+ const onKeyDown = (e) => {
1907
+ if (e.key === "ArrowLeft") stepLightbox(-1);
1908
+ else if (e.key === "ArrowRight") stepLightbox(1);
1909
+ };
1910
+ window.addEventListener("keydown", onKeyDown);
1911
+ return () => window.removeEventListener("keydown", onKeyDown);
1912
+ }, [
1913
+ lightboxOpen,
1914
+ currentBatch,
1915
+ selected
1916
+ ]);
1776
1917
  const flash = (message) => {
1777
1918
  setNotice(message);
1778
1919
  window.setTimeout(() => setNotice(null), 2200);
@@ -1933,13 +2074,29 @@ window.__ModuleLoader__.load({
1933
2074
  className: "dsh-ig-generation-orbit",
1934
2075
  children: (0, react_jsx_runtime.jsx)(Sparkles, { size: 28 })
1935
2076
  }),
1936
- (0, react_jsx_runtime.jsx)("strong", { children: t("generating") }),
2077
+ (0, react_jsx_runtime.jsx)("strong", { children: count > 1 ? t("generatingCount", { count: String(count) }) : t("generating") }),
1937
2078
  (0, react_jsx_runtime.jsxs)("span", { children: [
1938
2079
  activeProfile?.label,
1939
2080
  " · ",
1940
2081
  model
1941
2082
  ] })
1942
2083
  ]
2084
+ }) : currentBatch && currentBatch.length > 1 ? (0, react_jsx_runtime.jsx)("div", {
2085
+ className: "dsh-ig-canvas-cluster",
2086
+ "data-count": currentBatch.length,
2087
+ style: {
2088
+ transform: `translate(${offset.x}px, ${offset.y}px) scale(${zoom / 100})`,
2089
+ transformOrigin: "center center",
2090
+ transition: isDragging ? "none" : "transform 0.08s ease-out"
2091
+ },
2092
+ children: currentBatch.map((batchItem, index) => (0, react_jsx_runtime.jsx)(BatchCanvasItem, {
2093
+ item: batchItem,
2094
+ index,
2095
+ isSelected: selected?.id === batchItem.id,
2096
+ onSelect: () => {
2097
+ if (!hasDraggedRef.current) setSelected(batchItem);
2098
+ }
2099
+ }, batchItem.id))
1943
2100
  }) : selected === null ? (0, react_jsx_runtime.jsxs)("div", {
1944
2101
  className: "dsh-ig-canvas-empty",
1945
2102
  children: [(0, react_jsx_runtime.jsx)(ImagePlus, { size: 36 }), (0, react_jsx_runtime.jsx)("span", { children: t("selectHistory") })]
@@ -1984,6 +2141,14 @@ window.__ModuleLoader__.load({
1984
2141
  (0, react_jsx_runtime.jsxs)("div", {
1985
2142
  className: "dsh-ig-result-actions",
1986
2143
  children: [(0, react_jsx_runtime.jsx)("p", { children: selected.prompt }), (0, react_jsx_runtime.jsxs)("div", { children: [
2144
+ (0, react_jsx_runtime.jsxs)("button", {
2145
+ type: "button",
2146
+ className: `dsh-ig-save-gallery-btn ${isSelectedInGallery ? "is-saved" : ""}`,
2147
+ onClick: () => void handleSaveToGallery(),
2148
+ title: isSelectedInGallery ? t("inGallery") : t("saveToGallery"),
2149
+ disabled: isSelectedInGallery,
2150
+ children: [isSelectedInGallery ? (0, react_jsx_runtime.jsx)(Check, { size: 15 }) : (0, react_jsx_runtime.jsx)(BookmarkPlus, { size: 15 }), (0, react_jsx_runtime.jsx)("span", { children: isSelectedInGallery ? t("inGallery") : t("saveToGallery") })]
2151
+ }),
1987
2152
  (0, react_jsx_runtime.jsxs)("button", {
1988
2153
  type: "button",
1989
2154
  onClick: () => void toggleFavorite(),
@@ -2177,39 +2342,59 @@ window.__ModuleLoader__.load({
2177
2342
  onClick: () => void loadConfig(),
2178
2343
  children: [(0, react_jsx_runtime.jsx)(RefreshCw, { size: 13 }), (0, react_jsx_runtime.jsx)("span", { children: t("retry") })]
2179
2344
  })]
2180
- }) : (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsxs)("div", {
2181
- className: "dsh-ig-field-grid",
2182
- children: [(0, react_jsx_runtime.jsx)(FieldSelect, {
2183
- label: t("provider"),
2184
- value: provider,
2185
- onChange: changeProvider,
2186
- options: config.providers.map((item) => ({
2187
- value: item.provider,
2188
- label: `${item.label}${item.configured ? "" : ` · ${t("unconfigured")}`}`
2189
- }))
2190
- }), (0, react_jsx_runtime.jsx)(FieldSelect, {
2191
- label: t("model"),
2192
- value: model,
2193
- onChange: setModel,
2194
- options: activeProfile === void 0 ? [] : [{
2195
- value: activeProfile.model,
2196
- label: activeProfile.model
2197
- }]
2198
- })]
2199
- }), (0, react_jsx_runtime.jsxs)("div", {
2200
- className: "dsh-ig-field-grid",
2201
- children: [(0, react_jsx_runtime.jsx)(FieldSelect, {
2202
- label: t("ratio"),
2203
- value: ratio,
2204
- onChange: setRatio,
2205
- options: activeProfile?.ratioOptions ?? []
2206
- }), (0, react_jsx_runtime.jsx)(FieldSelect, {
2207
- label: t("quality"),
2208
- value: quality,
2209
- onChange: setQuality,
2210
- options: activeProfile?.qualityOptions ?? []
2211
- })]
2212
- })] }),
2345
+ }) : (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2346
+ (0, react_jsx_runtime.jsxs)("div", {
2347
+ className: "dsh-ig-field-grid",
2348
+ children: [(0, react_jsx_runtime.jsx)(FieldSelect, {
2349
+ label: t("provider"),
2350
+ value: provider,
2351
+ onChange: changeProvider,
2352
+ options: config.providers.map((item) => ({
2353
+ value: item.provider,
2354
+ label: `${item.label}${item.configured ? "" : ` · ${t("unconfigured")}`}`
2355
+ }))
2356
+ }), (0, react_jsx_runtime.jsx)(FieldSelect, {
2357
+ label: t("model"),
2358
+ value: model,
2359
+ onChange: setModel,
2360
+ options: activeProfile === void 0 ? [] : [{
2361
+ value: activeProfile.model,
2362
+ label: activeProfile.model
2363
+ }]
2364
+ })]
2365
+ }),
2366
+ (0, react_jsx_runtime.jsxs)("div", {
2367
+ className: "dsh-ig-field-grid",
2368
+ children: [(0, react_jsx_runtime.jsx)(FieldSelect, {
2369
+ label: t("ratio"),
2370
+ value: ratio,
2371
+ onChange: setRatio,
2372
+ options: activeProfile?.ratioOptions ?? []
2373
+ }), (0, react_jsx_runtime.jsx)(FieldSelect, {
2374
+ label: t("quality"),
2375
+ value: quality,
2376
+ onChange: setQuality,
2377
+ options: activeProfile?.qualityOptions ?? []
2378
+ })]
2379
+ }),
2380
+ (0, react_jsx_runtime.jsxs)("div", {
2381
+ className: "dsh-ig-field",
2382
+ children: [(0, react_jsx_runtime.jsx)("label", { children: t("count") }), (0, react_jsx_runtime.jsx)("div", {
2383
+ className: "dsh-ig-count-row",
2384
+ children: [
2385
+ 1,
2386
+ 2,
2387
+ 3,
2388
+ 4
2389
+ ].map((option) => (0, react_jsx_runtime.jsx)("button", {
2390
+ type: "button",
2391
+ className: `dsh-ig-count-pill ${count === option ? "is-active" : ""}`,
2392
+ onClick: () => setCount(option),
2393
+ children: t("countUnit", { n: String(option) })
2394
+ }, option))
2395
+ })]
2396
+ })
2397
+ ] }),
2213
2398
  configuredCount === 0 && config !== null && (0, react_jsx_runtime.jsx)("div", {
2214
2399
  className: "dsh-ig-inline-note",
2215
2400
  children: t("noProvider")
@@ -2231,7 +2416,7 @@ window.__ModuleLoader__.load({
2231
2416
  className: "dsh-ig-generate-button",
2232
2417
  disabled: config === null,
2233
2418
  onClick: () => void submit(),
2234
- children: [(0, react_jsx_runtime.jsx)(Sparkles, { size: 17 }), (0, react_jsx_runtime.jsx)("span", { children: t("start") })]
2419
+ children: [(0, react_jsx_runtime.jsx)(Sparkles, { size: 17 }), (0, react_jsx_runtime.jsxs)("span", { children: [t("start"), count > 1 ? ` (${count})` : ""] })]
2235
2420
  })
2236
2421
  ]
2237
2422
  })]
@@ -2255,6 +2440,14 @@ window.__ModuleLoader__.load({
2255
2440
  className: "dsh-ig-tag dsh-ig-tag-model",
2256
2441
  children: selected.model
2257
2442
  }),
2443
+ currentBatch && currentBatch.length > 1 && (0, react_jsx_runtime.jsxs)("span", {
2444
+ className: "dsh-ig-tag",
2445
+ children: [
2446
+ currentBatch.findIndex((i) => i.id === selected.id) + 1,
2447
+ " / ",
2448
+ currentBatch.length
2449
+ ]
2450
+ }),
2258
2451
  selected.attachment.width && selected.attachment.height ? (0, react_jsx_runtime.jsxs)("span", {
2259
2452
  className: "dsh-ig-tag",
2260
2453
  children: [
@@ -2272,6 +2465,25 @@ window.__ModuleLoader__.load({
2272
2465
  children: (0, react_jsx_runtime.jsx)(X, { size: 18 })
2273
2466
  })]
2274
2467
  }),
2468
+ currentBatch && currentBatch.length > 1 && (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsx)("button", {
2469
+ type: "button",
2470
+ className: "dsh-ig-lightbox-nav is-prev",
2471
+ onClick: (e) => {
2472
+ e.stopPropagation();
2473
+ stepLightbox(-1);
2474
+ },
2475
+ title: "上一张",
2476
+ children: (0, react_jsx_runtime.jsx)(ChevronLeft, { size: 24 })
2477
+ }), (0, react_jsx_runtime.jsx)("button", {
2478
+ type: "button",
2479
+ className: "dsh-ig-lightbox-nav is-next",
2480
+ onClick: (e) => {
2481
+ e.stopPropagation();
2482
+ stepLightbox(1);
2483
+ },
2484
+ title: "下一张",
2485
+ children: (0, react_jsx_runtime.jsx)(ChevronRight, { size: 24 })
2486
+ })] }),
2275
2487
  (0, react_jsx_runtime.jsx)("div", {
2276
2488
  className: "dsh-ig-lightbox-img-wrap",
2277
2489
  onClick: (e) => e.stopPropagation(),
@@ -2322,6 +2534,14 @@ window.__ModuleLoader__.load({
2322
2534
  onClick: downloadSelected,
2323
2535
  children: [(0, react_jsx_runtime.jsx)(Download, { size: 14 }), (0, react_jsx_runtime.jsx)("span", { children: t("download") })]
2324
2536
  }),
2537
+ (0, react_jsx_runtime.jsxs)("button", {
2538
+ type: "button",
2539
+ className: `dsh-ig-lightbox-btn ${isSelectedInGallery ? "is-saved" : ""}`,
2540
+ title: isSelectedInGallery ? t("inGallery") : t("saveToGallery"),
2541
+ onClick: () => void handleSaveToGallery(),
2542
+ disabled: isSelectedInGallery,
2543
+ children: [isSelectedInGallery ? (0, react_jsx_runtime.jsx)(Check, { size: 14 }) : (0, react_jsx_runtime.jsx)(BookmarkPlus, { size: 14 }), (0, react_jsx_runtime.jsx)("span", { children: isSelectedInGallery ? t("inGallery") : t("saveToGallery") })]
2544
+ }),
2325
2545
  (0, react_jsx_runtime.jsxs)("button", {
2326
2546
  type: "button",
2327
2547
  className: "dsh-ig-lightbox-btn",
@@ -2388,6 +2608,30 @@ window.__ModuleLoader__.load({
2388
2608
  ]
2389
2609
  });
2390
2610
  };
2611
+ const BatchCanvasItem = ({ item, index, isSelected, onSelect }) => {
2612
+ const image = useAttachmentImage(item.attachment, true);
2613
+ return (0, react_jsx_runtime.jsxs)("div", {
2614
+ className: `dsh-ig-canvas-item ${isSelected ? "is-selected" : ""}`,
2615
+ onClick: onSelect,
2616
+ children: [(0, react_jsx_runtime.jsxs)("span", {
2617
+ className: "dsh-ig-canvas-badge",
2618
+ children: ["#", index + 1]
2619
+ }), image.url !== null ? (0, react_jsx_runtime.jsx)("img", {
2620
+ src: image.url,
2621
+ alt: item.prompt,
2622
+ draggable: false
2623
+ }) : image.loading ? (0, react_jsx_runtime.jsx)("div", {
2624
+ className: "dsh-ig-canvas-item-loading",
2625
+ children: (0, react_jsx_runtime.jsx)(LoaderCircle, {
2626
+ className: "dsh-ig-spin",
2627
+ size: 24
2628
+ })
2629
+ }) : (0, react_jsx_runtime.jsx)("div", {
2630
+ className: "dsh-ig-canvas-item-loading",
2631
+ children: (0, react_jsx_runtime.jsx)(ImagePlus, { size: 24 })
2632
+ })]
2633
+ });
2634
+ };
2391
2635
  const FieldSelect = ({ label, value, options, onChange }) => (0, react_jsx_runtime.jsxs)("label", {
2392
2636
  className: "dsh-ig-field-select",
2393
2637
  children: [(0, react_jsx_runtime.jsx)("span", { children: label }), (0, react_jsx_runtime.jsx)("select", {
@@ -4103,13 +4347,13 @@ window.__ModuleLoader__.load({
4103
4347
  .is-configured .dsh-ig-status-item-tag{color:#059669;background:rgba(16,185,129,.14)}
4104
4348
  .is-missing .dsh-ig-status-item-tag{color:#dc2626;background:rgba(239,68,68,.14)}
4105
4349
  .dsh-ig-canvas{position:relative;flex:1 1 0;height:0;min-height:0;display:flex;align-items:center;justify-content:center;overflow:hidden;padding:24px;touch-action:none;user-select:none;cursor:grab;background-image:linear-gradient(45deg,rgba(114,128,156,.035) 25%,transparent 25%),linear-gradient(-45deg,rgba(114,128,156,.035) 25%,transparent 25%),linear-gradient(45deg,transparent 75%,rgba(114,128,156,.035) 75%),linear-gradient(-45deg,transparent 75%,rgba(114,128,156,.035) 75%);background-size:24px 24px;background-position:0 0,0 12px,12px -12px,-12px 0}
4106
- .dsh-ig-canvas.is-dragging{cursor:grabbing!important}
4350
+ .dsh-ig-canvas.is-dragging,.dsh-ig-canvas.is-dragging *{cursor:grabbing!important}
4107
4351
  .dsh-ig-canvas img{display:block;max-width:100%;max-height:100%;width:auto;height:auto;border-radius:10px;box-shadow:0 14px 45px rgba(25,35,63,.18);user-select:none;-webkit-user-drag:none;pointer-events:auto}
4108
4352
  .dsh-ig-canvas-empty,.dsh-ig-generating-state{display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;gap:12px;color:#929bad;font-size:12px;padding:26px;cursor:default}
4109
4353
  .dsh-ig-generating-state strong{font-size:15px;color:var(--foreground,#182033)}.dsh-ig-generating-state span{font-size:11px}.dsh-ig-generation-orbit{width:66px;height:66px;border-radius:50%;display:grid;place-items:center;color:var(--ig-blue);background:rgba(47,100,245,.09);box-shadow:0 0 0 10px rgba(47,100,245,.035);animation:dshIgPulse 1.8s ease-in-out infinite}
4110
4354
  .dsh-ig-result-strip{flex:0 0 auto;min-height:52px;padding:8px 14px;display:flex;align-items:center;justify-content:space-between;gap:12px;border-top:1px solid var(--ig-line)}.dsh-ig-result-strip>span{font-size:12px;font-weight:650}.dsh-ig-result-strip>div,.dsh-ig-result-actions>div{display:flex;gap:7px}.dsh-ig-result-strip button,.dsh-ig-result-actions button{height:32px;display:inline-flex;align-items:center;gap:6px;padding:0 10px;border:1px solid var(--ig-line);border-radius:7px;background:var(--background,#fff);font-size:11px;cursor:pointer}.dsh-ig-result-strip button:hover,.dsh-ig-result-actions button:hover{border-color:#9fb2ed;color:var(--ig-blue)}.dsh-ig-result-actions{flex:0 0 auto;min-height:54px;padding:8px 14px;display:flex;align-items:center;justify-content:space-between;gap:12px;border-top:1px solid var(--ig-line)}.dsh-ig-result-actions p{margin:0;min-width:0;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;font-size:11.5px;line-height:18px;color:#596277}.dsh-ig-result-meta{flex:0 0 auto;min-height:32px;padding:6px 14px;display:flex;gap:13px;align-items:center;flex-wrap:wrap;border-top:1px solid var(--ig-line);font-size:10px;color:#8790a3}.dsh-ig-result-meta span+span:before{content:'·';margin-right:13px}
4111
4355
  .dsh-ig-generate-panel{height:100%;display:flex;flex-direction:column;border-left:1px solid var(--ig-line);overflow:hidden}.dsh-ig-panel-tabs{height:48px;flex:0 0 48px;display:grid;grid-template-columns:1fr 1fr;border-bottom:1px solid var(--ig-line);padding:0 14px}.dsh-ig-panel-tabs button{position:relative;border:0;background:transparent;font-size:12px;color:#7c8598;cursor:pointer}.dsh-ig-panel-tabs button.is-active{color:var(--ig-blue);font-weight:650}.dsh-ig-panel-tabs button.is-active:after{content:'';position:absolute;left:20%;right:20%;bottom:-1px;height:2px;background:var(--ig-blue);border-radius:2px 2px 0 0}.dsh-ig-generator-form{flex:1 1 0;height:0;padding:15px;display:flex;flex-direction:column;gap:15px;overflow-x:hidden;overflow-y:auto;min-height:0}.dsh-ig-mode-switch{display:grid;grid-template-columns:1fr 1fr;gap:8px}.dsh-ig-mode-switch button{height:39px;border:1px solid var(--ig-line);border-radius:8px;background:var(--background,#fff);display:flex;align-items:center;justify-content:center;gap:7px;font-size:12px;cursor:pointer}.dsh-ig-mode-switch button.is-active{border-color:#789bff;color:var(--ig-blue);background:rgba(47,100,245,.065);box-shadow:inset 0 0 0 1px rgba(47,100,245,.1)}
4112
- .dsh-ig-field{position:relative;display:flex;flex-direction:column;gap:7px}.dsh-ig-field>label,.dsh-ig-field-label label{font-size:11.5px;font-weight:630}.dsh-ig-field>label span{font-weight:400;color:#929bad}.dsh-ig-field-label{display:flex;align-items:center;justify-content:space-between}.dsh-ig-field-label b{color:#e14d4d}.dsh-ig-field-label button{border:0;background:transparent;color:#798397;font-size:10.5px;cursor:pointer}.dsh-ig-field textarea{width:100%;height:112px;padding:11px 12px 22px;resize:vertical;border:1px solid var(--ig-line);border-radius:9px;background:var(--background,#fff);font-size:12px;line-height:19px;outline:none}.dsh-ig-field textarea:focus{border-color:var(--ig-blue);box-shadow:0 0 0 3px rgba(47,100,245,.09)}.dsh-ig-field>small{position:absolute;right:9px;bottom:7px;color:#9aa2b2;font-size:9.5px}.dsh-ig-field-grid{display:grid;grid-template-columns:1fr 1fr;gap:9px}.dsh-ig-field-select{display:flex;flex-direction:column;gap:6px;min-width:0}.dsh-ig-field-select>span{font-size:11px;font-weight:620}.dsh-ig-field-select select{width:100%;height:36px;padding:0 26px 0 9px;border:1px solid var(--ig-line);border-radius:8px;background:var(--background,#fff);font-size:11px;outline:none;text-overflow:ellipsis}.dsh-ig-field-select select:focus{border-color:var(--ig-blue)}
4356
+ .dsh-ig-field{position:relative;display:flex;flex-direction:column;gap:7px}.dsh-ig-field>label,.dsh-ig-field-label label{font-size:11.5px;font-weight:630}.dsh-ig-field>label span{font-weight:400;color:#929bad}.dsh-ig-field-label{display:flex;align-items:center;justify-content:space-between}.dsh-ig-field-label b{color:#e14d4d}.dsh-ig-field-label button{border:0;background:transparent;color:#798397;font-size:10.5px;cursor:pointer}.dsh-ig-field textarea{width:100%;height:112px;padding:11px 12px 22px;resize:vertical;border:1px solid var(--ig-line);border-radius:9px;background:var(--background,#fff);font-size:12px;line-height:19px;outline:none}.dsh-ig-field textarea:focus{border-color:var(--ig-blue);box-shadow:0 0 0 3px rgba(47,100,245,.09)}.dsh-ig-field>small{position:absolute;right:9px;bottom:7px;color:#9aa2b2;font-size:9.5px}.dsh-ig-field-grid{display:grid;grid-template-columns:1fr 1fr;gap:9px}.dsh-ig-field-select{display:flex;flex-direction:column;gap:6px;min-width:0}.dsh-ig-field-select>span{font-size:11px;font-weight:620}.dsh-ig-field-select select{width:100%;height:36px;padding:0 26px 0 9px;border:1px solid var(--ig-line);border-radius:8px;background:var(--background,#fff);font-size:11px;outline:none;text-overflow:ellipsis}.dsh-ig-field-select select:focus{border-color:var(--ig-blue)}.dsh-ig-count-row{display:grid;grid-template-columns:repeat(4,1fr);gap:6px}.dsh-ig-count-pill{height:34px;border:1px solid var(--ig-line);border-radius:7px;background:var(--background,#fff);font-size:12px;font-weight:600;color:#596277;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .15s}.dsh-ig-count-pill:hover{border-color:#9fb2ed;color:var(--ig-blue)}.dsh-ig-count-pill.is-active{border-color:var(--ig-blue);background:rgba(47,100,245,.08);color:var(--ig-blue);box-shadow:inset 0 0 0 1px var(--ig-blue)}
4113
4357
  .dsh-ig-upload{height:92px;border:1px dashed #bcc6d9;border-radius:9px;background:rgba(255,255,255,.45);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:5px;color:#7d879b;cursor:pointer}.dsh-ig-upload strong{font-size:11px;font-weight:570}.dsh-ig-upload small{font-size:9.5px}.dsh-ig-upload:hover,.dsh-ig-upload.is-dragging{border-color:var(--ig-blue);background:rgba(47,100,245,.05);color:var(--ig-blue)}
4114
4358
  .dsh-ig-reference-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(64px,1fr));gap:8px}
4115
4359
  .dsh-ig-reference-card{position:relative;aspect-ratio:1;border-radius:8px;overflow:hidden;background:#eef1f7;border:1px solid var(--ig-line)}
@@ -4148,6 +4392,23 @@ window.__ModuleLoader__.load({
4148
4392
  .dsh-ig-workspace-checkbox{cursor:pointer;accent-color:#2f64f5;width:13.5px;height:13.5px;margin:0}
4149
4393
  .dsh-ig-workspace-filter-text{font-weight:500;white-space:nowrap}
4150
4394
  .dsh-ig-workspace-filter-badge{font-size:10px;font-weight:600;padding:1px 6px;border-radius:999px;background:color-mix(in srgb,currentColor 7%,transparent);color:#64748b;font-variant-numeric:tabular-nums}
4395
+ .dsh-ig-canvas-cluster{display:grid;gap:16px;align-items:center;justify-content:center;user-select:none;pointer-events:auto;flex-shrink:0}
4396
+ .dsh-ig-canvas-cluster[data-count="2"]{grid-template-columns:repeat(2,1fr);max-width:880px}
4397
+ .dsh-ig-canvas-cluster[data-count="3"],.dsh-ig-canvas-cluster[data-count="4"]{grid-template-columns:repeat(2,1fr);max-width:760px}
4398
+ .dsh-ig-canvas-item{position:relative;display:flex;align-items:center;justify-content:center;border-radius:10px;cursor:pointer;transition:transform .12s ease-out}
4399
+ .dsh-ig-canvas-item img{display:block;max-width:100%;max-height:360px;width:auto;height:auto;object-fit:contain;border-radius:10px;box-shadow:0 10px 30px rgba(25,35,63,.16);user-select:none;-webkit-user-drag:none;pointer-events:auto;transition:box-shadow .15s,outline .15s}
4400
+ .dsh-ig-canvas-item:hover img{box-shadow:0 14px 40px rgba(47,100,245,.2)}
4401
+ .dsh-ig-canvas-item.is-selected img{outline:3px solid var(--ig-blue);outline-offset:2px;box-shadow:0 14px 40px rgba(47,100,245,.35)}
4402
+ .dsh-ig-canvas-badge{position:absolute;top:10px;left:10px;background:rgba(15,23,42,.7);color:#fff;font-size:11px;font-weight:700;padding:2px 7px;border-radius:5px;backdrop-filter:blur(4px);pointer-events:none;transition:background .15s;z-index:2}
4403
+ .dsh-ig-canvas-item.is-selected .dsh-ig-canvas-badge{background:var(--ig-blue)}
4404
+ .dsh-ig-canvas-item-loading{width:260px;height:260px;display:flex;align-items:center;justify-content:center;background:rgba(255,255,255,.45);border:1px solid var(--ig-line);border-radius:10px;color:#929bad}
4405
+ .dsh-ig-lightbox-nav{position:absolute;top:50%;transform:translateY(-50%);width:42px;height:42px;border-radius:50%;border:0;background:rgba(15,23,42,.65);color:#fff;display:grid;place-items:center;cursor:pointer;transition:background .15s,transform .15s;z-index:10;backdrop-filter:blur(4px)}
4406
+ .dsh-ig-lightbox-nav:hover{background:rgba(47,100,245,.85);transform:translateY(-50%) scale(1.08)}
4407
+ .dsh-ig-lightbox-nav.is-prev{left:18px}
4408
+ .dsh-ig-lightbox-nav.is-next{right:18px}
4409
+ .dsh-ig-save-gallery-btn{color:var(--ig-blue)!important;border-color:rgba(47,100,245,.35)!important;background:rgba(47,100,245,.06)!important}
4410
+ .dsh-ig-save-gallery-btn:hover{background:rgba(47,100,245,.12)!important}
4411
+ .dsh-ig-save-gallery-btn.is-saved{color:#64748b!important;border-color:var(--ig-line)!important;background:transparent!important}
4151
4412
  `;
4152
4413
  //#endregion
4153
4414
  //#region lib/types/client/image-result-node.js