dsh-image-gen 0.3.1 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/README.en.md +256 -90
  2. package/README.md +250 -94
  3. package/README.zh-CN.md +2 -171
  4. package/docs/assets/readme/chat-example.webp +0 -0
  5. package/docs/assets/readme/comfyui-workflows.webp +0 -0
  6. package/docs/assets/readme/gallery-management.webp +0 -0
  7. package/docs/assets/readme/hero-en.webp +0 -0
  8. package/docs/assets/readme/hero-zh.webp +0 -0
  9. package/docs/assets/readme/inspiration-library.webp +0 -0
  10. package/docs/assets/readme/multi-model-compare.webp +0 -0
  11. package/docs/assets/readme/provider-settings.webp +0 -0
  12. package/docs/assets/readme/regenerating.webp +0 -0
  13. package/docs/assets/readme/revision-switcher.webp +0 -0
  14. package/docs/assets/readme/studio-workbench.webp +0 -0
  15. package/docs/assets/readme/workflow-overview.webp +0 -0
  16. package/lib/client.js +1593 -68
  17. package/lib/client.js.map +1 -1
  18. package/lib/index.js +17177 -1427
  19. package/lib/types/client/gallery-view.d.ts +3 -1
  20. package/lib/types/client/image-ref.d.ts +24 -1
  21. package/lib/types/client/index.d.ts +9 -1
  22. package/lib/types/client/inspiration-catalog-cache.d.ts +4 -0
  23. package/lib/types/client/inspiration-image-cache.d.ts +3 -0
  24. package/lib/types/client/inspiration-style.d.ts +1 -0
  25. package/lib/types/client/inspiration-view.d.ts +8 -0
  26. package/lib/types/client/multi-model-compare.d.ts +11 -0
  27. package/lib/types/client/studio-style.d.ts +1 -1
  28. package/lib/types/client/studio-view.d.ts +3 -0
  29. package/lib/types/index.d.ts +1 -0
  30. package/lib/types/inspiration-route.d.ts +6 -0
  31. package/lib/types/inspiration.d.ts +53 -0
  32. package/lib/types/shared.d.ts +2 -0
  33. package/package.json +21 -25
