dsh-image-gen 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -13,6 +13,10 @@ 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
+ /** Same-origin route for built-in prompt inspiration metadata and images. */
17
+ const INSPIRATION_ROUTE = "/plugins/dsh-image-gen/inspiration";
18
+ /** Browser route used for saving generated images to workspace on demand. */
19
+ const SAVE_WORKSPACE_ROUTE = "/plugins/dsh-image-gen/save-workspace";
16
20
  /** Namespace persisted through DSH Settings. */
17
21
  const IMAGE_GENERATION_NAMESPACE = "image-generation";
18
22
  /** Providers supported by the first browser workbench release. */
@@ -148,7 +152,7 @@ window.__ModuleLoader__.load({
148
152
  }
149
153
  //#endregion
150
154
  //#region lib/types/client/gallery-store.js
151
- const DB_NAME = "dsh_image_gen_db";
155
+ const DB_NAME$2 = "dsh_image_gen_db";
152
156
  const DB_VERSION = 2;
153
157
  const STORE_NAME = "gallery_history";
154
158
  const TOMBSTONE_STORE = "gallery_tombstones";
@@ -161,7 +165,7 @@ window.__ModuleLoader__.load({
161
165
  reject(/* @__PURE__ */ new Error("IndexedDB is not supported in this environment."));
162
166
  return;
163
167
  }
164
- const request = indexedDB.open(DB_NAME, DB_VERSION);
168
+ const request = indexedDB.open(DB_NAME$2, DB_VERSION);
165
169
  request.onupgradeneeded = (event) => {
166
170
  const db = event.target.result;
167
171
  if (!db.objectStoreNames.contains(STORE_NAME)) db.createObjectStore(STORE_NAME, { keyPath: "id" }).createIndex("createdAt", "createdAt", { unique: false });
@@ -511,6 +515,26 @@ window.__ModuleLoader__.load({
511
515
  * This source code is licensed under the ISC license.
512
516
  * See the LICENSE file in the root directory of this source tree.
513
517
  */
518
+ const BookmarkPlus = createLucideIcon("bookmark-plus", [
519
+ ["path", {
520
+ d: "M12 7v6",
521
+ key: "lw1j43"
522
+ }],
523
+ ["path", {
524
+ d: "M15 10H9",
525
+ key: "o6yqo3"
526
+ }],
527
+ ["path", {
528
+ 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",
529
+ key: "oz39mx"
530
+ }]
531
+ ]);
532
+ /**
533
+ * @license lucide-react v1.38.0 - ISC
534
+ *
535
+ * This source code is licensed under the ISC license.
536
+ * See the LICENSE file in the root directory of this source tree.
537
+ */
514
538
  const Check = createLucideIcon("check", [["path", {
515
539
  d: "M20 6 9 17l-5-5",
516
540
  key: "1gmf2c"
@@ -647,6 +671,26 @@ window.__ModuleLoader__.load({
647
671
  * This source code is licensed under the ISC license.
648
672
  * See the LICENSE file in the root directory of this source tree.
649
673
  */
674
+ const ExternalLink = createLucideIcon("external-link", [
675
+ ["path", {
676
+ d: "M15 3h6v6",
677
+ key: "1q9fwt"
678
+ }],
679
+ ["path", {
680
+ d: "M10 14 21 3",
681
+ key: "gplh6r"
682
+ }],
683
+ ["path", {
684
+ d: "M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6",
685
+ key: "a6xqqp"
686
+ }]
687
+ ]);
688
+ /**
689
+ * @license lucide-react v1.38.0 - ISC
690
+ *
691
+ * This source code is licensed under the ISC license.
692
+ * See the LICENSE file in the root directory of this source tree.
693
+ */
650
694
  const FileText = createLucideIcon("file-text", [
651
695
  ["path", {
652
696
  d: "M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z",
@@ -752,6 +796,30 @@ window.__ModuleLoader__.load({
752
796
  * This source code is licensed under the ISC license.
753
797
  * See the LICENSE file in the root directory of this source tree.
754
798
  */
799
+ const Maximize2 = createLucideIcon("maximize-2", [
800
+ ["path", {
801
+ d: "M15 3h6v6",
802
+ key: "1q9fwt"
803
+ }],
804
+ ["path", {
805
+ d: "m21 3-7 7",
806
+ key: "1l2asr"
807
+ }],
808
+ ["path", {
809
+ d: "m3 21 7-7",
810
+ key: "tjx5ai"
811
+ }],
812
+ ["path", {
813
+ d: "M9 21H3v-6",
814
+ key: "wtvkvv"
815
+ }]
816
+ ]);
817
+ /**
818
+ * @license lucide-react v1.38.0 - ISC
819
+ *
820
+ * This source code is licensed under the ISC license.
821
+ * See the LICENSE file in the root directory of this source tree.
822
+ */
755
823
  const PanelLeftClose = createLucideIcon("panel-left-close", [
756
824
  ["rect", {
757
825
  width: "18",
@@ -935,6 +1003,16 @@ window.__ModuleLoader__.load({
935
1003
  * This source code is licensed under the ISC license.
936
1004
  * See the LICENSE file in the root directory of this source tree.
937
1005
  */
1006
+ const Star = createLucideIcon("star", [["path", {
1007
+ d: "M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z",
1008
+ key: "r04s7s"
1009
+ }]]);
1010
+ /**
1011
+ * @license lucide-react v1.38.0 - ISC
1012
+ *
1013
+ * This source code is licensed under the ISC license.
1014
+ * See the LICENSE file in the root directory of this source tree.
1015
+ */
938
1016
  const Trash2 = createLucideIcon("trash-2", [
939
1017
  ["path", {
940
1018
  d: "M10 11v6",
@@ -1219,20 +1297,42 @@ window.__ModuleLoader__.load({
1219
1297
  return `${d.getFullYear()}/${d.getMonth() + 1}/${d.getDate()}`;
1220
1298
  }
1221
1299
  //#endregion
1300
+ //#region lib/types/client/multi-model-compare.js
1301
+ /** Map one shared output intent to settings accepted by every target model. */
1302
+ function buildComparisonTargets(profiles, selectedProviders, ratio, quality) {
1303
+ const selected = new Set(selectedProviders);
1304
+ return profiles.filter((profile) => profile.configured && selected.has(profile.provider)).map((profile) => {
1305
+ const targetRatio = profile.ratioOptions.some((option) => option.value === ratio) ? ratio : profile.defaultRatio;
1306
+ const targetQuality = profile.qualityOptions.some((option) => option.value === quality) ? quality : profile.defaultQuality;
1307
+ return {
1308
+ profile,
1309
+ ratio: targetRatio,
1310
+ quality: targetQuality,
1311
+ adjusted: targetRatio !== ratio || targetQuality !== quality
1312
+ };
1313
+ });
1314
+ }
1315
+ /** Start with two models, not every configured API, to avoid surprise spend. */
1316
+ function initialComparisonProviders(profiles, activeProvider) {
1317
+ const configured = profiles.filter((profile) => profile.configured);
1318
+ const active = configured.find((profile) => profile.provider === activeProvider);
1319
+ return (active === void 0 ? configured : [active, ...configured.filter((profile) => profile.provider !== activeProvider)]).slice(0, 2).map((profile) => profile.provider);
1320
+ }
1321
+ //#endregion
1222
1322
  //#region lib/types/client/studio-view.js
1223
- const COPY = {
1323
+ const COPY$1 = {
1224
1324
  zh: {
1225
- title: "云端图像工作台",
1325
+ title: "云端生图工作台",
1226
1326
  configured: "API 已配置",
1227
1327
  unconfigured: "未配置",
1228
1328
  recent: "最近生成",
1229
- empty: "还没有生成记录",
1329
+ empty: "暂无生成历史",
1230
1330
  generate: "文生图",
1231
1331
  edit: "图生图",
1232
1332
  details: "图片详情",
1233
1333
  reference: "参考图",
1234
- optional: "可选",
1235
- upload: "点击上传或拖拽图片到此处",
1334
+ optional: "选填",
1335
+ upload: "点击或拖拽图片到此处",
1236
1336
  uploadHint: "支持 JPG / PNG / WebP / GIF,最大 10MB(最多 5 张)",
1237
1337
  prompt: "提示词 Prompt",
1238
1338
  clear: "清空",
@@ -1244,6 +1344,28 @@ window.__ModuleLoader__.load({
1244
1344
  start: "开始生成",
1245
1345
  generating: "正在生成…",
1246
1346
  cancelGenerate: "取消生成",
1347
+ count: "生成数量",
1348
+ countUnit: "{n} 张",
1349
+ partialSuccess: "已生成 {success} 张图片,{failed} 张失败",
1350
+ generatingCount: "正在生成(共 {count} 张)…",
1351
+ batchResult: "本次生成(共 {count} 张)",
1352
+ singleModel: "单模型",
1353
+ compareModels: "多模型对比",
1354
+ compareHint: "相同提示词,同时交给多个模型",
1355
+ compareSelect: "选择模型",
1356
+ compareSelected: "已选 {count} 个模型",
1357
+ compareNeedTwo: "请至少选择两个已配置模型",
1358
+ compareParameterHint: "不支持所选参数的模型会自动使用默认值",
1359
+ compareAdjusted: "已适配",
1360
+ comparePartial: "{success} 个模型生成成功,{failed} 个失败",
1361
+ comparing: "正在对比(共 {count} 个模型)…",
1362
+ compareStart: "开始对比",
1363
+ saveToGallery: "收藏进画廊",
1364
+ saveSelected: "收藏选中({count})",
1365
+ savedToGallery: "已收藏进画廊",
1366
+ savedSelected: "已收藏 {count} 张图片",
1367
+ inGallery: "已在画廊",
1368
+ needSelectResult: "请先勾选要收藏的图片",
1247
1369
  retry: "重新加载",
1248
1370
  configLoadFailed: "工作台配置加载失败,请检查服务后重试。",
1249
1371
  noProvider: "请先在设置中配置至少一个云端图像 Provider 的 API Key。",
@@ -1294,7 +1416,8 @@ window.__ModuleLoader__.load({
1294
1416
  unconfiguredStatus: "API 未配置",
1295
1417
  providerStatus: "云端提供商与模型状态",
1296
1418
  collapseSidebar: "折叠最近生成",
1297
- expandSidebar: "展开最近生成"
1419
+ expandSidebar: "展开最近生成",
1420
+ findInspiration: "找灵感"
1298
1421
  },
1299
1422
  en: {
1300
1423
  title: "Cloud Image Studio",
@@ -1319,6 +1442,28 @@ window.__ModuleLoader__.load({
1319
1442
  start: "Generate",
1320
1443
  generating: "Generating…",
1321
1444
  cancelGenerate: "Cancel",
1445
+ count: "Number of images",
1446
+ countUnit: "{n}",
1447
+ partialSuccess: "Generated {success} images, {failed} failed",
1448
+ generatingCount: "Generating ({count} images)…",
1449
+ batchResult: "Generated {count} images",
1450
+ singleModel: "Single model",
1451
+ compareModels: "Compare models",
1452
+ compareHint: "Send the same prompt to multiple models",
1453
+ compareSelect: "Choose models",
1454
+ compareSelected: "{count} models selected",
1455
+ compareNeedTwo: "Select at least two configured models",
1456
+ compareParameterHint: "Unsupported settings use each model’s default",
1457
+ compareAdjusted: "Adjusted",
1458
+ comparePartial: "{success} models succeeded, {failed} failed",
1459
+ comparing: "Comparing {count} models…",
1460
+ compareStart: "Compare models",
1461
+ saveToGallery: "Save to Gallery",
1462
+ saveSelected: "Save selected ({count})",
1463
+ savedToGallery: "Saved to Gallery",
1464
+ savedSelected: "Saved {count} images",
1465
+ inGallery: "In Gallery",
1466
+ needSelectResult: "Select images to save first",
1322
1467
  retry: "Retry",
1323
1468
  configLoadFailed: "Failed to load studio configuration.",
1324
1469
  noProvider: "Configure an API key for at least one cloud image provider in Settings.",
@@ -1369,10 +1514,11 @@ window.__ModuleLoader__.load({
1369
1514
  unconfiguredStatus: "Not configured",
1370
1515
  providerStatus: "Provider & Model Status",
1371
1516
  collapseSidebar: "Collapse sidebar",
1372
- expandSidebar: "Expand recent list"
1517
+ expandSidebar: "Expand recent list",
1518
+ findInspiration: "Find inspiration"
1373
1519
  }
1374
1520
  };
1375
- const StudioView = ({ locale, workspace }) => {
1521
+ const StudioView = ({ locale, workspace, initialPrompt, onInitialPromptApplied, onOpenInspiration }) => {
1376
1522
  const [lang, setLang] = (0, react.useState)(() => locale?.getSnapshot?.().active?.startsWith("en") ? "en" : "zh");
1377
1523
  const [config, setConfig] = (0, react.useState)(null);
1378
1524
  const [configLoading, setConfigLoading] = (0, react.useState)(true);
@@ -1389,6 +1535,12 @@ window.__ModuleLoader__.load({
1389
1535
  const [quality, setQuality] = (0, react.useState)("1K");
1390
1536
  const [prompt, setPrompt] = (0, react.useState)("");
1391
1537
  const [references, setReferences] = (0, react.useState)([]);
1538
+ const [count, setCount] = (0, react.useState)(1);
1539
+ const [currentBatch, setCurrentBatch] = (0, react.useState)(null);
1540
+ const [selectedBatchIds, setSelectedBatchIds] = (0, react.useState)([]);
1541
+ const [batchKind, setBatchKind] = (0, react.useState)(null);
1542
+ const [comparisonEnabled, setComparisonEnabled] = (0, react.useState)(false);
1543
+ const [comparisonProviders, setComparisonProviders] = (0, react.useState)([]);
1392
1544
  const [isGenerating, setIsGenerating] = (0, react.useState)(false);
1393
1545
  const [error, setError] = (0, react.useState)(null);
1394
1546
  const [notice, setNotice] = (0, react.useState)(null);
@@ -1409,11 +1561,18 @@ window.__ModuleLoader__.load({
1409
1561
  const canvasRef = (0, react.useRef)(null);
1410
1562
  const fileInputRef = (0, react.useRef)(null);
1411
1563
  const image = useAttachmentImage(selected?.attachment);
1412
- const maxReferences = provider === "dashscope" ? 3 : 5;
1564
+ (0, react.useEffect)(() => {
1565
+ if (initialPrompt === void 0) return;
1566
+ setMode("generate");
1567
+ setPanelTab("generate");
1568
+ setPrompt(initialPrompt);
1569
+ onInitialPromptApplied?.();
1570
+ }, [initialPrompt, onInitialPromptApplied]);
1571
+ const maxReferences = comparisonEnabled ? comparisonProviders.includes("dashscope") ? 3 : 5 : provider === "dashscope" ? 3 : 5;
1413
1572
  const referencesRef = (0, react.useRef)(references);
1414
1573
  referencesRef.current = references;
1415
1574
  const t = (key, values) => {
1416
- let text = COPY[lang][key];
1575
+ let text = COPY$1[lang][key];
1417
1576
  for (const [name, value] of Object.entries(values ?? {})) text = text.replace(`{${name}}`, value);
1418
1577
  return text;
1419
1578
  };
@@ -1431,7 +1590,10 @@ window.__ModuleLoader__.load({
1431
1590
  if (!response.ok || !("providers" in payload)) throw new Error("error" in payload && payload.error ? payload.error : "Studio unavailable");
1432
1591
  setConfig(payload);
1433
1592
  const initial = payload.providers.find((item) => item.provider === payload.activeProvider) ?? payload.providers[0];
1434
- if (initial !== void 0) applyProvider(initial);
1593
+ if (initial !== void 0) {
1594
+ applyProvider(initial);
1595
+ setComparisonProviders(initialComparisonProviders(payload.providers, initial.provider));
1596
+ }
1435
1597
  } catch (fetchError) {
1436
1598
  if (!controller.signal.aborted) setConfigError(messageOf(fetchError));
1437
1599
  } finally {
@@ -1476,7 +1638,7 @@ window.__ModuleLoader__.load({
1476
1638
  const canvasEl = canvasRef.current;
1477
1639
  if (!canvasEl) return;
1478
1640
  const onWheel = (e) => {
1479
- if (selected === null || !image.url) return;
1641
+ if (selected === null && (!currentBatch || currentBatch.length === 0)) return;
1480
1642
  e.preventDefault();
1481
1643
  const delta = e.deltaY < 0 ? 15 : -15;
1482
1644
  setFit(false);
@@ -1484,7 +1646,7 @@ window.__ModuleLoader__.load({
1484
1646
  };
1485
1647
  canvasEl.addEventListener("wheel", onWheel, { passive: false });
1486
1648
  return () => canvasEl.removeEventListener("wheel", onWheel);
1487
- }, [selected, image.url]);
1649
+ }, [selected, currentBatch]);
1488
1650
  (0, react.useEffect)(() => {
1489
1651
  const handleMouseMove = (e) => {
1490
1652
  if (!dragStartRef.current) return;
@@ -1513,6 +1675,13 @@ window.__ModuleLoader__.load({
1513
1675
  const activeProfile = (0, react.useMemo)(() => config?.providers.find((item) => item.provider === provider), [config, provider]);
1514
1676
  const configuredCount = config?.providers.filter((item) => item.configured).length ?? 0;
1515
1677
  const displayItems = (0, react.useMemo)(() => items.slice(0, visibleLimit), [items, visibleLimit]);
1678
+ const comparisonProfiles = (0, react.useMemo)(() => (config?.providers ?? []).filter((item) => item.configured && (mode === "generate" || item.supportsEditing)), [config, mode]);
1679
+ const comparisonTargets = (0, react.useMemo)(() => buildComparisonTargets(comparisonProfiles, comparisonProviders, ratio, quality), [
1680
+ comparisonProfiles,
1681
+ comparisonProviders,
1682
+ ratio,
1683
+ quality
1684
+ ]);
1516
1685
  const applyProvider = (profile) => {
1517
1686
  setProvider(profile.provider);
1518
1687
  setModel(profile.model);
@@ -1544,18 +1713,24 @@ window.__ModuleLoader__.load({
1544
1713
  });
1545
1714
  };
1546
1715
  const selectItem = (item) => {
1716
+ setCurrentBatch(null);
1717
+ setSelectedBatchIds([]);
1718
+ setBatchKind(null);
1547
1719
  setSelected(item);
1548
1720
  setPanelTab("details");
1549
1721
  resetFit();
1550
1722
  };
1551
1723
  const startNew = () => {
1724
+ setCurrentBatch(null);
1725
+ setSelectedBatchIds([]);
1726
+ setBatchKind(null);
1552
1727
  setSelected(null);
1553
1728
  setPanelTab("generate");
1554
1729
  resetFit();
1555
1730
  };
1556
1731
  const handleCanvasMouseDown = (e) => {
1557
1732
  if (e.button !== 0 && e.button !== 1) return;
1558
- if (selected === null || !image.url) return;
1733
+ if (selected === null && (!currentBatch || currentBatch.length === 0)) return;
1559
1734
  setIsDragging(true);
1560
1735
  hasDraggedRef.current = false;
1561
1736
  dragStartRef.current = {
@@ -1565,9 +1740,7 @@ window.__ModuleLoader__.load({
1565
1740
  startY: offset.y
1566
1741
  };
1567
1742
  };
1568
- const handleImageClick = () => {
1569
- if (!hasDraggedRef.current) setLightboxOpen(true);
1570
- };
1743
+ const handleImageClick = () => {};
1571
1744
  const addReferenceFiles = (fileList) => {
1572
1745
  if (!fileList || fileList.length === 0) return;
1573
1746
  const files = Array.from(fileList);
@@ -1661,10 +1834,46 @@ window.__ModuleLoader__.load({
1661
1834
  setIsGenerating(false);
1662
1835
  }
1663
1836
  };
1837
+ const toggleComparison = (enabled) => {
1838
+ setComparisonEnabled(enabled);
1839
+ setError(null);
1840
+ if (!enabled) {
1841
+ if (activeProfile !== void 0) {
1842
+ if (!activeProfile.ratioOptions.some((option) => option.value === ratio)) setRatio(activeProfile.defaultRatio);
1843
+ if (!activeProfile.qualityOptions.some((option) => option.value === quality)) setQuality(activeProfile.defaultQuality);
1844
+ }
1845
+ return;
1846
+ }
1847
+ if (config === null) return;
1848
+ const initial = initialComparisonProviders(comparisonProfiles, provider).filter((item) => mode !== "edit" || referencesRef.current.length <= 3 || item !== "dashscope");
1849
+ setComparisonProviders(initial);
1850
+ if (![
1851
+ "1:1",
1852
+ "3:2",
1853
+ "2:3",
1854
+ "16:9",
1855
+ "9:16"
1856
+ ].includes(ratio)) setRatio("1:1");
1857
+ if (![
1858
+ "standard",
1859
+ "1K",
1860
+ "2K",
1861
+ "4K"
1862
+ ].includes(quality)) setQuality("1K");
1863
+ };
1864
+ const toggleComparisonProvider = (target) => {
1865
+ if (mode === "edit" && target === "dashscope" && referencesRef.current.length > 3 && !comparisonProviders.includes(target)) {
1866
+ setError(t("maxReferencesExceeded", { max: "3" }));
1867
+ return;
1868
+ }
1869
+ setComparisonProviders((current) => current.includes(target) ? current.filter((item) => item !== target) : [...current, target]);
1870
+ setError(null);
1871
+ };
1664
1872
  const submit = async () => {
1665
1873
  if (isGenerating || activeProfile === void 0) return;
1666
1874
  if (prompt.trim().length === 0) return setError(t("needPrompt"));
1667
- if (!activeProfile.configured) return setError(t("selectConfigured"));
1875
+ if (comparisonEnabled && comparisonTargets.length < 2) return setError(t("compareNeedTwo"));
1876
+ if (!comparisonEnabled && !activeProfile.configured) return setError(t("selectConfigured"));
1668
1877
  if (mode === "edit" && references.length === 0) return setError(t("needReference"));
1669
1878
  setIsGenerating(true);
1670
1879
  setError(null);
@@ -1680,6 +1889,64 @@ window.__ModuleLoader__.load({
1680
1889
  };
1681
1890
  throw new Error("无效参考图");
1682
1891
  })) : void 0;
1892
+ if (comparisonEnabled) {
1893
+ const settled = await Promise.allSettled(comparisonTargets.map(async (target) => {
1894
+ const response = await fetch(STUDIO_ROUTE, {
1895
+ method: "POST",
1896
+ credentials: "same-origin",
1897
+ signal: controller.signal,
1898
+ headers: { "content-type": "application/json" },
1899
+ body: JSON.stringify({
1900
+ mode,
1901
+ provider: target.profile.provider,
1902
+ model: target.profile.model,
1903
+ prompt: prompt.trim(),
1904
+ ratio: target.ratio,
1905
+ quality: target.quality,
1906
+ ...workspace?.path ? { workspaceRoot: workspace.path } : {},
1907
+ ...referencesPayload === void 0 ? {} : { references: referencesPayload }
1908
+ })
1909
+ });
1910
+ const payload = await response.json();
1911
+ if (!response.ok || !("attachment" in payload)) throw new Error("error" in payload && payload.error ? payload.error : `${target.profile.label}: ${t("generationFailed")}`);
1912
+ return {
1913
+ payload,
1914
+ target
1915
+ };
1916
+ }));
1917
+ if (controller.signal.aborted) return;
1918
+ const successes = settled.filter((result) => result.status === "fulfilled");
1919
+ const failed = settled.length - successes.length;
1920
+ if (successes.length === 0) {
1921
+ const firstFailure = settled.find((result) => result.status === "rejected");
1922
+ throw firstFailure?.status === "rejected" ? firstFailure.reason : new Error(t("generationFailed"));
1923
+ }
1924
+ const galleryEntries = successes.map(({ value }, idx) => ({
1925
+ id: String(value.payload.attachment.attachmentId || `${value.payload.createdAt}-${idx}`),
1926
+ attachment: value.payload.attachment,
1927
+ prompt: value.payload.prompt,
1928
+ provider: value.payload.provider,
1929
+ model: value.payload.model,
1930
+ createdAt: value.payload.createdAt + idx,
1931
+ aspectRatio: value.target.ratio,
1932
+ imageSize: value.target.quality,
1933
+ output: value.payload.output,
1934
+ ...value.payload.savedTo ? { savedTo: value.payload.savedTo } : {},
1935
+ ...workspace?.path ? { workspacePath: workspace.path } : {},
1936
+ ...workspace?.workspaceId ? { workspaceId: workspace.workspaceId } : {}
1937
+ }));
1938
+ setCurrentBatch(galleryEntries.length > 1 ? galleryEntries : null);
1939
+ setSelectedBatchIds(galleryEntries.length > 1 ? [galleryEntries[0].id] : []);
1940
+ setBatchKind(galleryEntries.length > 1 ? "multi-model" : null);
1941
+ setSelected(galleryEntries[0]);
1942
+ setPanelTab("details");
1943
+ resetFit();
1944
+ if (failed > 0) flash(t("comparePartial", {
1945
+ success: String(successes.length),
1946
+ failed: String(failed)
1947
+ }));
1948
+ return;
1949
+ }
1683
1950
  const response = await fetch(STUDIO_ROUTE, {
1684
1951
  method: "POST",
1685
1952
  credentials: "same-origin",
@@ -1692,30 +1959,49 @@ window.__ModuleLoader__.load({
1692
1959
  prompt: prompt.trim(),
1693
1960
  ratio,
1694
1961
  quality,
1962
+ ...count > 1 ? { count } : {},
1695
1963
  ...workspace?.path ? { workspaceRoot: workspace.path } : {},
1696
1964
  ...referencesPayload === void 0 ? {} : { references: referencesPayload }
1697
1965
  })
1698
1966
  });
1699
1967
  const payload = await response.json();
1700
1968
  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),
1969
+ const generatedList = Array.isArray(payload.items) && payload.items.length > 0 ? payload.items : [{
1703
1970
  attachment: payload.attachment,
1971
+ output: payload.output,
1972
+ ...payload.savedTo ? { savedTo: payload.savedTo } : {}
1973
+ }];
1974
+ const galleryEntries = generatedList.map((gen, idx) => ({
1975
+ id: String(gen.attachment.attachmentId || `${payload.createdAt}-${idx}`),
1976
+ attachment: gen.attachment,
1704
1977
  prompt: payload.prompt,
1705
1978
  provider: payload.provider,
1706
1979
  model: payload.model,
1707
- createdAt: payload.createdAt,
1980
+ createdAt: payload.createdAt + idx,
1708
1981
  aspectRatio: ratio,
1709
1982
  imageSize: quality,
1710
- output: payload.output,
1711
- ...payload.savedTo ? { savedTo: payload.savedTo } : {},
1983
+ output: gen.output,
1984
+ ...gen.savedTo ? { savedTo: gen.savedTo } : {},
1712
1985
  ...workspace?.path ? { workspacePath: workspace.path } : {},
1713
1986
  ...workspace?.workspaceId ? { workspaceId: workspace.workspaceId } : {}
1714
- };
1715
- await saveGalleryItem(item);
1716
- setSelected(item);
1987
+ }));
1988
+ if (galleryEntries.length > 1) {
1989
+ setCurrentBatch(galleryEntries);
1990
+ setSelectedBatchIds([galleryEntries[0].id]);
1991
+ setBatchKind("multi-image");
1992
+ setSelected(galleryEntries[0]);
1993
+ } else if (galleryEntries.length === 1) {
1994
+ setCurrentBatch(null);
1995
+ setSelectedBatchIds([]);
1996
+ setBatchKind(null);
1997
+ setSelected(galleryEntries[0]);
1998
+ }
1717
1999
  setPanelTab("details");
1718
2000
  resetFit();
2001
+ if (payload.failedCount && payload.failedCount > 0) flash(t("partialSuccess", {
2002
+ success: String(generatedList.length),
2003
+ failed: String(payload.failedCount)
2004
+ }));
1719
2005
  } catch (submitError) {
1720
2006
  if (controller.signal.aborted) return;
1721
2007
  setError(messageOf(submitError));
@@ -1726,6 +2012,73 @@ window.__ModuleLoader__.load({
1726
2012
  }
1727
2013
  }
1728
2014
  };
2015
+ const isSelectedInGallery = (0, react.useMemo)(() => {
2016
+ if (selected === null) return false;
2017
+ return items.some((item) => item.id === selected.id);
2018
+ }, [items, selected]);
2019
+ const selectedBatchItems = (0, react.useMemo)(() => {
2020
+ if (currentBatch === null) return [];
2021
+ const ids = new Set(selectedBatchIds);
2022
+ return currentBatch.filter((item) => ids.has(item.id));
2023
+ }, [currentBatch, selectedBatchIds]);
2024
+ const pendingGalleryItems = (0, react.useMemo)(() => {
2025
+ const galleryIds = new Set(items.map((item) => item.id));
2026
+ return (currentBatch === null ? selected === null ? [] : [selected] : selectedBatchItems).filter((item) => !galleryIds.has(item.id));
2027
+ }, [
2028
+ currentBatch,
2029
+ items,
2030
+ selected,
2031
+ selectedBatchItems
2032
+ ]);
2033
+ const saveButtonLabel = currentBatch !== null ? pendingGalleryItems.length > 0 ? t("saveSelected", { count: String(pendingGalleryItems.length) }) : selectedBatchItems.length === 0 ? t("saveSelected", { count: "0" }) : t("inGallery") : isSelectedInGallery ? t("inGallery") : t("saveToGallery");
2034
+ const saveSelectionComplete = currentBatch === null ? isSelectedInGallery : selectedBatchItems.length > 0 && pendingGalleryItems.length === 0;
2035
+ const saveGalleryEntry = async (item) => {
2036
+ let savedTo = item.savedTo;
2037
+ const targetRoot = workspace?.path || config?.workspaceRoot;
2038
+ if (!savedTo) try {
2039
+ const res = await fetch(SAVE_WORKSPACE_ROUTE, {
2040
+ method: "POST",
2041
+ credentials: "same-origin",
2042
+ headers: { "content-type": "application/json" },
2043
+ body: JSON.stringify({
2044
+ attachment: item.attachment,
2045
+ ...targetRoot ? { workspaceRoot: targetRoot } : {}
2046
+ })
2047
+ });
2048
+ if (res.ok) {
2049
+ const data = await res.json();
2050
+ if (data.savedTo) savedTo = data.savedTo;
2051
+ }
2052
+ } catch (saveError) {
2053
+ console.warn("Failed to save to workspace:", saveError);
2054
+ }
2055
+ const updatedItem = {
2056
+ ...item,
2057
+ ...savedTo ? { savedTo } : {},
2058
+ ...targetRoot ? { workspacePath: targetRoot } : {},
2059
+ ...workspace?.workspaceId ? { workspaceId: workspace.workspaceId } : {}
2060
+ };
2061
+ await saveGalleryItem(updatedItem);
2062
+ return updatedItem;
2063
+ };
2064
+ const handleSaveToGallery = async () => {
2065
+ if (selected === null) return;
2066
+ if (currentBatch !== null && selectedBatchItems.length === 0) return setError(t("needSelectResult"));
2067
+ if (pendingGalleryItems.length === 0) return;
2068
+ const activeId = selected.id;
2069
+ const settled = await Promise.allSettled(pendingGalleryItems.map(saveGalleryEntry));
2070
+ const saved = settled.flatMap((result) => result.status === "fulfilled" ? [result.value] : []);
2071
+ if (saved.length === 0) {
2072
+ const failed = settled.find((result) => result.status === "rejected");
2073
+ setError(failed?.status === "rejected" ? messageOf(failed.reason) : t("generationFailed"));
2074
+ return;
2075
+ }
2076
+ const savedById = new Map(saved.map((item) => [item.id, item]));
2077
+ setCurrentBatch((current) => current?.map((item) => savedById.get(item.id) ?? item) ?? null);
2078
+ setSelected(savedById.get(activeId) ?? saved[0]);
2079
+ setError(null);
2080
+ flash(saved.length > 1 ? t("savedSelected", { count: String(saved.length) }) : t("savedToGallery"));
2081
+ };
1729
2082
  const handleConfirmDelete = async () => {
1730
2083
  if (selected === null) return;
1731
2084
  const idToDelete = selected.id;
@@ -1747,8 +2100,22 @@ window.__ModuleLoader__.load({
1747
2100
  throw new Error(reason);
1748
2101
  }
1749
2102
  }
1750
- await deleteGalleryItem(idToDelete);
2103
+ if (isSelectedInGallery) await deleteGalleryItem(idToDelete);
1751
2104
  evictAttachmentCache(attachmentId);
2105
+ setSelectedBatchIds((current) => current.filter((id) => id !== idToDelete));
2106
+ if (currentBatch) {
2107
+ const remaining = currentBatch.filter((i) => i.id !== idToDelete);
2108
+ if (remaining.length > 1) {
2109
+ setCurrentBatch(remaining);
2110
+ setSelected(remaining[0]);
2111
+ } else if (remaining.length === 1) {
2112
+ setCurrentBatch(null);
2113
+ setSelected(remaining[0]);
2114
+ } else {
2115
+ setCurrentBatch(null);
2116
+ setSelected(null);
2117
+ }
2118
+ } else setSelected(null);
1752
2119
  flash(t("deletedToast"));
1753
2120
  } catch (delError) {
1754
2121
  setError(messageOf(delError));
@@ -1766,13 +2133,50 @@ window.__ModuleLoader__.load({
1766
2133
  const toggleFavorite = async () => {
1767
2134
  if (selected === null) return;
1768
2135
  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"));
2136
+ if (isSelectedInGallery) {
2137
+ const nextFav = await toggleFavoriteGalleryItem(targetId);
2138
+ setSelected((curr) => curr !== null && curr.id === targetId ? {
2139
+ ...curr,
2140
+ isFavorite: nextFav
2141
+ } : curr);
2142
+ if (currentBatch) setCurrentBatch((prev) => prev ? prev.map((i) => i.id === targetId ? {
2143
+ ...i,
2144
+ isFavorite: nextFav
2145
+ } : i) : null);
2146
+ flash(nextFav ? t("favoriteAdded") : t("favoriteRemoved"));
2147
+ } else {
2148
+ const nextFav = !selected.isFavorite;
2149
+ setSelected((curr) => curr !== null && curr.id === targetId ? {
2150
+ ...curr,
2151
+ isFavorite: nextFav
2152
+ } : curr);
2153
+ if (currentBatch) setCurrentBatch((prev) => prev ? prev.map((i) => i.id === targetId ? {
2154
+ ...i,
2155
+ isFavorite: nextFav
2156
+ } : i) : null);
2157
+ flash(nextFav ? t("favoriteAdded") : t("favoriteRemoved"));
2158
+ }
2159
+ };
2160
+ const stepLightbox = (delta) => {
2161
+ if (!currentBatch || currentBatch.length <= 1 || selected === null) return;
2162
+ const currIdx = currentBatch.findIndex((i) => i.id === selected.id);
2163
+ if (currIdx === -1) return;
2164
+ const nextIdx = (currIdx + delta + currentBatch.length) % currentBatch.length;
2165
+ setSelected(currentBatch[nextIdx]);
1775
2166
  };
2167
+ (0, react.useEffect)(() => {
2168
+ if (!lightboxOpen || !currentBatch || currentBatch.length <= 1) return;
2169
+ const onKeyDown = (e) => {
2170
+ if (e.key === "ArrowLeft") stepLightbox(-1);
2171
+ else if (e.key === "ArrowRight") stepLightbox(1);
2172
+ };
2173
+ window.addEventListener("keydown", onKeyDown);
2174
+ return () => window.removeEventListener("keydown", onKeyDown);
2175
+ }, [
2176
+ lightboxOpen,
2177
+ currentBatch,
2178
+ selected
2179
+ ]);
1776
2180
  const flash = (message) => {
1777
2181
  setNotice(message);
1778
2182
  window.setTimeout(() => setNotice(null), 2200);
@@ -1933,13 +2337,30 @@ window.__ModuleLoader__.load({
1933
2337
  className: "dsh-ig-generation-orbit",
1934
2338
  children: (0, react_jsx_runtime.jsx)(Sparkles, { size: 28 })
1935
2339
  }),
1936
- (0, react_jsx_runtime.jsx)("strong", { children: t("generating") }),
1937
- (0, react_jsx_runtime.jsxs)("span", { children: [
1938
- activeProfile?.label,
1939
- " · ",
1940
- model
1941
- ] })
2340
+ (0, react_jsx_runtime.jsx)("strong", { children: comparisonEnabled ? t("comparing", { count: String(comparisonTargets.length) }) : count > 1 ? t("generatingCount", { count: String(count) }) : t("generating") }),
2341
+ (0, react_jsx_runtime.jsx)("span", { children: comparisonEnabled ? comparisonTargets.map((target) => target.profile.label).join(" · ") : `${activeProfile?.label ?? ""} · ${model}` })
1942
2342
  ]
2343
+ }) : currentBatch && currentBatch.length > 1 ? (0, react_jsx_runtime.jsx)("div", {
2344
+ className: "dsh-ig-canvas-cluster",
2345
+ "data-count": currentBatch.length,
2346
+ style: {
2347
+ transform: `translate(${offset.x}px, ${offset.y}px) scale(${zoom / 100})`,
2348
+ transformOrigin: "center center",
2349
+ transition: isDragging ? "none" : "transform 0.08s ease-out"
2350
+ },
2351
+ children: currentBatch.map((batchItem, index) => (0, react_jsx_runtime.jsx)(BatchCanvasItem, {
2352
+ item: batchItem,
2353
+ index,
2354
+ badge: batchKind === "multi-model" ? batchItem.model : void 0,
2355
+ isActive: selected?.id === batchItem.id,
2356
+ isChecked: selectedBatchIds.includes(batchItem.id),
2357
+ onSelect: () => {
2358
+ if (!hasDraggedRef.current) {
2359
+ setSelected(batchItem);
2360
+ setSelectedBatchIds((current) => current.includes(batchItem.id) ? current.filter((id) => id !== batchItem.id) : [...current, batchItem.id]);
2361
+ }
2362
+ }
2363
+ }, batchItem.id))
1943
2364
  }) : selected === null ? (0, react_jsx_runtime.jsxs)("div", {
1944
2365
  className: "dsh-ig-canvas-empty",
1945
2366
  children: [(0, react_jsx_runtime.jsx)(ImagePlus, { size: 36 }), (0, react_jsx_runtime.jsx)("span", { children: t("selectHistory") })]
@@ -1984,6 +2405,14 @@ window.__ModuleLoader__.load({
1984
2405
  (0, react_jsx_runtime.jsxs)("div", {
1985
2406
  className: "dsh-ig-result-actions",
1986
2407
  children: [(0, react_jsx_runtime.jsx)("p", { children: selected.prompt }), (0, react_jsx_runtime.jsxs)("div", { children: [
2408
+ (0, react_jsx_runtime.jsxs)("button", {
2409
+ type: "button",
2410
+ className: `dsh-ig-save-gallery-btn ${saveSelectionComplete ? "is-saved" : ""}`,
2411
+ onClick: () => void handleSaveToGallery(),
2412
+ title: saveButtonLabel,
2413
+ disabled: saveSelectionComplete,
2414
+ children: [saveSelectionComplete ? (0, react_jsx_runtime.jsx)(Check, { size: 15 }) : (0, react_jsx_runtime.jsx)(BookmarkPlus, { size: 15 }), (0, react_jsx_runtime.jsx)("span", { children: saveButtonLabel })]
2415
+ }),
1987
2416
  (0, react_jsx_runtime.jsxs)("button", {
1988
2417
  type: "button",
1989
2418
  onClick: () => void toggleFavorite(),
@@ -2141,11 +2570,16 @@ window.__ModuleLoader__.load({
2141
2570
  " ",
2142
2571
  (0, react_jsx_runtime.jsx)("b", { children: "*" })
2143
2572
  ]
2573
+ }), (0, react_jsx_runtime.jsxs)("span", { children: [(0, react_jsx_runtime.jsxs)("button", {
2574
+ type: "button",
2575
+ className: "dsh-ig-find-inspiration",
2576
+ onClick: onOpenInspiration,
2577
+ children: [(0, react_jsx_runtime.jsx)(Sparkles, { size: 11 }), t("findInspiration")]
2144
2578
  }), (0, react_jsx_runtime.jsx)("button", {
2145
2579
  type: "button",
2146
2580
  onClick: () => setPrompt(""),
2147
2581
  children: t("clear")
2148
- })]
2582
+ })] })]
2149
2583
  }),
2150
2584
  (0, react_jsx_runtime.jsx)("textarea", {
2151
2585
  id: "dsh-ig-prompt",
@@ -2177,39 +2611,130 @@ window.__ModuleLoader__.load({
2177
2611
  onClick: () => void loadConfig(),
2178
2612
  children: [(0, react_jsx_runtime.jsx)(RefreshCw, { size: 13 }), (0, react_jsx_runtime.jsx)("span", { children: t("retry") })]
2179
2613
  })]
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
- }]
2614
+ }) : (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [comparisonProfiles.length >= 2 && (0, react_jsx_runtime.jsxs)("div", {
2615
+ className: "dsh-ig-generation-kind",
2616
+ role: "group",
2617
+ "aria-label": t("compareModels"),
2618
+ children: [(0, react_jsx_runtime.jsx)("button", {
2619
+ type: "button",
2620
+ className: !comparisonEnabled ? "is-active" : "",
2621
+ onClick: () => toggleComparison(false),
2622
+ children: t("singleModel")
2623
+ }), (0, react_jsx_runtime.jsxs)("button", {
2624
+ type: "button",
2625
+ className: comparisonEnabled ? "is-active" : "",
2626
+ onClick: () => toggleComparison(true),
2627
+ children: [(0, react_jsx_runtime.jsx)(Copy, { size: 13 }), t("compareModels")]
2198
2628
  })]
2629
+ }), comparisonEnabled ? (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsxs)("div", {
2630
+ className: "dsh-ig-compare-box",
2631
+ children: [
2632
+ (0, react_jsx_runtime.jsxs)("div", {
2633
+ className: "dsh-ig-compare-heading",
2634
+ children: [(0, react_jsx_runtime.jsxs)("div", { children: [(0, react_jsx_runtime.jsx)("strong", { children: t("compareSelect") }), (0, react_jsx_runtime.jsx)("small", { children: t("compareHint") })] }), (0, react_jsx_runtime.jsx)("span", { children: t("compareSelected", { count: String(comparisonTargets.length) }) })]
2635
+ }),
2636
+ (0, react_jsx_runtime.jsx)("div", {
2637
+ className: "dsh-ig-model-checks",
2638
+ children: comparisonProfiles.map((item) => {
2639
+ const target = comparisonTargets.find((candidate) => candidate.profile.provider === item.provider);
2640
+ return (0, react_jsx_runtime.jsxs)("button", {
2641
+ type: "button",
2642
+ className: target ? "is-selected" : "",
2643
+ "aria-pressed": Boolean(target),
2644
+ onClick: () => toggleComparisonProvider(item.provider),
2645
+ children: [
2646
+ (0, react_jsx_runtime.jsx)("span", {
2647
+ className: "dsh-ig-model-checkmark",
2648
+ children: target ? (0, react_jsx_runtime.jsx)(Check, { size: 12 }) : null
2649
+ }),
2650
+ (0, react_jsx_runtime.jsxs)("span", {
2651
+ className: "dsh-ig-model-copy",
2652
+ children: [(0, react_jsx_runtime.jsx)("strong", { children: item.label }), (0, react_jsx_runtime.jsx)("small", {
2653
+ title: item.model,
2654
+ children: item.model
2655
+ })]
2656
+ }),
2657
+ target && (0, react_jsx_runtime.jsxs)("span", {
2658
+ className: "dsh-ig-model-settings",
2659
+ children: [
2660
+ target.ratio,
2661
+ " · ",
2662
+ target.quality,
2663
+ target.adjusted ? (0, react_jsx_runtime.jsx)("em", { children: t("compareAdjusted") }) : null
2664
+ ]
2665
+ })
2666
+ ]
2667
+ }, item.provider);
2668
+ })
2669
+ }),
2670
+ (0, react_jsx_runtime.jsx)("p", { children: t("compareParameterHint") })
2671
+ ]
2199
2672
  }), (0, react_jsx_runtime.jsxs)("div", {
2200
2673
  className: "dsh-ig-field-grid",
2201
2674
  children: [(0, react_jsx_runtime.jsx)(FieldSelect, {
2202
2675
  label: t("ratio"),
2203
2676
  value: ratio,
2204
2677
  onChange: setRatio,
2205
- options: activeProfile?.ratioOptions ?? []
2678
+ options: comparisonRatioOptions(lang)
2206
2679
  }), (0, react_jsx_runtime.jsx)(FieldSelect, {
2207
2680
  label: t("quality"),
2208
2681
  value: quality,
2209
2682
  onChange: setQuality,
2210
- options: activeProfile?.qualityOptions ?? []
2683
+ options: comparisonQualityOptions(lang)
2211
2684
  })]
2212
- })] }),
2685
+ })] }) : (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2686
+ (0, react_jsx_runtime.jsxs)("div", {
2687
+ className: "dsh-ig-field-grid",
2688
+ children: [(0, react_jsx_runtime.jsx)(FieldSelect, {
2689
+ label: t("provider"),
2690
+ value: provider,
2691
+ onChange: changeProvider,
2692
+ options: config.providers.map((item) => ({
2693
+ value: item.provider,
2694
+ label: `${item.label}${item.configured ? "" : ` · ${t("unconfigured")}`}`
2695
+ }))
2696
+ }), (0, react_jsx_runtime.jsx)(FieldSelect, {
2697
+ label: t("model"),
2698
+ value: model,
2699
+ onChange: setModel,
2700
+ options: activeProfile === void 0 ? [] : [{
2701
+ value: activeProfile.model,
2702
+ label: activeProfile.model
2703
+ }]
2704
+ })]
2705
+ }),
2706
+ (0, react_jsx_runtime.jsxs)("div", {
2707
+ className: "dsh-ig-field-grid",
2708
+ children: [(0, react_jsx_runtime.jsx)(FieldSelect, {
2709
+ label: t("ratio"),
2710
+ value: ratio,
2711
+ onChange: setRatio,
2712
+ options: localizeRatioOptions(activeProfile?.ratioOptions ?? [], lang)
2713
+ }), (0, react_jsx_runtime.jsx)(FieldSelect, {
2714
+ label: t("quality"),
2715
+ value: quality,
2716
+ onChange: setQuality,
2717
+ options: localizeQualityOptions(activeProfile?.qualityOptions ?? [], lang)
2718
+ })]
2719
+ }),
2720
+ (0, react_jsx_runtime.jsxs)("div", {
2721
+ className: "dsh-ig-field",
2722
+ children: [(0, react_jsx_runtime.jsx)("label", { children: t("count") }), (0, react_jsx_runtime.jsx)("div", {
2723
+ className: "dsh-ig-count-row",
2724
+ children: [
2725
+ 1,
2726
+ 2,
2727
+ 3,
2728
+ 4
2729
+ ].map((option) => (0, react_jsx_runtime.jsx)("button", {
2730
+ type: "button",
2731
+ className: `dsh-ig-count-pill ${count === option ? "is-active" : ""}`,
2732
+ onClick: () => setCount(option),
2733
+ children: t("countUnit", { n: String(option) })
2734
+ }, option))
2735
+ })]
2736
+ })
2737
+ ] })] }),
2213
2738
  configuredCount === 0 && config !== null && (0, react_jsx_runtime.jsx)("div", {
2214
2739
  className: "dsh-ig-inline-note",
2215
2740
  children: t("noProvider")
@@ -2231,7 +2756,7 @@ window.__ModuleLoader__.load({
2231
2756
  className: "dsh-ig-generate-button",
2232
2757
  disabled: config === null,
2233
2758
  onClick: () => void submit(),
2234
- children: [(0, react_jsx_runtime.jsx)(Sparkles, { size: 17 }), (0, react_jsx_runtime.jsx)("span", { children: t("start") })]
2759
+ children: [(0, react_jsx_runtime.jsx)(Sparkles, { size: 17 }), (0, react_jsx_runtime.jsx)("span", { children: comparisonEnabled ? `${t("compareStart")} (${comparisonTargets.length})` : `${t("start")}${count > 1 ? ` (${count})` : ""}` })]
2235
2760
  })
2236
2761
  ]
2237
2762
  })]
@@ -2255,6 +2780,14 @@ window.__ModuleLoader__.load({
2255
2780
  className: "dsh-ig-tag dsh-ig-tag-model",
2256
2781
  children: selected.model
2257
2782
  }),
2783
+ currentBatch && currentBatch.length > 1 && (0, react_jsx_runtime.jsxs)("span", {
2784
+ className: "dsh-ig-tag",
2785
+ children: [
2786
+ currentBatch.findIndex((i) => i.id === selected.id) + 1,
2787
+ " / ",
2788
+ currentBatch.length
2789
+ ]
2790
+ }),
2258
2791
  selected.attachment.width && selected.attachment.height ? (0, react_jsx_runtime.jsxs)("span", {
2259
2792
  className: "dsh-ig-tag",
2260
2793
  children: [
@@ -2272,6 +2805,25 @@ window.__ModuleLoader__.load({
2272
2805
  children: (0, react_jsx_runtime.jsx)(X, { size: 18 })
2273
2806
  })]
2274
2807
  }),
2808
+ currentBatch && currentBatch.length > 1 && (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsx)("button", {
2809
+ type: "button",
2810
+ className: "dsh-ig-lightbox-nav is-prev",
2811
+ onClick: (e) => {
2812
+ e.stopPropagation();
2813
+ stepLightbox(-1);
2814
+ },
2815
+ title: "上一张",
2816
+ children: (0, react_jsx_runtime.jsx)(ChevronLeft, { size: 24 })
2817
+ }), (0, react_jsx_runtime.jsx)("button", {
2818
+ type: "button",
2819
+ className: "dsh-ig-lightbox-nav is-next",
2820
+ onClick: (e) => {
2821
+ e.stopPropagation();
2822
+ stepLightbox(1);
2823
+ },
2824
+ title: "下一张",
2825
+ children: (0, react_jsx_runtime.jsx)(ChevronRight, { size: 24 })
2826
+ })] }),
2275
2827
  (0, react_jsx_runtime.jsx)("div", {
2276
2828
  className: "dsh-ig-lightbox-img-wrap",
2277
2829
  onClick: (e) => e.stopPropagation(),
@@ -2322,6 +2874,14 @@ window.__ModuleLoader__.load({
2322
2874
  onClick: downloadSelected,
2323
2875
  children: [(0, react_jsx_runtime.jsx)(Download, { size: 14 }), (0, react_jsx_runtime.jsx)("span", { children: t("download") })]
2324
2876
  }),
2877
+ (0, react_jsx_runtime.jsxs)("button", {
2878
+ type: "button",
2879
+ className: `dsh-ig-lightbox-btn ${saveSelectionComplete ? "is-saved" : ""}`,
2880
+ title: saveButtonLabel,
2881
+ onClick: () => void handleSaveToGallery(),
2882
+ disabled: saveSelectionComplete,
2883
+ children: [saveSelectionComplete ? (0, react_jsx_runtime.jsx)(Check, { size: 14 }) : (0, react_jsx_runtime.jsx)(BookmarkPlus, { size: 14 }), (0, react_jsx_runtime.jsx)("span", { children: saveButtonLabel })]
2884
+ }),
2325
2885
  (0, react_jsx_runtime.jsxs)("button", {
2326
2886
  type: "button",
2327
2887
  className: "dsh-ig-lightbox-btn",
@@ -2388,6 +2948,39 @@ window.__ModuleLoader__.load({
2388
2948
  ]
2389
2949
  });
2390
2950
  };
2951
+ const BatchCanvasItem = ({ item, index, badge, isActive, isChecked, onSelect }) => {
2952
+ const image = useAttachmentImage(item.attachment, true);
2953
+ return (0, react_jsx_runtime.jsxs)("div", {
2954
+ className: `dsh-ig-canvas-item ${isActive ? "is-active" : ""} ${isChecked ? "is-checked" : ""}`,
2955
+ onClick: onSelect,
2956
+ children: [
2957
+ (0, react_jsx_runtime.jsx)("span", {
2958
+ className: `dsh-ig-canvas-badge ${badge ? "is-model" : ""}`,
2959
+ title: badge,
2960
+ children: badge ?? `#${index + 1}`
2961
+ }),
2962
+ (0, react_jsx_runtime.jsx)("span", {
2963
+ className: "dsh-ig-canvas-check",
2964
+ "aria-hidden": "true",
2965
+ children: isChecked ? (0, react_jsx_runtime.jsx)(Check, { size: 14 }) : null
2966
+ }),
2967
+ image.url !== null ? (0, react_jsx_runtime.jsx)("img", {
2968
+ src: image.url,
2969
+ alt: item.prompt,
2970
+ draggable: false
2971
+ }) : image.loading ? (0, react_jsx_runtime.jsx)("div", {
2972
+ className: "dsh-ig-canvas-item-loading",
2973
+ children: (0, react_jsx_runtime.jsx)(LoaderCircle, {
2974
+ className: "dsh-ig-spin",
2975
+ size: 24
2976
+ })
2977
+ }) : (0, react_jsx_runtime.jsx)("div", {
2978
+ className: "dsh-ig-canvas-item-loading",
2979
+ children: (0, react_jsx_runtime.jsx)(ImagePlus, { size: 24 })
2980
+ })
2981
+ ]
2982
+ });
2983
+ };
2391
2984
  const FieldSelect = ({ label, value, options, onChange }) => (0, react_jsx_runtime.jsxs)("label", {
2392
2985
  className: "dsh-ig-field-select",
2393
2986
  children: [(0, react_jsx_runtime.jsx)("span", { children: label }), (0, react_jsx_runtime.jsx)("select", {
@@ -2399,6 +2992,94 @@ window.__ModuleLoader__.load({
2399
2992
  }, option.value))
2400
2993
  })]
2401
2994
  });
2995
+ const LOCALIZED_RATIOS = {
2996
+ auto: {
2997
+ zh: "自动",
2998
+ en: "Auto"
2999
+ },
3000
+ "1:1": {
3001
+ zh: "1:1 方形",
3002
+ en: "1:1 Square"
3003
+ },
3004
+ "3:2": {
3005
+ zh: "3:2 横向",
3006
+ en: "3:2 Landscape"
3007
+ },
3008
+ "2:3": {
3009
+ zh: "2:3 肖像",
3010
+ en: "2:3 Portrait"
3011
+ },
3012
+ "4:3": {
3013
+ zh: "4:3 横向",
3014
+ en: "4:3 Landscape"
3015
+ },
3016
+ "3:4": {
3017
+ zh: "3:4 竖向",
3018
+ en: "3:4 Portrait"
3019
+ },
3020
+ "16:9": {
3021
+ zh: "16:9 宽屏",
3022
+ en: "16:9 Widescreen"
3023
+ },
3024
+ "9:16": {
3025
+ zh: "9:16 竖屏",
3026
+ en: "9:16 Portrait"
3027
+ }
3028
+ };
3029
+ const LOCALIZED_QUALITIES = {
3030
+ standard: {
3031
+ zh: "标准(推荐)",
3032
+ en: "Standard (Recommended)"
3033
+ },
3034
+ auto: {
3035
+ zh: "模型自动",
3036
+ en: "Model Auto"
3037
+ }
3038
+ };
3039
+ function localizeRatioOptions(options, lang) {
3040
+ return options.map((opt) => ({
3041
+ value: opt.value,
3042
+ label: LOCALIZED_RATIOS[opt.value]?.[lang] ?? opt.label
3043
+ }));
3044
+ }
3045
+ function localizeQualityOptions(options, lang) {
3046
+ return options.map((opt) => ({
3047
+ value: opt.value,
3048
+ label: LOCALIZED_QUALITIES[opt.value]?.[lang] ?? opt.label
3049
+ }));
3050
+ }
3051
+ function comparisonRatioOptions(lang) {
3052
+ return [
3053
+ "1:1",
3054
+ "3:2",
3055
+ "2:3",
3056
+ "16:9",
3057
+ "9:16"
3058
+ ].map((value) => ({
3059
+ value,
3060
+ label: LOCALIZED_RATIOS[value]?.[lang] ?? value
3061
+ }));
3062
+ }
3063
+ function comparisonQualityOptions(lang) {
3064
+ return [
3065
+ {
3066
+ value: "standard",
3067
+ label: lang === "zh" ? "标准" : "Standard"
3068
+ },
3069
+ {
3070
+ value: "1K",
3071
+ label: "1K"
3072
+ },
3073
+ {
3074
+ value: "2K",
3075
+ label: "2K"
3076
+ },
3077
+ {
3078
+ value: "4K",
3079
+ label: "4K"
3080
+ }
3081
+ ];
3082
+ }
2402
3083
  const DetailsPanel = ({ item, t }) => item === null ? (0, react_jsx_runtime.jsxs)("div", {
2403
3084
  className: "dsh-ig-details-empty",
2404
3085
  children: [(0, react_jsx_runtime.jsx)(ImagePlus, { size: 28 }), (0, react_jsx_runtime.jsx)("span", { children: t("selectHistory") })]
@@ -2528,6 +3209,1036 @@ window.__ModuleLoader__.load({
2528
3209
  return error instanceof Error ? error.message : String(error);
2529
3210
  }
2530
3211
  //#endregion
3212
+ //#region lib/types/client/inspiration-image-cache.js
3213
+ /** Persistent, bounded browser cache for publicly proxied inspiration images. */
3214
+ const DB_NAME$1 = "dsh-image-gen-inspiration";
3215
+ const STORE$1 = "images";
3216
+ const CATALOG_STORE = "catalog";
3217
+ const MAX_ENTRIES = 120;
3218
+ const MAX_BYTES = 188743680;
3219
+ const pending = /* @__PURE__ */ new Map();
3220
+ let cacheEpoch = 0;
3221
+ function fetchInspirationImage(sourceId, caseId) {
3222
+ const key = `${sourceId}:${caseId}`;
3223
+ const existing = pending.get(key);
3224
+ if (existing !== void 0) return existing;
3225
+ const requestEpoch = cacheEpoch;
3226
+ const task = getCached(key).then(async (cached) => {
3227
+ if (cached !== void 0) return cached;
3228
+ const response = await fetch(`${INSPIRATION_ROUTE}/image/${encodeURIComponent(sourceId)}/${encodeURIComponent(caseId)}`, {
3229
+ credentials: "same-origin",
3230
+ headers: { accept: "image/avif,image/webp,image/png,image/jpeg" }
3231
+ });
3232
+ if (!response.ok) throw new Error(`灵感图片读取失败 (${String(response.status)})`);
3233
+ const blob = await response.blob();
3234
+ if (blob.size < 64 || ![
3235
+ "image/jpeg",
3236
+ "image/png",
3237
+ "image/webp"
3238
+ ].includes(blob.type)) throw new Error("灵感图片格式不受支持");
3239
+ if (requestEpoch === cacheEpoch) putCached(key, blob);
3240
+ return blob;
3241
+ }).finally(() => pending.delete(key));
3242
+ pending.set(key, task);
3243
+ return task;
3244
+ }
3245
+ async function evictInspirationImage(sourceId, caseId) {
3246
+ const key = `${sourceId}:${caseId}`;
3247
+ pending.delete(key);
3248
+ const db = await openDatabase$1();
3249
+ if (db === void 0) return;
3250
+ try {
3251
+ await new Promise((resolve) => {
3252
+ const req = db.transaction(STORE$1, "readwrite").objectStore(STORE$1).delete(key);
3253
+ req.onsuccess = () => resolve();
3254
+ req.onerror = () => resolve();
3255
+ });
3256
+ } finally {
3257
+ db.close();
3258
+ }
3259
+ }
3260
+ function openDatabase$1() {
3261
+ if (typeof indexedDB === "undefined") return Promise.resolve(void 0);
3262
+ return new Promise((resolve) => {
3263
+ try {
3264
+ const request = indexedDB.open(DB_NAME$1, 2);
3265
+ request.onupgradeneeded = () => {
3266
+ const db = request.result;
3267
+ if (!db.objectStoreNames.contains(STORE$1)) db.createObjectStore(STORE$1, { keyPath: "key" }).createIndex("accessedAt", "accessedAt");
3268
+ if (!db.objectStoreNames.contains(CATALOG_STORE)) db.createObjectStore(CATALOG_STORE, { keyPath: "id" });
3269
+ };
3270
+ request.onsuccess = () => resolve(request.result);
3271
+ request.onerror = () => resolve(void 0);
3272
+ } catch {
3273
+ resolve(void 0);
3274
+ }
3275
+ });
3276
+ }
3277
+ async function getCached(key) {
3278
+ const db = await openDatabase$1();
3279
+ if (db === void 0) return void 0;
3280
+ try {
3281
+ return await new Promise((resolve) => {
3282
+ const store = db.transaction(STORE$1, "readwrite").objectStore(STORE$1);
3283
+ const request = store.get(key);
3284
+ request.onsuccess = () => {
3285
+ const entry = request.result;
3286
+ if (entry === void 0 || !(entry.blob instanceof Blob) || entry.blob.size < 64) {
3287
+ if (entry !== void 0) try {
3288
+ store.delete(key);
3289
+ } catch {}
3290
+ return resolve(void 0);
3291
+ }
3292
+ entry.accessedAt = Date.now();
3293
+ store.put(entry);
3294
+ resolve(entry.blob);
3295
+ };
3296
+ request.onerror = () => resolve(void 0);
3297
+ });
3298
+ } finally {
3299
+ db.close();
3300
+ }
3301
+ }
3302
+ async function putCached(key, blob) {
3303
+ const db = await openDatabase$1();
3304
+ if (db === void 0) return;
3305
+ try {
3306
+ await new Promise((resolve) => {
3307
+ const tx = db.transaction(STORE$1, "readwrite");
3308
+ tx.objectStore(STORE$1).put({
3309
+ key,
3310
+ blob,
3311
+ bytes: blob.size,
3312
+ accessedAt: Date.now()
3313
+ });
3314
+ tx.oncomplete = () => resolve();
3315
+ tx.onerror = () => resolve();
3316
+ tx.onabort = () => resolve();
3317
+ });
3318
+ await trimCache(db);
3319
+ } finally {
3320
+ db.close();
3321
+ }
3322
+ }
3323
+ async function trimCache(db) {
3324
+ await new Promise((resolve) => {
3325
+ const tx = db.transaction(STORE$1, "readwrite");
3326
+ const store = tx.objectStore(STORE$1);
3327
+ const items = [];
3328
+ const cursor = store.openCursor();
3329
+ cursor.onsuccess = () => {
3330
+ const result = cursor.result;
3331
+ if (result !== null) {
3332
+ items.push(result.value);
3333
+ result.continue();
3334
+ return;
3335
+ }
3336
+ items.sort((a, b) => a.accessedAt - b.accessedAt);
3337
+ let count = items.length;
3338
+ let bytes = items.reduce((total, item) => total + Math.max(0, item.bytes), 0);
3339
+ for (const item of items) {
3340
+ if (count <= MAX_ENTRIES && bytes <= MAX_BYTES) break;
3341
+ store.delete(item.key);
3342
+ count -= 1;
3343
+ bytes -= Math.max(0, item.bytes);
3344
+ }
3345
+ };
3346
+ tx.oncomplete = () => resolve();
3347
+ tx.onerror = () => resolve();
3348
+ tx.onabort = () => resolve();
3349
+ });
3350
+ }
3351
+ async function clearInspirationImageCache() {
3352
+ cacheEpoch += 1;
3353
+ pending.clear();
3354
+ const db = await openDatabase$1();
3355
+ if (db === void 0) return;
3356
+ try {
3357
+ await new Promise((resolve) => {
3358
+ const tx = db.transaction(STORE$1, "readwrite");
3359
+ tx.objectStore(STORE$1).clear();
3360
+ tx.oncomplete = () => resolve();
3361
+ tx.onerror = () => resolve();
3362
+ tx.onabort = () => resolve();
3363
+ });
3364
+ } finally {
3365
+ db.close();
3366
+ }
3367
+ }
3368
+ //#endregion
3369
+ //#region lib/types/client/inspiration-catalog-cache.js
3370
+ const DB_NAME = "dsh-image-gen-inspiration";
3371
+ const STORE = "catalog";
3372
+ const IMAGE_STORE = "images";
3373
+ const KEY = "latest";
3374
+ async function loadCachedInspirationCatalog() {
3375
+ const db = await openDatabase();
3376
+ if (db === void 0) return void 0;
3377
+ try {
3378
+ return await new Promise((resolve) => {
3379
+ const request = db.transaction(STORE, "readonly").objectStore(STORE).get(KEY);
3380
+ request.onsuccess = () => {
3381
+ const record = request.result;
3382
+ resolve(isCatalog(record?.catalog) ? record.catalog : void 0);
3383
+ };
3384
+ request.onerror = () => resolve(void 0);
3385
+ });
3386
+ } finally {
3387
+ db.close();
3388
+ }
3389
+ }
3390
+ async function saveCachedInspirationCatalog(catalog) {
3391
+ if (!isCatalog(catalog)) return;
3392
+ const db = await openDatabase();
3393
+ if (db === void 0) return;
3394
+ try {
3395
+ await new Promise((resolve) => {
3396
+ const tx = db.transaction(STORE, "readwrite");
3397
+ tx.objectStore(STORE).put({
3398
+ id: KEY,
3399
+ catalog,
3400
+ storedAt: Date.now()
3401
+ });
3402
+ tx.oncomplete = () => resolve();
3403
+ tx.onerror = () => resolve();
3404
+ tx.onabort = () => resolve();
3405
+ });
3406
+ } finally {
3407
+ db.close();
3408
+ }
3409
+ }
3410
+ function openDatabase() {
3411
+ if (typeof indexedDB === "undefined") return Promise.resolve(void 0);
3412
+ return new Promise((resolve) => {
3413
+ try {
3414
+ const request = indexedDB.open(DB_NAME, 2);
3415
+ request.onupgradeneeded = () => {
3416
+ const db = request.result;
3417
+ if (!db.objectStoreNames.contains(STORE)) db.createObjectStore(STORE, { keyPath: "id" });
3418
+ if (!db.objectStoreNames.contains(IMAGE_STORE)) db.createObjectStore(IMAGE_STORE, { keyPath: "key" }).createIndex("accessedAt", "accessedAt");
3419
+ };
3420
+ request.onsuccess = () => resolve(request.result);
3421
+ request.onerror = () => resolve(void 0);
3422
+ } catch {
3423
+ resolve(void 0);
3424
+ }
3425
+ });
3426
+ }
3427
+ function isCatalog(value) {
3428
+ if (typeof value !== "object" || value === null || value.schemaVersion !== 1) return false;
3429
+ const sources = value.sources;
3430
+ return Array.isArray(sources) && sources.length > 0 && sources.every((source) => typeof source === "object" && source !== null && Array.isArray(source.cases));
3431
+ }
3432
+ //#endregion
3433
+ //#region lib/types/client/inspiration-view.js
3434
+ const FAVORITES_STORAGE_KEY = "dsh-ig-inspiration-favorites";
3435
+ function loadInspirationFavorites() {
3436
+ if (typeof localStorage === "undefined") return /* @__PURE__ */ new Set();
3437
+ try {
3438
+ const raw = localStorage.getItem(FAVORITES_STORAGE_KEY);
3439
+ if (raw) {
3440
+ const parsed = JSON.parse(raw);
3441
+ if (Array.isArray(parsed)) return new Set(parsed);
3442
+ }
3443
+ } catch {}
3444
+ return /* @__PURE__ */ new Set();
3445
+ }
3446
+ function saveInspirationFavorites(favorites) {
3447
+ if (typeof localStorage === "undefined") return;
3448
+ try {
3449
+ localStorage.setItem(FAVORITES_STORAGE_KEY, JSON.stringify(Array.from(favorites)));
3450
+ } catch {}
3451
+ }
3452
+ const TAG_MAP_ZH = {
3453
+ "Architecture & Spaces": "建筑空间",
3454
+ "Brand & Logos": "品牌 Logo",
3455
+ "Characters & People": "角色人物",
3456
+ "Charts & Infographics": "图表信息图",
3457
+ "Documents & Publishing": "文档出版",
3458
+ "History & Classical Themes": "历史古典",
3459
+ "Illustration & Art": "插画艺术",
3460
+ "Other Use Cases": "其他案例",
3461
+ "Photography & Realism": "摄影写真",
3462
+ "Posters & Typography": "海报排版",
3463
+ "Products & E-commerce": "产品电商",
3464
+ "Scenes & Storytelling": "场景故事",
3465
+ "UI & Interfaces": "UI 界面",
3466
+ "3D": "3D 渲染",
3467
+ "Architecture": "建筑空间",
3468
+ "Brand": "品牌视觉",
3469
+ "Character": "角色人物",
3470
+ "Characters": "人物群像",
3471
+ "Charts": "图表图解",
3472
+ "Classical": "古典艺术",
3473
+ "Documents": "文档出版",
3474
+ "History": "历史人文",
3475
+ "Illustration": "插画手绘",
3476
+ "Infographic": "信息图表",
3477
+ "Photography": "摄影写真",
3478
+ "Poster": "海报封面",
3479
+ "Product": "产品设计",
3480
+ "Products": "商业产品",
3481
+ "Realistic": "写实逼真",
3482
+ "Scenes": "场景概念",
3483
+ "UI": "UI 界面",
3484
+ "Commerce": "商业电商",
3485
+ "Creative": "创意设计",
3486
+ "Education": "教育科普",
3487
+ "Fashion": "时尚潮流",
3488
+ "Food": "美食餐饮",
3489
+ "Social": "社媒传播",
3490
+ "Story": "故事剧情",
3491
+ "Tech": "科技未来",
3492
+ "Travel": "旅游出行",
3493
+ "Art": "艺术创作",
3494
+ "Minimalist": "极简主义",
3495
+ "Vintage": "复古胶片",
3496
+ "Cyberpunk": "赛博朋克",
3497
+ "Anime": "二次元日系",
3498
+ "Watercolor": "水彩手绘",
3499
+ "Oil Painting": "油画质感"
3500
+ };
3501
+ function translateTag(name, lang) {
3502
+ if (lang !== "zh") return name;
3503
+ return TAG_MAP_ZH[name] ?? name;
3504
+ }
3505
+ const COPY = {
3506
+ zh: {
3507
+ kicker: "Prompt inspiration",
3508
+ title: "灵感素材",
3509
+ subtitle: "从公开案例中找构图、质感和文字处理,再带回工作台继续调整。",
3510
+ refresh: "检查更新",
3511
+ refreshing: "正在更新…",
3512
+ clearCache: "清理缓存",
3513
+ clearingCache: "正在清理…",
3514
+ cacheCleared: "已清空本地图片缓存",
3515
+ cacheClearFailed: "清理缓存失败",
3516
+ clearCacheRestart: "后端未就绪,请重启 DSH 后生效",
3517
+ clearModalTitle: "清理本地图片缓存?",
3518
+ clearModalDesc: "将删除本地保存的灵感素材图片以释放磁盘空间。案例列表、Prompt 和您的收藏夹不会受到任何影响,后续浏览时会自动重新拉取。",
3519
+ cancel: "取消",
3520
+ confirmClear: "确认清理",
3521
+ allCategories: "全部分类",
3522
+ allStyles: "全部风格",
3523
+ allScenes: "全部场景",
3524
+ search: "搜索案例、Prompt、风格…",
3525
+ results: "找到 {count} 个案例",
3526
+ noResults: "没有匹配的素材,换个关键词或筛选条件试试。",
3527
+ selectHint: "选择一张素材,查看完整 Prompt 并带回工作台。",
3528
+ prompt: "完整 Prompt",
3529
+ copy: "复制 Prompt",
3530
+ copied: "已复制",
3531
+ use: "使用这个 Prompt",
3532
+ source: "查看原来源",
3533
+ loading: "正在读取素材库…",
3534
+ loadFailed: "素材库读取失败,请稍后重试。",
3535
+ copyFailed: "复制失败",
3536
+ retry: "重新加载",
3537
+ imageFailed: "图片暂时无法读取",
3538
+ featured: "精选",
3539
+ updated: "素材已更新({count} 条)",
3540
+ updateFailed: "更新失败,仍在使用当前内置素材。",
3541
+ allLoaded: "已展示全部 {count} 个案例",
3542
+ onlyFavorites: "仅看收藏",
3543
+ noFavorites: "暂无收藏的灵感案例,浏览时点击星标即可收藏。",
3544
+ favorite: "收藏",
3545
+ favorited: "已收藏",
3546
+ zoomHint: "点击放大查看",
3547
+ close: "关闭"
3548
+ },
3549
+ en: {
3550
+ kicker: "Prompt inspiration",
3551
+ title: "Inspiration",
3552
+ subtitle: "Explore public examples, then bring a prompt back to Studio to make it your own.",
3553
+ refresh: "Check updates",
3554
+ refreshing: "Updating…",
3555
+ clearCache: "Clear cache",
3556
+ clearingCache: "Clearing…",
3557
+ cacheCleared: "Local image cache cleared",
3558
+ cacheClearFailed: "Failed to clear cache",
3559
+ clearCacheRestart: "Backend not ready, please restart DSH",
3560
+ clearModalTitle: "Clear local image cache?",
3561
+ clearModalDesc: "This will delete locally cached inspiration images to free up disk space. The catalog, prompts, and your bookmarks will remain intact. Images will be re-fetched on demand.",
3562
+ cancel: "Cancel",
3563
+ confirmClear: "Clear Cache",
3564
+ allCategories: "All categories",
3565
+ allStyles: "All styles",
3566
+ allScenes: "All scenes",
3567
+ search: "Search examples, prompts, styles…",
3568
+ results: "{count} examples",
3569
+ noResults: "No matching examples. Try another keyword or filter.",
3570
+ selectHint: "Choose an example to read its full prompt and use it in Studio.",
3571
+ prompt: "Full prompt",
3572
+ copy: "Copy prompt",
3573
+ copied: "Copied",
3574
+ use: "Use this prompt",
3575
+ source: "View source",
3576
+ loading: "Loading inspiration…",
3577
+ loadFailed: "Could not load the inspiration library.",
3578
+ copyFailed: "Failed to copy",
3579
+ retry: "Retry",
3580
+ imageFailed: "Image is temporarily unavailable",
3581
+ featured: "Featured",
3582
+ updated: "Updated ({count} examples)",
3583
+ updateFailed: "Update failed. The current bundled library is still available.",
3584
+ allLoaded: "All {count} examples displayed",
3585
+ onlyFavorites: "Favorites only",
3586
+ noFavorites: "No favorited examples yet. Click the star icon on any card to save.",
3587
+ favorite: "Favorite",
3588
+ favorited: "Favorited",
3589
+ zoomHint: "Click to zoom in",
3590
+ close: "Close"
3591
+ }
3592
+ };
3593
+ const InspirationView = ({ locale, onUsePrompt }) => {
3594
+ const [language, setLanguage] = (0, react.useState)(() => locale?.getSnapshot?.().active?.startsWith("en") ? "en" : "zh");
3595
+ const [catalog, setCatalog] = (0, react.useState)(null);
3596
+ const [catalogError, setCatalogError] = (0, react.useState)(false);
3597
+ const [search, setSearch] = (0, react.useState)("");
3598
+ const [category, setCategory] = (0, react.useState)("");
3599
+ const [style, setStyle] = (0, react.useState)("");
3600
+ const [scene, setScene] = (0, react.useState)("");
3601
+ const [visibleLimit, setVisibleLimit] = (0, react.useState)(60);
3602
+ const [selected, setSelected] = (0, react.useState)(null);
3603
+ const [refreshing, setRefreshing] = (0, react.useState)(false);
3604
+ const [clearingCache, setClearingCache] = (0, react.useState)(false);
3605
+ const [confirmClearOpen, setConfirmClearOpen] = (0, react.useState)(false);
3606
+ const [copied, setCopied] = (0, react.useState)(false);
3607
+ const [favorites, setFavorites] = (0, react.useState)(() => loadInspirationFavorites());
3608
+ const [onlyFavorites, setOnlyFavorites] = (0, react.useState)(false);
3609
+ const [lightboxCase, setLightboxCase] = (0, react.useState)(null);
3610
+ const [toast, setToast] = (0, react.useState)(null);
3611
+ const toastTimerRef = (0, react.useRef)(null);
3612
+ const sentinelRef = (0, react.useRef)(null);
3613
+ (0, react.useEffect)(() => {
3614
+ if (!locale?.subscribe) return;
3615
+ return locale.subscribe(() => setLanguage(locale.getSnapshot?.().active?.startsWith("en") ? "en" : "zh"));
3616
+ }, [locale]);
3617
+ const t = (key, values) => {
3618
+ let value = COPY[language][key] ?? COPY.zh[key];
3619
+ for (const [name, replacement] of Object.entries(values ?? {})) value = value.replace(`{${name}}`, replacement);
3620
+ return value;
3621
+ };
3622
+ const showToast = (text, isError = false) => {
3623
+ if (toastTimerRef.current !== null) window.clearTimeout(toastTimerRef.current);
3624
+ setToast({
3625
+ text,
3626
+ isError
3627
+ });
3628
+ toastTimerRef.current = window.setTimeout(() => {
3629
+ setToast(null);
3630
+ toastTimerRef.current = null;
3631
+ }, 2400);
3632
+ };
3633
+ const toggleFavorite = (id) => {
3634
+ setFavorites((prev) => {
3635
+ const next = new Set(prev);
3636
+ if (next.has(id)) next.delete(id);
3637
+ else next.add(id);
3638
+ saveInspirationFavorites(next);
3639
+ return next;
3640
+ });
3641
+ };
3642
+ const loadCatalog = async (method = "GET") => {
3643
+ const response = await fetch(`${INSPIRATION_ROUTE}/${method === "GET" ? "catalog" : "refresh"}`, {
3644
+ method,
3645
+ credentials: "same-origin"
3646
+ });
3647
+ const payload = await response.json().catch(() => null);
3648
+ if (!response.ok || payload === null || !("schemaVersion" in payload) || payload.schemaVersion !== 1) throw new Error(payload && "error" in payload && payload.error ? payload.error : t("loadFailed"));
3649
+ return payload;
3650
+ };
3651
+ const retryCatalog = async () => {
3652
+ setCatalogError(false);
3653
+ try {
3654
+ const data = await loadCatalog("GET");
3655
+ setCatalog(data);
3656
+ setSelected((previous) => previous === null ? data.sources[0]?.cases.find((item) => item.featured) ?? data.sources[0]?.cases[0] ?? null : previous);
3657
+ } catch {
3658
+ setCatalogError(true);
3659
+ showToast(t("loadFailed"), true);
3660
+ }
3661
+ };
3662
+ (0, react.useEffect)(() => {
3663
+ let mounted = true;
3664
+ setCatalogError(false);
3665
+ Promise.allSettled([loadCatalog(), loadCachedInspirationCatalog()]).then(([server, cache]) => {
3666
+ if (!mounted) return;
3667
+ const cached = cache.status === "fulfilled" ? cache.value : void 0;
3668
+ const next = resolveActiveCatalog(server.status === "fulfilled" ? server.value : void 0, cached);
3669
+ if (next === void 0) {
3670
+ setCatalogError(true);
3671
+ showToast(t("loadFailed"), true);
3672
+ return;
3673
+ }
3674
+ setCatalogError(false);
3675
+ setCatalog(next);
3676
+ setSelected((previous) => previous === null ? next.sources[0]?.cases.find((item) => item.featured) ?? next.sources[0]?.cases[0] ?? null : previous);
3677
+ });
3678
+ return () => {
3679
+ mounted = false;
3680
+ };
3681
+ }, []);
3682
+ const source = catalog?.sources[0] ?? null;
3683
+ const matchingCases = (0, react.useMemo)(() => {
3684
+ if (source === null) return [];
3685
+ const query = search.trim().toLowerCase();
3686
+ return source.cases.filter((item) => {
3687
+ if (onlyFavorites && !favorites.has(item.id)) return false;
3688
+ if (category && item.category !== category) return false;
3689
+ if (style && !item.styles.includes(style)) return false;
3690
+ if (scene && !item.scenes.includes(scene)) return false;
3691
+ if (!query) return true;
3692
+ const translatedCat = translateTag(item.category, language);
3693
+ const translatedStyles = item.styles.map((s) => translateTag(s, language));
3694
+ const translatedScenes = item.scenes.map((s) => translateTag(s, language));
3695
+ return [
3696
+ item.title,
3697
+ item.prompt,
3698
+ item.category,
3699
+ translatedCat,
3700
+ ...item.styles,
3701
+ ...translatedStyles,
3702
+ ...item.scenes,
3703
+ ...translatedScenes
3704
+ ].some((value) => value.toLowerCase().includes(query));
3705
+ });
3706
+ }, [
3707
+ source,
3708
+ search,
3709
+ category,
3710
+ style,
3711
+ scene,
3712
+ onlyFavorites,
3713
+ favorites,
3714
+ language
3715
+ ]);
3716
+ (0, react.useEffect)(() => setVisibleLimit(60), [
3717
+ search,
3718
+ category,
3719
+ style,
3720
+ scene,
3721
+ onlyFavorites
3722
+ ]);
3723
+ const visibleCases = matchingCases.slice(0, visibleLimit);
3724
+ (0, react.useEffect)(() => {
3725
+ const el = sentinelRef.current;
3726
+ if (!el) return;
3727
+ const observer = new IntersectionObserver((entries) => {
3728
+ if (entries.some((e) => e.isIntersecting)) setVisibleLimit((limit) => Math.min(limit + 48, matchingCases.length));
3729
+ }, { rootMargin: "360px" });
3730
+ observer.observe(el);
3731
+ return () => observer.disconnect();
3732
+ }, [matchingCases.length, visibleCases.length]);
3733
+ (0, react.useEffect)(() => {
3734
+ if (lightboxCase === null) return;
3735
+ const onKeyDown = (e) => {
3736
+ if (e.key === "Escape") setLightboxCase(null);
3737
+ };
3738
+ window.addEventListener("keydown", onKeyDown);
3739
+ return () => window.removeEventListener("keydown", onKeyDown);
3740
+ }, [lightboxCase]);
3741
+ const refresh = async () => {
3742
+ if (refreshing) return;
3743
+ setRefreshing(true);
3744
+ try {
3745
+ const next = await loadCatalog("POST");
3746
+ setCatalog(next);
3747
+ saveCachedInspirationCatalog(next);
3748
+ setSelected((previous) => previous === null ? next.sources[0]?.cases[0] ?? null : next.sources[0]?.cases.find((item) => item.id === previous.id) ?? next.sources[0]?.cases[0] ?? null);
3749
+ showToast(t("updated", { count: String(next.sources[0]?.cases.length ?? 0) }));
3750
+ } catch {
3751
+ showToast(t("updateFailed"), true);
3752
+ } finally {
3753
+ setRefreshing(false);
3754
+ }
3755
+ };
3756
+ const clearCache = async () => {
3757
+ if (clearingCache) return;
3758
+ setClearingCache(true);
3759
+ setConfirmClearOpen(false);
3760
+ try {
3761
+ await clearInspirationImageCache();
3762
+ const res = await fetch(`${INSPIRATION_ROUTE}/cache/clear`, {
3763
+ method: "POST",
3764
+ credentials: "same-origin"
3765
+ });
3766
+ if (!res.ok) {
3767
+ if (res.status === 404) throw new Error(t("clearCacheRestart"));
3768
+ throw new Error(`HTTP ${res.status}`);
3769
+ }
3770
+ showToast(t("cacheCleared"));
3771
+ } catch (err) {
3772
+ const msg = err instanceof Error && err.message ? err.message : t("cacheClearFailed");
3773
+ showToast(msg, true);
3774
+ } finally {
3775
+ setClearingCache(false);
3776
+ }
3777
+ };
3778
+ const copyPrompt = async () => {
3779
+ if (selected === null) return;
3780
+ try {
3781
+ await copyText(selected.prompt);
3782
+ setCopied(true);
3783
+ showToast(t("copied"));
3784
+ window.setTimeout(() => setCopied(false), 1800);
3785
+ } catch {
3786
+ showToast(t("copyFailed"), true);
3787
+ }
3788
+ };
3789
+ return (0, react_jsx_runtime.jsxs)("section", {
3790
+ className: "dsh-ig-inspiration",
3791
+ "aria-label": t("title"),
3792
+ children: [
3793
+ (0, react_jsx_runtime.jsxs)("header", {
3794
+ className: "dsh-ig-inspiration-head",
3795
+ children: [(0, react_jsx_runtime.jsxs)("div", { children: [
3796
+ (0, react_jsx_runtime.jsx)("p", {
3797
+ className: "dsh-ig-inspiration-kicker",
3798
+ children: t("kicker")
3799
+ }),
3800
+ (0, react_jsx_runtime.jsx)("h1", { children: t("title") }),
3801
+ (0, react_jsx_runtime.jsx)("p", {
3802
+ className: "dsh-ig-inspiration-subtitle",
3803
+ children: t("subtitle")
3804
+ })
3805
+ ] }), (0, react_jsx_runtime.jsxs)("div", {
3806
+ className: "dsh-ig-inspiration-head-actions",
3807
+ children: [(0, react_jsx_runtime.jsxs)("button", {
3808
+ type: "button",
3809
+ className: "dsh-ig-inspiration-clear-cache",
3810
+ onClick: () => setConfirmClearOpen(true),
3811
+ disabled: clearingCache,
3812
+ title: t("clearCache"),
3813
+ children: [clearingCache ? (0, react_jsx_runtime.jsx)(LoaderCircle, {
3814
+ className: "dsh-ig-spin",
3815
+ size: 13
3816
+ }) : (0, react_jsx_runtime.jsx)(Trash2, { size: 13 }), clearingCache ? t("clearingCache") : t("clearCache")]
3817
+ }), (0, react_jsx_runtime.jsxs)("button", {
3818
+ type: "button",
3819
+ className: "dsh-ig-inspiration-refresh",
3820
+ onClick: () => void refresh(),
3821
+ disabled: refreshing,
3822
+ title: t("refresh"),
3823
+ children: [refreshing ? (0, react_jsx_runtime.jsx)(LoaderCircle, {
3824
+ className: "dsh-ig-spin",
3825
+ size: 13
3826
+ }) : (0, react_jsx_runtime.jsx)(Sparkles, { size: 13 }), refreshing ? t("refreshing") : t("refresh")]
3827
+ })]
3828
+ })]
3829
+ }),
3830
+ catalogError ? (0, react_jsx_runtime.jsxs)("div", {
3831
+ className: "dsh-ig-inspiration-empty is-catalog-error",
3832
+ children: [
3833
+ (0, react_jsx_runtime.jsx)(TriangleAlert, { size: 28 }),
3834
+ (0, react_jsx_runtime.jsx)("p", { children: t("loadFailed") }),
3835
+ (0, react_jsx_runtime.jsx)("button", {
3836
+ type: "button",
3837
+ className: "dsh-ig-inspiration-retry-btn",
3838
+ onClick: () => void retryCatalog(),
3839
+ children: t("retry")
3840
+ })
3841
+ ]
3842
+ }) : source === null ? (0, react_jsx_runtime.jsxs)("div", {
3843
+ className: "dsh-ig-inspiration-empty",
3844
+ children: [(0, react_jsx_runtime.jsx)(LoaderCircle, {
3845
+ className: "dsh-ig-spin",
3846
+ size: 23
3847
+ }), (0, react_jsx_runtime.jsx)("span", { children: t("loading") })]
3848
+ }) : (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsxs)("div", {
3849
+ className: "dsh-ig-inspiration-toolbar",
3850
+ children: [
3851
+ (0, react_jsx_runtime.jsxs)("label", {
3852
+ className: "dsh-ig-inspiration-search",
3853
+ children: [(0, react_jsx_runtime.jsx)(Search, { size: 15 }), (0, react_jsx_runtime.jsx)("input", {
3854
+ value: search,
3855
+ onChange: (event) => setSearch(event.target.value),
3856
+ placeholder: t("search")
3857
+ })]
3858
+ }),
3859
+ (0, react_jsx_runtime.jsxs)("button", {
3860
+ type: "button",
3861
+ className: `dsh-ig-inspiration-fav-filter ${onlyFavorites ? "is-active" : ""}`,
3862
+ onClick: () => setOnlyFavorites((prev) => !prev),
3863
+ title: t("onlyFavorites"),
3864
+ children: [(0, react_jsx_runtime.jsx)(Star, {
3865
+ size: 14,
3866
+ className: onlyFavorites ? "fill-star" : ""
3867
+ }), (0, react_jsx_runtime.jsxs)("span", { children: [t("onlyFavorites"), favorites.size > 0 ? ` (${favorites.size})` : ""] })]
3868
+ }),
3869
+ (0, react_jsx_runtime.jsxs)("select", {
3870
+ value: category,
3871
+ onChange: (event) => setCategory(event.target.value),
3872
+ "aria-label": t("allCategories"),
3873
+ children: [(0, react_jsx_runtime.jsx)("option", {
3874
+ value: "",
3875
+ children: t("allCategories")
3876
+ }), source.categories.map((item) => (0, react_jsx_runtime.jsx)("option", {
3877
+ value: item,
3878
+ children: translateTag(item, language)
3879
+ }, item))]
3880
+ }),
3881
+ (0, react_jsx_runtime.jsxs)("select", {
3882
+ value: style,
3883
+ onChange: (event) => setStyle(event.target.value),
3884
+ "aria-label": t("allStyles"),
3885
+ children: [(0, react_jsx_runtime.jsx)("option", {
3886
+ value: "",
3887
+ children: t("allStyles")
3888
+ }), source.styles.map((item) => (0, react_jsx_runtime.jsx)("option", {
3889
+ value: item,
3890
+ children: translateTag(item, language)
3891
+ }, item))]
3892
+ }),
3893
+ (0, react_jsx_runtime.jsxs)("select", {
3894
+ value: scene,
3895
+ onChange: (event) => setScene(event.target.value),
3896
+ "aria-label": t("allScenes"),
3897
+ children: [(0, react_jsx_runtime.jsx)("option", {
3898
+ value: "",
3899
+ children: t("allScenes")
3900
+ }), source.scenes.map((item) => (0, react_jsx_runtime.jsx)("option", {
3901
+ value: item,
3902
+ children: translateTag(item, language)
3903
+ }, item))]
3904
+ })
3905
+ ]
3906
+ }), (0, react_jsx_runtime.jsxs)("div", {
3907
+ className: "dsh-ig-inspiration-layout",
3908
+ children: [(0, react_jsx_runtime.jsxs)("div", { children: [(0, react_jsx_runtime.jsxs)("div", {
3909
+ className: "dsh-ig-inspiration-summary",
3910
+ children: [(0, react_jsx_runtime.jsx)("strong", { children: t("results", { count: String(matchingCases.length) }) }), (0, react_jsx_runtime.jsx)("span", { children: source.version.slice(0, 8) })]
3911
+ }), matchingCases.length === 0 ? (0, react_jsx_runtime.jsx)("div", {
3912
+ className: "dsh-ig-inspiration-empty",
3913
+ children: onlyFavorites ? t("noFavorites") : t("noResults")
3914
+ }) : (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsx)("div", {
3915
+ className: "dsh-ig-inspiration-grid",
3916
+ children: visibleCases.map((item) => (0, react_jsx_runtime.jsx)(InspirationCard, {
3917
+ item,
3918
+ sourceId: source.id,
3919
+ selected: selected?.id === item.id,
3920
+ isFavorited: favorites.has(item.id),
3921
+ language,
3922
+ featuredLabel: t("featured"),
3923
+ retryLabel: t("retry"),
3924
+ onSelect: () => {
3925
+ setSelected(item);
3926
+ setCopied(false);
3927
+ },
3928
+ onToggleFavorite: () => toggleFavorite(item.id)
3929
+ }, item.id))
3930
+ }), visibleCases.length < matchingCases.length ? (0, react_jsx_runtime.jsx)("div", {
3931
+ ref: sentinelRef,
3932
+ style: {
3933
+ height: 32,
3934
+ margin: "16px 0"
3935
+ }
3936
+ }) : (0, react_jsx_runtime.jsx)("div", {
3937
+ className: "dsh-ig-inspiration-end-hint",
3938
+ children: t("allLoaded", { count: String(matchingCases.length) })
3939
+ })] })] }), (0, react_jsx_runtime.jsx)("aside", {
3940
+ className: "dsh-ig-inspiration-inspector",
3941
+ children: selected === null ? (0, react_jsx_runtime.jsxs)("div", {
3942
+ className: "dsh-ig-inspiration-inspector-empty",
3943
+ children: [(0, react_jsx_runtime.jsx)(Image$1, { size: 28 }), (0, react_jsx_runtime.jsx)("span", { children: t("selectHint") })]
3944
+ }) : (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsxs)("div", {
3945
+ className: "dsh-ig-inspiration-inspector-image",
3946
+ onClick: () => setLightboxCase({
3947
+ sourceId: source.id,
3948
+ caseId: selected.id,
3949
+ title: selected.title,
3950
+ alt: selected.imageAlt
3951
+ }),
3952
+ title: t("zoomHint"),
3953
+ children: [(0, react_jsx_runtime.jsx)(InspirationImage, {
3954
+ sourceId: source.id,
3955
+ caseId: selected.id,
3956
+ alt: selected.imageAlt,
3957
+ retryLabel: t("retry")
3958
+ }), (0, react_jsx_runtime.jsxs)("span", {
3959
+ className: "dsh-ig-inspiration-inspector-zoom-hint",
3960
+ children: [(0, react_jsx_runtime.jsx)(Maximize2, { size: 11 }), t("zoomHint")]
3961
+ })]
3962
+ }), (0, react_jsx_runtime.jsxs)("div", {
3963
+ className: "dsh-ig-inspiration-inspector-body",
3964
+ children: [
3965
+ (0, react_jsx_runtime.jsxs)("div", {
3966
+ className: "dsh-ig-inspiration-inspector-head",
3967
+ children: [(0, react_jsx_runtime.jsx)("h2", { children: selected.title }), (0, react_jsx_runtime.jsx)("button", {
3968
+ type: "button",
3969
+ className: `dsh-ig-inspiration-inspector-fav ${favorites.has(selected.id) ? "is-favorited" : ""}`,
3970
+ onClick: () => toggleFavorite(selected.id),
3971
+ title: favorites.has(selected.id) ? t("favorited") : t("favorite"),
3972
+ children: (0, react_jsx_runtime.jsx)(Star, {
3973
+ size: 15,
3974
+ className: favorites.has(selected.id) ? "fill-star" : ""
3975
+ })
3976
+ })]
3977
+ }),
3978
+ selected.sourceLabel && (0, react_jsx_runtime.jsx)("p", {
3979
+ className: "dsh-ig-inspiration-origin",
3980
+ children: selected.sourceLabel
3981
+ }),
3982
+ (0, react_jsx_runtime.jsxs)("div", {
3983
+ className: "dsh-ig-inspiration-tags",
3984
+ children: [(0, react_jsx_runtime.jsx)("span", { children: translateTag(selected.category, language) }), selected.styles.slice(0, 3).map((item) => (0, react_jsx_runtime.jsx)("span", { children: translateTag(item, language) }, item))]
3985
+ }),
3986
+ (0, react_jsx_runtime.jsxs)("div", {
3987
+ className: "dsh-ig-inspiration-prompt-label",
3988
+ children: [(0, react_jsx_runtime.jsx)("span", { children: t("prompt") }), (0, react_jsx_runtime.jsx)("span", { children: selected.prompt.length })]
3989
+ }),
3990
+ (0, react_jsx_runtime.jsx)("p", {
3991
+ className: "dsh-ig-inspiration-prompt",
3992
+ children: selected.prompt
3993
+ }),
3994
+ (0, react_jsx_runtime.jsxs)("div", {
3995
+ className: "dsh-ig-inspiration-actions",
3996
+ children: [
3997
+ (0, react_jsx_runtime.jsxs)("button", {
3998
+ type: "button",
3999
+ onClick: () => void copyPrompt(),
4000
+ children: [copied ? (0, react_jsx_runtime.jsx)(Check, { size: 14 }) : (0, react_jsx_runtime.jsx)(Clipboard, { size: 14 }), copied ? t("copied") : t("copy")]
4001
+ }),
4002
+ selected.sourceUrl ?? selected.githubUrl ? (0, react_jsx_runtime.jsxs)("a", {
4003
+ className: "dsh-ig-inspiration-source-link",
4004
+ href: selected.sourceUrl ?? selected.githubUrl,
4005
+ target: "_blank",
4006
+ rel: "noreferrer",
4007
+ children: [(0, react_jsx_runtime.jsx)(ExternalLink, { size: 14 }), t("source")]
4008
+ }) : (0, react_jsx_runtime.jsx)("span", {}),
4009
+ (0, react_jsx_runtime.jsxs)("button", {
4010
+ type: "button",
4011
+ className: "dsh-ig-inspiration-use",
4012
+ onClick: () => onUsePrompt(selected.prompt),
4013
+ children: [(0, react_jsx_runtime.jsx)(Sparkles, { size: 14 }), t("use")]
4014
+ })
4015
+ ]
4016
+ })
4017
+ ]
4018
+ })] })
4019
+ })]
4020
+ })] }),
4021
+ lightboxCase !== null && (0, react_jsx_runtime.jsx)("div", {
4022
+ className: "dsh-ig-inspiration-lightbox",
4023
+ onClick: () => setLightboxCase(null),
4024
+ children: (0, react_jsx_runtime.jsxs)("div", {
4025
+ className: "dsh-ig-inspiration-lightbox-content",
4026
+ onClick: (e) => e.stopPropagation(),
4027
+ children: [
4028
+ (0, react_jsx_runtime.jsx)("button", {
4029
+ type: "button",
4030
+ className: "dsh-ig-inspiration-lightbox-close",
4031
+ onClick: () => setLightboxCase(null),
4032
+ "aria-label": t("close"),
4033
+ children: (0, react_jsx_runtime.jsx)(X, { size: 18 })
4034
+ }),
4035
+ (0, react_jsx_runtime.jsx)("div", {
4036
+ className: "dsh-ig-inspiration-lightbox-img-wrap",
4037
+ children: (0, react_jsx_runtime.jsx)(InspirationImage, {
4038
+ sourceId: lightboxCase.sourceId,
4039
+ caseId: lightboxCase.caseId,
4040
+ alt: lightboxCase.alt,
4041
+ retryLabel: t("retry")
4042
+ })
4043
+ }),
4044
+ (0, react_jsx_runtime.jsx)("div", {
4045
+ className: "dsh-ig-inspiration-lightbox-caption",
4046
+ children: lightboxCase.title
4047
+ })
4048
+ ]
4049
+ })
4050
+ }),
4051
+ confirmClearOpen && (0, react_jsx_runtime.jsx)("div", {
4052
+ className: "dsh-ig-inspiration-modal-backdrop",
4053
+ onClick: () => setConfirmClearOpen(false),
4054
+ children: (0, react_jsx_runtime.jsxs)("div", {
4055
+ className: "dsh-ig-inspiration-modal",
4056
+ onClick: (e) => e.stopPropagation(),
4057
+ role: "dialog",
4058
+ "aria-modal": "true",
4059
+ children: [
4060
+ (0, react_jsx_runtime.jsx)("h3", { children: t("clearModalTitle") }),
4061
+ (0, react_jsx_runtime.jsx)("p", { children: t("clearModalDesc") }),
4062
+ (0, react_jsx_runtime.jsxs)("div", {
4063
+ className: "dsh-ig-inspiration-modal-actions",
4064
+ children: [(0, react_jsx_runtime.jsx)("button", {
4065
+ type: "button",
4066
+ className: "dsh-ig-inspiration-modal-btn",
4067
+ onClick: () => setConfirmClearOpen(false),
4068
+ disabled: clearingCache,
4069
+ children: t("cancel")
4070
+ }), (0, react_jsx_runtime.jsxs)("button", {
4071
+ type: "button",
4072
+ className: "dsh-ig-inspiration-modal-btn is-danger",
4073
+ onClick: () => void clearCache(),
4074
+ disabled: clearingCache,
4075
+ children: [clearingCache ? (0, react_jsx_runtime.jsx)(LoaderCircle, {
4076
+ className: "dsh-ig-spin",
4077
+ size: 13
4078
+ }) : null, t("confirmClear")]
4079
+ })]
4080
+ })
4081
+ ]
4082
+ })
4083
+ }),
4084
+ toast !== null && (0, react_jsx_runtime.jsxs)("div", {
4085
+ className: `dsh-ig-inspiration-toast ${toast.isError ? "is-error" : "is-success"}`,
4086
+ role: "status",
4087
+ children: [toast.isError ? (0, react_jsx_runtime.jsx)(X, { size: 14 }) : (0, react_jsx_runtime.jsx)(Check, { size: 14 }), (0, react_jsx_runtime.jsx)("span", { children: toast.text })]
4088
+ })
4089
+ ]
4090
+ });
4091
+ };
4092
+ const InspirationCard = ({ item, sourceId, selected, isFavorited, language, featuredLabel, retryLabel, onSelect, onToggleFavorite }) => (0, react_jsx_runtime.jsxs)("button", {
4093
+ type: "button",
4094
+ className: `dsh-ig-inspiration-card ${selected ? "is-selected" : ""}`,
4095
+ onClick: onSelect,
4096
+ children: [(0, react_jsx_runtime.jsxs)("div", {
4097
+ className: "dsh-ig-inspiration-visual",
4098
+ children: [
4099
+ item.featured && (0, react_jsx_runtime.jsxs)("span", {
4100
+ className: "dsh-ig-inspiration-featured",
4101
+ children: [(0, react_jsx_runtime.jsx)(Sparkles, { size: 10 }), featuredLabel]
4102
+ }),
4103
+ (0, react_jsx_runtime.jsx)("button", {
4104
+ type: "button",
4105
+ className: `dsh-ig-inspiration-card-star ${isFavorited ? "is-favorited" : ""}`,
4106
+ onClick: (e) => {
4107
+ e.stopPropagation();
4108
+ onToggleFavorite();
4109
+ },
4110
+ "aria-label": "收藏",
4111
+ children: (0, react_jsx_runtime.jsx)(Star, {
4112
+ size: 13,
4113
+ className: isFavorited ? "fill-star" : ""
4114
+ })
4115
+ }),
4116
+ (0, react_jsx_runtime.jsx)(InspirationImage, {
4117
+ sourceId,
4118
+ caseId: item.id,
4119
+ alt: item.imageAlt,
4120
+ retryLabel
4121
+ })
4122
+ ]
4123
+ }), (0, react_jsx_runtime.jsxs)("div", {
4124
+ className: "dsh-ig-inspiration-card-copy",
4125
+ children: [(0, react_jsx_runtime.jsx)("strong", { children: item.title }), (0, react_jsx_runtime.jsx)("span", { children: translateTag(item.category, language) })]
4126
+ })]
4127
+ });
4128
+ const InspirationImage = ({ sourceId, caseId, alt, retryLabel = "Retry" }) => {
4129
+ const [node, setNode] = (0, react.useState)(null);
4130
+ const [visible, setVisible] = (0, react.useState)(false);
4131
+ const [url, setUrl] = (0, react.useState)(null);
4132
+ const [failed, setFailed] = (0, react.useState)(false);
4133
+ const urlRef = (0, react.useRef)(null);
4134
+ (0, react.useEffect)(() => {
4135
+ if (node === null) return;
4136
+ const observer = new IntersectionObserver((entries) => setVisible(entries.some((entry) => entry.isIntersecting)), { rootMargin: "320px" });
4137
+ observer.observe(node);
4138
+ return () => observer.disconnect();
4139
+ }, [node]);
4140
+ (0, react.useEffect)(() => {
4141
+ if (!visible) return;
4142
+ let active = true;
4143
+ setFailed(false);
4144
+ if (urlRef.current !== null) {
4145
+ URL.revokeObjectURL(urlRef.current);
4146
+ urlRef.current = null;
4147
+ }
4148
+ setUrl(null);
4149
+ fetchInspirationImage(sourceId, caseId).then((blob) => {
4150
+ if (!active) return;
4151
+ const next = URL.createObjectURL(blob);
4152
+ if (urlRef.current !== null) URL.revokeObjectURL(urlRef.current);
4153
+ urlRef.current = next;
4154
+ setUrl(next);
4155
+ }).catch(() => {
4156
+ if (active) setFailed(true);
4157
+ });
4158
+ return () => {
4159
+ active = false;
4160
+ };
4161
+ }, [
4162
+ visible,
4163
+ sourceId,
4164
+ caseId
4165
+ ]);
4166
+ (0, react.useEffect)(() => {
4167
+ return () => {
4168
+ if (urlRef.current !== null) {
4169
+ URL.revokeObjectURL(urlRef.current);
4170
+ urlRef.current = null;
4171
+ }
4172
+ };
4173
+ }, []);
4174
+ return (0, react_jsx_runtime.jsx)("div", {
4175
+ ref: setNode,
4176
+ style: {
4177
+ width: "100%",
4178
+ height: "100%"
4179
+ },
4180
+ children: url !== null && !failed ? (0, react_jsx_runtime.jsx)("img", {
4181
+ src: url,
4182
+ alt,
4183
+ loading: "lazy",
4184
+ onError: () => {
4185
+ setFailed(true);
4186
+ evictInspirationImage(sourceId, caseId);
4187
+ }
4188
+ }) : (0, react_jsx_runtime.jsx)("div", {
4189
+ className: `dsh-ig-inspiration-image-placeholder ${failed ? "is-error" : ""}`,
4190
+ children: failed ? (0, react_jsx_runtime.jsx)("button", {
4191
+ type: "button",
4192
+ className: "dsh-ig-inspiration-retry-btn",
4193
+ onClick: (e) => {
4194
+ e.stopPropagation();
4195
+ setFailed(false);
4196
+ if (urlRef.current !== null) {
4197
+ URL.revokeObjectURL(urlRef.current);
4198
+ urlRef.current = null;
4199
+ }
4200
+ setUrl(null);
4201
+ evictInspirationImage(sourceId, caseId).then(() => fetchInspirationImage(sourceId, caseId)).then((blob) => {
4202
+ const next = URL.createObjectURL(blob);
4203
+ if (urlRef.current !== null) URL.revokeObjectURL(urlRef.current);
4204
+ urlRef.current = next;
4205
+ setUrl(next);
4206
+ }).catch(() => setFailed(true));
4207
+ },
4208
+ children: retryLabel
4209
+ }) : (0, react_jsx_runtime.jsx)(LoaderCircle, {
4210
+ className: "dsh-ig-spin",
4211
+ size: 18
4212
+ })
4213
+ })
4214
+ });
4215
+ };
4216
+ async function copyText(value) {
4217
+ if (navigator.clipboard?.writeText) return navigator.clipboard.writeText(value);
4218
+ const textarea = document.createElement("textarea");
4219
+ textarea.value = value;
4220
+ textarea.style.position = "fixed";
4221
+ textarea.style.opacity = "0";
4222
+ document.body.appendChild(textarea);
4223
+ textarea.select();
4224
+ const copied = document.execCommand("copy");
4225
+ textarea.remove();
4226
+ if (!copied) throw new Error("copy-failed");
4227
+ }
4228
+ function resolveActiveCatalog(bundled, cached) {
4229
+ if (!bundled) return cached;
4230
+ if (!cached) return bundled;
4231
+ const bundledSource = bundled.sources[0];
4232
+ const cachedSource = cached.sources[0];
4233
+ if (!bundledSource) return cached;
4234
+ if (!cachedSource) return bundled;
4235
+ if (bundledSource.version === cachedSource.version) return bundled;
4236
+ const bundledTime = bundledSource.updatedAt ? Date.parse(bundledSource.updatedAt) : NaN;
4237
+ const cachedTime = cachedSource.updatedAt ? Date.parse(cachedSource.updatedAt) : NaN;
4238
+ if (Number.isFinite(bundledTime) && Number.isFinite(cachedTime)) return cachedTime > bundledTime ? cached : bundled;
4239
+ return cachedSource.cases.length > bundledSource.cases.length ? cached : bundled;
4240
+ }
4241
+ //#endregion
2531
4242
  //#region lib/types/client/conversation-regenerate.js
2532
4243
  /** Build the workbench request that reproduces a conversation image with an edited prompt. */
2533
4244
  function conversationRegenerateRequest(image, prompt, remembered) {
@@ -2592,6 +4303,7 @@ window.__ModuleLoader__.load({
2592
4303
  zh: {
2593
4304
  tabGallery: "图库",
2594
4305
  tabStudio: "工作台",
4306
+ tabInspiration: "灵感",
2595
4307
  tabFavorites: "收藏",
2596
4308
  tabCompare: "对比",
2597
4309
  tabTasks: "任务",
@@ -2673,6 +4385,7 @@ window.__ModuleLoader__.load({
2673
4385
  en: {
2674
4386
  tabGallery: "Gallery",
2675
4387
  tabStudio: "Studio",
4388
+ tabInspiration: "Inspiration",
2676
4389
  tabFavorites: "Favorites",
2677
4390
  tabCompare: "Compare",
2678
4391
  tabTasks: "Tasks",
@@ -2810,6 +4523,7 @@ window.__ModuleLoader__.load({
2810
4523
  const GalleryViewTab = (props) => {
2811
4524
  const { locale, sessionId, useSessions, useWorkspaces } = props;
2812
4525
  const [activeTab, setActiveTab] = (0, react.useState)("gallery");
4526
+ const [studioDraft, setStudioDraft] = (0, react.useState)(void 0);
2813
4527
  const [items, setItems] = (0, react.useState)([]);
2814
4528
  const [search, setSearch] = (0, react.useState)("");
2815
4529
  const [selectedProvider, setSelectedProvider] = (0, react.useState)("all");
@@ -2920,6 +4634,11 @@ window.__ModuleLoader__.load({
2920
4634
  setToast(null);
2921
4635
  }, 2e3);
2922
4636
  };
4637
+ const useInspirationPrompt = (0, react.useCallback)((prompt) => {
4638
+ setStudioDraft(prompt);
4639
+ setActiveTab("studio");
4640
+ }, []);
4641
+ const clearStudioDraft = (0, react.useCallback)(() => setStudioDraft(void 0), []);
2923
4642
  (0, react.useEffect)(() => {
2924
4643
  const seat = document.querySelector("[data-composer-seat]");
2925
4644
  if (seat) {
@@ -3324,6 +5043,12 @@ window.__ModuleLoader__.load({
3324
5043
  onClick: () => setActiveTab("studio"),
3325
5044
  children: [(0, react_jsx_runtime.jsx)(SlidersHorizontal, { size: 15 }), (0, react_jsx_runtime.jsx)("span", { children: t("tabStudio") })]
3326
5045
  }),
5046
+ (0, react_jsx_runtime.jsxs)("button", {
5047
+ type: "button",
5048
+ className: `dsh-ig-studio-tab-btn ${activeTab === "inspiration" ? "is-active" : ""}`,
5049
+ onClick: () => setActiveTab("inspiration"),
5050
+ children: [(0, react_jsx_runtime.jsx)(Sparkles, { size: 15 }), (0, react_jsx_runtime.jsx)("span", { children: t("tabInspiration") })]
5051
+ }),
3327
5052
  (0, react_jsx_runtime.jsxs)("button", {
3328
5053
  type: "button",
3329
5054
  className: `dsh-ig-studio-tab-btn ${activeTab === "favorites" ? "is-active" : ""}`,
@@ -3535,9 +5260,15 @@ window.__ModuleLoader__.load({
3535
5260
  onBlobLoaded: (b) => blobCache.set(item.id, b),
3536
5261
  onToast: showToast
3537
5262
  }, item.id))
5263
+ }) : activeTab === "inspiration" ? (0, react_jsx_runtime.jsx)(InspirationView, {
5264
+ locale,
5265
+ onUsePrompt: useInspirationPrompt
3538
5266
  }) : (0, react_jsx_runtime.jsx)(StudioView, {
3539
5267
  locale,
3540
- workspace: activeWorkspace
5268
+ workspace: activeWorkspace,
5269
+ initialPrompt: studioDraft,
5270
+ onInitialPromptApplied: clearStudioDraft,
5271
+ onOpenInspiration: () => setActiveTab("inspiration")
3541
5272
  })
3542
5273
  }),
3543
5274
  toast && (0, react_jsx_runtime.jsx)("div", {
@@ -4103,13 +5834,15 @@ window.__ModuleLoader__.load({
4103
5834
  .is-configured .dsh-ig-status-item-tag{color:#059669;background:rgba(16,185,129,.14)}
4104
5835
  .is-missing .dsh-ig-status-item-tag{color:#dc2626;background:rgba(239,68,68,.14)}
4105
5836
  .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}
5837
+ .dsh-ig-canvas.is-dragging,.dsh-ig-canvas.is-dragging *{cursor:grabbing!important}
4107
5838
  .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
5839
  .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
5840
  .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
5841
  .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
5842
  .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)}
5843
+ .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)}
5844
+ .dsh-ig-generation-kind{display:grid;grid-template-columns:1fr 1fr;gap:4px;padding:3px;border:1px solid var(--ig-line);border-radius:9px;background:var(--ig-soft)}.dsh-ig-generation-kind button{height:31px;border:0;border-radius:6px;background:transparent;color:#778196;font-size:11.5px;display:flex;align-items:center;justify-content:center;gap:6px;cursor:pointer;transition:background .15s,color .15s,box-shadow .15s}.dsh-ig-generation-kind button.is-active{background:var(--background,#fff);color:var(--ig-blue);font-weight:650;box-shadow:0 2px 8px rgba(30,42,76,.09)}
5845
+ .dsh-ig-compare-box{padding:10px;border:1px solid rgba(47,100,245,.2);border-radius:10px;background:linear-gradient(145deg,rgba(47,100,245,.055),rgba(99,102,241,.018));display:flex;flex-direction:column;gap:9px}.dsh-ig-compare-heading{display:flex;align-items:flex-start;justify-content:space-between;gap:8px}.dsh-ig-compare-heading>div{min-width:0;display:flex;flex-direction:column;gap:2px}.dsh-ig-compare-heading strong{font-size:11.5px}.dsh-ig-compare-heading small{font-size:9.5px;color:#838da1}.dsh-ig-compare-heading>span{flex:none;padding:2px 6px;border-radius:999px;background:rgba(47,100,245,.11);color:var(--ig-blue);font-size:9.5px;font-weight:650}.dsh-ig-model-checks{display:flex;flex-direction:column;gap:6px}.dsh-ig-model-checks>button{min-height:43px;width:100%;padding:6px 7px;display:grid;grid-template-columns:18px minmax(0,1fr) auto;align-items:center;gap:7px;border:1px solid var(--ig-line);border-radius:8px;background:var(--background,#fff);text-align:left;cursor:pointer;transition:border-color .15s,background .15s,transform .15s}.dsh-ig-model-checks>button:hover{border-color:#9cb2ef;transform:translateY(-1px)}.dsh-ig-model-checks>button.is-selected{border-color:#86a4fa;background:rgba(47,100,245,.055);box-shadow:inset 3px 0 0 var(--ig-blue)}.dsh-ig-model-checkmark{width:17px;height:17px;border:1px solid #bdc6d8;border-radius:5px;display:grid;place-items:center;color:#fff;background:var(--background,#fff)}.dsh-ig-model-checks>button.is-selected .dsh-ig-model-checkmark{border-color:var(--ig-blue);background:var(--ig-blue)}.dsh-ig-model-copy{display:flex;flex-direction:column;min-width:0}.dsh-ig-model-copy strong{font-size:11px;line-height:15px}.dsh-ig-model-copy small{max-width:130px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#80899b;font-size:9.5px;line-height:14px}.dsh-ig-model-settings{display:flex;flex-direction:column;align-items:flex-end;gap:2px;color:#596277;font-size:9.5px;white-space:nowrap}.dsh-ig-model-settings em{font-style:normal;color:#a16a0a;font-size:8.5px}.dsh-ig-compare-box>p{margin:0;color:#8a7180;font-size:9.5px;line-height:14px}
4113
5846
  .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
5847
  .dsh-ig-reference-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(64px,1fr));gap:8px}
4115
5848
  .dsh-ig-reference-card{position:relative;aspect-ratio:1;border-radius:8px;overflow:hidden;background:#eef1f7;border:1px solid var(--ig-line)}
@@ -4143,11 +5876,64 @@ window.__ModuleLoader__.load({
4143
5876
  @media(max-width:900px){.dsh-ig-gallery-page-body.is-workbench{height:auto;overflow:auto}.dsh-ig-workbench{height:auto;min-height:100%;overflow:visible;padding:12px}.dsh-ig-workbench-heading{height:auto;padding-bottom:12px;align-items:center}.dsh-ig-provider-quick>span{display:none}.dsh-ig-workbench-grid{height:auto;display:flex;flex:none;flex-direction:column;overflow:visible}.dsh-ig-recent-panel{height:auto;overflow:hidden;border-right:0;border-bottom:1px solid var(--ig-line)}.dsh-ig-recent-scroll{height:auto;display:flex;overflow-x:auto;overflow-y:hidden;padding:8px}.dsh-ig-recent-item{min-width:220px}.dsh-ig-canvas-column{height:auto;min-height:560px}.dsh-ig-generate-panel{height:auto;border-left:0;border-top:1px solid var(--ig-line);min-height:560px}.dsh-ig-generator-form{height:auto;overflow:visible}.dsh-ig-canvas{height:auto;min-height:360px}}
4144
5877
  @media(max-width:560px){.dsh-ig-workbench-heading>div{display:block}.dsh-ig-workbench-heading h1{font-size:15px}.dsh-ig-provider-quick select{min-width:128px;max-width:150px}.dsh-ig-canvas{padding:12px;min-height:300px}.dsh-ig-result-strip,.dsh-ig-result-actions{align-items:flex-start;flex-direction:column}.dsh-ig-result-strip>div,.dsh-ig-result-actions>div{width:100%}.dsh-ig-result-strip button,.dsh-ig-result-actions button{flex:1;justify-content:center}.dsh-ig-result-actions button span{display:inline}.dsh-ig-field-grid{grid-template-columns:1fr}.dsh-ig-result-meta span+span:before{display:none}}
4145
5878
  @media(prefers-reduced-motion:reduce){.dsh-ig-workbench *{scroll-behavior:auto!important;animation-duration:.01ms!important;transition-duration:.01ms!important}}
5879
+ .dsh-ig-field-label>span{display:flex;align-items:center;gap:5px}.dsh-ig-field-label .dsh-ig-find-inspiration{display:inline-flex;align-items:center;gap:3px;color:var(--ig-blue);font-weight:600}.dsh-ig-field-label .dsh-ig-find-inspiration:hover{color:#1644c1}
4146
5880
  .dsh-ig-workspace-filter-label{height:30px;display:inline-flex;align-items:center;gap:6px;padding:0 10px;border:1px solid var(--ig-line,rgba(0,0,0,.1));border-radius:7px;background:var(--background,#fff);font-size:11.5px;color:var(--foreground,#242f44);cursor:pointer;user-select:none;transition:border-color .15s,background .15s}
4147
5881
  .dsh-ig-workspace-filter-label:hover{border-color:#9bb4f5;background:rgba(47,100,245,.04)}
4148
5882
  .dsh-ig-workspace-checkbox{cursor:pointer;accent-color:#2f64f5;width:13.5px;height:13.5px;margin:0}
4149
5883
  .dsh-ig-workspace-filter-text{font-weight:500;white-space:nowrap}
4150
5884
  .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}
5885
+ .dsh-ig-canvas-cluster{display:grid;gap:16px;align-items:center;justify-content:center;user-select:none;pointer-events:auto;flex-shrink:0}
5886
+ .dsh-ig-canvas-cluster[data-count="2"]{grid-template-columns:repeat(2,1fr);max-width:880px}
5887
+ .dsh-ig-canvas-cluster[data-count="3"],.dsh-ig-canvas-cluster[data-count="4"]{grid-template-columns:repeat(2,1fr);max-width:760px}
5888
+ .dsh-ig-canvas-item{position:relative;display:flex;align-items:center;justify-content:center;border-radius:10px;cursor:pointer;transition:transform .12s ease-out}
5889
+ .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}
5890
+ .dsh-ig-canvas-item:hover img{box-shadow:0 14px 40px rgba(47,100,245,.2)}
5891
+ .dsh-ig-canvas-item.is-active img{box-shadow:0 14px 40px rgba(47,100,245,.28)}
5892
+ .dsh-ig-canvas-item.is-checked img{outline:3px solid var(--ig-blue);outline-offset:2px;box-shadow:0 14px 40px rgba(47,100,245,.35)}
5893
+ .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}
5894
+ .dsh-ig-canvas-badge.is-model{max-width:calc(100% - 20px);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:600}
5895
+ .dsh-ig-canvas-item.is-checked .dsh-ig-canvas-badge{background:var(--ig-blue)}
5896
+ .dsh-ig-canvas-check{position:absolute;top:10px;right:10px;width:22px;height:22px;border:1.5px solid rgba(255,255,255,.85);border-radius:50%;display:grid;place-items:center;background:rgba(15,23,42,.48);color:#fff;box-shadow:0 2px 8px rgba(15,23,42,.18);backdrop-filter:blur(4px);z-index:2;pointer-events:none}.dsh-ig-canvas-item.is-checked .dsh-ig-canvas-check{border-color:#fff;background:var(--ig-blue)}
5897
+ .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}
5898
+ .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)}
5899
+ .dsh-ig-lightbox-nav:hover{background:rgba(47,100,245,.85);transform:translateY(-50%) scale(1.08)}
5900
+ .dsh-ig-lightbox-nav.is-prev{left:18px}
5901
+ .dsh-ig-lightbox-nav.is-next{right:18px}
5902
+ .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}
5903
+ .dsh-ig-save-gallery-btn:hover{background:rgba(47,100,245,.12)!important}
5904
+ .dsh-ig-save-gallery-btn.is-saved{color:#64748b!important;border-color:var(--ig-line)!important;background:transparent!important}
5905
+ `;
5906
+ //#endregion
5907
+ //#region lib/types/client/inspiration-style.js
5908
+ const INSPIRATION_STYLE = `
5909
+ .dsh-ig-inspiration{--inspiration-blue:#2f64f5;--inspiration-ink:#172033;--inspiration-muted:#718099;--inspiration-line:color-mix(in srgb,currentColor 11%,transparent);min-height:100%;padding:18px 24px 26px;background:radial-gradient(ellipse 80% 45% at 50% -20%,rgba(47,100,245,.12),transparent 70%),var(--dsw-alias-bg-layer-1,#fff);color:var(--foreground,var(--inspiration-ink));box-sizing:border-box}
5910
+ .dsh-ig-inspiration-head{display:flex;align-items:flex-start;justify-content:space-between;gap:18px;margin-bottom:18px}.dsh-ig-inspiration-head-actions{display:flex;align-items:center;gap:8px}.dsh-ig-inspiration-kicker{margin:0 0 4px;color:var(--inspiration-blue);font-size:11px;letter-spacing:.08em;font-weight:700;text-transform:uppercase}.dsh-ig-inspiration-head h1{margin:0;font-size:18px;letter-spacing:-.025em;line-height:1.25}.dsh-ig-inspiration-subtitle{margin:5px 0 0;color:var(--inspiration-muted);font-size:12px;line-height:1.5}.dsh-ig-inspiration-refresh,.dsh-ig-inspiration-clear-cache{height:32px;padding:0 11px;display:inline-flex;align-items:center;gap:6px;border:1px solid var(--inspiration-line);border-radius:7px;background:var(--dsw-alias-bg-layer-1,#fff);color:var(--inspiration-muted);font:inherit;font-size:12px;white-space:nowrap;cursor:pointer;transition:all .15s ease}.dsh-ig-inspiration-refresh:hover:not(:disabled){border-color:rgba(47,100,245,.42);color:var(--inspiration-blue);background:rgba(47,100,245,.04)}.dsh-ig-inspiration-clear-cache:hover:not(:disabled){border-color:rgba(239,68,68,.4);color:#ef4444;background:rgba(239,68,68,.04)}.dsh-ig-inspiration-refresh:disabled,.dsh-ig-inspiration-clear-cache:disabled{opacity:.55;cursor:wait}
5911
+ .dsh-ig-inspiration-toolbar{display:grid;grid-template-columns:minmax(180px,1fr) auto repeat(3,minmax(110px,140px));gap:8px;padding:10px;border:1px solid var(--inspiration-line);border-radius:10px;background:color-mix(in srgb,var(--dsw-alias-bg-layer-1,#fff) 90%,#edf2ff);margin-bottom:16px}.dsh-ig-inspiration-search{min-width:0;position:relative}.dsh-ig-inspiration-search svg{position:absolute;left:10px;top:50%;transform:translateY(-50%);color:#8995aa}.dsh-ig-inspiration-toolbar input,.dsh-ig-inspiration-toolbar select{width:100%;height:34px;box-sizing:border-box;border:1px solid transparent;border-radius:6px;background:var(--dsw-alias-bg-layer-1,#fff);color:inherit;padding:0 9px;font:inherit;font-size:12px;outline:none}.dsh-ig-inspiration-toolbar input{padding-left:31px}.dsh-ig-inspiration-toolbar input:focus,.dsh-ig-inspiration-toolbar select:focus{border-color:var(--inspiration-blue);box-shadow:0 0 0 3px rgba(47,100,245,.1)}
5912
+ .dsh-ig-inspiration-fav-filter{height:34px;padding:0 12px;display:inline-flex;align-items:center;gap:6px;border:1px solid var(--inspiration-line);border-radius:6px;background:var(--dsw-alias-bg-layer-1,#fff);color:var(--inspiration-muted);font:inherit;font-size:12px;cursor:pointer;white-space:nowrap;transition:all .15s ease}.dsh-ig-inspiration-fav-filter:hover{border-color:rgba(47,100,245,.4);color:var(--inspiration-blue)}.dsh-ig-inspiration-fav-filter.is-active{background:#eef3ff;border-color:var(--inspiration-blue);color:var(--inspiration-blue);font-weight:600}.fill-star{fill:currentColor}
5913
+ .dsh-ig-inspiration-layout{display:grid;grid-template-columns:minmax(0,1fr) minmax(260px,310px);gap:18px;align-items:start}.dsh-ig-inspiration-summary{height:24px;display:flex;align-items:center;justify-content:space-between;color:var(--inspiration-muted);font-size:11.5px}.dsh-ig-inspiration-summary strong{color:inherit;font-weight:650}.dsh-ig-inspiration-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(185px,1fr));gap:12px}.dsh-ig-inspiration-card{position:relative;min-width:0;padding:0;border:1px solid var(--inspiration-line);border-radius:10px;overflow:hidden;background:var(--dsw-alias-bg-layer-1,#fff);text-align:left;cursor:pointer;transition:transform .16s ease,border-color .16s ease,box-shadow .16s ease;content-visibility:auto;contain-intrinsic-size:245px}.dsh-ig-inspiration-card:hover,.dsh-ig-inspiration-card.is-selected{border-color:rgba(47,100,245,.58);box-shadow:0 8px 22px rgba(37,62,123,.11)}.dsh-ig-inspiration-card:hover{transform:translateY(-2px)}.dsh-ig-inspiration-card.is-selected:after{content:'';position:absolute;inset:0;border:2px solid var(--inspiration-blue);border-radius:9px;pointer-events:none}.dsh-ig-inspiration-visual{aspect-ratio:1/1;position:relative;background:linear-gradient(135deg,#f0f4fc,#e3eafa);overflow:hidden}.dsh-ig-inspiration-visual img{width:100%;height:100%;object-fit:cover;display:block}.dsh-ig-inspiration-image-placeholder{height:100%;display:grid;place-items:center;color:#8a99b3}.dsh-ig-inspiration-image-placeholder.is-error{font-size:11px;padding:10px;text-align:center}.dsh-ig-inspiration-card-copy{padding:9px 10px 10px}.dsh-ig-inspiration-card-copy strong{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px;line-height:17px}.dsh-ig-inspiration-card-copy span{display:block;margin-top:3px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--inspiration-muted);font-size:10.5px}.dsh-ig-inspiration-featured{position:absolute;top:8px;left:8px;display:inline-flex;align-items:center;gap:3px;padding:3px 6px;border-radius:999px;background:rgba(20,31,52,.72);backdrop-filter:blur(5px);color:#fff;font-size:10px;font-weight:600;z-index:1}.dsh-ig-inspiration-card-star{position:absolute;top:8px;right:8px;width:26px;height:26px;border-radius:50%;background:rgba(20,31,52,.6);backdrop-filter:blur(4px);border:none;color:#fff;display:grid;place-items:center;cursor:pointer;z-index:2;opacity:0;transition:opacity .15s,transform .15s}.dsh-ig-inspiration-card:hover .dsh-ig-inspiration-card-star,.dsh-ig-inspiration-card-star.is-favorited{opacity:1}.dsh-ig-inspiration-card-star.is-favorited{color:#f59e0b;background:rgba(20,31,52,.85)}.dsh-ig-inspiration-card-star:hover{transform:scale(1.15)}
5914
+ .dsh-ig-inspiration-empty{min-height:310px;display:grid;place-items:center;padding:30px;text-align:center;color:var(--inspiration-muted);border:1px dashed var(--inspiration-line);border-radius:10px;font-size:12px}
5915
+ .dsh-ig-inspiration-empty.is-catalog-error{display:flex;flex-direction:column;gap:12px;color:var(--inspiration-text)}
5916
+ .dsh-ig-inspiration-empty.is-catalog-error p{margin:0;font-size:13px;color:var(--inspiration-muted)}
5917
+ .dsh-ig-inspiration-toast{position:fixed;bottom:28px;left:50%;transform:translateX(-50%);display:inline-flex;align-items:center;gap:7px;padding:9px 16px;border-radius:999px;background:rgba(15,23,42,.92);backdrop-filter:blur(8px);color:#fff;font-size:12px;font-weight:500;box-shadow:0 10px 30px rgba(0,0,0,.35);z-index:9999;animation:dsh-ig-slide-up .2s ease}
5918
+ .dsh-ig-inspiration-toast.is-error{background:rgba(185,28,28,.92)}
5919
+ @keyframes dsh-ig-slide-up{from{opacity:0;transform:translate(-50%,12px)}to{opacity:1;transform:translate(-50%,0)}}
5920
+ .dsh-ig-inspiration-modal-backdrop{position:fixed;inset:0;z-index:10001;background:rgba(15,23,42,.6);backdrop-filter:blur(4px);display:flex;align-items:center;justify-content:center;padding:16px;box-sizing:border-box}
5921
+ .dsh-ig-inspiration-modal{width:100%;max-width:400px;background:var(--dsw-alias-bg-layer-1,#fff);border:1px solid var(--inspiration-line);border-radius:12px;padding:22px;box-shadow:0 24px 60px rgba(0,0,0,.3);animation:dsh-ig-scale-in .15s ease}
5922
+ @keyframes dsh-ig-scale-in{from{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}
5923
+ .dsh-ig-inspiration-modal h3{margin:0 0 8px;font-size:15px;font-weight:650;color:inherit}
5924
+ .dsh-ig-inspiration-modal p{margin:0 0 20px;color:var(--inspiration-muted);font-size:12.5px;line-height:1.55}
5925
+ .dsh-ig-inspiration-modal-actions{display:flex;justify-content:flex-end;gap:10px}
5926
+ .dsh-ig-inspiration-modal-btn{height:34px;padding:0 14px;border-radius:7px;border:1px solid var(--inspiration-line);background:var(--dsw-alias-bg-layer-1,#fff);color:inherit;font:inherit;font-size:12px;font-weight:600;cursor:pointer;transition:all .15s ease}
5927
+ .dsh-ig-inspiration-modal-btn:hover{background:color-mix(in srgb,currentColor 6%,transparent)}
5928
+ .dsh-ig-inspiration-modal-btn.is-danger{background:#ef4444;border-color:#ef4444;color:#fff}
5929
+ .dsh-ig-inspiration-modal-btn.is-danger:hover{background:#dc2626}
5930
+ .dsh-ig-inspiration-modal-btn:disabled{opacity:.5;cursor:wait}
5931
+ .dsh-ig-inspiration-retry-btn{border:none;background:rgba(47,100,245,.08);color:var(--inspiration-blue);padding:6px 12px;border-radius:6px;font:inherit;font-size:11px;font-weight:600;cursor:pointer;transition:background .15s}
5932
+ .dsh-ig-inspiration-retry-btn:hover{background:rgba(47,100,245,.18)}
5933
+ .dsh-ig-inspiration-end-hint{display:flex;align-items:center;justify-content:center;padding:24px 0 16px;color:var(--inspiration-muted);font-size:12px}
5934
+ .dsh-ig-inspiration-inspector{position:sticky;top:0;overflow:hidden;border:1px solid var(--inspiration-line);border-radius:11px;background:var(--dsw-alias-bg-layer-1,#fff);box-shadow:0 9px 28px rgba(27,45,88,.06)}.dsh-ig-inspiration-inspector-empty{min-height:310px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:9px;padding:22px;color:var(--inspiration-muted);font-size:12px;text-align:center}.dsh-ig-inspiration-inspector-image{aspect-ratio:16/10;background:#f1f4fa;position:relative;cursor:zoom-in}.dsh-ig-inspiration-inspector-image img{width:100%;height:100%;object-fit:cover}.dsh-ig-inspiration-inspector-zoom-hint{position:absolute;bottom:8px;right:8px;display:inline-flex;align-items:center;gap:4px;padding:4px 8px;border-radius:6px;background:rgba(15,23,42,.75);backdrop-filter:blur(5px);color:#fff;font-size:10.5px;font-weight:500;opacity:0;transition:opacity .15s ease;pointer-events:none}.dsh-ig-inspiration-inspector-image:hover .dsh-ig-inspiration-inspector-zoom-hint{opacity:1}.dsh-ig-inspiration-inspector-body{padding:14px}.dsh-ig-inspiration-inspector-head{display:flex;align-items:flex-start;justify-content:space-between;gap:8px}.dsh-ig-inspiration-inspector-body h2{margin:0;font-size:14px;line-height:1.4;flex:1}.dsh-ig-inspiration-inspector-fav{width:30px;height:30px;flex-shrink:0;border:1px solid var(--inspiration-line);border-radius:7px;background:var(--dsw-alias-bg-layer-1,#fff);color:var(--inspiration-muted);display:grid;place-items:center;cursor:pointer;transition:all .15s ease}.dsh-ig-inspiration-inspector-fav:hover{border-color:rgba(245,158,11,.4);color:#f59e0b}.dsh-ig-inspiration-inspector-fav.is-favorited{color:#f59e0b;border-color:rgba(245,158,11,.5);background:rgba(245,158,11,.08)}.dsh-ig-inspiration-origin{margin:5px 0 12px;color:var(--inspiration-muted);font-size:11px}.dsh-ig-inspiration-tags{display:flex;flex-wrap:wrap;gap:5px;margin:0 0 12px}.dsh-ig-inspiration-tags span{max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding:3px 6px;border-radius:4px;background:#eef3ff;color:#3e5fb8;font-size:10px}.dsh-ig-inspiration-prompt-label{display:flex;align-items:center;justify-content:space-between;margin:0 0 6px;color:var(--inspiration-muted);font-size:10.5px;font-weight:650}.dsh-ig-inspiration-prompt{max-height:155px;margin:0;padding:9px;overflow:auto;white-space:pre-wrap;border:1px solid var(--inspiration-line);border-radius:7px;background:color-mix(in srgb,var(--dsw-alias-bg-layer-2,#f8fafc) 90%,#e9efff);font-size:11px;line-height:1.55;color:#526078}.dsh-ig-inspiration-actions{display:grid;grid-template-columns:1fr 1fr;gap:7px;margin-top:12px}.dsh-ig-inspiration-actions button,.dsh-ig-inspiration-source-link{height:34px;display:inline-flex;align-items:center;justify-content:center;gap:6px;border:1px solid var(--inspiration-line);border-radius:7px;background:var(--dsw-alias-bg-layer-1,#fff);color:#536075;font:inherit;font-size:11.5px;font-weight:600;cursor:pointer;text-decoration:none}.dsh-ig-inspiration-actions button:hover,.dsh-ig-inspiration-source-link:hover{border-color:rgba(47,100,245,.46);color:var(--inspiration-blue)}.dsh-ig-inspiration-actions .dsh-ig-inspiration-use{grid-column:1/-1;border-color:var(--inspiration-blue);background:linear-gradient(135deg,#386cf6,#2758e7);color:#fff;box-shadow:0 7px 15px rgba(47,100,245,.2)}.dsh-ig-inspiration-actions .dsh-ig-inspiration-use:hover{color:#fff;transform:translateY(-1px)}
5935
+ .dsh-ig-inspiration-lightbox{position:fixed;inset:0;z-index:10000;background:rgba(8,12,22,.88);backdrop-filter:blur(8px);display:flex;align-items:center;justify-content:center;padding:24px;box-sizing:border-box}.dsh-ig-inspiration-lightbox-content{position:relative;max-width:90vw;max-height:90vh;display:flex;flex-direction:column;align-items:center}.dsh-ig-inspiration-lightbox-img-wrap{max-width:85vw;max-height:82vh;border-radius:10px;overflow:hidden;box-shadow:0 25px 60px rgba(0,0,0,.7);background:#05070d}.dsh-ig-inspiration-lightbox-img-wrap img{width:auto;height:auto;max-width:85vw;max-height:82vh;object-fit:contain;display:block}.dsh-ig-inspiration-lightbox-close{position:absolute;top:-16px;right:-16px;width:34px;height:34px;border-radius:50%;background:#1e293b;color:#fff;border:1px solid rgba(255,255,255,.2);cursor:pointer;display:grid;place-items:center;transition:background .15s,transform .15s;z-index:10}.dsh-ig-inspiration-lightbox-close:hover{background:#334155;transform:scale(1.1)}.dsh-ig-inspiration-lightbox-caption{margin-top:12px;color:rgba(255,255,255,.9);font-size:13px;font-weight:500;text-shadow:0 2px 4px rgba(0,0,0,.6)}
5936
+ @media(max-width:980px){.dsh-ig-inspiration-layout{grid-template-columns:1fr}.dsh-ig-inspiration-inspector{position:relative;top:auto}.dsh-ig-inspiration-inspector-empty{min-height:150px}.dsh-ig-inspiration-inspector-image{max-height:290px}.dsh-ig-inspiration-inspector-image img{object-fit:contain;background:#f1f4fa}}@media(max-width:700px){.dsh-ig-inspiration{padding:14px}.dsh-ig-inspiration-head{align-items:center}.dsh-ig-inspiration-toolbar{grid-template-columns:1fr 1fr}.dsh-ig-inspiration-search{grid-column:1/-1}.dsh-ig-inspiration-fav-filter{grid-column:1/-1}.dsh-ig-inspiration-grid{grid-template-columns:repeat(auto-fill,minmax(145px,1fr));gap:9px}}@media(max-width:420px){.dsh-ig-inspiration-toolbar{grid-template-columns:1fr}.dsh-ig-inspiration-search{grid-column:auto}}
4151
5937
  `;
4152
5938
  //#endregion
4153
5939
  //#region lib/types/client/image-result-node.js
@@ -4765,7 +6551,7 @@ window.__ModuleLoader__.load({
4765
6551
  ctx.effect(() => {
4766
6552
  const style = document.createElement("style");
4767
6553
  style.dataset.plugin = "dsh-image-gen";
4768
- style.textContent = `${STYLE}\n${STUDIO_STYLE}`;
6554
+ style.textContent = `${STYLE}\n${STUDIO_STYLE}\n${INSPIRATION_STYLE}`;
4769
6555
  document.head.appendChild(style);
4770
6556
  return () => {
4771
6557
  style.remove();