package/lib/client.js CHANGED
@@ -13,6 +13,8 @@ window.__ModuleLoader__.load({
13
13
  const DELETE_ROUTE = "/plugins/dsh-image-gen/delete";
14
14
  /** Same-origin route used by the browser image workbench. */
15
15
  const STUDIO_ROUTE = "/plugins/dsh-image-gen/studio";
16
+ /** Same-origin route for built-in prompt inspiration metadata and images. */
17
+ const INSPIRATION_ROUTE = "/plugins/dsh-image-gen/inspiration";
16
18
  /** Browser route used for saving generated images to workspace on demand. */
17
19
  const SAVE_WORKSPACE_ROUTE = "/plugins/dsh-image-gen/save-workspace";
18
20
  /** Namespace persisted through DSH Settings. */
@@ -150,7 +152,7 @@ window.__ModuleLoader__.load({
150
152
  }
151
153
  //#endregion
152
154
  //#region lib/types/client/gallery-store.js
153
- const DB_NAME = "dsh_image_gen_db";
155
+ const DB_NAME$2 = "dsh_image_gen_db";
154
156
  const DB_VERSION = 2;
155
157
  const STORE_NAME = "gallery_history";
156
158
  const TOMBSTONE_STORE = "gallery_tombstones";
@@ -163,7 +165,7 @@ window.__ModuleLoader__.load({
163
165
  reject(/* @__PURE__ */ new Error("IndexedDB is not supported in this environment."));
164
166
  return;
165
167
  }
166
- const request = indexedDB.open(DB_NAME, DB_VERSION);
168
+ const request = indexedDB.open(DB_NAME$2, DB_VERSION);
167
169
  request.onupgradeneeded = (event) => {
168
170
  const db = event.target.result;
169
171
  if (!db.objectStoreNames.contains(STORE_NAME)) db.createObjectStore(STORE_NAME, { keyPath: "id" }).createIndex("createdAt", "createdAt", { unique: false });
@@ -669,6 +671,26 @@ window.__ModuleLoader__.load({
669
671
  * This source code is licensed under the ISC license.
670
672
  * See the LICENSE file in the root directory of this source tree.
671
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
+ */
672
694
  const FileText = createLucideIcon("file-text", [
673
695
  ["path", {
674
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",
@@ -774,6 +796,30 @@ window.__ModuleLoader__.load({
774
796
  * This source code is licensed under the ISC license.
775
797
  * See the LICENSE file in the root directory of this source tree.
776
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
+ */
777
823
  const PanelLeftClose = createLucideIcon("panel-left-close", [
778
824
  ["rect", {
779
825
  width: "18",
@@ -957,6 +1003,16 @@ window.__ModuleLoader__.load({
957
1003
  * This source code is licensed under the ISC license.
958
1004
  * See the LICENSE file in the root directory of this source tree.
959
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
+ */
960
1016
  const Trash2 = createLucideIcon("trash-2", [
961
1017
  ["path", {
962
1018
  d: "M10 11v6",
@@ -1241,8 +1297,30 @@ window.__ModuleLoader__.load({
1241
1297
  return `${d.getFullYear()}/${d.getMonth() + 1}/${d.getDate()}`;
1242
1298
  }
1243
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
1244
1322
  //#region lib/types/client/studio-view.js
1245
- const COPY = {
1323
+ const COPY$1 = {
1246
1324
  zh: {
1247
1325
  title: "云端生图工作台",
1248
1326
  configured: "API 已配置",
@@ -1271,9 +1349,23 @@ window.__ModuleLoader__.load({
1271
1349
  partialSuccess: "已生成 {success} 张图片,{failed} 张失败",
1272
1350
  generatingCount: "正在生成(共 {count} 张)…",
1273
1351
  batchResult: "本次生成(共 {count} 张)",
1274
- saveToGallery: "收藏进画廊",
1275
- savedToGallery: "已收藏进画廊",
1276
- inGallery: "已在画廊",
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: "请先勾选要保存的图片",
1277
1369
  retry: "重新加载",
1278
1370
  configLoadFailed: "工作台配置加载失败,请检查服务后重试。",
1279
1371
  noProvider: "请先在设置中配置至少一个云端图像 Provider 的 API Key。",
@@ -1324,7 +1416,8 @@ window.__ModuleLoader__.load({
1324
1416
  unconfiguredStatus: "API 未配置",
1325
1417
  providerStatus: "云端提供商与模型状态",
1326
1418
  collapseSidebar: "折叠最近生成",
1327
- expandSidebar: "展开最近生成"
1419
+ expandSidebar: "展开最近生成",
1420
+ findInspiration: "找灵感"
1328
1421
  },
1329
1422
  en: {
1330
1423
  title: "Cloud Image Studio",
@@ -1354,9 +1447,23 @@ window.__ModuleLoader__.load({
1354
1447
  partialSuccess: "Generated {success} images, {failed} failed",
1355
1448
  generatingCount: "Generating ({count} images)…",
1356
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",
1357
1461
  saveToGallery: "Save to Gallery",
1462
+ saveSelected: "Save selected ({count})",
1358
1463
  savedToGallery: "Saved to Gallery",
1464
+ savedSelected: "Saved {count} images",
1359
1465
  inGallery: "In Gallery",
1466
+ needSelectResult: "Select images to save first",
1360
1467
  retry: "Retry",
1361
1468
  configLoadFailed: "Failed to load studio configuration.",
1362
1469
  noProvider: "Configure an API key for at least one cloud image provider in Settings.",
@@ -1407,10 +1514,11 @@ window.__ModuleLoader__.load({
1407
1514
  unconfiguredStatus: "Not configured",
1408
1515
  providerStatus: "Provider & Model Status",
1409
1516
  collapseSidebar: "Collapse sidebar",
1410
- expandSidebar: "Expand recent list"
1517
+ expandSidebar: "Expand recent list",
1518
+ findInspiration: "Find inspiration"
1411
1519
  }
1412
1520
  };
1413
- const StudioView = ({ locale, workspace }) => {
1521
+ const StudioView = ({ locale, workspace, initialPrompt, onInitialPromptApplied, onOpenInspiration }) => {
1414
1522
  const [lang, setLang] = (0, react.useState)(() => locale?.getSnapshot?.().active?.startsWith("en") ? "en" : "zh");
1415
1523
  const [config, setConfig] = (0, react.useState)(null);
1416
1524
  const [configLoading, setConfigLoading] = (0, react.useState)(true);
@@ -1429,6 +1537,10 @@ window.__ModuleLoader__.load({
1429
1537
  const [references, setReferences] = (0, react.useState)([]);
1430
1538
  const [count, setCount] = (0, react.useState)(1);
1431
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)([]);
1432
1544
  const [isGenerating, setIsGenerating] = (0, react.useState)(false);
1433
1545
  const [error, setError] = (0, react.useState)(null);
1434
1546
  const [notice, setNotice] = (0, react.useState)(null);
@@ -1449,11 +1561,18 @@ window.__ModuleLoader__.load({
1449
1561
  const canvasRef = (0, react.useRef)(null);
1450
1562
  const fileInputRef = (0, react.useRef)(null);
1451
1563
  const image = useAttachmentImage(selected?.attachment);
1452
- 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;
1453
1572
  const referencesRef = (0, react.useRef)(references);
1454
1573
  referencesRef.current = references;
1455
1574
  const t = (key, values) => {
1456
- let text = COPY[lang][key];
1575
+ let text = COPY$1[lang][key];
1457
1576
  for (const [name, value] of Object.entries(values ?? {})) text = text.replace(`{${name}}`, value);
1458
1577
  return text;
1459
1578
  };
@@ -1471,7 +1590,10 @@ window.__ModuleLoader__.load({
1471
1590
  if (!response.ok || !("providers" in payload)) throw new Error("error" in payload && payload.error ? payload.error : "Studio unavailable");
1472
1591
  setConfig(payload);
1473
1592
  const initial = payload.providers.find((item) => item.provider === payload.activeProvider) ?? payload.providers[0];
1474
- if (initial !== void 0) applyProvider(initial);
1593
+ if (initial !== void 0) {
1594
+ applyProvider(initial);
1595
+ setComparisonProviders(initialComparisonProviders(payload.providers, initial.provider));
1596
+ }
1475
1597
  } catch (fetchError) {
1476
1598
  if (!controller.signal.aborted) setConfigError(messageOf(fetchError));
1477
1599
  } finally {
@@ -1553,6 +1675,13 @@ window.__ModuleLoader__.load({
1553
1675
  const activeProfile = (0, react.useMemo)(() => config?.providers.find((item) => item.provider === provider), [config, provider]);
1554
1676
  const configuredCount = config?.providers.filter((item) => item.configured).length ?? 0;
1555
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
+ ]);
1556
1685
  const applyProvider = (profile) => {
1557
1686
  setProvider(profile.provider);
1558
1687
  setModel(profile.model);
@@ -1585,12 +1714,16 @@ window.__ModuleLoader__.load({
1585
1714
  };
1586
1715
  const selectItem = (item) => {
1587
1716
  setCurrentBatch(null);
1717
+ setSelectedBatchIds([]);
1718
+ setBatchKind(null);
1588
1719
  setSelected(item);
1589
1720
  setPanelTab("details");
1590
1721
  resetFit();
1591
1722
  };
1592
1723
  const startNew = () => {
1593
1724
  setCurrentBatch(null);
1725
+ setSelectedBatchIds([]);
1726
+ setBatchKind(null);
1594
1727
  setSelected(null);
1595
1728
  setPanelTab("generate");
1596
1729
  resetFit();
@@ -1701,10 +1834,46 @@ window.__ModuleLoader__.load({
1701
1834
  setIsGenerating(false);
1702
1835
  }
1703
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
+ };
1704
1872
  const submit = async () => {
1705
1873
  if (isGenerating || activeProfile === void 0) return;
1706
1874
  if (prompt.trim().length === 0) return setError(t("needPrompt"));
1707
- 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"));
1708
1877
  if (mode === "edit" && references.length === 0) return setError(t("needReference"));
1709
1878
  setIsGenerating(true);
1710
1879
  setError(null);
@@ -1720,6 +1889,64 @@ window.__ModuleLoader__.load({
1720
1889
  };
1721
1890
  throw new Error("无效参考图");
1722
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
+ }
1723
1950
  const response = await fetch(STUDIO_ROUTE, {
1724
1951
  method: "POST",
1725
1952
  credentials: "same-origin",
@@ -1760,9 +1987,13 @@ window.__ModuleLoader__.load({
1760
1987
  }));
1761
1988
  if (galleryEntries.length > 1) {
1762
1989
  setCurrentBatch(galleryEntries);
1990
+ setSelectedBatchIds([galleryEntries[0].id]);
1991
+ setBatchKind("multi-image");
1763
1992
  setSelected(galleryEntries[0]);
1764
1993
  } else if (galleryEntries.length === 1) {
1765
1994
  setCurrentBatch(null);
1995
+ setSelectedBatchIds([]);
1996
+ setBatchKind(null);
1766
1997
  setSelected(galleryEntries[0]);
1767
1998
  }
1768
1999
  setPanelTab("details");
@@ -1785,9 +2016,24 @@ window.__ModuleLoader__.load({
1785
2016
  if (selected === null) return false;
1786
2017
  return items.some((item) => item.id === selected.id);
1787
2018
  }, [items, selected]);
1788
- const handleSaveToGallery = async () => {
1789
- if (selected === null || isSelectedInGallery) return;
1790
- let savedTo = selected.savedTo;
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;
1791
2037
  const targetRoot = workspace?.path || config?.workspaceRoot;
1792
2038
  if (!savedTo) try {
1793
2039
  const res = await fetch(SAVE_WORKSPACE_ROUTE, {
@@ -1795,7 +2041,7 @@ window.__ModuleLoader__.load({
1795
2041
  credentials: "same-origin",
1796
2042
  headers: { "content-type": "application/json" },
1797
2043
  body: JSON.stringify({
1798
- attachment: selected.attachment,
2044
+ attachment: item.attachment,
1799
2045
  ...targetRoot ? { workspaceRoot: targetRoot } : {}
1800
2046
  })
1801
2047
  });
@@ -1803,19 +2049,35 @@ window.__ModuleLoader__.load({
1803
2049
  const data = await res.json();
1804
2050
  if (data.savedTo) savedTo = data.savedTo;
1805
2051
  }
1806
- } catch (e) {
1807
- console.warn("Failed to save to workspace:", e);
2052
+ } catch (saveError) {
2053
+ console.warn("Failed to save to workspace:", saveError);
1808
2054
  }
1809
2055
  const updatedItem = {
1810
- ...selected,
2056
+ ...item,
1811
2057
  ...savedTo ? { savedTo } : {},
1812
2058
  ...targetRoot ? { workspacePath: targetRoot } : {},
1813
2059
  ...workspace?.workspaceId ? { workspaceId: workspace.workspaceId } : {}
1814
2060
  };
1815
2061
  await saveGalleryItem(updatedItem);
1816
- setSelected(updatedItem);
1817
- if (currentBatch) setCurrentBatch((prev) => prev ? prev.map((i) => i.id === updatedItem.id ? updatedItem : i) : prev);
1818
- flash(t("savedToGallery"));
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"));
1819
2081
  };
1820
2082
  const handleConfirmDelete = async () => {
1821
2083
  if (selected === null) return;
@@ -1840,6 +2102,7 @@ window.__ModuleLoader__.load({
1840
2102
  }
1841
2103
  if (isSelectedInGallery) await deleteGalleryItem(idToDelete);
1842
2104
  evictAttachmentCache(attachmentId);
2105
+ setSelectedBatchIds((current) => current.filter((id) => id !== idToDelete));
1843
2106
  if (currentBatch) {
1844
2107
  const remaining = currentBatch.filter((i) => i.id !== idToDelete);
1845
2108
  if (remaining.length > 1) {
@@ -2074,12 +2337,8 @@ window.__ModuleLoader__.load({
2074
2337
  className: "dsh-ig-generation-orbit",
2075
2338
  children: (0, react_jsx_runtime.jsx)(Sparkles, { size: 28 })
2076
2339
  }),
2077
- (0, react_jsx_runtime.jsx)("strong", { children: count > 1 ? t("generatingCount", { count: String(count) }) : t("generating") }),
2078
- (0, react_jsx_runtime.jsxs)("span", { children: [
2079
- activeProfile?.label,
2080
- " · ",
2081
- model
2082
- ] })
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}` })
2083
2342
  ]
2084
2343
  }) : currentBatch && currentBatch.length > 1 ? (0, react_jsx_runtime.jsx)("div", {
2085
2344
  className: "dsh-ig-canvas-cluster",
@@ -2092,9 +2351,14 @@ window.__ModuleLoader__.load({
2092
2351
  children: currentBatch.map((batchItem, index) => (0, react_jsx_runtime.jsx)(BatchCanvasItem, {
2093
2352
  item: batchItem,
2094
2353
  index,
2095
- isSelected: selected?.id === batchItem.id,
2354
+ badge: batchKind === "multi-model" ? batchItem.model : void 0,
2355
+ isActive: selected?.id === batchItem.id,
2356
+ isChecked: selectedBatchIds.includes(batchItem.id),
2096
2357
  onSelect: () => {
2097
- if (!hasDraggedRef.current) setSelected(batchItem);
2358
+ if (!hasDraggedRef.current) {
2359
+ setSelected(batchItem);
2360
+ setSelectedBatchIds((current) => current.includes(batchItem.id) ? current.filter((id) => id !== batchItem.id) : [...current, batchItem.id]);
2361
+ }
2098
2362
  }
2099
2363
  }, batchItem.id))
2100
2364
  }) : selected === null ? (0, react_jsx_runtime.jsxs)("div", {
@@ -2143,11 +2407,11 @@ window.__ModuleLoader__.load({
2143
2407
  children: [(0, react_jsx_runtime.jsx)("p", { children: selected.prompt }), (0, react_jsx_runtime.jsxs)("div", { children: [
2144
2408
  (0, react_jsx_runtime.jsxs)("button", {
2145
2409
  type: "button",
2146
- className: `dsh-ig-save-gallery-btn ${isSelectedInGallery ? "is-saved" : ""}`,
2410
+ className: `dsh-ig-save-gallery-btn ${saveSelectionComplete ? "is-saved" : ""}`,
2147
2411
  onClick: () => void handleSaveToGallery(),
2148
- title: isSelectedInGallery ? t("inGallery") : t("saveToGallery"),
2149
- disabled: isSelectedInGallery,
2150
- children: [isSelectedInGallery ? (0, react_jsx_runtime.jsx)(Check, { size: 15 }) : (0, react_jsx_runtime.jsx)(BookmarkPlus, { size: 15 }), (0, react_jsx_runtime.jsx)("span", { children: isSelectedInGallery ? t("inGallery") : t("saveToGallery") })]
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 })]
2151
2415
  }),
2152
2416
  (0, react_jsx_runtime.jsxs)("button", {
2153
2417
  type: "button",
@@ -2306,11 +2570,16 @@ window.__ModuleLoader__.load({
2306
2570
  " ",
2307
2571
  (0, react_jsx_runtime.jsx)("b", { children: "*" })
2308
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")]
2309
2578
  }), (0, react_jsx_runtime.jsx)("button", {
2310
2579
  type: "button",
2311
2580
  onClick: () => setPrompt(""),
2312
2581
  children: t("clear")
2313
- })]
2582
+ })] })]
2314
2583
  }),
2315
2584
  (0, react_jsx_runtime.jsx)("textarea", {
2316
2585
  id: "dsh-ig-prompt",
@@ -2342,7 +2611,78 @@ window.__ModuleLoader__.load({
2342
2611
  onClick: () => void loadConfig(),
2343
2612
  children: [(0, react_jsx_runtime.jsx)(RefreshCw, { size: 13 }), (0, react_jsx_runtime.jsx)("span", { children: t("retry") })]
2344
2613
  })]
2345
- }) : (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
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")]
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
+ ]
2672
+ }), (0, react_jsx_runtime.jsxs)("div", {
2673
+ className: "dsh-ig-field-grid",
2674
+ children: [(0, react_jsx_runtime.jsx)(FieldSelect, {
2675
+ label: t("ratio"),
2676
+ value: ratio,
2677
+ onChange: setRatio,
2678
+ options: comparisonRatioOptions(lang)
2679
+ }), (0, react_jsx_runtime.jsx)(FieldSelect, {
2680
+ label: t("quality"),
2681
+ value: quality,
2682
+ onChange: setQuality,
2683
+ options: comparisonQualityOptions(lang)
2684
+ })]
2685
+ })] }) : (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2346
2686
  (0, react_jsx_runtime.jsxs)("div", {
2347
2687
  className: "dsh-ig-field-grid",
2348
2688
  children: [(0, react_jsx_runtime.jsx)(FieldSelect, {
@@ -2369,12 +2709,12 @@ window.__ModuleLoader__.load({
2369
2709
  label: t("ratio"),
2370
2710
  value: ratio,
2371
2711
  onChange: setRatio,
2372
- options: activeProfile?.ratioOptions ?? []
2712
+ options: localizeRatioOptions(activeProfile?.ratioOptions ?? [], lang)
2373
2713
  }), (0, react_jsx_runtime.jsx)(FieldSelect, {
2374
2714
  label: t("quality"),
2375
2715
  value: quality,
2376
2716
  onChange: setQuality,
2377
- options: activeProfile?.qualityOptions ?? []
2717
+ options: localizeQualityOptions(activeProfile?.qualityOptions ?? [], lang)
2378
2718
  })]
2379
2719
  }),
2380
2720
  (0, react_jsx_runtime.jsxs)("div", {
@@ -2394,7 +2734,7 @@ window.__ModuleLoader__.load({
2394
2734
  }, option))
2395
2735
  })]
2396
2736
  })
2397
- ] }),
2737
+ ] })] }),
2398
2738
  configuredCount === 0 && config !== null && (0, react_jsx_runtime.jsx)("div", {
2399
2739
  className: "dsh-ig-inline-note",
2400
2740
  children: t("noProvider")
@@ -2416,7 +2756,7 @@ window.__ModuleLoader__.load({
2416
2756
  className: "dsh-ig-generate-button",
2417
2757
  disabled: config === null,
2418
2758
  onClick: () => void submit(),
2419
- children: [(0, react_jsx_runtime.jsx)(Sparkles, { size: 17 }), (0, react_jsx_runtime.jsxs)("span", { children: [t("start"), count > 1 ? ` (${count})` : ""] })]
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})` : ""}` })]
2420
2760
  })
2421
2761
  ]
2422
2762
  })]
@@ -2536,11 +2876,11 @@ window.__ModuleLoader__.load({
2536
2876
  }),
2537
2877
  (0, react_jsx_runtime.jsxs)("button", {
2538
2878
  type: "button",
2539
- className: `dsh-ig-lightbox-btn ${isSelectedInGallery ? "is-saved" : ""}`,
2540
- title: isSelectedInGallery ? t("inGallery") : t("saveToGallery"),
2879
+ className: `dsh-ig-lightbox-btn ${saveSelectionComplete ? "is-saved" : ""}`,
2880
+ title: saveButtonLabel,
2541
2881
  onClick: () => void handleSaveToGallery(),
2542
- disabled: isSelectedInGallery,
2543
- children: [isSelectedInGallery ? (0, react_jsx_runtime.jsx)(Check, { size: 14 }) : (0, react_jsx_runtime.jsx)(BookmarkPlus, { size: 14 }), (0, react_jsx_runtime.jsx)("span", { children: isSelectedInGallery ? t("inGallery") : t("saveToGallery") })]
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 })]
2544
2884
  }),
2545
2885
  (0, react_jsx_runtime.jsxs)("button", {
2546
2886
  type: "button",
@@ -2608,28 +2948,37 @@ window.__ModuleLoader__.load({
2608
2948
  ]
2609
2949
  });
2610
2950
  };
2611
- const BatchCanvasItem = ({ item, index, isSelected, onSelect }) => {
2951
+ const BatchCanvasItem = ({ item, index, badge, isActive, isChecked, onSelect }) => {
2612
2952
  const image = useAttachmentImage(item.attachment, true);
2613
2953
  return (0, react_jsx_runtime.jsxs)("div", {
2614
- className: `dsh-ig-canvas-item ${isSelected ? "is-selected" : ""}`,
2954
+ className: `dsh-ig-canvas-item ${isActive ? "is-active" : ""} ${isChecked ? "is-checked" : ""}`,
2615
2955
  onClick: onSelect,
2616
- children: [(0, react_jsx_runtime.jsxs)("span", {
2617
- className: "dsh-ig-canvas-badge",
2618
- children: ["#", index + 1]
2619
- }), image.url !== null ? (0, react_jsx_runtime.jsx)("img", {
2620
- src: image.url,
2621
- alt: item.prompt,
2622
- draggable: false
2623
- }) : image.loading ? (0, react_jsx_runtime.jsx)("div", {
2624
- className: "dsh-ig-canvas-item-loading",
2625
- children: (0, react_jsx_runtime.jsx)(LoaderCircle, {
2626
- className: "dsh-ig-spin",
2627
- size: 24
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 })
2628
2980
  })
2629
- }) : (0, react_jsx_runtime.jsx)("div", {
2630
- className: "dsh-ig-canvas-item-loading",
2631
- children: (0, react_jsx_runtime.jsx)(ImagePlus, { size: 24 })
2632
- })]
2981
+ ]
2633
2982
  });
2634
2983
  };
2635
2984
  const FieldSelect = ({ label, value, options, onChange }) => (0, react_jsx_runtime.jsxs)("label", {
@@ -2643,6 +2992,94 @@ window.__ModuleLoader__.load({
2643
2992
  }, option.value))
2644
2993
  })]
2645
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
+ }
2646
3083
  const DetailsPanel = ({ item, t }) => item === null ? (0, react_jsx_runtime.jsxs)("div", {
2647
3084
  className: "dsh-ig-details-empty",
2648
3085
  children: [(0, react_jsx_runtime.jsx)(ImagePlus, { size: 28 }), (0, react_jsx_runtime.jsx)("span", { children: t("selectHistory") })]
@@ -2772,6 +3209,1036 @@ window.__ModuleLoader__.load({
2772
3209
  return error instanceof Error ? error.message : String(error);
2773
3210
  }
2774
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
2775
4242
  //#region lib/types/client/conversation-regenerate.js
2776
4243
  /** Build the workbench request that reproduces a conversation image with an edited prompt. */
2777
4244
  function conversationRegenerateRequest(image, prompt, remembered) {
@@ -2836,6 +4303,7 @@ window.__ModuleLoader__.load({
2836
4303
  zh: {
2837
4304
  tabGallery: "图库",
2838
4305
  tabStudio: "工作台",
4306
+ tabInspiration: "灵感",
2839
4307
  tabFavorites: "收藏",
2840
4308
  tabCompare: "对比",
2841
4309
  tabTasks: "任务",
@@ -2917,6 +4385,7 @@ window.__ModuleLoader__.load({
2917
4385
  en: {
2918
4386
  tabGallery: "Gallery",
2919
4387
  tabStudio: "Studio",
4388
+ tabInspiration: "Inspiration",
2920
4389
  tabFavorites: "Favorites",
2921
4390
  tabCompare: "Compare",
2922
4391
  tabTasks: "Tasks",
@@ -3054,6 +4523,7 @@ window.__ModuleLoader__.load({
3054
4523
  const GalleryViewTab = (props) => {
3055
4524
  const { locale, sessionId, useSessions, useWorkspaces } = props;
3056
4525
  const [activeTab, setActiveTab] = (0, react.useState)("gallery");
4526
+ const [studioDraft, setStudioDraft] = (0, react.useState)(void 0);
3057
4527
  const [items, setItems] = (0, react.useState)([]);
3058
4528
  const [search, setSearch] = (0, react.useState)("");
3059
4529
  const [selectedProvider, setSelectedProvider] = (0, react.useState)("all");
@@ -3164,6 +4634,11 @@ window.__ModuleLoader__.load({
3164
4634
  setToast(null);
3165
4635
  }, 2e3);
3166
4636
  };
4637
+ const useInspirationPrompt = (0, react.useCallback)((prompt) => {
4638
+ setStudioDraft(prompt);
4639
+ setActiveTab("studio");
4640
+ }, []);
4641
+ const clearStudioDraft = (0, react.useCallback)(() => setStudioDraft(void 0), []);
3167
4642
  (0, react.useEffect)(() => {
3168
4643
  const seat = document.querySelector("[data-composer-seat]");
3169
4644
  if (seat) {
@@ -3568,6 +5043,12 @@ window.__ModuleLoader__.load({
3568
5043
  onClick: () => setActiveTab("studio"),
3569
5044
  children: [(0, react_jsx_runtime.jsx)(SlidersHorizontal, { size: 15 }), (0, react_jsx_runtime.jsx)("span", { children: t("tabStudio") })]
3570
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
+ }),
3571
5052
  (0, react_jsx_runtime.jsxs)("button", {
3572
5053
  type: "button",
3573
5054
  className: `dsh-ig-studio-tab-btn ${activeTab === "favorites" ? "is-active" : ""}`,
@@ -3779,9 +5260,15 @@ window.__ModuleLoader__.load({
3779
5260
  onBlobLoaded: (b) => blobCache.set(item.id, b),
3780
5261
  onToast: showToast
3781
5262
  }, item.id))
5263
+ }) : activeTab === "inspiration" ? (0, react_jsx_runtime.jsx)(InspirationView, {
5264
+ locale,
5265
+ onUsePrompt: useInspirationPrompt
3782
5266
  }) : (0, react_jsx_runtime.jsx)(StudioView, {
3783
5267
  locale,
3784
- workspace: activeWorkspace
5268
+ workspace: activeWorkspace,
5269
+ initialPrompt: studioDraft,
5270
+ onInitialPromptApplied: clearStudioDraft,
5271
+ onOpenInspiration: () => setActiveTab("inspiration")
3785
5272
  })
3786
5273
  }),
3787
5274
  toast && (0, react_jsx_runtime.jsx)("div", {
@@ -4354,6 +5841,8 @@ window.__ModuleLoader__.load({
4354
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}
4355
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)}
4356
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}
4357
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)}
4358
5847
  .dsh-ig-reference-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(64px,1fr));gap:8px}
4359
5848
  .dsh-ig-reference-card{position:relative;aspect-ratio:1;border-radius:8px;overflow:hidden;background:#eef1f7;border:1px solid var(--ig-line)}
@@ -4383,10 +5872,11 @@ window.__ModuleLoader__.load({
4383
5872
  @keyframes dshIgPulse{50%{transform:scale(1.06);box-shadow:0 0 0 16px rgba(47,100,245,.025)}}
4384
5873
  .dsh-ig-workbench-toast{position:fixed;bottom:28px;left:50%;transform:translateX(-50%);background:rgba(15,23,42,0.88);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);color:#fff;padding:8px 18px;border-radius:10px;font-size:12.5px;font-weight:500;box-shadow:0 12px 32px rgba(0,0,0,0.3);z-index:99999;animation:dshIgFade .15s ease-out;pointer-events:none}
4385
5874
  @keyframes dshIgFade{from{opacity:0;transform:translate(-50%,6px)}to{opacity:1;transform:translate(-50%,0)}}
4386
- @media(max-width:1120px){.dsh-ig-workbench-grid{grid-template-columns:190px minmax(360px,1fr) 300px}.dsh-ig-result-actions button span{display:none}}
5875
+ @media(max-width:1120px){.dsh-ig-workbench-grid{grid-template-columns:190px minmax(360px,1fr) 300px}.dsh-ig-result-actions button,.dsh-ig-result-strip button{padding:0 7px;font-size:10px;gap:4px}.dsh-ig-result-actions>div,.dsh-ig-result-strip>div{gap:5px}}
4387
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}}
4388
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}}
4389
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}
4390
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}
4391
5881
  .dsh-ig-workspace-filter-label:hover{border-color:#9bb4f5;background:rgba(47,100,245,.04)}
4392
5882
  .dsh-ig-workspace-checkbox{cursor:pointer;accent-color:#2f64f5;width:13.5px;height:13.5px;margin:0}
@@ -4398,9 +5888,12 @@ window.__ModuleLoader__.load({
4398
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}
4399
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}
4400
5890
  .dsh-ig-canvas-item:hover img{box-shadow:0 14px 40px rgba(47,100,245,.2)}
4401
- .dsh-ig-canvas-item.is-selected img{outline:3px solid var(--ig-blue);outline-offset:2px;box-shadow:0 14px 40px rgba(47,100,245,.35)}
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)}
4402
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}
4403
- .dsh-ig-canvas-item.is-selected .dsh-ig-canvas-badge{background:var(--ig-blue)}
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)}
4404
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}
4405
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)}
4406
5899
  .dsh-ig-lightbox-nav:hover{background:rgba(47,100,245,.85);transform:translateY(-50%) scale(1.08)}
@@ -4409,6 +5902,38 @@ window.__ModuleLoader__.load({
4409
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}
4410
5903
  .dsh-ig-save-gallery-btn:hover{background:rgba(47,100,245,.12)!important}
4411
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}}
4412
5937
  `;
4413
5938
  //#endregion
4414
5939
  //#region lib/types/client/image-result-node.js
@@ -5026,7 +6551,7 @@ window.__ModuleLoader__.load({
5026
6551
  ctx.effect(() => {
5027
6552
  const style = document.createElement("style");
5028
6553
  style.dataset.plugin = "dsh-image-gen";
5029
- style.textContent = `${STYLE}\n${STUDIO_STYLE}`;
6554
+ style.textContent = `${STYLE}\n${STUDIO_STYLE}\n${INSPIRATION_STYLE}`;
5030
6555
  document.head.appendChild(style);
5031
6556
  return () => {
5032
6557
  style.remove();