dsh-image-gen 0.2.6 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -7,13 +7,21 @@ window.__ModuleLoader__.load({
7
7
  let react_jsx_runtime = require("react/jsx-runtime");
8
8
  let react = require("react");
9
9
  //#region lib/types/shared.js
10
- /** Values shared by the Host and browser Bundle faces. */
11
10
  /** Browser route used by the generated-image card. */
12
11
  const IMAGE_ROUTE = "/plugins/dsh-image-gen/image";
13
12
  /** Browser route used for deleting generated images and workspace files. */
14
13
  const DELETE_ROUTE = "/plugins/dsh-image-gen/delete";
14
+ /** Same-origin route used by the browser image workbench. */
15
+ const STUDIO_ROUTE = "/plugins/dsh-image-gen/studio";
15
16
  /** Namespace persisted through DSH Settings. */
16
17
  const IMAGE_GENERATION_NAMESPACE = "image-generation";
18
+ /** Providers supported by the first browser workbench release. */
19
+ const CLOUD_IMAGE_PROVIDERS = [
20
+ "google",
21
+ "openai",
22
+ "seedream",
23
+ "dashscope"
24
+ ];
17
25
  /** Default endpoints and base URLs. */
18
26
  const DEFAULT_GOOGLE_ENDPOINT = "https://generativelanguage.googleapis.com/v1beta/interactions";
19
27
  const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";
@@ -300,6 +308,29 @@ window.__ModuleLoader__.load({
300
308
  }
301
309
  }
302
310
  /**
311
+ * Delete a single gallery record by ID and record a tombstone.
312
+ */
313
+ async function deleteGalleryItem(id) {
314
+ const db = await getDB();
315
+ const tombstones = await loadTombstones(db);
316
+ await new Promise((resolve, reject) => {
317
+ const tx = db.transaction([STORE_NAME, TOMBSTONE_STORE], "readwrite");
318
+ const store = tx.objectStore(STORE_NAME);
319
+ const tombstoneStore = tx.objectStore(TOMBSTONE_STORE);
320
+ store.delete(id);
321
+ tombstoneStore.put({
322
+ id,
323
+ deletedAt: Date.now()
324
+ });
325
+ tx.oncomplete = () => {
326
+ tombstones.add(id);
327
+ resolve();
328
+ };
329
+ tx.onerror = () => reject(tx.error);
330
+ });
331
+ notifyListeners();
332
+ }
333
+ /**
303
334
  * Bulk delete multiple gallery records by IDs and record tombstones in a single transaction.
304
335
  */
305
336
  async function bulkDeleteGalleryItems(ids) {
@@ -327,6 +358,30 @@ window.__ModuleLoader__.load({
327
358
  });
328
359
  notifyListeners();
329
360
  }
361
+ /**
362
+ * Standardize path format across Windows and POSIX (lowercase, forward slashes, no trailing slash).
363
+ */
364
+ function normalizeWorkspacePath(rawPath) {
365
+ return rawPath.replace(/\\/g, "/").toLowerCase().replace(/\/+$/, "");
366
+ }
367
+ /**
368
+ * Determine if a gallery item belongs to the given workspace context.
369
+ * Compares workspaceId, sessionId membership, workspacePath, and savedTo file path prefix.
370
+ */
371
+ function isItemInWorkspace(item, workspace) {
372
+ if (!workspace || !workspace.workspaceId && !workspace.path && (!workspace.sessionIds || workspace.sessionIds.length === 0)) return true;
373
+ if (item.workspaceId && workspace.workspaceId && item.workspaceId === workspace.workspaceId) return true;
374
+ if (item.sessionId && Array.isArray(workspace.sessionIds) && workspace.sessionIds.includes(item.sessionId)) return true;
375
+ if (item.workspacePath && workspace.path) {
376
+ if (normalizeWorkspacePath(item.workspacePath) === normalizeWorkspacePath(workspace.path)) return true;
377
+ }
378
+ if (item.savedTo && workspace.path) {
379
+ const normSaved = normalizeWorkspacePath(item.savedTo);
380
+ const normWs = normalizeWorkspacePath(workspace.path);
381
+ if (normSaved === normWs || normSaved.startsWith(normWs + "/")) return true;
382
+ }
383
+ return false;
384
+ }
330
385
  //#endregion
331
386
  //#region node_modules/.pnpm/lucide-react@1.38.0_react@18.3.1/node_modules/lucide-react/dist/esm/shared/src/utils/mergeClasses.mjs
332
387
  /**
@@ -466,6 +521,16 @@ window.__ModuleLoader__.load({
466
521
  * This source code is licensed under the ISC license.
467
522
  * See the LICENSE file in the root directory of this source tree.
468
523
  */
524
+ const ChevronDown = createLucideIcon("chevron-down", [["path", {
525
+ d: "m6 9 6 6 6-6",
526
+ key: "qrunsl"
527
+ }]]);
528
+ /**
529
+ * @license lucide-react v1.38.0 - ISC
530
+ *
531
+ * This source code is licensed under the ISC license.
532
+ * See the LICENSE file in the root directory of this source tree.
533
+ */
469
534
  const ChevronLeft = createLucideIcon("chevron-left", [["path", {
470
535
  d: "m15 18-6-6 6-6",
471
536
  key: "1wnfg3"
@@ -486,6 +551,24 @@ window.__ModuleLoader__.load({
486
551
  * This source code is licensed under the ISC license.
487
552
  * See the LICENSE file in the root directory of this source tree.
488
553
  */
554
+ const Clipboard = createLucideIcon("clipboard", [["rect", {
555
+ width: "8",
556
+ height: "4",
557
+ x: "8",
558
+ y: "2",
559
+ rx: "1",
560
+ ry: "1",
561
+ key: "tgr4d6"
562
+ }], ["path", {
563
+ d: "M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2",
564
+ key: "116196"
565
+ }]]);
566
+ /**
567
+ * @license lucide-react v1.38.0 - ISC
568
+ *
569
+ * This source code is licensed under the ISC license.
570
+ * See the LICENSE file in the root directory of this source tree.
571
+ */
489
572
  const Copy = createLucideIcon("copy", [["rect", {
490
573
  width: "14",
491
574
  height: "14",
@@ -524,6 +607,46 @@ window.__ModuleLoader__.load({
524
607
  * This source code is licensed under the ISC license.
525
608
  * See the LICENSE file in the root directory of this source tree.
526
609
  */
610
+ const Expand = createLucideIcon("expand", [
611
+ ["path", {
612
+ d: "m15 15 6 6",
613
+ key: "1s409w"
614
+ }],
615
+ ["path", {
616
+ d: "m15 9 6-6",
617
+ key: "ko1vev"
618
+ }],
619
+ ["path", {
620
+ d: "M21 16v5h-5",
621
+ key: "1ck2sf"
622
+ }],
623
+ ["path", {
624
+ d: "M21 8V3h-5",
625
+ key: "1qoq8a"
626
+ }],
627
+ ["path", {
628
+ d: "M3 16v5h5",
629
+ key: "1t08am"
630
+ }],
631
+ ["path", {
632
+ d: "m3 21 6-6",
633
+ key: "wwnumi"
634
+ }],
635
+ ["path", {
636
+ d: "M3 8V3h5",
637
+ key: "1ln10m"
638
+ }],
639
+ ["path", {
640
+ d: "M9 9 3 3",
641
+ key: "v551iv"
642
+ }]
643
+ ]);
644
+ /**
645
+ * @license lucide-react v1.38.0 - ISC
646
+ *
647
+ * This source code is licensed under the ISC license.
648
+ * See the LICENSE file in the root directory of this source tree.
649
+ */
527
650
  const FileText = createLucideIcon("file-text", [
528
651
  ["path", {
529
652
  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",
@@ -552,26 +675,38 @@ window.__ModuleLoader__.load({
552
675
  * This source code is licensed under the ISC license.
553
676
  * See the LICENSE file in the root directory of this source tree.
554
677
  */
555
- const GitCompare = createLucideIcon("git-compare", [
556
- ["circle", {
557
- cx: "18",
558
- cy: "18",
559
- r: "3",
560
- key: "1xkwt0"
678
+ const Heart = createLucideIcon("heart", [["path", {
679
+ d: "M2 9.5a5.5 5.5 0 0 1 9.591-3.676.56.56 0 0 0 .818 0A5.49 5.49 0 0 1 22 9.5c0 2.29-1.5 4-3 5.5l-5.492 5.313a2 2 0 0 1-3 .019L5 15c-1.5-1.5-3-3.2-3-5.5",
680
+ key: "mvr1a0"
681
+ }]]);
682
+ /**
683
+ * @license lucide-react v1.38.0 - ISC
684
+ *
685
+ * This source code is licensed under the ISC license.
686
+ * See the LICENSE file in the root directory of this source tree.
687
+ */
688
+ const ImagePlus = createLucideIcon("image-plus", [
689
+ ["path", {
690
+ d: "M16 5h6",
691
+ key: "1vod17"
561
692
  }],
562
- ["circle", {
563
- cx: "6",
564
- cy: "6",
565
- r: "3",
566
- key: "1lh9wr"
693
+ ["path", {
694
+ d: "M19 2v6",
695
+ key: "4bpg5p"
567
696
  }],
568
697
  ["path", {
569
- d: "M13 6h3a2 2 0 0 1 2 2v7",
570
- key: "1yeb86"
698
+ d: "M21 11.5V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7.5",
699
+ key: "1ue2ih"
571
700
  }],
572
701
  ["path", {
573
- d: "M11 18H8a2 2 0 0 1-2-2V9",
574
- key: "19pyzm"
702
+ d: "m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21",
703
+ key: "1xmnt7"
704
+ }],
705
+ ["circle", {
706
+ cx: "9",
707
+ cy: "9",
708
+ r: "2",
709
+ key: "af1f0g"
575
710
  }]
576
711
  ]);
577
712
  /**
@@ -580,16 +715,6 @@ window.__ModuleLoader__.load({
580
715
  * This source code is licensed under the ISC license.
581
716
  * See the LICENSE file in the root directory of this source tree.
582
717
  */
583
- const Heart = createLucideIcon("heart", [["path", {
584
- d: "M2 9.5a5.5 5.5 0 0 1 9.591-3.676.56.56 0 0 0 .818 0A5.49 5.49 0 0 1 22 9.5c0 2.29-1.5 4-3 5.5l-5.492 5.313a2 2 0 0 1-3 .019L5 15c-1.5-1.5-3-3.2-3-5.5",
585
- key: "mvr1a0"
586
- }]]);
587
- /**
588
- * @license lucide-react v1.38.0 - ISC
589
- *
590
- * This source code is licensed under the ISC license.
591
- * See the LICENSE file in the root directory of this source tree.
592
- */
593
718
  const Image$1 = createLucideIcon("image", [
594
719
  ["rect", {
595
720
  width: "18",
@@ -617,30 +742,106 @@ window.__ModuleLoader__.load({
617
742
  * This source code is licensed under the ISC license.
618
743
  * See the LICENSE file in the root directory of this source tree.
619
744
  */
620
- const ListTodo = createLucideIcon("list-todo", [
745
+ const LoaderCircle = createLucideIcon("loader-circle", [["path", {
746
+ d: "M21 12a9 9 0 1 1-6.219-8.56",
747
+ key: "13zald"
748
+ }]]);
749
+ /**
750
+ * @license lucide-react v1.38.0 - ISC
751
+ *
752
+ * This source code is licensed under the ISC license.
753
+ * See the LICENSE file in the root directory of this source tree.
754
+ */
755
+ const PanelLeftClose = createLucideIcon("panel-left-close", [
756
+ ["rect", {
757
+ width: "18",
758
+ height: "18",
759
+ x: "3",
760
+ y: "3",
761
+ rx: "2",
762
+ key: "afitv7"
763
+ }],
764
+ ["path", {
765
+ d: "M9 3v18",
766
+ key: "fh3hqa"
767
+ }],
768
+ ["path", {
769
+ d: "m16 15-3-3 3-3",
770
+ key: "14y99z"
771
+ }]
772
+ ]);
773
+ /**
774
+ * @license lucide-react v1.38.0 - ISC
775
+ *
776
+ * This source code is licensed under the ISC license.
777
+ * See the LICENSE file in the root directory of this source tree.
778
+ */
779
+ const PanelLeft = createLucideIcon("panel-left", [["rect", {
780
+ width: "18",
781
+ height: "18",
782
+ x: "3",
783
+ y: "3",
784
+ rx: "2",
785
+ key: "afitv7"
786
+ }], ["path", {
787
+ d: "M9 3v18",
788
+ key: "fh3hqa"
789
+ }]]);
790
+ /**
791
+ * @license lucide-react v1.38.0 - ISC
792
+ *
793
+ * This source code is licensed under the ISC license.
794
+ * See the LICENSE file in the root directory of this source tree.
795
+ */
796
+ const PencilLine = createLucideIcon("pencil-line", [
797
+ ["path", {
798
+ d: "M13 21h8",
799
+ key: "1jsn5i"
800
+ }],
621
801
  ["path", {
622
- d: "M13 5h8",
623
- key: "a7qcls"
802
+ d: "m15 5 4 4",
803
+ key: "1mk7zo"
624
804
  }],
625
805
  ["path", {
626
- d: "M13 12h8",
627
- key: "h98zly"
806
+ d: "M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z",
807
+ key: "1a8usu"
808
+ }]
809
+ ]);
810
+ /**
811
+ * @license lucide-react v1.38.0 - ISC
812
+ *
813
+ * This source code is licensed under the ISC license.
814
+ * See the LICENSE file in the root directory of this source tree.
815
+ */
816
+ const Plus = createLucideIcon("plus", [["path", {
817
+ d: "M5 12h14",
818
+ key: "1ays0h"
819
+ }], ["path", {
820
+ d: "M12 5v14",
821
+ key: "s699le"
822
+ }]]);
823
+ /**
824
+ * @license lucide-react v1.38.0 - ISC
825
+ *
826
+ * This source code is licensed under the ISC license.
827
+ * See the LICENSE file in the root directory of this source tree.
828
+ */
829
+ const RefreshCw = createLucideIcon("refresh-cw", [
830
+ ["path", {
831
+ d: "M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8",
832
+ key: "v9h5vc"
628
833
  }],
629
834
  ["path", {
630
- d: "M13 19h8",
631
- key: "c3s6r1"
835
+ d: "M21 3v5h-5",
836
+ key: "1q7to0"
632
837
  }],
633
838
  ["path", {
634
- d: "m3 17 2 2 4-4",
635
- key: "1jhpwq"
839
+ d: "M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16",
840
+ key: "3uifl3"
636
841
  }],
637
- ["rect", {
638
- x: "3",
639
- y: "4",
640
- width: "6",
641
- height: "6",
642
- rx: "1",
643
- key: "cif1o7"
842
+ ["path", {
843
+ d: "M8 16H3v5",
844
+ key: "1cv678"
644
845
  }]
645
846
  ]);
646
847
  /**
@@ -708,6 +909,32 @@ window.__ModuleLoader__.load({
708
909
  * This source code is licensed under the ISC license.
709
910
  * See the LICENSE file in the root directory of this source tree.
710
911
  */
912
+ const Sparkles = createLucideIcon("sparkles", [
913
+ ["path", {
914
+ d: "M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594z",
915
+ key: "1s2grr"
916
+ }],
917
+ ["path", {
918
+ d: "M20 2v4",
919
+ key: "1rf3ol"
920
+ }],
921
+ ["path", {
922
+ d: "M22 4h-4",
923
+ key: "gwowj6"
924
+ }],
925
+ ["circle", {
926
+ cx: "4",
927
+ cy: "20",
928
+ r: "2",
929
+ key: "6kqj1y"
930
+ }]
931
+ ]);
932
+ /**
933
+ * @license lucide-react v1.38.0 - ISC
934
+ *
935
+ * This source code is licensed under the ISC license.
936
+ * See the LICENSE file in the root directory of this source tree.
937
+ */
711
938
  const Trash2 = createLucideIcon("trash-2", [
712
939
  ["path", {
713
940
  d: "M10 11v6",
@@ -756,6 +983,26 @@ window.__ModuleLoader__.load({
756
983
  * This source code is licensed under the ISC license.
757
984
  * See the LICENSE file in the root directory of this source tree.
758
985
  */
986
+ const Upload = createLucideIcon("upload", [
987
+ ["path", {
988
+ d: "M12 3v12",
989
+ key: "1x0j5s"
990
+ }],
991
+ ["path", {
992
+ d: "m17 8-5-5-5 5",
993
+ key: "7q97r8"
994
+ }],
995
+ ["path", {
996
+ d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",
997
+ key: "ih7n3h"
998
+ }]
999
+ ]);
1000
+ /**
1001
+ * @license lucide-react v1.38.0 - ISC
1002
+ *
1003
+ * This source code is licensed under the ISC license.
1004
+ * See the LICENSE file in the root directory of this source tree.
1005
+ */
759
1006
  const X = createLucideIcon("x", [["path", {
760
1007
  d: "M18 6 6 18",
761
1008
  key: "1bl5f8"
@@ -763,21 +1010,1592 @@ window.__ModuleLoader__.load({
763
1010
  d: "m6 6 12 12",
764
1011
  key: "d8bk6v"
765
1012
  }]]);
1013
+ /**
1014
+ * @license lucide-react v1.38.0 - ISC
1015
+ *
1016
+ * This source code is licensed under the ISC license.
1017
+ * See the LICENSE file in the root directory of this source tree.
1018
+ */
1019
+ const ZoomIn = createLucideIcon("zoom-in", [
1020
+ ["circle", {
1021
+ cx: "11",
1022
+ cy: "11",
1023
+ r: "8",
1024
+ key: "4ej97u"
1025
+ }],
1026
+ ["line", {
1027
+ x1: "21",
1028
+ x2: "16.65",
1029
+ y1: "21",
1030
+ y2: "16.65",
1031
+ key: "13gj7c"
1032
+ }],
1033
+ ["line", {
1034
+ x1: "11",
1035
+ x2: "11",
1036
+ y1: "8",
1037
+ y2: "14",
1038
+ key: "1vmskp"
1039
+ }],
1040
+ ["line", {
1041
+ x1: "8",
1042
+ x2: "14",
1043
+ y1: "11",
1044
+ y2: "11",
1045
+ key: "durymu"
1046
+ }]
1047
+ ]);
1048
+ /**
1049
+ * @license lucide-react v1.38.0 - ISC
1050
+ *
1051
+ * This source code is licensed under the ISC license.
1052
+ * See the LICENSE file in the root directory of this source tree.
1053
+ */
1054
+ const ZoomOut = createLucideIcon("zoom-out", [
1055
+ ["circle", {
1056
+ cx: "11",
1057
+ cy: "11",
1058
+ r: "8",
1059
+ key: "4ej97u"
1060
+ }],
1061
+ ["line", {
1062
+ x1: "21",
1063
+ x2: "16.65",
1064
+ y1: "21",
1065
+ y2: "16.65",
1066
+ key: "13gj7c"
1067
+ }],
1068
+ ["line", {
1069
+ x1: "8",
1070
+ x2: "14",
1071
+ y1: "11",
1072
+ y2: "11",
1073
+ key: "durymu"
1074
+ }]
1075
+ ]);
1076
+ let currentCacheBytes = 0;
1077
+ const cache = /* @__PURE__ */ new Map();
1078
+ /**
1079
+ * Fetch or reuse an image Blob by attachment ID.
1080
+ * Refreshes LRU recency on hit and enforces count/byte limits on arrival.
1081
+ */
1082
+ function fetchAttachmentBlob(attachment) {
1083
+ const key = String(attachment.attachmentId);
1084
+ const existing = cache.get(key);
1085
+ if (existing !== void 0) {
1086
+ cache.delete(key);
1087
+ cache.set(key, existing);
1088
+ return existing.promise;
1089
+ }
1090
+ const entry = {
1091
+ promise: Promise.resolve().then(async () => {
1092
+ try {
1093
+ const response = await fetch(IMAGE_ROUTE, {
1094
+ method: "POST",
1095
+ credentials: "same-origin",
1096
+ headers: { "content-type": "application/json" },
1097
+ body: JSON.stringify({ attachment })
1098
+ });
1099
+ if (!response.ok) throw new Error(`Image unavailable (${response.status})`);
1100
+ const blob = await response.blob();
1101
+ if (cache.get(key) === entry) {
1102
+ entry.bytes = blob.size;
1103
+ currentCacheBytes += blob.size;
1104
+ enforceLimits();
1105
+ }
1106
+ return blob;
1107
+ } catch (err) {
1108
+ if (cache.get(key) === entry) cache.delete(key);
1109
+ throw err;
1110
+ }
1111
+ }),
1112
+ bytes: 0
1113
+ };
1114
+ cache.set(key, entry);
1115
+ enforceLimits();
1116
+ return entry.promise;
1117
+ }
1118
+ /** Evict one item from memory cache (e.g. when deleted from gallery). */
1119
+ function evictAttachmentCache(attachmentId) {
1120
+ const key = String(attachmentId);
1121
+ const entry = cache.get(key);
1122
+ if (entry !== void 0) {
1123
+ currentCacheBytes = Math.max(0, currentCacheBytes - entry.bytes);
1124
+ cache.delete(key);
1125
+ }
1126
+ }
1127
+ function enforceLimits() {
1128
+ while (cache.size > 30 || currentCacheBytes > 134217728) {
1129
+ const oldestKey = cache.keys().next().value;
1130
+ if (oldestKey === void 0) break;
1131
+ const oldest = cache.get(oldestKey);
1132
+ if (oldest !== void 0) currentCacheBytes = Math.max(0, currentCacheBytes - oldest.bytes);
1133
+ cache.delete(oldestKey);
1134
+ }
1135
+ }
766
1136
  //#endregion
767
- //#region lib/types/client/gallery-view.js
1137
+ //#region lib/types/client/browser-image-utils.js
768
1138
  /**
769
- * Native Workspace Gallery & Studio View Component for DSH `conversation.view` slot.
770
- * Fully i18n-reactive (Chinese & English) with modular tabs, multi-dimensional filters,
771
- * responsive image grid, and placeholder routes.
1139
+ * Shared browser image and DOM helpers for client views.
1140
+ * Completely decoupled from gallery-view and studio-view to prevent circular dependencies.
772
1141
  */
773
- const DICT$1 = {
1142
+ /** Copy an image Blob to the OS clipboard, converting to PNG if required by the browser. */
1143
+ async function copyImageBlob(blob) {
1144
+ try {
1145
+ if (blob.type === "image/png") {
1146
+ await navigator.clipboard.write([new ClipboardItem({ "image/png": blob })]);
1147
+ return true;
1148
+ }
1149
+ if (typeof createImageBitmap === "function") {
1150
+ const bitmap = await createImageBitmap(blob);
1151
+ try {
1152
+ const canvas = document.createElement("canvas");
1153
+ canvas.width = bitmap.width;
1154
+ canvas.height = bitmap.height;
1155
+ const ctx = canvas.getContext("2d");
1156
+ if (!ctx) return false;
1157
+ ctx.drawImage(bitmap, 0, 0);
1158
+ const pngBlob = await new Promise((resolve) => canvas.toBlob(resolve, "image/png"));
1159
+ if (!pngBlob) return false;
1160
+ await navigator.clipboard.write([new ClipboardItem({ "image/png": pngBlob })]);
1161
+ return true;
1162
+ } finally {
1163
+ bitmap.close();
1164
+ }
1165
+ }
1166
+ const img = new Image();
1167
+ const url = URL.createObjectURL(blob);
1168
+ try {
1169
+ await new Promise((resolve, reject) => {
1170
+ img.onload = () => resolve();
1171
+ img.onerror = () => reject(/* @__PURE__ */ new Error("Image decode failed"));
1172
+ img.src = url;
1173
+ });
1174
+ const canvas = document.createElement("canvas");
1175
+ canvas.width = img.naturalWidth;
1176
+ canvas.height = img.naturalHeight;
1177
+ const ctx = canvas.getContext("2d");
1178
+ if (!ctx) return false;
1179
+ ctx.drawImage(img, 0, 0);
1180
+ const pngBlob = await new Promise((resolve) => canvas.toBlob(resolve, "image/png"));
1181
+ if (!pngBlob) return false;
1182
+ await navigator.clipboard.write([new ClipboardItem({ "image/png": pngBlob })]);
1183
+ return true;
1184
+ } finally {
1185
+ URL.revokeObjectURL(url);
1186
+ }
1187
+ } catch {
1188
+ return false;
1189
+ }
1190
+ }
1191
+ /** Safely trigger a browser file download by mounting an anchor element into the DOM. */
1192
+ function downloadBlobUrl(url, filename) {
1193
+ const link = document.createElement("a");
1194
+ link.href = url;
1195
+ link.download = filename;
1196
+ document.body.appendChild(link);
1197
+ try {
1198
+ link.click();
1199
+ } finally {
1200
+ document.body.removeChild(link);
1201
+ }
1202
+ }
1203
+ /** Human-readable relative time formatter supporting zh and en. */
1204
+ function formatRelativeTime$1(timestamp, lang) {
1205
+ const diff = Math.max(0, Date.now() - timestamp);
1206
+ if (lang === "zh") {
1207
+ if (diff < 6e4) return "刚刚";
1208
+ if (diff < 36e5) return `${Math.floor(diff / 6e4)} 分钟前`;
1209
+ if (diff < 864e5) return `${Math.floor(diff / 36e5)} 小时前`;
1210
+ if (diff < 2592e6) return `${Math.floor(diff / 864e5)} 天前`;
1211
+ const d = new Date(timestamp);
1212
+ return `${d.getFullYear()}/${d.getMonth() + 1}/${d.getDate()}`;
1213
+ }
1214
+ if (diff < 6e4) return "Just now";
1215
+ if (diff < 36e5) return `${Math.floor(diff / 6e4)}m ago`;
1216
+ if (diff < 864e5) return `${Math.floor(diff / 36e5)}h ago`;
1217
+ if (diff < 2592e6) return `${Math.floor(diff / 864e5)}d ago`;
1218
+ const d = new Date(timestamp);
1219
+ return `${d.getFullYear()}/${d.getMonth() + 1}/${d.getDate()}`;
1220
+ }
1221
+ //#endregion
1222
+ //#region lib/types/client/studio-view.js
1223
+ const COPY = {
774
1224
  zh: {
775
- tabGallery: "图库",
776
- tabStudio: "工作台",
777
- tabFavorites: "收藏",
778
- tabCompare: "对比",
779
- tabTasks: "任务",
780
- filterAllProviders: "全部提供商",
1225
+ title: "云端图像工作台",
1226
+ configured: "API 已配置",
1227
+ unconfigured: "未配置",
1228
+ recent: "最近生成",
1229
+ empty: "还没有生成记录",
1230
+ generate: "文生图",
1231
+ edit: "图生图",
1232
+ details: "图片详情",
1233
+ reference: "参考图",
1234
+ optional: "可选",
1235
+ upload: "点击上传或拖拽图片到此处",
1236
+ uploadHint: "支持 JPG / PNG / WebP / GIF,最大 10MB(最多 5 张)",
1237
+ prompt: "提示词 Prompt",
1238
+ clear: "清空",
1239
+ promptPlaceholder: "描述主体、构图、风格、光线与需要出现的文字…(支持 Ctrl+Enter 快捷生成)",
1240
+ provider: "Provider",
1241
+ model: "Model",
1242
+ ratio: "比例",
1243
+ quality: "清晰度",
1244
+ start: "开始生成",
1245
+ generating: "正在生成…",
1246
+ cancelGenerate: "取消生成",
1247
+ retry: "重新加载",
1248
+ configLoadFailed: "工作台配置加载失败,请检查服务后重试。",
1249
+ noProvider: "请先在设置中配置至少一个云端图像 Provider 的 API Key。",
1250
+ selectConfigured: "该 Provider 尚未配置,请先到设置中配置 API Key。",
1251
+ needPrompt: "请输入提示词",
1252
+ needReference: "请先添加至少一张参考图",
1253
+ result: "本次结果",
1254
+ continueEdit: "继续编辑(垫图)",
1255
+ regenerate: "再次生成",
1256
+ copy: "复制",
1257
+ copied: "已复制到剪贴板",
1258
+ copyFailed: "复制失败",
1259
+ download: "下载",
1260
+ remove: "删除",
1261
+ fit: "适应窗口",
1262
+ loading: "正在读取图片…",
1263
+ generationFailed: "生成失败",
1264
+ selectHistory: "从左侧选择一张图片,或在右侧开始新的生成。",
1265
+ created: "生成时间",
1266
+ elapsed: "耗时",
1267
+ dimensions: "尺寸",
1268
+ output: "输出参数",
1269
+ closeReference: "移除参考图",
1270
+ uploadInvalid: "请选择有效的图片文件(最大 10MB)",
1271
+ imageLoadFailed: "图片读取失败",
1272
+ fullscreen: "大图全屏",
1273
+ close: "关闭",
1274
+ copyPpt: "复制 Prompt",
1275
+ copiedPrompt: "已复制 Prompt",
1276
+ copiedImage: "已复制图片",
1277
+ favorite: "收藏",
1278
+ favorited: "已收藏",
1279
+ favoriteAdded: "已添加到收藏",
1280
+ favoriteRemoved: "已取消收藏",
1281
+ loadMore: "加载更多 ({n})",
1282
+ deleteModalTitle: "从图库删除",
1283
+ deleteModalDesc: "确定从图库中删除这张图片吗?(原聊天记录不会受影响)",
1284
+ deleteWorkspaceFilesLabel: "同时删除工作区本地文件",
1285
+ confirm: "确定删除",
1286
+ cancel: "取消",
1287
+ deletedToast: "已从图库删除",
1288
+ referencesCount: "参考图 ({current}/{max})",
1289
+ addMoreRef: "+ 添加",
1290
+ maxReferencesExceeded: "最多支持添加 {max} 张参考图",
1291
+ newGeneration: "新建生成",
1292
+ new: "新建",
1293
+ configuredCount: "API 已配置 · {count}",
1294
+ unconfiguredStatus: "API 未配置",
1295
+ providerStatus: "云端提供商与模型状态",
1296
+ collapseSidebar: "折叠最近生成",
1297
+ expandSidebar: "展开最近生成"
1298
+ },
1299
+ en: {
1300
+ title: "Cloud Image Studio",
1301
+ configured: "API configured",
1302
+ unconfigured: "Not configured",
1303
+ recent: "Recent generations",
1304
+ empty: "No generated images yet",
1305
+ generate: "Text to image",
1306
+ edit: "Image to image",
1307
+ details: "Image details",
1308
+ reference: "Reference image",
1309
+ optional: "optional",
1310
+ upload: "Click or drop images here",
1311
+ uploadHint: "JPG / PNG / WebP / GIF, up to 10MB (max 5)",
1312
+ prompt: "Prompt",
1313
+ clear: "Clear",
1314
+ promptPlaceholder: "Describe the subject, composition, style, lighting, and exact text… (Ctrl+Enter to generate)",
1315
+ provider: "Provider",
1316
+ model: "Model",
1317
+ ratio: "Aspect ratio",
1318
+ quality: "Quality",
1319
+ start: "Generate",
1320
+ generating: "Generating…",
1321
+ cancelGenerate: "Cancel",
1322
+ retry: "Retry",
1323
+ configLoadFailed: "Failed to load studio configuration.",
1324
+ noProvider: "Configure an API key for at least one cloud image provider in Settings.",
1325
+ selectConfigured: "This provider is not configured. Add its API key in Settings first.",
1326
+ needPrompt: "Enter a prompt",
1327
+ needReference: "Add at least one reference image first",
1328
+ result: "Current result",
1329
+ continueEdit: "Continue editing",
1330
+ regenerate: "Generate again",
1331
+ copy: "Copy",
1332
+ copied: "Copied to clipboard",
1333
+ copyFailed: "Copy failed",
1334
+ download: "Download",
1335
+ remove: "Delete",
1336
+ fit: "Fit",
1337
+ loading: "Loading image…",
1338
+ generationFailed: "Generation failed",
1339
+ selectHistory: "Select an image on the left, or start a new generation on the right.",
1340
+ created: "Created",
1341
+ elapsed: "Elapsed",
1342
+ dimensions: "Dimensions",
1343
+ output: "Output",
1344
+ closeReference: "Remove reference",
1345
+ uploadInvalid: "Choose a valid image file up to 10MB.",
1346
+ imageLoadFailed: "Could not load image",
1347
+ fullscreen: "Fullscreen",
1348
+ close: "Close",
1349
+ copyPpt: "Copy Prompt",
1350
+ copiedPrompt: "Prompt copied",
1351
+ copiedImage: "Image copied",
1352
+ favorite: "Favorite",
1353
+ favorited: "Favorited",
1354
+ favoriteAdded: "Added to favorites",
1355
+ favoriteRemoved: "Removed from favorites",
1356
+ loadMore: "Load more ({n})",
1357
+ deleteModalTitle: "Delete from gallery",
1358
+ deleteModalDesc: "Remove this image from the local gallery? (Chat history remains unaffected)",
1359
+ deleteWorkspaceFilesLabel: "Also delete local workspace files",
1360
+ confirm: "Delete",
1361
+ cancel: "Cancel",
1362
+ deletedToast: "Deleted from gallery",
1363
+ referencesCount: "References ({current}/{max})",
1364
+ addMoreRef: "+ Add",
1365
+ maxReferencesExceeded: "Up to {max} reference images allowed",
1366
+ newGeneration: "New generation",
1367
+ new: "New",
1368
+ configuredCount: "API configured · {count}",
1369
+ unconfiguredStatus: "Not configured",
1370
+ providerStatus: "Provider & Model Status",
1371
+ collapseSidebar: "Collapse sidebar",
1372
+ expandSidebar: "Expand recent list"
1373
+ }
1374
+ };
1375
+ const StudioView = ({ locale, workspace }) => {
1376
+ const [lang, setLang] = (0, react.useState)(() => locale?.getSnapshot?.().active?.startsWith("en") ? "en" : "zh");
1377
+ const [config, setConfig] = (0, react.useState)(null);
1378
+ const [configLoading, setConfigLoading] = (0, react.useState)(true);
1379
+ const [configError, setConfigError] = (0, react.useState)(null);
1380
+ const [items, setItems] = (0, react.useState)([]);
1381
+ const [sidebarCollapsed, setSidebarCollapsed] = (0, react.useState)(false);
1382
+ const [visibleLimit, setVisibleLimit] = (0, react.useState)(30);
1383
+ const [selected, setSelected] = (0, react.useState)(null);
1384
+ const [mode, setMode] = (0, react.useState)("generate");
1385
+ const [panelTab, setPanelTab] = (0, react.useState)("generate");
1386
+ const [provider, setProvider] = (0, react.useState)("google");
1387
+ const [model, setModel] = (0, react.useState)("");
1388
+ const [ratio, setRatio] = (0, react.useState)("1:1");
1389
+ const [quality, setQuality] = (0, react.useState)("1K");
1390
+ const [prompt, setPrompt] = (0, react.useState)("");
1391
+ const [references, setReferences] = (0, react.useState)([]);
1392
+ const [isGenerating, setIsGenerating] = (0, react.useState)(false);
1393
+ const [error, setError] = (0, react.useState)(null);
1394
+ const [notice, setNotice] = (0, react.useState)(null);
1395
+ const [zoom, setZoom] = (0, react.useState)(100);
1396
+ const [fit, setFit] = (0, react.useState)(true);
1397
+ const [offset, setOffset] = (0, react.useState)({
1398
+ x: 0,
1399
+ y: 0
1400
+ });
1401
+ const [isDragging, setIsDragging] = (0, react.useState)(false);
1402
+ const dragStartRef = (0, react.useRef)(null);
1403
+ const hasDraggedRef = (0, react.useRef)(false);
1404
+ const [dragging, setDragging] = (0, react.useState)(false);
1405
+ const [lightboxOpen, setLightboxOpen] = (0, react.useState)(false);
1406
+ const [showDeleteModal, setShowDeleteModal] = (0, react.useState)(false);
1407
+ const [deleteWorkspaceFiles, setDeleteWorkspaceFiles] = (0, react.useState)(true);
1408
+ const requestRef = (0, react.useRef)(null);
1409
+ const canvasRef = (0, react.useRef)(null);
1410
+ const fileInputRef = (0, react.useRef)(null);
1411
+ const image = useAttachmentImage(selected?.attachment);
1412
+ const maxReferences = provider === "dashscope" ? 3 : 5;
1413
+ const referencesRef = (0, react.useRef)(references);
1414
+ referencesRef.current = references;
1415
+ const t = (key, values) => {
1416
+ let text = COPY[lang][key];
1417
+ for (const [name, value] of Object.entries(values ?? {})) text = text.replace(`{${name}}`, value);
1418
+ return text;
1419
+ };
1420
+ (0, react.useEffect)(() => locale?.subscribe?.(() => setLang(locale.getSnapshot().active?.startsWith("en") ? "en" : "zh")), [locale]);
1421
+ const loadConfig = (0, react.useCallback)(async () => {
1422
+ setConfigLoading(true);
1423
+ setConfigError(null);
1424
+ const controller = new AbortController();
1425
+ try {
1426
+ const response = await fetch(STUDIO_ROUTE, {
1427
+ signal: controller.signal,
1428
+ credentials: "same-origin"
1429
+ });
1430
+ const payload = await response.json();
1431
+ if (!response.ok || !("providers" in payload)) throw new Error("error" in payload && payload.error ? payload.error : "Studio unavailable");
1432
+ setConfig(payload);
1433
+ const initial = payload.providers.find((item) => item.provider === payload.activeProvider) ?? payload.providers[0];
1434
+ if (initial !== void 0) applyProvider(initial);
1435
+ } catch (fetchError) {
1436
+ if (!controller.signal.aborted) setConfigError(messageOf(fetchError));
1437
+ } finally {
1438
+ setConfigLoading(false);
1439
+ }
1440
+ }, []);
1441
+ (0, react.useEffect)(() => {
1442
+ loadConfig();
1443
+ }, [loadConfig]);
1444
+ (0, react.useEffect)(() => {
1445
+ let mounted = true;
1446
+ const load = () => void getGalleryItems().then((next) => {
1447
+ if (!mounted) return;
1448
+ setItems(next);
1449
+ setSelected((current) => current === null ? null : next.find((item) => item.id === current.id) ?? null);
1450
+ });
1451
+ load();
1452
+ const unsubscribe = subscribeGallery(load);
1453
+ return () => {
1454
+ mounted = false;
1455
+ unsubscribe();
1456
+ };
1457
+ }, []);
1458
+ (0, react.useEffect)(() => {
1459
+ return () => {
1460
+ requestRef.current?.abort();
1461
+ };
1462
+ }, []);
1463
+ (0, react.useEffect)(() => {
1464
+ return () => {
1465
+ for (const item of referencesRef.current) if (item.previewUrl.startsWith("blob:")) URL.revokeObjectURL(item.previewUrl);
1466
+ };
1467
+ }, []);
1468
+ (0, react.useEffect)(() => {
1469
+ const handleKeyDown = (e) => {
1470
+ if (e.key === "Escape" && lightboxOpen) setLightboxOpen(false);
1471
+ };
1472
+ window.addEventListener("keydown", handleKeyDown);
1473
+ return () => window.removeEventListener("keydown", handleKeyDown);
1474
+ }, [lightboxOpen]);
1475
+ (0, react.useEffect)(() => {
1476
+ const canvasEl = canvasRef.current;
1477
+ if (!canvasEl) return;
1478
+ const onWheel = (e) => {
1479
+ if (selected === null || !image.url) return;
1480
+ e.preventDefault();
1481
+ const delta = e.deltaY < 0 ? 15 : -15;
1482
+ setFit(false);
1483
+ setZoom((prev) => Math.min(500, Math.max(25, prev + delta)));
1484
+ };
1485
+ canvasEl.addEventListener("wheel", onWheel, { passive: false });
1486
+ return () => canvasEl.removeEventListener("wheel", onWheel);
1487
+ }, [selected, image.url]);
1488
+ (0, react.useEffect)(() => {
1489
+ const handleMouseMove = (e) => {
1490
+ if (!dragStartRef.current) return;
1491
+ const dx = e.clientX - dragStartRef.current.x;
1492
+ const dy = e.clientY - dragStartRef.current.y;
1493
+ if (Math.abs(dx) > 3 || Math.abs(dy) > 3) {
1494
+ hasDraggedRef.current = true;
1495
+ setFit(false);
1496
+ }
1497
+ setOffset({
1498
+ x: dragStartRef.current.startX + dx,
1499
+ y: dragStartRef.current.startY + dy
1500
+ });
1501
+ };
1502
+ const handleMouseUp = () => {
1503
+ dragStartRef.current = null;
1504
+ setIsDragging(false);
1505
+ };
1506
+ window.addEventListener("mousemove", handleMouseMove);
1507
+ window.addEventListener("mouseup", handleMouseUp);
1508
+ return () => {
1509
+ window.removeEventListener("mousemove", handleMouseMove);
1510
+ window.removeEventListener("mouseup", handleMouseUp);
1511
+ };
1512
+ }, []);
1513
+ const activeProfile = (0, react.useMemo)(() => config?.providers.find((item) => item.provider === provider), [config, provider]);
1514
+ const configuredCount = config?.providers.filter((item) => item.configured).length ?? 0;
1515
+ const displayItems = (0, react.useMemo)(() => items.slice(0, visibleLimit), [items, visibleLimit]);
1516
+ const applyProvider = (profile) => {
1517
+ setProvider(profile.provider);
1518
+ setModel(profile.model);
1519
+ setRatio(profile.defaultRatio);
1520
+ setQuality(profile.defaultQuality);
1521
+ setError(null);
1522
+ };
1523
+ const changeProvider = (value) => {
1524
+ const profile = config?.providers.find((item) => item.provider === value);
1525
+ if (profile !== void 0) {
1526
+ applyProvider(profile);
1527
+ const providerMax = value === "dashscope" ? 3 : 5;
1528
+ if (referencesRef.current.length > providerMax) {
1529
+ const keep = referencesRef.current.slice(0, providerMax);
1530
+ const overflow = referencesRef.current.slice(providerMax);
1531
+ for (const item of overflow) if (item.previewUrl.startsWith("blob:")) URL.revokeObjectURL(item.previewUrl);
1532
+ setReferences(keep);
1533
+ referencesRef.current = keep;
1534
+ setError(t("maxReferencesExceeded", { max: String(providerMax) }));
1535
+ }
1536
+ }
1537
+ };
1538
+ const resetFit = () => {
1539
+ setFit(true);
1540
+ setZoom(100);
1541
+ setOffset({
1542
+ x: 0,
1543
+ y: 0
1544
+ });
1545
+ };
1546
+ const selectItem = (item) => {
1547
+ setSelected(item);
1548
+ setPanelTab("details");
1549
+ resetFit();
1550
+ };
1551
+ const startNew = () => {
1552
+ setSelected(null);
1553
+ setPanelTab("generate");
1554
+ resetFit();
1555
+ };
1556
+ const handleCanvasMouseDown = (e) => {
1557
+ if (e.button !== 0 && e.button !== 1) return;
1558
+ if (selected === null || !image.url) return;
1559
+ setIsDragging(true);
1560
+ hasDraggedRef.current = false;
1561
+ dragStartRef.current = {
1562
+ x: e.clientX,
1563
+ y: e.clientY,
1564
+ startX: offset.x,
1565
+ startY: offset.y
1566
+ };
1567
+ };
1568
+ const handleImageClick = () => {
1569
+ if (!hasDraggedRef.current) setLightboxOpen(true);
1570
+ };
1571
+ const addReferenceFiles = (fileList) => {
1572
+ if (!fileList || fileList.length === 0) return;
1573
+ const files = Array.from(fileList);
1574
+ const validFiles = [];
1575
+ for (const file of files) {
1576
+ if (![
1577
+ "image/jpeg",
1578
+ "image/png",
1579
+ "image/webp",
1580
+ "image/gif"
1581
+ ].includes(file.type) || file.size > 10485760) {
1582
+ setError(t("uploadInvalid"));
1583
+ continue;
1584
+ }
1585
+ validFiles.push(file);
1586
+ }
1587
+ if (validFiles.length === 0) return;
1588
+ const currentLen = referencesRef.current.length;
1589
+ const available = maxReferences - currentLen;
1590
+ if (available <= 0) {
1591
+ setError(t("maxReferencesExceeded", { max: String(maxReferences) }));
1592
+ return;
1593
+ }
1594
+ const toAdd = validFiles.slice(0, available);
1595
+ if (validFiles.length > available) setError(t("maxReferencesExceeded", { max: String(maxReferences) }));
1596
+ else setError(null);
1597
+ const newItems = toAdd.map((file, idx) => ({
1598
+ id: `upload-${Date.now()}-${idx}-${Math.random().toString(36).slice(2, 7)}`,
1599
+ file,
1600
+ previewUrl: URL.createObjectURL(file)
1601
+ }));
1602
+ setReferences((prev) => [...prev, ...newItems]);
1603
+ };
1604
+ const removeReference = (id) => {
1605
+ const target = referencesRef.current.find((item) => item.id === id);
1606
+ if (target && target.previewUrl.startsWith("blob:")) URL.revokeObjectURL(target.previewUrl);
1607
+ setReferences((prev) => prev.filter((item) => item.id !== id));
1608
+ };
1609
+ const clearAllReferences = () => {
1610
+ for (const item of referencesRef.current) if (item.previewUrl.startsWith("blob:")) URL.revokeObjectURL(item.previewUrl);
1611
+ setReferences([]);
1612
+ };
1613
+ const continueEdit = async () => {
1614
+ if (selected === null) return;
1615
+ const targetItem = selected;
1616
+ const targetAttId = targetItem.attachment.attachmentId;
1617
+ const targetMax = targetItem.provider === "dashscope" ? 3 : 5;
1618
+ if (referencesRef.current.some((r) => r.attachment?.attachmentId === targetAttId)) {
1619
+ setMode("edit");
1620
+ setPanelTab("generate");
1621
+ changeProvider(targetItem.provider);
1622
+ return;
1623
+ }
1624
+ if (referencesRef.current.length >= targetMax) {
1625
+ setError(t("maxReferencesExceeded", { max: String(targetMax) }));
1626
+ return;
1627
+ }
1628
+ try {
1629
+ const blob = image.blob ?? await fetchAttachmentBlob(targetItem.attachment);
1630
+ if (referencesRef.current.some((r) => r.attachment?.attachmentId === targetAttId)) {
1631
+ setMode("edit");
1632
+ setPanelTab("generate");
1633
+ changeProvider(targetItem.provider);
1634
+ return;
1635
+ }
1636
+ if (referencesRef.current.length >= targetMax) {
1637
+ setError(t("maxReferencesExceeded", { max: String(targetMax) }));
1638
+ return;
1639
+ }
1640
+ const independentUrl = URL.createObjectURL(blob);
1641
+ const newItem = {
1642
+ id: `item-${targetItem.id}-${Date.now()}`,
1643
+ attachment: targetItem.attachment,
1644
+ previewUrl: independentUrl
1645
+ };
1646
+ const nextList = [...referencesRef.current, newItem];
1647
+ setReferences(nextList);
1648
+ referencesRef.current = nextList;
1649
+ setError(null);
1650
+ setMode("edit");
1651
+ setPanelTab("generate");
1652
+ changeProvider(targetItem.provider);
1653
+ } catch {
1654
+ setError(t("imageLoadFailed"));
1655
+ }
1656
+ };
1657
+ const cancelGeneration = () => {
1658
+ if (requestRef.current) {
1659
+ requestRef.current.abort();
1660
+ requestRef.current = null;
1661
+ setIsGenerating(false);
1662
+ }
1663
+ };
1664
+ const submit = async () => {
1665
+ if (isGenerating || activeProfile === void 0) return;
1666
+ if (prompt.trim().length === 0) return setError(t("needPrompt"));
1667
+ if (!activeProfile.configured) return setError(t("selectConfigured"));
1668
+ if (mode === "edit" && references.length === 0) return setError(t("needReference"));
1669
+ setIsGenerating(true);
1670
+ setError(null);
1671
+ const controller = new AbortController();
1672
+ requestRef.current = controller;
1673
+ try {
1674
+ const referencesPayload = mode === "edit" && references.length > 0 ? await Promise.all(references.map(async (ref) => {
1675
+ if (ref.attachment !== void 0) return { attachment: ref.attachment };
1676
+ if (ref.file !== void 0) return {
1677
+ mediaType: ref.file.type,
1678
+ data: await fileToBase64(ref.file),
1679
+ ...ref.file.name ? { name: ref.file.name } : {}
1680
+ };
1681
+ throw new Error("无效参考图");
1682
+ })) : void 0;
1683
+ const response = await fetch(STUDIO_ROUTE, {
1684
+ method: "POST",
1685
+ credentials: "same-origin",
1686
+ signal: controller.signal,
1687
+ headers: { "content-type": "application/json" },
1688
+ body: JSON.stringify({
1689
+ mode,
1690
+ provider,
1691
+ model,
1692
+ prompt: prompt.trim(),
1693
+ ratio,
1694
+ quality,
1695
+ ...workspace?.path ? { workspaceRoot: workspace.path } : {},
1696
+ ...referencesPayload === void 0 ? {} : { references: referencesPayload }
1697
+ })
1698
+ });
1699
+ const payload = await response.json();
1700
+ if (!response.ok || !("attachment" in payload)) throw new Error("error" in payload && payload.error ? payload.error : t("generationFailed"));
1701
+ const item = {
1702
+ id: String(payload.attachment.attachmentId),
1703
+ attachment: payload.attachment,
1704
+ prompt: payload.prompt,
1705
+ provider: payload.provider,
1706
+ model: payload.model,
1707
+ createdAt: payload.createdAt,
1708
+ aspectRatio: ratio,
1709
+ imageSize: quality,
1710
+ output: payload.output,
1711
+ ...payload.savedTo ? { savedTo: payload.savedTo } : {},
1712
+ ...workspace?.path ? { workspacePath: workspace.path } : {},
1713
+ ...workspace?.workspaceId ? { workspaceId: workspace.workspaceId } : {}
1714
+ };
1715
+ await saveGalleryItem(item);
1716
+ setSelected(item);
1717
+ setPanelTab("details");
1718
+ resetFit();
1719
+ } catch (submitError) {
1720
+ if (controller.signal.aborted) return;
1721
+ setError(messageOf(submitError));
1722
+ } finally {
1723
+ if (requestRef.current === controller) {
1724
+ requestRef.current = null;
1725
+ setIsGenerating(false);
1726
+ }
1727
+ }
1728
+ };
1729
+ const handleConfirmDelete = async () => {
1730
+ if (selected === null) return;
1731
+ const idToDelete = selected.id;
1732
+ const attachmentId = selected.attachment.attachmentId;
1733
+ const savedPath = selected.savedTo;
1734
+ setShowDeleteModal(false);
1735
+ try {
1736
+ if (deleteWorkspaceFiles && typeof savedPath === "string" && savedPath.trim().length > 0) {
1737
+ const res = await fetch(DELETE_ROUTE, {
1738
+ method: "POST",
1739
+ credentials: "same-origin",
1740
+ headers: { "content-type": "application/json" },
1741
+ body: JSON.stringify({ paths: [savedPath] })
1742
+ });
1743
+ if (!res.ok) throw new Error(`删除请求网络异常 (${res.status})`);
1744
+ const data = await res.json().catch(() => null);
1745
+ if (!data || data.ok === false || Array.isArray(data.failedFiles) && data.failedFiles.length > 0) {
1746
+ const reason = data?.failedFiles?.[0]?.error || "工作区文件删除失败";
1747
+ throw new Error(reason);
1748
+ }
1749
+ }
1750
+ await deleteGalleryItem(idToDelete);
1751
+ evictAttachmentCache(attachmentId);
1752
+ flash(t("deletedToast"));
1753
+ } catch (delError) {
1754
+ setError(messageOf(delError));
1755
+ }
1756
+ };
1757
+ const copySelected = async () => {
1758
+ if (image.blob === null) return;
1759
+ const ok = await copyImageBlob(image.blob);
1760
+ flash(ok ? t("copiedImage") : t("copyFailed"));
1761
+ };
1762
+ const downloadSelected = () => {
1763
+ if (image.url === null || selected === null) return;
1764
+ downloadBlobUrl(image.url, `dsh-${selected.provider}-${selected.id.slice(-8)}.${extension(selected.attachment.mediaType)}`);
1765
+ };
1766
+ const toggleFavorite = async () => {
1767
+ if (selected === null) return;
1768
+ const targetId = selected.id;
1769
+ const nextFav = await toggleFavoriteGalleryItem(targetId);
1770
+ setSelected((curr) => curr !== null && curr.id === targetId ? {
1771
+ ...curr,
1772
+ isFavorite: nextFav
1773
+ } : curr);
1774
+ flash(nextFav ? t("favoriteAdded") : t("favoriteRemoved"));
1775
+ };
1776
+ const flash = (message) => {
1777
+ setNotice(message);
1778
+ window.setTimeout(() => setNotice(null), 2200);
1779
+ };
1780
+ return (0, react_jsx_runtime.jsxs)("section", {
1781
+ className: "dsh-ig-workbench",
1782
+ "aria-label": t("title"),
1783
+ children: [
1784
+ (0, react_jsx_runtime.jsxs)("div", {
1785
+ className: `dsh-ig-workbench-grid ${sidebarCollapsed ? "is-sidebar-collapsed" : ""}`,
1786
+ children: [
1787
+ !sidebarCollapsed && (0, react_jsx_runtime.jsxs)("aside", {
1788
+ className: "dsh-ig-recent-panel",
1789
+ children: [(0, react_jsx_runtime.jsxs)("div", {
1790
+ className: "dsh-ig-panel-title",
1791
+ children: [(0, react_jsx_runtime.jsxs)("div", {
1792
+ style: {
1793
+ display: "flex",
1794
+ alignItems: "center",
1795
+ gap: "8px"
1796
+ },
1797
+ children: [(0, react_jsx_runtime.jsx)("span", { children: t("recent") }), (0, react_jsx_runtime.jsx)("span", {
1798
+ className: "dsh-ig-count-badge",
1799
+ children: items.length
1800
+ })]
1801
+ }), (0, react_jsx_runtime.jsx)("button", {
1802
+ type: "button",
1803
+ className: "dsh-ig-collapse-btn",
1804
+ onClick: () => setSidebarCollapsed(true),
1805
+ title: t("collapseSidebar"),
1806
+ children: (0, react_jsx_runtime.jsx)(PanelLeftClose, { size: 15 })
1807
+ })]
1808
+ }), (0, react_jsx_runtime.jsxs)("div", {
1809
+ className: "dsh-ig-recent-scroll",
1810
+ children: [items.length === 0 ? (0, react_jsx_runtime.jsxs)("div", {
1811
+ className: "dsh-ig-recent-empty",
1812
+ children: [(0, react_jsx_runtime.jsx)(ImagePlus, { size: 22 }), (0, react_jsx_runtime.jsx)("span", { children: t("empty") })]
1813
+ }) : displayItems.map((item) => (0, react_jsx_runtime.jsx)(RecentItem, {
1814
+ item,
1815
+ active: selected?.id === item.id,
1816
+ lang,
1817
+ onClick: () => selectItem(item)
1818
+ }, item.id)), items.length > visibleLimit && (0, react_jsx_runtime.jsx)("button", {
1819
+ type: "button",
1820
+ className: "dsh-ig-load-more",
1821
+ onClick: () => setVisibleLimit((l) => l + 30),
1822
+ children: t("loadMore", { n: String(items.length - visibleLimit) })
1823
+ })]
1824
+ })]
1825
+ }),
1826
+ (0, react_jsx_runtime.jsxs)("main", {
1827
+ className: "dsh-ig-canvas-column",
1828
+ children: [
1829
+ (0, react_jsx_runtime.jsxs)("div", {
1830
+ className: "dsh-ig-canvas-toolbar",
1831
+ children: [(0, react_jsx_runtime.jsxs)("div", {
1832
+ className: "dsh-ig-canvas-toolbar-left",
1833
+ children: [sidebarCollapsed && (0, react_jsx_runtime.jsxs)("button", {
1834
+ type: "button",
1835
+ onClick: () => setSidebarCollapsed(false),
1836
+ title: t("expandSidebar"),
1837
+ children: [(0, react_jsx_runtime.jsx)(PanelLeft, { size: 14 }), (0, react_jsx_runtime.jsx)("span", { children: t("recent") })]
1838
+ }), (0, react_jsx_runtime.jsxs)("div", {
1839
+ className: "dsh-ig-status-badge-wrap",
1840
+ children: [(0, react_jsx_runtime.jsxs)("div", {
1841
+ className: "dsh-ig-status-badge",
1842
+ children: [
1843
+ (0, react_jsx_runtime.jsx)("span", { className: `dsh-ig-status-dot ${configuredCount > 0 ? "is-ready" : "is-muted"}` }),
1844
+ (0, react_jsx_runtime.jsx)("span", {
1845
+ className: "dsh-ig-status-text",
1846
+ children: configuredCount > 0 ? t("configuredCount", { count: String(configuredCount) }) : t("unconfiguredStatus")
1847
+ }),
1848
+ (0, react_jsx_runtime.jsx)(ChevronDown, {
1849
+ size: 11,
1850
+ className: "dsh-ig-status-arrow"
1851
+ })
1852
+ ]
1853
+ }), config !== null && (0, react_jsx_runtime.jsxs)("div", {
1854
+ className: "dsh-ig-status-popover",
1855
+ children: [(0, react_jsx_runtime.jsx)("div", {
1856
+ className: "dsh-ig-status-popover-title",
1857
+ children: t("providerStatus")
1858
+ }), (0, react_jsx_runtime.jsx)("ul", {
1859
+ className: "dsh-ig-status-list",
1860
+ children: config.providers.map((p) => (0, react_jsx_runtime.jsxs)("li", {
1861
+ className: p.configured ? "is-configured" : "is-missing",
1862
+ children: [(0, react_jsx_runtime.jsxs)("div", {
1863
+ className: "dsh-ig-status-item-name",
1864
+ children: [(0, react_jsx_runtime.jsx)("strong", { children: p.label }), (0, react_jsx_runtime.jsx)("small", { children: p.model })]
1865
+ }), (0, react_jsx_runtime.jsx)("span", {
1866
+ className: "dsh-ig-status-item-tag",
1867
+ children: p.configured ? t("configured") : t("unconfigured")
1868
+ })]
1869
+ }, p.provider))
1870
+ })]
1871
+ })]
1872
+ })]
1873
+ }), (0, react_jsx_runtime.jsxs)("div", {
1874
+ className: "dsh-ig-canvas-toolbar-right",
1875
+ children: [
1876
+ selected !== null && (0, react_jsx_runtime.jsxs)("button", {
1877
+ type: "button",
1878
+ onClick: startNew,
1879
+ title: t("newGeneration"),
1880
+ children: [(0, react_jsx_runtime.jsx)(Plus, { size: 13 }), (0, react_jsx_runtime.jsx)("span", { children: t("newGeneration") })]
1881
+ }),
1882
+ (0, react_jsx_runtime.jsxs)("button", {
1883
+ type: "button",
1884
+ className: fit ? "is-active" : "",
1885
+ onClick: resetFit,
1886
+ children: [
1887
+ t("fit"),
1888
+ " ",
1889
+ (0, react_jsx_runtime.jsx)(ChevronDown, { size: 13 })
1890
+ ]
1891
+ }),
1892
+ (0, react_jsx_runtime.jsx)("button", {
1893
+ type: "button",
1894
+ onClick: () => {
1895
+ setFit(false);
1896
+ setZoom((value) => Math.max(25, value - 25));
1897
+ },
1898
+ title: "Zoom out",
1899
+ children: (0, react_jsx_runtime.jsx)(ZoomOut, { size: 16 })
1900
+ }),
1901
+ (0, react_jsx_runtime.jsx)("span", {
1902
+ className: "dsh-ig-zoom-display",
1903
+ children: fit ? "AUTO" : `${Math.round(zoom)}%`
1904
+ }),
1905
+ (0, react_jsx_runtime.jsx)("button", {
1906
+ type: "button",
1907
+ onClick: () => {
1908
+ setFit(false);
1909
+ setZoom((value) => Math.min(500, value + 25));
1910
+ },
1911
+ title: "Zoom in",
1912
+ children: (0, react_jsx_runtime.jsx)(ZoomIn, { size: 16 })
1913
+ }),
1914
+ (0, react_jsx_runtime.jsx)("button", {
1915
+ type: "button",
1916
+ onClick: () => setLightboxOpen(true),
1917
+ title: t("fullscreen"),
1918
+ disabled: selected === null,
1919
+ children: (0, react_jsx_runtime.jsx)(Expand, { size: 16 })
1920
+ })
1921
+ ]
1922
+ })]
1923
+ }),
1924
+ (0, react_jsx_runtime.jsx)("div", {
1925
+ className: `dsh-ig-canvas ${isDragging ? "is-dragging" : ""}`,
1926
+ ref: canvasRef,
1927
+ onMouseDown: handleCanvasMouseDown,
1928
+ onDoubleClick: resetFit,
1929
+ children: isGenerating ? (0, react_jsx_runtime.jsxs)("div", {
1930
+ className: "dsh-ig-generating-state",
1931
+ children: [
1932
+ (0, react_jsx_runtime.jsx)("div", {
1933
+ className: "dsh-ig-generation-orbit",
1934
+ children: (0, react_jsx_runtime.jsx)(Sparkles, { size: 28 })
1935
+ }),
1936
+ (0, react_jsx_runtime.jsx)("strong", { children: t("generating") }),
1937
+ (0, react_jsx_runtime.jsxs)("span", { children: [
1938
+ activeProfile?.label,
1939
+ " · ",
1940
+ model
1941
+ ] })
1942
+ ]
1943
+ }) : selected === null ? (0, react_jsx_runtime.jsxs)("div", {
1944
+ className: "dsh-ig-canvas-empty",
1945
+ children: [(0, react_jsx_runtime.jsx)(ImagePlus, { size: 36 }), (0, react_jsx_runtime.jsx)("span", { children: t("selectHistory") })]
1946
+ }) : image.loading ? (0, react_jsx_runtime.jsxs)("div", {
1947
+ className: "dsh-ig-canvas-empty",
1948
+ children: [(0, react_jsx_runtime.jsx)(LoaderCircle, {
1949
+ className: "dsh-ig-spin",
1950
+ size: 28
1951
+ }), (0, react_jsx_runtime.jsx)("span", { children: t("loading") })]
1952
+ }) : image.url !== null ? (0, react_jsx_runtime.jsx)("img", {
1953
+ src: image.url,
1954
+ alt: selected.prompt,
1955
+ draggable: false,
1956
+ onClick: handleImageClick,
1957
+ style: {
1958
+ transform: `translate(${offset.x}px, ${offset.y}px) scale(${zoom / 100})`,
1959
+ transformOrigin: "center center",
1960
+ transition: isDragging ? "none" : "transform 0.08s ease-out"
1961
+ }
1962
+ }) : (0, react_jsx_runtime.jsxs)("div", {
1963
+ className: "dsh-ig-canvas-empty",
1964
+ children: [(0, react_jsx_runtime.jsx)(ImagePlus, { size: 32 }), (0, react_jsx_runtime.jsx)("span", { children: t("imageLoadFailed") })]
1965
+ })
1966
+ }),
1967
+ selected !== null && (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1968
+ (0, react_jsx_runtime.jsxs)("div", {
1969
+ className: "dsh-ig-result-strip",
1970
+ children: [(0, react_jsx_runtime.jsx)("span", { children: t("result") }), (0, react_jsx_runtime.jsxs)("div", { children: [(0, react_jsx_runtime.jsxs)("button", {
1971
+ type: "button",
1972
+ onClick: () => void continueEdit(),
1973
+ children: [(0, react_jsx_runtime.jsx)(PencilLine, { size: 15 }), t("continueEdit")]
1974
+ }), (0, react_jsx_runtime.jsxs)("button", {
1975
+ type: "button",
1976
+ onClick: () => {
1977
+ setMode("generate");
1978
+ setPanelTab("generate");
1979
+ setPrompt(selected.prompt);
1980
+ },
1981
+ children: [(0, react_jsx_runtime.jsx)(RefreshCw, { size: 15 }), t("regenerate")]
1982
+ })] })]
1983
+ }),
1984
+ (0, react_jsx_runtime.jsxs)("div", {
1985
+ className: "dsh-ig-result-actions",
1986
+ children: [(0, react_jsx_runtime.jsx)("p", { children: selected.prompt }), (0, react_jsx_runtime.jsxs)("div", { children: [
1987
+ (0, react_jsx_runtime.jsxs)("button", {
1988
+ type: "button",
1989
+ onClick: () => void toggleFavorite(),
1990
+ title: selected.isFavorite ? t("favorited") : t("favorite"),
1991
+ children: [(0, react_jsx_runtime.jsx)(Heart, {
1992
+ size: 15,
1993
+ fill: selected.isFavorite ? "#ef4444" : "none",
1994
+ color: selected.isFavorite ? "#ef4444" : "currentColor"
1995
+ }), (0, react_jsx_runtime.jsx)("span", { children: selected.isFavorite ? t("favorited") : t("favorite") })]
1996
+ }),
1997
+ (0, react_jsx_runtime.jsxs)("button", {
1998
+ type: "button",
1999
+ onClick: () => void copySelected(),
2000
+ children: [(0, react_jsx_runtime.jsx)(Clipboard, { size: 15 }), (0, react_jsx_runtime.jsx)("span", { children: t("copy") })]
2001
+ }),
2002
+ (0, react_jsx_runtime.jsxs)("button", {
2003
+ type: "button",
2004
+ onClick: downloadSelected,
2005
+ children: [(0, react_jsx_runtime.jsx)(Download, { size: 15 }), (0, react_jsx_runtime.jsx)("span", { children: t("download") })]
2006
+ }),
2007
+ (0, react_jsx_runtime.jsxs)("button", {
2008
+ type: "button",
2009
+ onClick: () => setShowDeleteModal(true),
2010
+ children: [(0, react_jsx_runtime.jsx)(Trash2, { size: 15 }), (0, react_jsx_runtime.jsx)("span", { children: t("remove") })]
2011
+ })
2012
+ ] })]
2013
+ }),
2014
+ (0, react_jsx_runtime.jsxs)("div", {
2015
+ className: "dsh-ig-result-meta",
2016
+ children: [
2017
+ (0, react_jsx_runtime.jsx)("span", { children: formatRelativeTime$1(selected.createdAt, lang) }),
2018
+ (0, react_jsx_runtime.jsx)("span", { children: selected.provider }),
2019
+ (0, react_jsx_runtime.jsx)("span", { children: selected.model }),
2020
+ (0, react_jsx_runtime.jsx)("span", { children: selected.attachment.width && selected.attachment.height ? `${selected.attachment.width} × ${selected.attachment.height}` : "—" })
2021
+ ]
2022
+ })
2023
+ ] })
2024
+ ]
2025
+ }),
2026
+ (0, react_jsx_runtime.jsxs)("aside", {
2027
+ className: "dsh-ig-generate-panel",
2028
+ children: [(0, react_jsx_runtime.jsxs)("div", {
2029
+ className: "dsh-ig-panel-tabs",
2030
+ children: [(0, react_jsx_runtime.jsx)("button", {
2031
+ type: "button",
2032
+ className: panelTab === "generate" ? "is-active" : "",
2033
+ onClick: () => setPanelTab("generate"),
2034
+ children: t("generate")
2035
+ }), (0, react_jsx_runtime.jsx)("button", {
2036
+ type: "button",
2037
+ className: panelTab === "details" ? "is-active" : "",
2038
+ onClick: () => setPanelTab("details"),
2039
+ children: t("details")
2040
+ })]
2041
+ }), panelTab === "details" ? (0, react_jsx_runtime.jsx)(DetailsPanel, {
2042
+ item: selected,
2043
+ t
2044
+ }) : (0, react_jsx_runtime.jsxs)("div", {
2045
+ className: "dsh-ig-generator-form",
2046
+ children: [
2047
+ (0, react_jsx_runtime.jsxs)("div", {
2048
+ className: "dsh-ig-mode-switch",
2049
+ children: [(0, react_jsx_runtime.jsxs)("button", {
2050
+ type: "button",
2051
+ className: mode === "generate" ? "is-active" : "",
2052
+ onClick: () => setMode("generate"),
2053
+ children: [(0, react_jsx_runtime.jsx)(Sparkles, { size: 15 }), t("generate")]
2054
+ }), (0, react_jsx_runtime.jsxs)("button", {
2055
+ type: "button",
2056
+ className: mode === "edit" ? "is-active" : "",
2057
+ onClick: () => setMode("edit"),
2058
+ children: [(0, react_jsx_runtime.jsx)(ImagePlus, { size: 15 }), t("edit")]
2059
+ })]
2060
+ }),
2061
+ mode === "edit" && (0, react_jsx_runtime.jsxs)("div", {
2062
+ className: "dsh-ig-field",
2063
+ children: [
2064
+ (0, react_jsx_runtime.jsxs)("div", {
2065
+ className: "dsh-ig-field-label",
2066
+ children: [(0, react_jsx_runtime.jsxs)("label", { children: [
2067
+ t("referencesCount", {
2068
+ current: String(references.length),
2069
+ max: String(maxReferences)
2070
+ }),
2071
+ " ",
2072
+ (0, react_jsx_runtime.jsx)("b", { children: "*" })
2073
+ ] }), references.length > 0 && (0, react_jsx_runtime.jsx)("button", {
2074
+ type: "button",
2075
+ onClick: clearAllReferences,
2076
+ children: t("clear")
2077
+ })]
2078
+ }),
2079
+ references.length === 0 ? (0, react_jsx_runtime.jsxs)("button", {
2080
+ type: "button",
2081
+ className: `dsh-ig-upload ${dragging ? "is-dragging" : ""}`,
2082
+ onClick: () => fileInputRef.current?.click(),
2083
+ onDragOver: (event) => {
2084
+ event.preventDefault();
2085
+ setDragging(true);
2086
+ },
2087
+ onDragLeave: () => setDragging(false),
2088
+ onDrop: (event) => {
2089
+ event.preventDefault();
2090
+ setDragging(false);
2091
+ addReferenceFiles(event.dataTransfer.files);
2092
+ },
2093
+ children: [
2094
+ (0, react_jsx_runtime.jsx)(Upload, { size: 20 }),
2095
+ (0, react_jsx_runtime.jsx)("strong", { children: t("upload") }),
2096
+ (0, react_jsx_runtime.jsx)("small", { children: t("uploadHint") })
2097
+ ]
2098
+ }) : (0, react_jsx_runtime.jsxs)("div", {
2099
+ className: "dsh-ig-reference-grid",
2100
+ children: [references.map((item, index) => (0, react_jsx_runtime.jsxs)("div", {
2101
+ className: "dsh-ig-reference-card",
2102
+ children: [(0, react_jsx_runtime.jsx)("img", {
2103
+ src: item.previewUrl,
2104
+ alt: `Ref ${index + 1}`
2105
+ }), (0, react_jsx_runtime.jsx)("button", {
2106
+ type: "button",
2107
+ title: t("closeReference"),
2108
+ onClick: () => removeReference(item.id),
2109
+ children: (0, react_jsx_runtime.jsx)(X, { size: 12 })
2110
+ })]
2111
+ }, item.id)), references.length < maxReferences && (0, react_jsx_runtime.jsxs)("button", {
2112
+ type: "button",
2113
+ className: "dsh-ig-reference-add",
2114
+ onClick: () => fileInputRef.current?.click(),
2115
+ title: t("upload"),
2116
+ children: [(0, react_jsx_runtime.jsx)(Upload, { size: 16 }), (0, react_jsx_runtime.jsx)("span", { children: t("addMoreRef") })]
2117
+ })]
2118
+ }),
2119
+ (0, react_jsx_runtime.jsx)("input", {
2120
+ ref: fileInputRef,
2121
+ type: "file",
2122
+ multiple: true,
2123
+ accept: "image/png,image/jpeg,image/webp,image/gif",
2124
+ hidden: true,
2125
+ onChange: (event) => {
2126
+ addReferenceFiles(event.target.files);
2127
+ event.target.value = "";
2128
+ }
2129
+ })
2130
+ ]
2131
+ }),
2132
+ (0, react_jsx_runtime.jsxs)("div", {
2133
+ className: "dsh-ig-field",
2134
+ children: [
2135
+ (0, react_jsx_runtime.jsxs)("div", {
2136
+ className: "dsh-ig-field-label",
2137
+ children: [(0, react_jsx_runtime.jsxs)("label", {
2138
+ htmlFor: "dsh-ig-prompt",
2139
+ children: [
2140
+ t("prompt"),
2141
+ " ",
2142
+ (0, react_jsx_runtime.jsx)("b", { children: "*" })
2143
+ ]
2144
+ }), (0, react_jsx_runtime.jsx)("button", {
2145
+ type: "button",
2146
+ onClick: () => setPrompt(""),
2147
+ children: t("clear")
2148
+ })]
2149
+ }),
2150
+ (0, react_jsx_runtime.jsx)("textarea", {
2151
+ id: "dsh-ig-prompt",
2152
+ value: prompt,
2153
+ maxLength: 2e3,
2154
+ onChange: (event) => setPrompt(event.target.value),
2155
+ onKeyDown: (event) => {
2156
+ if (event.nativeEvent.isComposing) return;
2157
+ if ((event.ctrlKey || event.metaKey) && event.key === "Enter") {
2158
+ event.preventDefault();
2159
+ if (!isGenerating && config !== null && prompt.trim().length > 0) submit();
2160
+ }
2161
+ },
2162
+ placeholder: t("promptPlaceholder")
2163
+ }),
2164
+ (0, react_jsx_runtime.jsxs)("small", { children: [prompt.length, "/2000"] })
2165
+ ]
2166
+ }),
2167
+ config === null ? configLoading ? (0, react_jsx_runtime.jsx)("div", {
2168
+ className: "dsh-ig-form-loading",
2169
+ children: (0, react_jsx_runtime.jsx)(LoaderCircle, {
2170
+ className: "dsh-ig-spin",
2171
+ size: 18
2172
+ })
2173
+ }) : (0, react_jsx_runtime.jsxs)("div", {
2174
+ className: "dsh-ig-config-error",
2175
+ children: [(0, react_jsx_runtime.jsx)("p", { children: configError ?? t("configLoadFailed") }), (0, react_jsx_runtime.jsxs)("button", {
2176
+ type: "button",
2177
+ onClick: () => void loadConfig(),
2178
+ children: [(0, react_jsx_runtime.jsx)(RefreshCw, { size: 13 }), (0, react_jsx_runtime.jsx)("span", { children: t("retry") })]
2179
+ })]
2180
+ }) : (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsxs)("div", {
2181
+ className: "dsh-ig-field-grid",
2182
+ children: [(0, react_jsx_runtime.jsx)(FieldSelect, {
2183
+ label: t("provider"),
2184
+ value: provider,
2185
+ onChange: changeProvider,
2186
+ options: config.providers.map((item) => ({
2187
+ value: item.provider,
2188
+ label: `${item.label}${item.configured ? "" : ` · ${t("unconfigured")}`}`
2189
+ }))
2190
+ }), (0, react_jsx_runtime.jsx)(FieldSelect, {
2191
+ label: t("model"),
2192
+ value: model,
2193
+ onChange: setModel,
2194
+ options: activeProfile === void 0 ? [] : [{
2195
+ value: activeProfile.model,
2196
+ label: activeProfile.model
2197
+ }]
2198
+ })]
2199
+ }), (0, react_jsx_runtime.jsxs)("div", {
2200
+ className: "dsh-ig-field-grid",
2201
+ children: [(0, react_jsx_runtime.jsx)(FieldSelect, {
2202
+ label: t("ratio"),
2203
+ value: ratio,
2204
+ onChange: setRatio,
2205
+ options: activeProfile?.ratioOptions ?? []
2206
+ }), (0, react_jsx_runtime.jsx)(FieldSelect, {
2207
+ label: t("quality"),
2208
+ value: quality,
2209
+ onChange: setQuality,
2210
+ options: activeProfile?.qualityOptions ?? []
2211
+ })]
2212
+ })] }),
2213
+ configuredCount === 0 && config !== null && (0, react_jsx_runtime.jsx)("div", {
2214
+ className: "dsh-ig-inline-note",
2215
+ children: t("noProvider")
2216
+ }),
2217
+ error !== null && (0, react_jsx_runtime.jsx)("div", {
2218
+ className: "dsh-ig-form-error",
2219
+ children: error
2220
+ }),
2221
+ isGenerating ? (0, react_jsx_runtime.jsxs)("button", {
2222
+ type: "button",
2223
+ className: "dsh-ig-generate-button is-cancel",
2224
+ onClick: cancelGeneration,
2225
+ children: [(0, react_jsx_runtime.jsx)(LoaderCircle, {
2226
+ className: "dsh-ig-spin",
2227
+ size: 17
2228
+ }), (0, react_jsx_runtime.jsx)("span", { children: t("cancelGenerate") })]
2229
+ }) : (0, react_jsx_runtime.jsxs)("button", {
2230
+ type: "button",
2231
+ className: "dsh-ig-generate-button",
2232
+ disabled: config === null,
2233
+ onClick: () => void submit(),
2234
+ children: [(0, react_jsx_runtime.jsx)(Sparkles, { size: 17 }), (0, react_jsx_runtime.jsx)("span", { children: t("start") })]
2235
+ })
2236
+ ]
2237
+ })]
2238
+ })
2239
+ ]
2240
+ }),
2241
+ lightboxOpen && selected !== null && (0, react_jsx_runtime.jsxs)("div", {
2242
+ className: "dsh-ig-lightbox-backdrop",
2243
+ onClick: () => setLightboxOpen(false),
2244
+ children: [
2245
+ (0, react_jsx_runtime.jsxs)("div", {
2246
+ className: "dsh-ig-lightbox-topbar",
2247
+ children: [(0, react_jsx_runtime.jsxs)("div", {
2248
+ className: "dsh-ig-lightbox-meta",
2249
+ children: [
2250
+ (0, react_jsx_runtime.jsx)("span", {
2251
+ className: "dsh-ig-tag",
2252
+ children: selected.provider
2253
+ }),
2254
+ (0, react_jsx_runtime.jsx)("span", {
2255
+ className: "dsh-ig-tag dsh-ig-tag-model",
2256
+ children: selected.model
2257
+ }),
2258
+ selected.attachment.width && selected.attachment.height ? (0, react_jsx_runtime.jsxs)("span", {
2259
+ className: "dsh-ig-tag",
2260
+ children: [
2261
+ selected.attachment.width,
2262
+ " × ",
2263
+ selected.attachment.height
2264
+ ]
2265
+ }) : null
2266
+ ]
2267
+ }), (0, react_jsx_runtime.jsx)("button", {
2268
+ type: "button",
2269
+ className: "dsh-ig-lightbox-close-btn",
2270
+ title: t("close"),
2271
+ onClick: () => setLightboxOpen(false),
2272
+ children: (0, react_jsx_runtime.jsx)(X, { size: 18 })
2273
+ })]
2274
+ }),
2275
+ (0, react_jsx_runtime.jsx)("div", {
2276
+ className: "dsh-ig-lightbox-img-wrap",
2277
+ onClick: (e) => e.stopPropagation(),
2278
+ children: image.url !== null ? (0, react_jsx_runtime.jsx)("img", {
2279
+ className: "dsh-ig-lightbox-img",
2280
+ src: image.url,
2281
+ alt: selected.prompt
2282
+ }) : (0, react_jsx_runtime.jsx)("div", {
2283
+ className: "dsh-ig-lightbox-loading",
2284
+ children: (0, react_jsx_runtime.jsx)("div", { className: "dsh-ig-lightbox-spinner" })
2285
+ })
2286
+ }),
2287
+ (0, react_jsx_runtime.jsxs)("div", {
2288
+ className: "dsh-ig-lightbox-bottombar",
2289
+ onClick: (e) => e.stopPropagation(),
2290
+ children: [(0, react_jsx_runtime.jsx)("div", {
2291
+ className: "dsh-ig-lightbox-prompt-text",
2292
+ title: selected.prompt,
2293
+ children: selected.prompt
2294
+ }), (0, react_jsx_runtime.jsxs)("div", {
2295
+ className: "dsh-ig-lightbox-actions",
2296
+ children: [
2297
+ (0, react_jsx_runtime.jsxs)("button", {
2298
+ type: "button",
2299
+ className: "dsh-ig-lightbox-btn",
2300
+ title: t("copyPpt"),
2301
+ onClick: async () => {
2302
+ await navigator.clipboard.writeText(selected.prompt);
2303
+ flash(t("copiedPrompt"));
2304
+ },
2305
+ children: [(0, react_jsx_runtime.jsx)(FileText, { size: 14 }), (0, react_jsx_runtime.jsx)("span", { children: t("copyPpt") })]
2306
+ }),
2307
+ (0, react_jsx_runtime.jsxs)("button", {
2308
+ type: "button",
2309
+ className: "dsh-ig-lightbox-btn",
2310
+ title: t("copy"),
2311
+ onClick: async () => {
2312
+ if (!image.blob) return;
2313
+ const ok = await copyImageBlob(image.blob);
2314
+ flash(ok ? t("copiedImage") : t("copyFailed"));
2315
+ },
2316
+ children: [(0, react_jsx_runtime.jsx)(Copy, { size: 14 }), (0, react_jsx_runtime.jsx)("span", { children: t("copy") })]
2317
+ }),
2318
+ (0, react_jsx_runtime.jsxs)("button", {
2319
+ type: "button",
2320
+ className: "dsh-ig-lightbox-btn",
2321
+ title: t("download"),
2322
+ onClick: downloadSelected,
2323
+ children: [(0, react_jsx_runtime.jsx)(Download, { size: 14 }), (0, react_jsx_runtime.jsx)("span", { children: t("download") })]
2324
+ }),
2325
+ (0, react_jsx_runtime.jsxs)("button", {
2326
+ type: "button",
2327
+ className: "dsh-ig-lightbox-btn",
2328
+ title: selected.isFavorite ? t("favorited") : t("favorite"),
2329
+ onClick: () => void toggleFavorite(),
2330
+ children: [(0, react_jsx_runtime.jsx)(Heart, {
2331
+ size: 14,
2332
+ fill: selected.isFavorite ? "#ef4444" : "none",
2333
+ color: selected.isFavorite ? "#ef4444" : "currentColor"
2334
+ }), (0, react_jsx_runtime.jsx)("span", { children: selected.isFavorite ? t("favorited") : t("favorite") })]
2335
+ })
2336
+ ]
2337
+ })]
2338
+ })
2339
+ ]
2340
+ }),
2341
+ showDeleteModal && selected !== null && (0, react_jsx_runtime.jsx)("div", {
2342
+ className: "dsh-ig-workbench-modal-backdrop",
2343
+ onClick: () => setShowDeleteModal(false),
2344
+ children: (0, react_jsx_runtime.jsxs)("div", {
2345
+ className: "dsh-ig-workbench-modal-box",
2346
+ onClick: (e) => e.stopPropagation(),
2347
+ children: [
2348
+ (0, react_jsx_runtime.jsxs)("div", {
2349
+ className: "dsh-ig-workbench-modal-header",
2350
+ children: [(0, react_jsx_runtime.jsx)(TriangleAlert, {
2351
+ size: 20,
2352
+ color: "#dc2626"
2353
+ }), (0, react_jsx_runtime.jsx)("strong", { children: t("deleteModalTitle") })]
2354
+ }),
2355
+ (0, react_jsx_runtime.jsx)("p", {
2356
+ className: "dsh-ig-workbench-modal-desc",
2357
+ children: t("deleteModalDesc")
2358
+ }),
2359
+ Boolean(selected.savedTo) && (0, react_jsx_runtime.jsxs)("label", {
2360
+ className: "dsh-ig-workbench-modal-option",
2361
+ children: [(0, react_jsx_runtime.jsx)("input", {
2362
+ type: "checkbox",
2363
+ checked: deleteWorkspaceFiles,
2364
+ onChange: (e) => setDeleteWorkspaceFiles(e.target.checked)
2365
+ }), (0, react_jsx_runtime.jsx)("span", { children: t("deleteWorkspaceFilesLabel") })]
2366
+ }),
2367
+ (0, react_jsx_runtime.jsxs)("div", {
2368
+ className: "dsh-ig-workbench-modal-actions",
2369
+ children: [(0, react_jsx_runtime.jsx)("button", {
2370
+ type: "button",
2371
+ className: "dsh-ig-workbench-modal-cancel",
2372
+ onClick: () => setShowDeleteModal(false),
2373
+ children: t("cancel")
2374
+ }), (0, react_jsx_runtime.jsx)("button", {
2375
+ type: "button",
2376
+ className: "dsh-ig-workbench-modal-danger",
2377
+ onClick: () => void handleConfirmDelete(),
2378
+ children: t("confirm")
2379
+ })]
2380
+ })
2381
+ ]
2382
+ })
2383
+ }),
2384
+ notice !== null && (0, react_jsx_runtime.jsx)("div", {
2385
+ className: "dsh-ig-workbench-toast",
2386
+ children: notice
2387
+ })
2388
+ ]
2389
+ });
2390
+ };
2391
+ const FieldSelect = ({ label, value, options, onChange }) => (0, react_jsx_runtime.jsxs)("label", {
2392
+ className: "dsh-ig-field-select",
2393
+ children: [(0, react_jsx_runtime.jsx)("span", { children: label }), (0, react_jsx_runtime.jsx)("select", {
2394
+ value,
2395
+ onChange: (event) => onChange(event.target.value),
2396
+ children: options.map((option) => (0, react_jsx_runtime.jsx)("option", {
2397
+ value: option.value,
2398
+ children: option.label
2399
+ }, option.value))
2400
+ })]
2401
+ });
2402
+ const DetailsPanel = ({ item, t }) => item === null ? (0, react_jsx_runtime.jsxs)("div", {
2403
+ className: "dsh-ig-details-empty",
2404
+ children: [(0, react_jsx_runtime.jsx)(ImagePlus, { size: 28 }), (0, react_jsx_runtime.jsx)("span", { children: t("selectHistory") })]
2405
+ }) : (0, react_jsx_runtime.jsxs)("dl", {
2406
+ className: "dsh-ig-details",
2407
+ children: [
2408
+ (0, react_jsx_runtime.jsxs)("div", { children: [(0, react_jsx_runtime.jsx)("dt", { children: t("prompt") }), (0, react_jsx_runtime.jsx)("dd", { children: item.prompt })] }),
2409
+ (0, react_jsx_runtime.jsxs)("div", { children: [(0, react_jsx_runtime.jsx)("dt", { children: t("provider") }), (0, react_jsx_runtime.jsx)("dd", { children: item.provider })] }),
2410
+ (0, react_jsx_runtime.jsxs)("div", { children: [(0, react_jsx_runtime.jsx)("dt", { children: t("model") }), (0, react_jsx_runtime.jsx)("dd", { children: item.model })] }),
2411
+ (0, react_jsx_runtime.jsxs)("div", { children: [(0, react_jsx_runtime.jsx)("dt", { children: t("dimensions") }), (0, react_jsx_runtime.jsx)("dd", { children: item.attachment.width && item.attachment.height ? `${item.attachment.width} × ${item.attachment.height}` : "—" })] }),
2412
+ (0, react_jsx_runtime.jsxs)("div", { children: [(0, react_jsx_runtime.jsx)("dt", { children: t("output") }), (0, react_jsx_runtime.jsx)("dd", { children: item.output ?? "—" })] }),
2413
+ (0, react_jsx_runtime.jsxs)("div", { children: [(0, react_jsx_runtime.jsx)("dt", { children: t("created") }), (0, react_jsx_runtime.jsx)("dd", { children: new Date(item.createdAt).toLocaleString() })] })
2414
+ ]
2415
+ });
2416
+ const RecentItem = ({ item, active, lang, onClick }) => {
2417
+ const [setRef, inView] = useInView();
2418
+ const image = useAttachmentImage(item.attachment, inView);
2419
+ return (0, react_jsx_runtime.jsxs)("button", {
2420
+ ref: setRef,
2421
+ type: "button",
2422
+ className: `dsh-ig-recent-item ${active ? "is-active" : ""}`,
2423
+ onClick,
2424
+ children: [(0, react_jsx_runtime.jsx)("div", {
2425
+ className: "dsh-ig-recent-thumb",
2426
+ children: image.url !== null ? (0, react_jsx_runtime.jsx)("img", {
2427
+ src: image.url,
2428
+ alt: "",
2429
+ loading: "lazy"
2430
+ }) : (0, react_jsx_runtime.jsx)(ImagePlus, { size: 18 })
2431
+ }), (0, react_jsx_runtime.jsxs)("div", { children: [
2432
+ (0, react_jsx_runtime.jsx)("strong", { children: item.prompt }),
2433
+ (0, react_jsx_runtime.jsx)("span", { children: formatRelativeTime$1(item.createdAt, lang) }),
2434
+ (0, react_jsx_runtime.jsxs)("small", { children: [
2435
+ item.provider,
2436
+ " · ",
2437
+ item.model
2438
+ ] }),
2439
+ (0, react_jsx_runtime.jsx)("small", { children: item.attachment.width && item.attachment.height ? `${item.attachment.width} × ${item.attachment.height}` : "—" })
2440
+ ] })]
2441
+ });
2442
+ };
2443
+ function useInView() {
2444
+ const [inView, setInView] = (0, react.useState)(false);
2445
+ const observerRef = (0, react.useRef)(null);
2446
+ (0, react.useEffect)(() => {
2447
+ return () => {
2448
+ observerRef.current?.disconnect();
2449
+ };
2450
+ }, []);
2451
+ return [(0, react.useCallback)((el) => {
2452
+ observerRef.current?.disconnect();
2453
+ if (!el || inView) return;
2454
+ if (typeof IntersectionObserver === "undefined") {
2455
+ setInView(true);
2456
+ return;
2457
+ }
2458
+ const observer = new IntersectionObserver(([entry]) => {
2459
+ if (entry?.isIntersecting) {
2460
+ setInView(true);
2461
+ observer.disconnect();
2462
+ }
2463
+ }, { rootMargin: "120px" });
2464
+ observerRef.current = observer;
2465
+ observer.observe(el);
2466
+ }, [inView]), inView];
2467
+ }
2468
+ function useAttachmentImage(attachment, shouldLoad = true) {
2469
+ const [value, setValue] = (0, react.useState)({
2470
+ url: null,
2471
+ blob: null,
2472
+ loading: false
2473
+ });
2474
+ (0, react.useEffect)(() => {
2475
+ if (attachment === void 0 || !shouldLoad) {
2476
+ setValue({
2477
+ url: null,
2478
+ blob: null,
2479
+ loading: false
2480
+ });
2481
+ return;
2482
+ }
2483
+ let cancelled = false;
2484
+ let objectUrl = null;
2485
+ setValue({
2486
+ url: null,
2487
+ blob: null,
2488
+ loading: true
2489
+ });
2490
+ fetchAttachmentBlob(attachment).then((blob) => {
2491
+ if (cancelled) return;
2492
+ objectUrl = URL.createObjectURL(blob);
2493
+ setValue({
2494
+ url: objectUrl,
2495
+ blob,
2496
+ loading: false
2497
+ });
2498
+ }).catch(() => {
2499
+ if (!cancelled) setValue({
2500
+ url: null,
2501
+ blob: null,
2502
+ loading: false
2503
+ });
2504
+ });
2505
+ return () => {
2506
+ cancelled = true;
2507
+ if (objectUrl !== null) URL.revokeObjectURL(objectUrl);
2508
+ };
2509
+ }, [attachment?.attachmentId, shouldLoad]);
2510
+ return value;
2511
+ }
2512
+ function fileToBase64(file) {
2513
+ return new Promise((resolve, reject) => {
2514
+ const reader = new FileReader();
2515
+ reader.onload = () => {
2516
+ const result = reader.result;
2517
+ const commaIndex = result.indexOf(",");
2518
+ resolve(commaIndex >= 0 ? result.slice(commaIndex + 1) : result);
2519
+ };
2520
+ reader.onerror = () => reject(reader.error ?? /* @__PURE__ */ new Error("Failed to read image file"));
2521
+ reader.readAsDataURL(file);
2522
+ });
2523
+ }
2524
+ function extension(mediaType) {
2525
+ return mediaType === "image/jpeg" ? "jpg" : mediaType.split("/")[1] ?? "png";
2526
+ }
2527
+ function messageOf(error) {
2528
+ return error instanceof Error ? error.message : String(error);
2529
+ }
2530
+ //#endregion
2531
+ //#region lib/types/client/conversation-regenerate.js
2532
+ /** Build the workbench request that reproduces a conversation image with an edited prompt. */
2533
+ function conversationRegenerateRequest(image, prompt, remembered) {
2534
+ if (!CLOUD_IMAGE_PROVIDERS.includes(image.provider)) throw new Error("当前图片使用的 Provider 暂不支持重新生成");
2535
+ const provider = image.provider;
2536
+ const settings = remembered ?? outputSettings(provider, image.output);
2537
+ return {
2538
+ mode: "generate",
2539
+ provider,
2540
+ model: image.model,
2541
+ prompt: prompt.trim(),
2542
+ ratio: settings.ratio,
2543
+ quality: settings.quality
2544
+ };
2545
+ }
2546
+ function outputSettings(provider, output) {
2547
+ const normalized = (output ?? "").trim();
2548
+ if (provider === "google") {
2549
+ const [rawRatio, rawQuality] = normalized.split(",").map((value) => value.trim());
2550
+ return {
2551
+ ratio: isRatio(rawRatio) ? rawRatio : "1:1",
2552
+ quality: rawQuality === "1K" || rawQuality === "2K" || rawQuality === "4K" ? rawQuality : "1K"
2553
+ };
2554
+ }
2555
+ if (provider === "seedream") return {
2556
+ ratio: "auto",
2557
+ quality: normalized === "1K" || normalized === "4K" ? normalized : "2K"
2558
+ };
2559
+ if (provider === "openai") return {
2560
+ ratio: ratioFromSize(normalized, "x"),
2561
+ quality: "standard"
2562
+ };
2563
+ return {
2564
+ ratio: ratioFromSize(normalized, "*"),
2565
+ quality: "standard"
2566
+ };
2567
+ }
2568
+ function ratioFromSize(size, separator) {
2569
+ return (separator === "x" ? {
2570
+ "1024x1024": "1:1",
2571
+ "1536x1024": "3:2",
2572
+ "1024x1536": "2:3"
2573
+ } : {
2574
+ "1024*1024": "1:1",
2575
+ "1536*1024": "3:2",
2576
+ "1024*1536": "2:3",
2577
+ "1664*928": "16:9",
2578
+ "928*1664": "9:16"
2579
+ })[size] ?? "1:1";
2580
+ }
2581
+ function isRatio(value) {
2582
+ return value === "1:1" || value === "3:2" || value === "2:3" || value === "4:3" || value === "3:4" || value === "16:9" || value === "9:16";
2583
+ }
2584
+ //#endregion
2585
+ //#region lib/types/client/gallery-view.js
2586
+ /**
2587
+ * Native Workspace Gallery & Studio View Component for DSH `conversation.view` slot.
2588
+ * Fully i18n-reactive (Chinese & English) with modular tabs, multi-dimensional filters,
2589
+ * responsive image grid, and placeholder routes.
2590
+ */
2591
+ const DICT$1 = {
2592
+ zh: {
2593
+ tabGallery: "图库",
2594
+ tabStudio: "工作台",
2595
+ tabFavorites: "收藏",
2596
+ tabCompare: "对比",
2597
+ tabTasks: "任务",
2598
+ filterAllProviders: "全部提供商",
781
2599
  filterGoogle: "Google Gemini",
782
2600
  filterOpenAI: "OpenAI / 中转站",
783
2601
  filterSeedream: "字节 Seedream",
@@ -786,6 +2604,8 @@ window.__ModuleLoader__.load({
786
2604
  filterAllModels: "全部模型",
787
2605
  filterAllRatios: "全部比例",
788
2606
  searchPlaceholder: "搜索 Prompt、标签…",
2607
+ filterCurrentWorkspace: "仅看此工作区",
2608
+ filterCurrentWorkspaceHint: "只展示属于当前工作区的生图记录与物理文件",
789
2609
  sortNewest: "最新生成",
790
2610
  sortOldest: "最早生成",
791
2611
  totalCount: "共 {count} 张生成图片",
@@ -804,6 +2624,13 @@ window.__ModuleLoader__.load({
804
2624
  download: "下载图片",
805
2625
  copyImg: "复制图片",
806
2626
  copyPpt: "复制 Prompt",
2627
+ regenerate: "重新生成",
2628
+ regenerateTitle: "重新生成图片",
2629
+ regenerateHint: "可按需修改 Prompt,基于当前模型和比例在画廊生成一张新图片。",
2630
+ confirmRegenerate: "开始生成",
2631
+ regenerating: "正在重新生成…",
2632
+ regenerateSuccess: "已生成新图片并收录到画廊",
2633
+ regenerateFailed: "重新生成失败",
807
2634
  delete: "从画廊删除",
808
2635
  confirmDelete: "确定要从画廊中删除这张图片吗?(不会影响原聊天记录)",
809
2636
  deleted: "已从画廊删除",
@@ -858,6 +2685,8 @@ window.__ModuleLoader__.load({
858
2685
  filterAllModels: "All Models",
859
2686
  filterAllRatios: "All Ratios",
860
2687
  searchPlaceholder: "Search prompt, tags…",
2688
+ filterCurrentWorkspace: "Current workspace only",
2689
+ filterCurrentWorkspaceHint: "Only show images belonging to the active workspace",
861
2690
  sortNewest: "Newest first",
862
2691
  sortOldest: "Oldest first",
863
2692
  totalCount: "{count} images total",
@@ -876,6 +2705,13 @@ window.__ModuleLoader__.load({
876
2705
  download: "Download",
877
2706
  copyImg: "Copy Image",
878
2707
  copyPpt: "Copy Prompt",
2708
+ regenerate: "Regenerate",
2709
+ regenerateTitle: "Regenerate Image",
2710
+ regenerateHint: "Edit prompt if needed. A new image will be generated and added to the gallery.",
2711
+ confirmRegenerate: "Generate",
2712
+ regenerating: "Regenerating…",
2713
+ regenerateSuccess: "New image generated and added to gallery",
2714
+ regenerateFailed: "Regeneration failed",
879
2715
  delete: "Delete from gallery",
880
2716
  confirmDelete: "Are you sure you want to remove this image from the gallery? (Chat history will not be affected)",
881
2717
  deleted: "Deleted from gallery",
@@ -971,17 +2807,80 @@ window.__ModuleLoader__.load({
971
2807
  if (item.imageSize) return item.imageSize;
972
2808
  return "1:1";
973
2809
  }
974
- const GalleryViewTab = ({ locale }) => {
2810
+ const GalleryViewTab = (props) => {
2811
+ const { locale, sessionId, useSessions, useWorkspaces } = props;
975
2812
  const [activeTab, setActiveTab] = (0, react.useState)("gallery");
976
2813
  const [items, setItems] = (0, react.useState)([]);
977
2814
  const [search, setSearch] = (0, react.useState)("");
978
2815
  const [selectedProvider, setSelectedProvider] = (0, react.useState)("all");
979
2816
  const [selectedRatio, setSelectedRatio] = (0, react.useState)("all");
980
2817
  const [sortBy, setSortBy] = (0, react.useState)("newest");
2818
+ const [onlyCurrentWorkspace, setOnlyCurrentWorkspace] = (0, react.useState)(() => {
2819
+ try {
2820
+ const stored = localStorage.getItem("dsh-ig-only-current-workspace");
2821
+ return stored === null ? true : stored === "true";
2822
+ } catch {
2823
+ return true;
2824
+ }
2825
+ });
2826
+ const [serverWorkspaces, setServerWorkspaces] = (0, react.useState)([]);
2827
+ const [serverActiveRoot, setServerActiveRoot] = (0, react.useState)(null);
2828
+ (0, react.useEffect)(() => {
2829
+ try {
2830
+ localStorage.setItem("dsh-ig-only-current-workspace", String(onlyCurrentWorkspace));
2831
+ } catch {}
2832
+ }, [onlyCurrentWorkspace]);
2833
+ (0, react.useEffect)(() => {
2834
+ let mounted = true;
2835
+ fetch(STUDIO_ROUTE).then((res) => res.ok ? res.json() : null).then((data) => {
2836
+ if (!mounted || !data) return;
2837
+ if (Array.isArray(data.workspaces)) setServerWorkspaces(data.workspaces);
2838
+ if (typeof data.workspaceRoot === "string") setServerActiveRoot(data.workspaceRoot);
2839
+ }).catch(() => {});
2840
+ return () => {
2841
+ mounted = false;
2842
+ };
2843
+ }, []);
2844
+ const liveWorkspaces = useWorkspaces ? useWorkspaces((s) => s?.items) : null;
2845
+ const liveSessions = useSessions ? useSessions((s) => s) : null;
2846
+ const currentSessionId = sessionId || liveSessions?.current;
2847
+ const activeWorkspace = (0, react.useMemo)(() => {
2848
+ const allWs = Array.isArray(liveWorkspaces) && liveWorkspaces.length > 0 ? liveWorkspaces : serverWorkspaces;
2849
+ if (allWs.length > 0) {
2850
+ if (currentSessionId) {
2851
+ const matched = allWs.find((ws) => Array.isArray(ws.sessionIds) && ws.sessionIds.includes(currentSessionId));
2852
+ if (matched) return matched;
2853
+ }
2854
+ if (serverActiveRoot) {
2855
+ const normActive = serverActiveRoot.replace(/\\/g, "/").toLowerCase();
2856
+ const matched = allWs.find((ws) => ws.path && ws.path.replace(/\\/g, "/").toLowerCase() === normActive);
2857
+ if (matched) return matched;
2858
+ }
2859
+ return allWs[0];
2860
+ }
2861
+ if (serverActiveRoot) return {
2862
+ workspaceId: "default",
2863
+ path: serverActiveRoot,
2864
+ title: "Workspace",
2865
+ sessionIds: []
2866
+ };
2867
+ return null;
2868
+ }, [
2869
+ liveWorkspaces,
2870
+ serverWorkspaces,
2871
+ currentSessionId,
2872
+ serverActiveRoot
2873
+ ]);
981
2874
  const [previewItem, setPreviewItem] = (0, react.useState)(null);
982
2875
  const [previewUrl, setPreviewUrl] = (0, react.useState)(null);
2876
+ const previewUrlRef = (0, react.useRef)(null);
2877
+ previewUrlRef.current = previewUrl;
983
2878
  const [previewBlob, setPreviewBlob] = (0, react.useState)(null);
984
2879
  const [toast, setToast] = (0, react.useState)(null);
2880
+ const [showRegenerateModal, setShowRegenerateModal] = (0, react.useState)(false);
2881
+ const [regeneratePrompt, setRegeneratePrompt] = (0, react.useState)("");
2882
+ const [isRegenerating, setIsRegenerating] = (0, react.useState)(false);
2883
+ const regenerateControllerRef = (0, react.useRef)(null);
985
2884
  const [isManageMode, setIsManageMode] = (0, react.useState)(false);
986
2885
  const [selectedIds, setSelectedIds] = (0, react.useState)(/* @__PURE__ */ new Set());
987
2886
  const lastSelectedIndexRef = (0, react.useRef)(null);
@@ -996,7 +2895,8 @@ window.__ModuleLoader__.load({
996
2895
  search,
997
2896
  selectedProvider,
998
2897
  selectedRatio,
999
- sortBy
2898
+ sortBy,
2899
+ onlyCurrentWorkspace
1000
2900
  ]);
1001
2901
  const [lang, setLang] = (0, react.useState)(() => {
1002
2902
  return (locale?.getSnapshot?.()?.active)?.startsWith("en") ? "en" : "zh";
@@ -1062,6 +2962,9 @@ window.__ModuleLoader__.load({
1062
2962
  const matchTags = item.tags?.some((t) => t.toLowerCase().includes(q));
1063
2963
  if (!matchPrompt && !matchModel && !matchProvider && !matchTags) return false;
1064
2964
  }
2965
+ if (onlyCurrentWorkspace && activeWorkspace) {
2966
+ if (!isItemInWorkspace(item, activeWorkspace)) return false;
2967
+ }
1065
2968
  return true;
1066
2969
  }).sort((a, b) => {
1067
2970
  if (sortBy === "oldest") return a.createdAt - b.createdAt;
@@ -1073,8 +2976,14 @@ window.__ModuleLoader__.load({
1073
2976
  selectedProvider,
1074
2977
  selectedRatio,
1075
2978
  search,
1076
- sortBy
2979
+ sortBy,
2980
+ onlyCurrentWorkspace,
2981
+ activeWorkspace
1077
2982
  ]);
2983
+ const workspaceItemsCount = (0, react.useMemo)(() => {
2984
+ if (!activeWorkspace) return items.length;
2985
+ return items.filter((item) => isItemInWorkspace(item, activeWorkspace)).length;
2986
+ }, [items, activeWorkspace]);
1078
2987
  const currentPreviewIndex = (0, react.useMemo)(() => {
1079
2988
  if (!previewItem) return -1;
1080
2989
  return filteredItems.findIndex((i) => i.id === previewItem.id);
@@ -1086,11 +2995,15 @@ window.__ModuleLoader__.load({
1086
2995
  abortControllerRef.current.abort();
1087
2996
  abortControllerRef.current = null;
1088
2997
  }
1089
- if (previewUrl) URL.revokeObjectURL(previewUrl);
2998
+ if (previewUrlRef.current) {
2999
+ URL.revokeObjectURL(previewUrlRef.current);
3000
+ previewUrlRef.current = null;
3001
+ }
1090
3002
  setPreviewItem(item);
1091
3003
  if (blob) {
1092
3004
  blobCache.set(item.id, blob);
1093
3005
  const url = URL.createObjectURL(blob);
3006
+ previewUrlRef.current = url;
1094
3007
  setPreviewBlob(blob);
1095
3008
  setPreviewUrl(url);
1096
3009
  setPreviewLoading(false);
@@ -1099,6 +3012,7 @@ window.__ModuleLoader__.load({
1099
3012
  const cached = blobCache.get(item.id);
1100
3013
  if (cached) {
1101
3014
  const url = URL.createObjectURL(cached);
3015
+ previewUrlRef.current = url;
1102
3016
  setPreviewBlob(cached);
1103
3017
  setPreviewUrl(url);
1104
3018
  setPreviewLoading(false);
@@ -1109,18 +3023,11 @@ window.__ModuleLoader__.load({
1109
3023
  setPreviewUrl(null);
1110
3024
  const controller = new AbortController();
1111
3025
  abortControllerRef.current = controller;
1112
- fetch(IMAGE_ROUTE, {
1113
- method: "POST",
1114
- signal: controller.signal,
1115
- headers: { "content-type": "application/json" },
1116
- body: JSON.stringify({ attachment: item.attachment })
1117
- }).then(async (res) => {
1118
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
1119
- return res.blob();
1120
- }).then((fetchedBlob) => {
3026
+ fetchAttachmentBlob(item.attachment).then((fetchedBlob) => {
1121
3027
  if (controller.signal.aborted) return;
1122
3028
  blobCache.set(item.id, fetchedBlob);
1123
3029
  const url = URL.createObjectURL(fetchedBlob);
3030
+ previewUrlRef.current = url;
1124
3031
  setPreviewBlob(fetchedBlob);
1125
3032
  setPreviewUrl(url);
1126
3033
  setPreviewLoading(false);
@@ -1135,7 +3042,16 @@ window.__ModuleLoader__.load({
1135
3042
  abortControllerRef.current.abort();
1136
3043
  abortControllerRef.current = null;
1137
3044
  }
1138
- if (previewUrl) URL.revokeObjectURL(previewUrl);
3045
+ if (regenerateControllerRef.current) {
3046
+ regenerateControllerRef.current.abort();
3047
+ regenerateControllerRef.current = null;
3048
+ setIsRegenerating(false);
3049
+ }
3050
+ setShowRegenerateModal(false);
3051
+ if (previewUrlRef.current) {
3052
+ URL.revokeObjectURL(previewUrlRef.current);
3053
+ previewUrlRef.current = null;
3054
+ }
1139
3055
  setPreviewItem(null);
1140
3056
  setPreviewUrl(null);
1141
3057
  setPreviewBlob(null);
@@ -1153,15 +3069,86 @@ window.__ModuleLoader__.load({
1153
3069
  if (nextItem) openPreviewItem(nextItem);
1154
3070
  }
1155
3071
  };
3072
+ const handleOpenRegenerate = () => {
3073
+ if (!previewItem || isRegenerating) return;
3074
+ setRegeneratePrompt(previewItem.prompt);
3075
+ setShowRegenerateModal(true);
3076
+ };
3077
+ const handleCancelRegeneratePrompt = () => {
3078
+ setShowRegenerateModal(false);
3079
+ };
3080
+ const handleAbortRegenerating = () => {
3081
+ regenerateControllerRef.current?.abort();
3082
+ regenerateControllerRef.current = null;
3083
+ setIsRegenerating(false);
3084
+ showToast(t("cancel"));
3085
+ };
3086
+ const handleConfirmRegenerate = async () => {
3087
+ if (!previewItem || isRegenerating || regeneratePrompt.trim().length === 0) return;
3088
+ setShowRegenerateModal(false);
3089
+ setIsRegenerating(true);
3090
+ const controller = new AbortController();
3091
+ regenerateControllerRef.current = controller;
3092
+ try {
3093
+ const request = conversationRegenerateRequest(previewItem, regeneratePrompt, previewItem.aspectRatio ? {
3094
+ ratio: previewItem.aspectRatio,
3095
+ quality: previewItem.imageSize || "standard"
3096
+ } : void 0);
3097
+ if (activeWorkspace?.path) request.workspaceRoot = activeWorkspace.path;
3098
+ const response = await fetch(STUDIO_ROUTE, {
3099
+ method: "POST",
3100
+ credentials: "same-origin",
3101
+ signal: controller.signal,
3102
+ headers: { "content-type": "application/json" },
3103
+ body: JSON.stringify(request)
3104
+ });
3105
+ const payload = await response.json().catch(() => null);
3106
+ if (!response.ok || payload === null || !("attachment" in payload)) throw new Error(payload && "error" in payload && payload.error ? payload.error : t("regenerateFailed"));
3107
+ const newItem = {
3108
+ id: String(payload.attachment.attachmentId),
3109
+ attachment: payload.attachment,
3110
+ prompt: payload.prompt,
3111
+ provider: payload.provider,
3112
+ model: payload.model,
3113
+ createdAt: payload.createdAt,
3114
+ aspectRatio: request.ratio,
3115
+ imageSize: request.quality,
3116
+ output: payload.output,
3117
+ ...payload.savedTo ? { savedTo: payload.savedTo } : {},
3118
+ ...activeWorkspace?.path ? { workspacePath: activeWorkspace.path } : {},
3119
+ ...activeWorkspace?.workspaceId ? { workspaceId: activeWorkspace.workspaceId } : {},
3120
+ ...currentSessionId ? { sessionId: currentSessionId } : {}
3121
+ };
3122
+ await saveGalleryItem(newItem);
3123
+ if (activeTab !== "gallery") setActiveTab("gallery");
3124
+ if (search.trim().length > 0) setSearch("");
3125
+ openPreviewItem(newItem);
3126
+ showToast(t("regenerateSuccess"));
3127
+ } catch (err) {
3128
+ if (controller.signal.aborted) return;
3129
+ showToast(err instanceof Error ? err.message : String(err));
3130
+ } finally {
3131
+ setIsRegenerating(false);
3132
+ regenerateControllerRef.current = null;
3133
+ }
3134
+ };
1156
3135
  (0, react.useEffect)(() => {
1157
3136
  return () => {
1158
3137
  if (abortControllerRef.current) abortControllerRef.current.abort();
1159
- if (previewUrl) URL.revokeObjectURL(previewUrl);
3138
+ if (regenerateControllerRef.current) regenerateControllerRef.current.abort();
3139
+ if (previewUrlRef.current) URL.revokeObjectURL(previewUrlRef.current);
1160
3140
  };
1161
- }, [previewUrl]);
3141
+ }, []);
1162
3142
  (0, react.useEffect)(() => {
1163
3143
  const onKeyDown = (e) => {
1164
3144
  if (["INPUT", "TEXTAREA"].includes(e.target?.tagName)) return;
3145
+ if (showRegenerateModal) {
3146
+ if (e.key === "Escape") {
3147
+ e.preventDefault();
3148
+ setShowRegenerateModal(false);
3149
+ }
3150
+ return;
3151
+ }
1165
3152
  if (showDeleteModal) {
1166
3153
  if (e.key === "Escape" && !isDeleting) {
1167
3154
  e.preventDefault();
@@ -1184,6 +3171,7 @@ window.__ModuleLoader__.load({
1184
3171
  window.addEventListener("keydown", onKeyDown);
1185
3172
  return () => window.removeEventListener("keydown", onKeyDown);
1186
3173
  }, [
3174
+ showRegenerateModal,
1187
3175
  showDeleteModal,
1188
3176
  isDeleting,
1189
3177
  isManageMode,
@@ -1263,34 +3251,41 @@ window.__ModuleLoader__.load({
1263
3251
  setSelectedIds(/* @__PURE__ */ new Set([item.id]));
1264
3252
  setShowDeleteModal(true);
1265
3253
  };
3254
+ const hasWorkspaceFilesToDelete = (0, react.useMemo)(() => items.some((i) => selectedIds.has(i.id) && typeof i.savedTo === "string" && i.savedTo.trim().length > 0), [items, selectedIds]);
1266
3255
  const handleConfirmBatchDelete = async () => {
1267
3256
  if (selectedIds.size === 0 || isDeleting) return;
1268
3257
  setIsDeleting(true);
1269
3258
  const idsToDelete = Array.from(selectedIds);
1270
3259
  const itemsToDelete = items.filter((i) => selectedIds.has(i.id));
1271
3260
  const pathsToDelete = itemsToDelete.map((i) => i.savedTo).filter((p) => typeof p === "string" && p.trim().length > 0);
1272
- const fallbackAttachmentIds = itemsToDelete.filter((i) => !i.savedTo || !i.savedTo.trim()).map((i) => i.id);
1273
3261
  let deletedFiles = 0;
1274
- let failedFilesCount = 0;
1275
- if (deleteWorkspaceFiles && (pathsToDelete.length > 0 || fallbackAttachmentIds.length > 0)) try {
3262
+ if (deleteWorkspaceFiles && pathsToDelete.length > 0) try {
1276
3263
  const res = await fetch(DELETE_ROUTE, {
1277
3264
  method: "POST",
1278
3265
  headers: { "content-type": "application/json" },
1279
- body: JSON.stringify({
1280
- paths: pathsToDelete,
1281
- attachmentIds: fallbackAttachmentIds
1282
- })
3266
+ body: JSON.stringify({ paths: pathsToDelete })
1283
3267
  });
1284
- if (res.ok) {
1285
- const data = await res.json();
1286
- deletedFiles = data.deletedCount ?? 0;
1287
- if (Array.isArray(data.failedFiles)) failedFilesCount = data.failedFiles.length;
3268
+ if (!res.ok) {
3269
+ setIsDeleting(false);
3270
+ showToast(`工作区文件删除失败 (${res.status})`);
3271
+ return;
1288
3272
  }
3273
+ const data = await res.json().catch(() => null);
3274
+ if (!data || data.ok === false || Array.isArray(data.failedFiles) && data.failedFiles.length > 0) {
3275
+ setIsDeleting(false);
3276
+ const reason = data?.failedFiles?.[0]?.error || "工作区文件未能删除";
3277
+ showToast(`删除失败:${reason}`);
3278
+ return;
3279
+ }
3280
+ deletedFiles = data.deletedCount ?? 0;
1289
3281
  } catch (err) {
1290
- console.warn("[dsh-image-gen] Workspace file deletion failed:", err);
3282
+ setIsDeleting(false);
3283
+ showToast(err instanceof Error ? err.message : "工作区文件删除网络异常");
3284
+ return;
1291
3285
  }
1292
3286
  try {
1293
3287
  await bulkDeleteGalleryItems(idsToDelete);
3288
+ for (const item of itemsToDelete) evictAttachmentCache(item.attachment.attachmentId);
1294
3289
  } catch (err) {
1295
3290
  setIsDeleting(false);
1296
3291
  showToast(t("deleteFailedDatabase"));
@@ -1304,11 +3299,7 @@ window.__ModuleLoader__.load({
1304
3299
  setSelectedIds(/* @__PURE__ */ new Set());
1305
3300
  lastSelectedIndexRef.current = null;
1306
3301
  }
1307
- if (failedFilesCount > 0) showToast(t("deleteFailedFileLocked", {
1308
- count: String(idsToDelete.length),
1309
- files: String(failedFilesCount)
1310
- }));
1311
- else if (deletedFiles > 0) showToast(t("batchDeletedWithFilesToast", {
3302
+ if (deletedFiles > 0) showToast(t("batchDeletedWithFilesToast", {
1312
3303
  count: String(idsToDelete.length),
1313
3304
  files: String(deletedFiles)
1314
3305
  }));
@@ -1316,6 +3307,7 @@ window.__ModuleLoader__.load({
1316
3307
  };
1317
3308
  return (0, react_jsx_runtime.jsxs)("div", {
1318
3309
  className: "dsh-ig-gallery-page",
3310
+ "data-conversation-composer-overlay": "",
1319
3311
  children: [
1320
3312
  (0, react_jsx_runtime.jsxs)("header", {
1321
3313
  className: "dsh-ig-studio-tabs-bar",
@@ -1337,18 +3329,6 @@ window.__ModuleLoader__.load({
1337
3329
  className: `dsh-ig-studio-tab-btn ${activeTab === "favorites" ? "is-active" : ""}`,
1338
3330
  onClick: () => setActiveTab("favorites"),
1339
3331
  children: [(0, react_jsx_runtime.jsx)(Heart, { size: 15 }), (0, react_jsx_runtime.jsx)("span", { children: t("tabFavorites") })]
1340
- }),
1341
- (0, react_jsx_runtime.jsxs)("button", {
1342
- type: "button",
1343
- className: `dsh-ig-studio-tab-btn ${activeTab === "compare" ? "is-active" : ""}`,
1344
- onClick: () => setActiveTab("compare"),
1345
- children: [(0, react_jsx_runtime.jsx)(GitCompare, { size: 15 }), (0, react_jsx_runtime.jsx)("span", { children: t("tabCompare") })]
1346
- }),
1347
- (0, react_jsx_runtime.jsxs)("button", {
1348
- type: "button",
1349
- className: `dsh-ig-studio-tab-btn ${activeTab === "tasks" ? "is-active" : ""}`,
1350
- onClick: () => setActiveTab("tasks"),
1351
- children: [(0, react_jsx_runtime.jsx)(ListTodo, { size: 15 }), (0, react_jsx_runtime.jsx)("span", { children: t("tabTasks") })]
1352
3332
  })
1353
3333
  ]
1354
3334
  }),
@@ -1439,6 +3419,30 @@ window.__ModuleLoader__.load({
1439
3419
  value: search,
1440
3420
  onChange: (e) => setSearch(e.target.value)
1441
3421
  })]
3422
+ }),
3423
+ (0, react_jsx_runtime.jsxs)("label", {
3424
+ className: "dsh-ig-workspace-filter-label",
3425
+ title: t("filterCurrentWorkspaceHint"),
3426
+ children: [
3427
+ (0, react_jsx_runtime.jsx)("input", {
3428
+ type: "checkbox",
3429
+ className: "dsh-ig-workspace-checkbox",
3430
+ checked: onlyCurrentWorkspace,
3431
+ onChange: (e) => setOnlyCurrentWorkspace(e.target.checked)
3432
+ }),
3433
+ (0, react_jsx_runtime.jsx)("span", {
3434
+ className: "dsh-ig-workspace-filter-text",
3435
+ children: t("filterCurrentWorkspace")
3436
+ }),
3437
+ (0, react_jsx_runtime.jsxs)("span", {
3438
+ className: "dsh-ig-workspace-filter-badge",
3439
+ children: [
3440
+ workspaceItemsCount,
3441
+ "/",
3442
+ items.length
3443
+ ]
3444
+ })
3445
+ ]
1442
3446
  })
1443
3447
  ]
1444
3448
  }), (0, react_jsx_runtime.jsxs)("div", {
@@ -1467,7 +3471,7 @@ window.__ModuleLoader__.load({
1467
3471
  })]
1468
3472
  }),
1469
3473
  (0, react_jsx_runtime.jsx)("div", {
1470
- className: "dsh-ig-gallery-page-body",
3474
+ className: `dsh-ig-gallery-page-body ${activeTab === "studio" ? "is-workbench" : ""}`,
1471
3475
  children: activeTab === "gallery" || activeTab === "favorites" ? items.length === 0 ? (0, react_jsx_runtime.jsxs)("div", {
1472
3476
  className: "dsh-ig-gallery-empty",
1473
3477
  children: [
@@ -1531,22 +3535,9 @@ window.__ModuleLoader__.load({
1531
3535
  onBlobLoaded: (b) => blobCache.set(item.id, b),
1532
3536
  onToast: showToast
1533
3537
  }, item.id))
1534
- }) : activeTab === "studio" ? (0, react_jsx_runtime.jsx)(PlaceholderView, {
1535
- icon: "🎨",
1536
- title: t("studioTitle"),
1537
- description: t("studioDesc"),
1538
- tip: t("studioTip"),
1539
- badge: t("comingSoonBadge")
1540
- }) : activeTab === "compare" ? (0, react_jsx_runtime.jsx)(PlaceholderView, {
1541
- icon: "🔀",
1542
- title: t("compareTitle"),
1543
- description: t("compareDesc"),
1544
- badge: t("comingSoonBadge")
1545
- }) : (0, react_jsx_runtime.jsx)(PlaceholderView, {
1546
- icon: "📋",
1547
- title: t("tasksTitle"),
1548
- description: t("tasksDesc"),
1549
- badge: t("comingSoonBadge")
3538
+ }) : (0, react_jsx_runtime.jsx)(StudioView, {
3539
+ locale,
3540
+ workspace: activeWorkspace
1550
3541
  })
1551
3542
  }),
1552
3543
  toast && (0, react_jsx_runtime.jsx)("div", {
@@ -1624,7 +3615,7 @@ window.__ModuleLoader__.load({
1624
3615
  children: [(0, react_jsx_runtime.jsx)("p", {
1625
3616
  className: "dsh-ig-modal-desc",
1626
3617
  children: t("batchDeleteDesc")
1627
- }), (0, react_jsx_runtime.jsxs)("label", {
3618
+ }), hasWorkspaceFilesToDelete && (0, react_jsx_runtime.jsxs)("label", {
1628
3619
  className: "dsh-ig-modal-checkbox-label",
1629
3620
  children: [(0, react_jsx_runtime.jsx)("input", {
1630
3621
  type: "checkbox",
@@ -1656,6 +3647,74 @@ window.__ModuleLoader__.load({
1656
3647
  ]
1657
3648
  })
1658
3649
  }),
3650
+ showRegenerateModal && previewItem && (0, react_jsx_runtime.jsx)("div", {
3651
+ className: "dsh-ig-modal-backdrop",
3652
+ onClick: handleCancelRegeneratePrompt,
3653
+ children: (0, react_jsx_runtime.jsxs)("div", {
3654
+ className: "dsh-ig-modal-box dsh-ig-regenerate-modal-box",
3655
+ onClick: (e) => e.stopPropagation(),
3656
+ children: [
3657
+ (0, react_jsx_runtime.jsxs)("div", {
3658
+ className: "dsh-ig-modal-header",
3659
+ children: [(0, react_jsx_runtime.jsx)("div", {
3660
+ className: "dsh-ig-regenerate-modal-icon",
3661
+ children: (0, react_jsx_runtime.jsx)(Sparkles, { size: 18 })
3662
+ }), (0, react_jsx_runtime.jsxs)("div", {
3663
+ className: "dsh-ig-regenerate-modal-title-wrap",
3664
+ children: [(0, react_jsx_runtime.jsx)("div", {
3665
+ className: "dsh-ig-modal-title",
3666
+ children: t("regenerateTitle")
3667
+ }), (0, react_jsx_runtime.jsxs)("div", {
3668
+ className: "dsh-ig-regenerate-modal-meta",
3669
+ children: [
3670
+ (0, react_jsx_runtime.jsx)("span", {
3671
+ className: "dsh-ig-tag",
3672
+ children: previewItem.provider
3673
+ }),
3674
+ previewItem.model && (0, react_jsx_runtime.jsx)("span", {
3675
+ className: "dsh-ig-tag dsh-ig-tag-model",
3676
+ children: previewItem.model
3677
+ }),
3678
+ (0, react_jsx_runtime.jsx)("span", {
3679
+ className: "dsh-ig-tag",
3680
+ children: formatCardMeta(previewItem)
3681
+ })
3682
+ ]
3683
+ })]
3684
+ })]
3685
+ }),
3686
+ (0, react_jsx_runtime.jsxs)("div", {
3687
+ className: "dsh-ig-modal-body",
3688
+ children: [(0, react_jsx_runtime.jsx)("p", {
3689
+ className: "dsh-ig-modal-desc",
3690
+ children: t("regenerateHint")
3691
+ }), (0, react_jsx_runtime.jsx)("textarea", {
3692
+ className: "dsh-ig-regenerate-modal-textarea",
3693
+ value: regeneratePrompt,
3694
+ onChange: (e) => setRegeneratePrompt(e.target.value),
3695
+ placeholder: t("prompt"),
3696
+ rows: 4,
3697
+ autoFocus: true
3698
+ })]
3699
+ }),
3700
+ (0, react_jsx_runtime.jsxs)("div", {
3701
+ className: "dsh-ig-modal-footer",
3702
+ children: [(0, react_jsx_runtime.jsx)("button", {
3703
+ type: "button",
3704
+ className: "dsh-ig-modal-btn dsh-ig-modal-btn-cancel",
3705
+ onClick: handleCancelRegeneratePrompt,
3706
+ children: t("cancel")
3707
+ }), (0, react_jsx_runtime.jsxs)("button", {
3708
+ type: "button",
3709
+ className: "dsh-ig-modal-btn dsh-ig-modal-btn-primary",
3710
+ onClick: handleConfirmRegenerate,
3711
+ disabled: regeneratePrompt.trim().length === 0,
3712
+ children: [(0, react_jsx_runtime.jsx)(Sparkles, { size: 14 }), (0, react_jsx_runtime.jsx)("span", { children: t("confirmRegenerate") })]
3713
+ })]
3714
+ })
3715
+ ]
3716
+ })
3717
+ }),
1659
3718
  previewItem && (0, react_jsx_runtime.jsxs)("div", {
1660
3719
  className: "dsh-ig-lightbox-backdrop",
1661
3720
  onClick: handleClosePreview,
@@ -1771,6 +3830,27 @@ window.__ModuleLoader__.load({
1771
3830
  },
1772
3831
  children: [(0, react_jsx_runtime.jsx)(Download, { size: 14 }), (0, react_jsx_runtime.jsx)("span", { children: t("download") })]
1773
3832
  }),
3833
+ (0, react_jsx_runtime.jsxs)("button", {
3834
+ type: "button",
3835
+ className: "dsh-ig-lightbox-btn dsh-ig-lightbox-btn-regenerate",
3836
+ title: t("regenerate"),
3837
+ disabled: isRegenerating,
3838
+ onClick: handleOpenRegenerate,
3839
+ children: [(0, react_jsx_runtime.jsx)(Sparkles, { size: 14 }), (0, react_jsx_runtime.jsx)("span", { children: t("regenerate") })]
3840
+ }),
3841
+ isRegenerating && (0, react_jsx_runtime.jsxs)("div", {
3842
+ className: "dsh-ig-lightbox-generating-indicator",
3843
+ children: [
3844
+ (0, react_jsx_runtime.jsx)("div", { className: "dsh-ig-lightbox-spinner-sm" }),
3845
+ (0, react_jsx_runtime.jsx)("span", { children: t("regenerating") }),
3846
+ (0, react_jsx_runtime.jsx)("button", {
3847
+ type: "button",
3848
+ className: "dsh-ig-lightbox-abort-btn",
3849
+ onClick: handleAbortRegenerating,
3850
+ children: t("cancel")
3851
+ })
3852
+ ]
3853
+ }),
1774
3854
  (0, react_jsx_runtime.jsxs)("button", {
1775
3855
  type: "button",
1776
3856
  className: "dsh-ig-lightbox-btn dsh-ig-lightbox-btn-danger",
@@ -1963,68 +4043,6 @@ window.__ModuleLoader__.load({
1963
4043
  })]
1964
4044
  });
1965
4045
  };
1966
- const PlaceholderView = ({ icon, title, description, tip, badge }) => {
1967
- return (0, react_jsx_runtime.jsx)("div", {
1968
- className: "dsh-ig-placeholder-view",
1969
- children: (0, react_jsx_runtime.jsxs)("div", {
1970
- className: "dsh-ig-placeholder-card",
1971
- children: [
1972
- (0, react_jsx_runtime.jsx)("div", {
1973
- className: "dsh-ig-placeholder-icon",
1974
- children: icon
1975
- }),
1976
- (0, react_jsx_runtime.jsxs)("div", {
1977
- className: "dsh-ig-placeholder-header",
1978
- children: [(0, react_jsx_runtime.jsx)("h2", {
1979
- className: "dsh-ig-placeholder-title",
1980
- children: title
1981
- }), badge && (0, react_jsx_runtime.jsx)("span", {
1982
- className: "dsh-ig-placeholder-badge",
1983
- children: badge
1984
- })]
1985
- }),
1986
- (0, react_jsx_runtime.jsx)("p", {
1987
- className: "dsh-ig-placeholder-desc",
1988
- children: description
1989
- }),
1990
- tip && (0, react_jsx_runtime.jsx)("div", {
1991
- className: "dsh-ig-placeholder-tip",
1992
- children: tip
1993
- })
1994
- ]
1995
- })
1996
- });
1997
- };
1998
- async function copyImageBlob(blob) {
1999
- try {
2000
- if (blob.type === "image/png") {
2001
- await navigator.clipboard.write([new ClipboardItem({ "image/png": blob })]);
2002
- return true;
2003
- }
2004
- const img = new Image();
2005
- const url = URL.createObjectURL(blob);
2006
- await new Promise((resolve, reject) => {
2007
- img.onload = resolve;
2008
- img.onerror = reject;
2009
- img.src = url;
2010
- });
2011
- const canvas = document.createElement("canvas");
2012
- canvas.width = img.naturalWidth;
2013
- canvas.height = img.naturalHeight;
2014
- const ctx = canvas.getContext("2d");
2015
- if (!ctx) throw new Error("Canvas unavailable");
2016
- ctx.drawImage(img, 0, 0);
2017
- URL.revokeObjectURL(url);
2018
- const pngBlob = await new Promise((res) => {
2019
- canvas.toBlob(res, "image/png");
2020
- });
2021
- if (!pngBlob) throw new Error("Blob conversion failed");
2022
- await navigator.clipboard.write([new ClipboardItem({ "image/png": pngBlob })]);
2023
- return true;
2024
- } catch (_err) {
2025
- return false;
2026
- }
2027
- }
2028
4046
  //#endregion
2029
4047
  //#region lib/types/client/image-ref.js
2030
4048
  /**
@@ -2038,6 +4056,99 @@ window.__ModuleLoader__.load({
2038
4056
  const fromBlockContent = Array.isArray(block.content) ? block.content.find((item) => item.type === "image") : void 0;
2039
4057
  if (fromBlockContent?.type === "image") return fromBlockContent.attachment;
2040
4058
  }
4059
+ //#endregion
4060
+ //#region lib/types/client/studio-style.js
4061
+ const STUDIO_STYLE = `
4062
+ .dsh-ig-gallery-page-body.is-workbench{flex:1 1 0;height:100%;min-height:0;padding:0;overflow:hidden;display:flex;flex-direction:column}
4063
+ .dsh-ig-workbench{--ig-blue:#2f64f5;--ig-line:color-mix(in srgb,currentColor 11%,transparent);--ig-soft:color-mix(in srgb,currentColor 4%,transparent);position:relative;display:flex;flex-direction:column;min-height:0;height:100%;overflow:hidden;padding:10px 12px 12px;color:var(--foreground,#182033);box-sizing:border-box;background:radial-gradient(circle at 60% -20%,rgba(63,104,246,.07),transparent 30%)}
4064
+ .dsh-ig-workbench *{box-sizing:border-box}.dsh-ig-workbench button,.dsh-ig-workbench select,.dsh-ig-workbench textarea{font:inherit;color:inherit}
4065
+ .dsh-ig-workbench-grid{display:grid;grid-template-columns:minmax(190px,238px) minmax(0,1fr) minmax(286px,340px);grid-template-rows:minmax(0,1fr);min-height:0;height:0;flex:1 1 0;border:1px solid var(--ig-line);border-radius:12px;overflow:hidden;background:var(--background,#fff);box-shadow:0 12px 32px rgba(30,42,76,.05);transition:grid-template-columns .2s ease}
4066
+ .dsh-ig-workbench-grid.is-sidebar-collapsed{grid-template-columns:minmax(0,1fr) minmax(286px,340px)}
4067
+ .dsh-ig-recent-panel,.dsh-ig-generate-panel{min-width:0;min-height:0;overflow:hidden;background:color-mix(in srgb,var(--background,#fff) 96%,#f5f7fb)}.dsh-ig-recent-panel{display:flex;flex-direction:column;border-right:1px solid var(--ig-line)}
4068
+ .dsh-ig-panel-title{height:48px;flex:0 0 48px;padding:0 12px 0 14px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--ig-line);font-size:13px;font-weight:650}
4069
+ .dsh-ig-count-badge{font-size:11px;min-width:22px;text-align:center;padding:2px 6px;border-radius:999px;background:var(--ig-soft);color:#7b8498;font-weight:500}
4070
+ .dsh-ig-collapse-btn{width:28px;height:28px;display:inline-flex;align-items:center;justify-content:center;border:1px solid transparent;border-radius:6px;background:transparent;color:#717b90;cursor:pointer;transition:all .15s}
4071
+ .dsh-ig-collapse-btn:hover{border-color:var(--ig-line);background:var(--ig-soft);color:var(--foreground,#182033)}
4072
+ .dsh-ig-recent-scroll{flex:1 1 0;height:0;padding:8px;overflow-x:hidden;overflow-y:auto;min-height:0}.dsh-ig-recent-empty{min-height:180px;display:flex;flex-direction:column;justify-content:center;align-items:center;gap:10px;color:#929bad;text-align:center;font-size:12px}
4073
+ .dsh-ig-recent-item{display:grid;grid-template-columns:70px minmax(0,1fr);gap:10px;width:100%;padding:8px;border:1px solid transparent;border-radius:9px;background:transparent;text-align:left;cursor:pointer;transition:background .16s,border-color .16s,transform .16s}.dsh-ig-recent-item:hover{background:var(--ig-soft);transform:translateY(-1px)}.dsh-ig-recent-item.is-active{border-color:#89a7ff;background:rgba(47,100,245,.075);box-shadow:inset 3px 0 0 var(--ig-blue)}.dsh-ig-recent-thumb{width:70px;height:70px;border-radius:7px;display:flex;align-items:center;justify-content:center;overflow:hidden;background:#eef1f7;color:#929bad}.dsh-ig-recent-thumb img{width:100%;height:100%;object-fit:cover}.dsh-ig-recent-item>div:last-child{min-width:0;padding:1px 0}.dsh-ig-recent-item strong{display:block;white-space:nowrap;text-overflow:ellipsis;overflow:hidden;font-size:12.5px;line-height:19px;font-weight:630}.dsh-ig-recent-item span,.dsh-ig-recent-item small{display:block;white-space:nowrap;text-overflow:ellipsis;overflow:hidden;color:#828b9e;font-size:10.5px;line-height:16px}
4074
+ .dsh-ig-load-more{width:100%;height:32px;margin:8px 0;display:flex;align-items:center;justify-content:center;border:1px dashed var(--ig-line);border-radius:7px;background:transparent;font-size:11px;color:#6b758b;cursor:pointer;transition:background .15s,color .15s}.dsh-ig-load-more:hover{background:var(--ig-soft);color:var(--ig-blue);border-color:#a2b9f6}
4075
+ .dsh-ig-mini-btn{height:22px;padding:0 7px;display:inline-flex;align-items:center;gap:3px;border:1px solid var(--ig-line);border-radius:5px;background:var(--background,#fff);font-size:10.5px;font-weight:500;color:var(--foreground,#242f44);cursor:pointer;transition:all .15s}.dsh-ig-mini-btn:hover{border-color:#9bb4f5;color:var(--ig-blue);background:rgba(47,100,245,.06)}
4076
+ .dsh-ig-canvas-column{min-width:0;min-height:0;height:100%;overflow:hidden;display:flex;flex-direction:column;background:color-mix(in srgb,var(--background,#fff) 97%,#e8edf7)}
4077
+ .dsh-ig-canvas-toolbar{height:46px;flex:0 0 46px;display:flex;align-items:center;justify-content:space-between;gap:8px;padding:0 14px;border-bottom:1px solid var(--ig-line)}
4078
+ .dsh-ig-canvas-toolbar-left,.dsh-ig-canvas-toolbar-right{display:flex;align-items:center;gap:6px}
4079
+ .dsh-ig-canvas-toolbar button{height:30px;min-width:30px;padding:0 9px;display:inline-flex;align-items:center;justify-content:center;gap:5px;border:1px solid var(--ig-line);border-radius:7px;background:var(--background,#fff);color:var(--foreground,#242f44);font-size:11.5px;font-weight:500;cursor:pointer;transition:border-color .15s,background .15s,color .15s}
4080
+ .dsh-ig-canvas-toolbar button:hover{border-color:#9bb4f5;background:rgba(47,100,245,.05);color:var(--ig-blue)}
4081
+ .dsh-ig-canvas-toolbar button.is-active{border-color:#8ba8f8;background:rgba(47,100,245,.08);color:var(--ig-blue);font-weight:550}
4082
+ .dsh-ig-zoom-display{font-size:11px;min-width:38px;text-align:center;color:#64748b;user-select:none;font-variant-numeric:tabular-nums}
4083
+ .dsh-ig-status-badge-wrap{position:relative;display:inline-flex;align-items:center}
4084
+ .dsh-ig-status-badge{height:30px;display:inline-flex;align-items:center;gap:7px;padding:0 10px;border:1px solid var(--ig-line);border-radius:7px;background:var(--background,#fff);cursor:pointer;user-select:none;transition:border-color .15s,background .15s,box-shadow .15s}
4085
+ .dsh-ig-status-badge:hover{border-color:#9bb4f5;background:rgba(47,100,245,.04)}
4086
+ .dsh-ig-status-dot{width:7px;height:7px;min-width:7px;max-width:7px;border-radius:50%;flex-shrink:0;box-sizing:border-box}
4087
+ .dsh-ig-status-dot.is-ready{background:#10b981;box-shadow:0 0 0 2px rgba(16,185,129,.2)}
4088
+ .dsh-ig-status-dot.is-muted{background:#ef4444;box-shadow:0 0 0 2px rgba(239,68,68,.2)}
4089
+ .dsh-ig-status-text{font-size:11.5px;font-weight:500;color:var(--foreground,#242f44);line-height:1}
4090
+ .dsh-ig-status-arrow{color:#94a3b8;transition:transform .15s,color .15s}
4091
+ .dsh-ig-status-badge-wrap:hover .dsh-ig-status-arrow{transform:rotate(180deg);color:var(--ig-blue)}
4092
+ .dsh-ig-status-popover{position:absolute;left:0;top:calc(100% + 6px);width:260px;background:var(--background,#fff);border:1px solid var(--ig-line);border-radius:9px;box-shadow:0 12px 36px rgba(20,28,45,.14);padding:10px 12px;z-index:40;opacity:0;pointer-events:none;transform:translateY(4px);transition:opacity .15s cubic-bezier(.16,1,.3,1),transform .15s cubic-bezier(.16,1,.3,1)}
4093
+ .dsh-ig-status-badge-wrap:hover .dsh-ig-status-popover{opacity:1;pointer-events:auto;transform:translateY(0)}
4094
+ .dsh-ig-status-popover-title{display:flex;align-items:center;justify-content:space-between;font-size:10px;font-weight:650;color:#8590a6;margin-bottom:8px;letter-spacing:.02em;text-transform:uppercase}
4095
+ .dsh-ig-status-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:5px}
4096
+ .dsh-ig-status-list li{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:6px 8px;border-radius:6px;transition:background .12s}
4097
+ .dsh-ig-status-list li.is-configured{background:rgba(16,185,129,.05);border:1px solid rgba(16,185,129,.12)}
4098
+ .dsh-ig-status-list li.is-missing{background:rgba(239,68,68,.04);border:1px solid rgba(239,68,68,.12);opacity:.75}
4099
+ .dsh-ig-status-item-name{display:flex;flex-direction:column;min-width:0}
4100
+ .dsh-ig-status-item-name strong{font-size:11.5px;font-weight:600;color:var(--foreground,#182033);line-height:16px}
4101
+ .dsh-ig-status-item-name small{font-size:9.5px;color:#7c869a;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:145px;line-height:14px}
4102
+ .dsh-ig-status-item-tag{font-size:10px;font-weight:600;padding:2px 6px;border-radius:4px;flex-shrink:0}
4103
+ .is-configured .dsh-ig-status-item-tag{color:#059669;background:rgba(16,185,129,.14)}
4104
+ .is-missing .dsh-ig-status-item-tag{color:#dc2626;background:rgba(239,68,68,.14)}
4105
+ .dsh-ig-canvas{position:relative;flex:1 1 0;height:0;min-height:0;display:flex;align-items:center;justify-content:center;overflow:hidden;padding:24px;touch-action:none;user-select:none;cursor:grab;background-image:linear-gradient(45deg,rgba(114,128,156,.035) 25%,transparent 25%),linear-gradient(-45deg,rgba(114,128,156,.035) 25%,transparent 25%),linear-gradient(45deg,transparent 75%,rgba(114,128,156,.035) 75%),linear-gradient(-45deg,transparent 75%,rgba(114,128,156,.035) 75%);background-size:24px 24px;background-position:0 0,0 12px,12px -12px,-12px 0}
4106
+ .dsh-ig-canvas.is-dragging{cursor:grabbing!important}
4107
+ .dsh-ig-canvas img{display:block;max-width:100%;max-height:100%;width:auto;height:auto;border-radius:10px;box-shadow:0 14px 45px rgba(25,35,63,.18);user-select:none;-webkit-user-drag:none;pointer-events:auto}
4108
+ .dsh-ig-canvas-empty,.dsh-ig-generating-state{display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;gap:12px;color:#929bad;font-size:12px;padding:26px;cursor:default}
4109
+ .dsh-ig-generating-state strong{font-size:15px;color:var(--foreground,#182033)}.dsh-ig-generating-state span{font-size:11px}.dsh-ig-generation-orbit{width:66px;height:66px;border-radius:50%;display:grid;place-items:center;color:var(--ig-blue);background:rgba(47,100,245,.09);box-shadow:0 0 0 10px rgba(47,100,245,.035);animation:dshIgPulse 1.8s ease-in-out infinite}
4110
+ .dsh-ig-result-strip{flex:0 0 auto;min-height:52px;padding:8px 14px;display:flex;align-items:center;justify-content:space-between;gap:12px;border-top:1px solid var(--ig-line)}.dsh-ig-result-strip>span{font-size:12px;font-weight:650}.dsh-ig-result-strip>div,.dsh-ig-result-actions>div{display:flex;gap:7px}.dsh-ig-result-strip button,.dsh-ig-result-actions button{height:32px;display:inline-flex;align-items:center;gap:6px;padding:0 10px;border:1px solid var(--ig-line);border-radius:7px;background:var(--background,#fff);font-size:11px;cursor:pointer}.dsh-ig-result-strip button:hover,.dsh-ig-result-actions button:hover{border-color:#9fb2ed;color:var(--ig-blue)}.dsh-ig-result-actions{flex:0 0 auto;min-height:54px;padding:8px 14px;display:flex;align-items:center;justify-content:space-between;gap:12px;border-top:1px solid var(--ig-line)}.dsh-ig-result-actions p{margin:0;min-width:0;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;font-size:11.5px;line-height:18px;color:#596277}.dsh-ig-result-meta{flex:0 0 auto;min-height:32px;padding:6px 14px;display:flex;gap:13px;align-items:center;flex-wrap:wrap;border-top:1px solid var(--ig-line);font-size:10px;color:#8790a3}.dsh-ig-result-meta span+span:before{content:'·';margin-right:13px}
4111
+ .dsh-ig-generate-panel{height:100%;display:flex;flex-direction:column;border-left:1px solid var(--ig-line);overflow:hidden}.dsh-ig-panel-tabs{height:48px;flex:0 0 48px;display:grid;grid-template-columns:1fr 1fr;border-bottom:1px solid var(--ig-line);padding:0 14px}.dsh-ig-panel-tabs button{position:relative;border:0;background:transparent;font-size:12px;color:#7c8598;cursor:pointer}.dsh-ig-panel-tabs button.is-active{color:var(--ig-blue);font-weight:650}.dsh-ig-panel-tabs button.is-active:after{content:'';position:absolute;left:20%;right:20%;bottom:-1px;height:2px;background:var(--ig-blue);border-radius:2px 2px 0 0}.dsh-ig-generator-form{flex:1 1 0;height:0;padding:15px;display:flex;flex-direction:column;gap:15px;overflow-x:hidden;overflow-y:auto;min-height:0}.dsh-ig-mode-switch{display:grid;grid-template-columns:1fr 1fr;gap:8px}.dsh-ig-mode-switch button{height:39px;border:1px solid var(--ig-line);border-radius:8px;background:var(--background,#fff);display:flex;align-items:center;justify-content:center;gap:7px;font-size:12px;cursor:pointer}.dsh-ig-mode-switch button.is-active{border-color:#789bff;color:var(--ig-blue);background:rgba(47,100,245,.065);box-shadow:inset 0 0 0 1px rgba(47,100,245,.1)}
4112
+ .dsh-ig-field{position:relative;display:flex;flex-direction:column;gap:7px}.dsh-ig-field>label,.dsh-ig-field-label label{font-size:11.5px;font-weight:630}.dsh-ig-field>label span{font-weight:400;color:#929bad}.dsh-ig-field-label{display:flex;align-items:center;justify-content:space-between}.dsh-ig-field-label b{color:#e14d4d}.dsh-ig-field-label button{border:0;background:transparent;color:#798397;font-size:10.5px;cursor:pointer}.dsh-ig-field textarea{width:100%;height:112px;padding:11px 12px 22px;resize:vertical;border:1px solid var(--ig-line);border-radius:9px;background:var(--background,#fff);font-size:12px;line-height:19px;outline:none}.dsh-ig-field textarea:focus{border-color:var(--ig-blue);box-shadow:0 0 0 3px rgba(47,100,245,.09)}.dsh-ig-field>small{position:absolute;right:9px;bottom:7px;color:#9aa2b2;font-size:9.5px}.dsh-ig-field-grid{display:grid;grid-template-columns:1fr 1fr;gap:9px}.dsh-ig-field-select{display:flex;flex-direction:column;gap:6px;min-width:0}.dsh-ig-field-select>span{font-size:11px;font-weight:620}.dsh-ig-field-select select{width:100%;height:36px;padding:0 26px 0 9px;border:1px solid var(--ig-line);border-radius:8px;background:var(--background,#fff);font-size:11px;outline:none;text-overflow:ellipsis}.dsh-ig-field-select select:focus{border-color:var(--ig-blue)}
4113
+ .dsh-ig-upload{height:92px;border:1px dashed #bcc6d9;border-radius:9px;background:rgba(255,255,255,.45);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:5px;color:#7d879b;cursor:pointer}.dsh-ig-upload strong{font-size:11px;font-weight:570}.dsh-ig-upload small{font-size:9.5px}.dsh-ig-upload:hover,.dsh-ig-upload.is-dragging{border-color:var(--ig-blue);background:rgba(47,100,245,.05);color:var(--ig-blue)}
4114
+ .dsh-ig-reference-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(64px,1fr));gap:8px}
4115
+ .dsh-ig-reference-card{position:relative;aspect-ratio:1;border-radius:8px;overflow:hidden;background:#eef1f7;border:1px solid var(--ig-line)}
4116
+ .dsh-ig-reference-card img{width:100%;height:100%;object-fit:cover}
4117
+ .dsh-ig-reference-card button{position:absolute;right:4px;top:4px;width:18px;height:18px;border:0;border-radius:50%;display:grid;place-items:center;background:rgba(20,28,45,.75);color:#fff;cursor:pointer;padding:0;transition:background .15s}
4118
+ .dsh-ig-reference-card button:hover{background:#ef4444}
4119
+ .dsh-ig-reference-add{aspect-ratio:1;border:1px dashed #bcc6d9;border-radius:8px;background:rgba(255,255,255,.45);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;color:#7d879b;cursor:pointer;font-size:10px;transition:border-color .15s,color .15s,background .15s}
4120
+ .dsh-ig-reference-add:hover{border-color:var(--ig-blue);background:rgba(47,100,245,.05);color:var(--ig-blue)}
4121
+ .dsh-ig-reference-preview{position:relative;height:108px;border-radius:9px;overflow:hidden;background:#eef1f7}.dsh-ig-reference-preview img{width:100%;height:100%;object-fit:contain}.dsh-ig-reference-preview button{position:absolute;right:7px;top:7px;width:25px;height:25px;border:0;border-radius:50%;display:grid;place-items:center;background:rgba(20,28,45,.72);color:#fff;cursor:pointer}
4122
+ .dsh-ig-inline-note,.dsh-ig-form-error{padding:9px 10px;border-radius:7px;font-size:10.5px;line-height:16px}.dsh-ig-inline-note{background:#fff8e7;color:#8b671c;border:1px solid #f1dfae}.dsh-ig-form-error{background:#fff1f1;color:#b63737;border:1px solid #f2caca}.dsh-ig-form-loading{height:70px;display:grid;place-items:center;color:var(--ig-blue)}
4123
+ .dsh-ig-config-error{padding:14px;display:flex;flex-direction:column;align-items:center;gap:10px;background:#fff1f1;border:1px solid #f2caca;border-radius:8px;color:#b63737;font-size:11.5px;text-align:center}.dsh-ig-config-error p{margin:0;line-height:17px}.dsh-ig-config-error button{height:30px;display:inline-flex;align-items:center;gap:6px;padding:0 12px;border:1px solid #e0a8a8;border-radius:6px;background:#fff;color:#b63737;font-size:11.5px;font-weight:600;cursor:pointer}.dsh-ig-config-error button:hover{background:#ffebeb}
4124
+ .dsh-ig-generate-button{height:42px;flex:0 0 auto;border:0;border-radius:8px;background:linear-gradient(135deg,#386cf6,#2758e7);color:#fff!important;display:flex;align-items:center;justify-content:center;gap:8px;font-size:12px;font-weight:650;cursor:pointer;box-shadow:0 8px 18px rgba(47,100,245,.22);transition:transform .16s,box-shadow .16s}.dsh-ig-generate-button:hover:not(:disabled){transform:translateY(-1px);box-shadow:0 10px 24px rgba(47,100,245,.28)}.dsh-ig-generate-button:disabled{opacity:.55;cursor:not-allowed;box-shadow:none}
4125
+ .dsh-ig-generate-button.is-cancel{background:linear-gradient(135deg,#ef4444,#dc2626);box-shadow:0 8px 18px rgba(239,68,68,.25)}.dsh-ig-generate-button.is-cancel:hover{background:linear-gradient(135deg,#dc2626,#b91c1c);box-shadow:0 10px 24px rgba(239,68,68,.35)}
4126
+ .dsh-ig-details{padding:16px;margin:0;overflow:auto}.dsh-ig-details>div{padding:12px 0;border-bottom:1px solid var(--ig-line)}.dsh-ig-details dt{font-size:10.5px;color:#8992a4;margin-bottom:5px}.dsh-ig-details dd{margin:0;font-size:11.5px;line-height:18px;word-break:break-word}.dsh-ig-details-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;height:220px;padding:20px;color:#929bad;font-size:11px;text-align:center}
4127
+ .dsh-ig-workbench-modal-backdrop{position:fixed;inset:0;background:rgba(15,23,42,.45);backdrop-filter:blur(3px);z-index:999;display:grid;place-items:center;padding:16px}
4128
+ .dsh-ig-workbench-modal-box{width:100%;max-width:380px;background:var(--background,#fff);border-radius:12px;padding:20px;box-shadow:0 20px 48px rgba(0,0,0,.22);display:flex;flex-direction:column;gap:14px}
4129
+ .dsh-ig-workbench-modal-header{display:flex;align-items:center;gap:10px}
4130
+ .dsh-ig-workbench-modal-header strong{font-size:14px;font-weight:650;color:#b91c1c}
4131
+ .dsh-ig-workbench-modal-desc{font-size:12px;line-height:18px;color:#555f77;margin:0}
4132
+ .dsh-ig-workbench-modal-option{display:flex;align-items:center;gap:8px;font-size:11.5px;color:#4b5563;cursor:pointer}
4133
+ .dsh-ig-workbench-modal-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:4px}
4134
+ .dsh-ig-workbench-modal-actions button{height:32px;padding:0 14px;border-radius:7px;font-size:11.5px;font-weight:600;cursor:pointer}
4135
+ .dsh-ig-workbench-modal-cancel{border:1px solid var(--ig-line);background:var(--background,#fff);color:#6b7280}
4136
+ .dsh-ig-workbench-modal-cancel:hover{background:#f3f4f6}
4137
+ .dsh-ig-workbench-modal-danger{border:0;background:#dc2626;color:#fff}
4138
+ .dsh-ig-workbench-modal-danger:hover{background:#b91c1c}
4139
+ @keyframes dshIgPulse{50%{transform:scale(1.06);box-shadow:0 0 0 16px rgba(47,100,245,.025)}}
4140
+ .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}
4141
+ @keyframes dshIgFade{from{opacity:0;transform:translate(-50%,6px)}to{opacity:1;transform:translate(-50%,0)}}
4142
+ @media(max-width:1120px){.dsh-ig-workbench-grid{grid-template-columns:190px minmax(360px,1fr) 300px}.dsh-ig-result-actions button span{display:none}}
4143
+ @media(max-width:900px){.dsh-ig-gallery-page-body.is-workbench{height:auto;overflow:auto}.dsh-ig-workbench{height:auto;min-height:100%;overflow:visible;padding:12px}.dsh-ig-workbench-heading{height:auto;padding-bottom:12px;align-items:center}.dsh-ig-provider-quick>span{display:none}.dsh-ig-workbench-grid{height:auto;display:flex;flex:none;flex-direction:column;overflow:visible}.dsh-ig-recent-panel{height:auto;overflow:hidden;border-right:0;border-bottom:1px solid var(--ig-line)}.dsh-ig-recent-scroll{height:auto;display:flex;overflow-x:auto;overflow-y:hidden;padding:8px}.dsh-ig-recent-item{min-width:220px}.dsh-ig-canvas-column{height:auto;min-height:560px}.dsh-ig-generate-panel{height:auto;border-left:0;border-top:1px solid var(--ig-line);min-height:560px}.dsh-ig-generator-form{height:auto;overflow:visible}.dsh-ig-canvas{height:auto;min-height:360px}}
4144
+ @media(max-width:560px){.dsh-ig-workbench-heading>div{display:block}.dsh-ig-workbench-heading h1{font-size:15px}.dsh-ig-provider-quick select{min-width:128px;max-width:150px}.dsh-ig-canvas{padding:12px;min-height:300px}.dsh-ig-result-strip,.dsh-ig-result-actions{align-items:flex-start;flex-direction:column}.dsh-ig-result-strip>div,.dsh-ig-result-actions>div{width:100%}.dsh-ig-result-strip button,.dsh-ig-result-actions button{flex:1;justify-content:center}.dsh-ig-result-actions button span{display:inline}.dsh-ig-field-grid{grid-template-columns:1fr}.dsh-ig-result-meta span+span:before{display:none}}
4145
+ @media(prefers-reduced-motion:reduce){.dsh-ig-workbench *{scroll-behavior:auto!important;animation-duration:.01ms!important;transition-duration:.01ms!important}}
4146
+ .dsh-ig-workspace-filter-label{height:30px;display:inline-flex;align-items:center;gap:6px;padding:0 10px;border:1px solid var(--ig-line,rgba(0,0,0,.1));border-radius:7px;background:var(--background,#fff);font-size:11.5px;color:var(--foreground,#242f44);cursor:pointer;user-select:none;transition:border-color .15s,background .15s}
4147
+ .dsh-ig-workspace-filter-label:hover{border-color:#9bb4f5;background:rgba(47,100,245,.04)}
4148
+ .dsh-ig-workspace-checkbox{cursor:pointer;accent-color:#2f64f5;width:13.5px;height:13.5px;margin:0}
4149
+ .dsh-ig-workspace-filter-text{font-weight:500;white-space:nowrap}
4150
+ .dsh-ig-workspace-filter-badge{font-size:10px;font-weight:600;padding:1px 6px;border-radius:999px;background:color-mix(in srgb,currentColor 7%,transparent);color:#64748b;font-variant-numeric:tabular-nums}
4151
+ `;
2041
4152
  //#endregion
2042
4153
  //#region lib/types/client/image-result-node.js
2043
4154
  const IMAGE_RESULT_NODE_KIND = "dsh-image-result";
@@ -2179,6 +4290,75 @@ window.__ModuleLoader__.load({
2179
4290
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
2180
4291
  }
2181
4292
  //#endregion
4293
+ //#region lib/types/client/conversation-image-revisions.js
4294
+ const STORAGE_PREFIX = "dsh_image_gen_conversation_revisions:";
4295
+ /** Read the locally persisted replacement history for one immutable conversation image. */
4296
+ function loadConversationImageRevisionChain(originId) {
4297
+ const empty = () => ({
4298
+ originId,
4299
+ currentIndex: 0,
4300
+ revisions: []
4301
+ });
4302
+ try {
4303
+ const raw = globalThis.localStorage?.getItem(storageKey(originId));
4304
+ if (!raw) return empty();
4305
+ const value = JSON.parse(raw);
4306
+ if (!isChain(value, originId)) return empty();
4307
+ return {
4308
+ originId,
4309
+ currentIndex: Math.min(value.currentIndex, value.revisions.length),
4310
+ revisions: value.revisions
4311
+ };
4312
+ } catch {
4313
+ return empty();
4314
+ }
4315
+ }
4316
+ /** Append a generated version and make it the version shown by the conversation card. */
4317
+ function appendConversationImageRevision(originId, revision) {
4318
+ const revisions = [...loadConversationImageRevisionChain(originId).revisions.filter((item) => item.attachment.attachmentId !== revision.attachment.attachmentId), revision].slice(-20);
4319
+ const next = {
4320
+ originId,
4321
+ currentIndex: revisions.length,
4322
+ revisions
4323
+ };
4324
+ persist(next);
4325
+ return next;
4326
+ }
4327
+ /** Select an earlier or later version without altering the underlying conversation event. */
4328
+ function selectConversationImageRevision(originId, requestedIndex) {
4329
+ const current = loadConversationImageRevisionChain(originId);
4330
+ const currentIndex = Math.max(0, Math.min(Math.trunc(requestedIndex), current.revisions.length));
4331
+ const next = {
4332
+ ...current,
4333
+ currentIndex
4334
+ };
4335
+ persist(next);
4336
+ return next;
4337
+ }
4338
+ function persist(chain) {
4339
+ try {
4340
+ globalThis.localStorage?.setItem(storageKey(chain.originId), JSON.stringify(chain));
4341
+ } catch {}
4342
+ }
4343
+ function storageKey(originId) {
4344
+ return `${STORAGE_PREFIX}${originId}`;
4345
+ }
4346
+ function isChain(value, originId) {
4347
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
4348
+ const candidate = value;
4349
+ return candidate.originId === originId && typeof candidate.currentIndex === "number" && Number.isSafeInteger(candidate.currentIndex) && candidate.currentIndex >= 0 && Array.isArray(candidate.revisions) && candidate.revisions.every(isRevision);
4350
+ }
4351
+ function isRevision(value) {
4352
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
4353
+ const candidate = value;
4354
+ return isAttachment(candidate.attachment) && (candidate.provider === "google" || candidate.provider === "openai" || candidate.provider === "seedream" || candidate.provider === "dashscope") && typeof candidate.prompt === "string" && typeof candidate.model === "string" && typeof candidate.output === "string" && typeof candidate.createdAt === "number" && Number.isFinite(candidate.createdAt) && typeof candidate.ratio === "string" && typeof candidate.quality === "string";
4355
+ }
4356
+ function isAttachment(value) {
4357
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
4358
+ const candidate = value;
4359
+ return typeof candidate.attachmentId === "string" && (candidate.mediaType === "image/png" || candidate.mediaType === "image/jpeg" || candidate.mediaType === "image/webp" || candidate.mediaType === "image/gif") && typeof candidate.bytes === "number" && typeof candidate.width === "number" && typeof candidate.height === "number";
4360
+ }
4361
+ //#endregion
2182
4362
  //#region lib/types/client/index.js
2183
4363
  const KEY_REF = {
2184
4364
  google: "GEMINI_API_KEY",
@@ -2242,7 +4422,18 @@ window.__ModuleLoader__.load({
2242
4422
  download: "下载图片",
2243
4423
  openNewTab: "新标签页打开",
2244
4424
  copiedImage: "已复制图片",
2245
- copyFailed: "复制失败"
4425
+ copyFailed: "复制失败",
4426
+ regenerate: "重新生成",
4427
+ regenerateTitle: "重新生成图片",
4428
+ regenerateHint: "如有需要可微调提示词。生成的新图片将替代当前展示,原图依然可在版本中查看。",
4429
+ prompt: "提示词",
4430
+ cancel: "取消",
4431
+ confirmRegenerate: "确认生成",
4432
+ regenerating: "重新生成中…",
4433
+ regenerateFailed: "重新生成失败",
4434
+ versionPrevious: "上一版本",
4435
+ versionNext: "下一版本",
4436
+ versionLabel: "图片版本 {current}/{total}"
2246
4437
  },
2247
4438
  en: {
2248
4439
  title: "Image Generation",
@@ -2299,7 +4490,18 @@ window.__ModuleLoader__.load({
2299
4490
  download: "Download Image",
2300
4491
  openNewTab: "Open in new tab",
2301
4492
  copiedImage: "Image copied",
2302
- copyFailed: "Copy failed"
4493
+ copyFailed: "Copy failed",
4494
+ regenerate: "Regenerate",
4495
+ regenerateTitle: "Regenerate image",
4496
+ regenerateHint: "Edit the prompt if needed. The new image replaces this view while the original remains available.",
4497
+ prompt: "Prompt",
4498
+ cancel: "Cancel",
4499
+ confirmRegenerate: "Regenerate",
4500
+ regenerating: "Regenerating…",
4501
+ regenerateFailed: "Regeneration failed",
4502
+ versionPrevious: "Previous version",
4503
+ versionNext: "Next version",
4504
+ versionLabel: "Image version {current}/{total}"
2303
4505
  }
2304
4506
  };
2305
4507
  const STYLE = `
@@ -2345,14 +4547,39 @@ window.__ModuleLoader__.load({
2345
4547
  .dsh-ig-result{display:grid;gap:10px;max-width:520px}
2346
4548
  .dsh-ig-promoted-results{display:grid;gap:16px}
2347
4549
  .dsh-ig-result-title{font-size:14px;font-weight:600}
2348
- .dsh-ig-container{position:relative;display:inline-block;width:fit-content;max-width:100%;justify-self:start;border-radius:12px;overflow:hidden;line-height:0}
2349
- .dsh-ig-container:hover .dsh-ig-toolbar{opacity:1;pointer-events:auto}
2350
- .dsh-ig-toolbar{position:absolute;top:8px;left:8px;display:flex;align-items:center;gap:5px;padding:3px 5px;border-radius:8px;background:rgba(0,0,0,0.65);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);opacity:0;pointer-events:none;transition:opacity .18s ease;z-index:10;line-height:1}
4550
+ .dsh-ig-container{position:relative;display:inline-block;width:fit-content;max-width:100%;justify-self:start;border-radius:12px;overflow:hidden;line-height:0;isolation:isolate}
4551
+ .dsh-ig-container:hover .dsh-ig-toolbar,.dsh-ig-container:focus-within .dsh-ig-toolbar{opacity:1;pointer-events:auto}
4552
+ .dsh-ig-toolbar{position:absolute;top:8px;left:8px;display:flex;align-items:center;gap:5px;padding:3px 5px;border-radius:8px;background:rgba(0,0,0,0.65);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);opacity:0;pointer-events:none;transition:opacity .18s ease;z-index:2;line-height:1}
2351
4553
  .dsh-ig-tool-btn{appearance:none;border:0;background:transparent;color:#fff;padding:5px;border-radius:6px;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;transition:background .15s,color .15s}
2352
4554
  .dsh-ig-tool-btn:hover{background:rgba(255,255,255,0.25)}
2353
4555
  .dsh-ig-tool-btn-danger:hover{background:rgba(239,68,68,0.75)!important;color:#fff!important}
2354
- .dsh-ig-toast{position:absolute;top:100%;left:0;margin-top:5px;padding:3px 8px;border-radius:6px;background:rgba(0,0,0,0.85);color:#fff;font-size:11px;white-space:nowrap;pointer-events:none;z-index:20}
4556
+ .dsh-ig-toast{position:absolute;top:100%;left:0;margin-top:5px;padding:3px 8px;border-radius:6px;background:rgba(0,0,0,0.85);color:#fff;font-size:11px;white-space:nowrap;pointer-events:none;z-index:4}
2355
4557
  .dsh-ig-image{display:block;max-width:100%;max-height:520px;border-radius:12px;background:#f2f3f5;cursor:pointer}
4558
+ .dsh-ig-version-nav{position:absolute;right:8px;bottom:8px;display:flex;align-items:center;gap:2px;padding:3px;border-radius:999px;background:rgba(15,23,42,.72);color:#fff;backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);font-size:11px;line-height:1;z-index:2}
4559
+ .dsh-ig-version-nav button{appearance:none;border:0;background:transparent;color:inherit;width:25px;height:25px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;cursor:pointer;font-size:17px;line-height:1}
4560
+ .dsh-ig-version-nav button:hover:not(:disabled){background:rgba(255,255,255,.2)}
4561
+ .dsh-ig-version-nav button:disabled{opacity:.3;cursor:default}
4562
+ .dsh-ig-version-count{min-width:34px;text-align:center;font-variant-numeric:tabular-nums}
4563
+ .dsh-ig-regenerate-overlay{position:absolute;inset:0;background:rgba(15,23,42,0.52);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;color:#fff;z-index:3;animation:dsh-ig-fade .18s ease-out;line-height:1.4;border-radius:12px}
4564
+ .dsh-ig-regenerate-spinner{width:28px;height:28px;border:3px solid rgba(255,255,255,0.25);border-top-color:#fff;border-radius:50%;animation:dsh-ig-spin .8s linear infinite}
4565
+ .dsh-ig-regenerate-overlay-text{font-size:12.5px;font-weight:550;color:#fff;letter-spacing:0.2px;text-shadow:0 1px 2px rgba(0,0,0,0.4)}
4566
+ .dsh-ig-regenerate-overlay-cancel{appearance:none;border:1px solid rgba(255,255,255,0.4);border-radius:6px;background:rgba(255,255,255,0.15);color:#fff;padding:3px 12px;font-size:11.5px;cursor:pointer;transition:background .15s}
4567
+ .dsh-ig-regenerate-overlay-cancel:hover{background:rgba(255,255,255,0.3)}
4568
+ @keyframes dsh-ig-spin{to{transform:rotate(360deg)}}
4569
+ .dsh-ig-regenerate-backdrop{position:fixed;inset:0;z-index:100000;display:flex;align-items:center;justify-content:center;padding:18px;background:rgba(11,17,29,.6);backdrop-filter:blur(5px);-webkit-backdrop-filter:blur(5px);animation:dsh-ig-fade .15s ease-out}
4570
+ .dsh-ig-regenerate-dialog{width:min(520px,100%);border:1px solid var(--dsw-alias-border-l2,#dfe3ea);border-radius:14px;background:var(--dsw-alias-bg-layer-1,#fff);color:var(--dsw-alias-label-primary,#172033);box-shadow:0 24px 72px rgba(11,17,29,.28);padding:20px;box-sizing:border-box;line-height:1.4}
4571
+ .dsh-ig-regenerate-dialog h3{margin:0;font-size:16px;font-weight:650}
4572
+ .dsh-ig-regenerate-dialog p{margin:7px 0 16px;color:var(--dsw-alias-label-tertiary,#737d8f);font-size:12.5px;line-height:1.55}
4573
+ .dsh-ig-regenerate-dialog label{display:grid;gap:7px;font-size:12.5px;font-weight:600}
4574
+ .dsh-ig-regenerate-dialog textarea{box-sizing:border-box;width:100%;min-height:132px;resize:vertical;border:1px solid var(--dsw-alias-border-l2,#d7dce5);border-radius:9px;padding:11px 12px;background:var(--dsw-alias-bg-layer-2,#fff);color:inherit;font:inherit;font-size:13px;line-height:1.55;outline:none}
4575
+ .dsh-ig-regenerate-dialog textarea:focus{border-color:var(--dsw-alias-brand-primary,#4c78ff);box-shadow:0 0 0 3px rgba(76,120,255,.12)}
4576
+ .dsh-ig-regenerate-error{margin-top:10px!important;color:var(--dsw-alias-label-error,#d33)!important}
4577
+ .dsh-ig-regenerate-actions{display:flex;justify-content:flex-end;gap:9px;margin-top:16px}
4578
+ .dsh-ig-regenerate-actions button{height:35px;padding:0 14px;border-radius:7px;font:inherit;font-size:13px;font-weight:550;cursor:pointer}
4579
+ .dsh-ig-regenerate-cancel{border:1px solid var(--dsw-alias-border-l2,#d7dce5);background:transparent;color:inherit}
4580
+ .dsh-ig-regenerate-confirm{border:1px solid var(--dsw-alias-brand-primary,#3569ed);background:var(--dsw-alias-brand-primary,#3569ed);color:#fff}
4581
+ .dsh-ig-regenerate-actions button:disabled{opacity:.5;cursor:default}
4582
+ @media(hover:none){.dsh-ig-container .dsh-ig-toolbar{opacity:1;pointer-events:auto}}
2356
4583
  @keyframes dsh-ig-fade{from{opacity:0}to{opacity:1}}
2357
4584
  .dsh-ig-error{color:var(--dsw-alias-label-error,#d33);font-size:13px}
2358
4585
  .dsh-ig-loading{color:var(--dsw-alias-label-tertiary,#7b818b);font-size:13px}
@@ -2483,7 +4710,7 @@ window.__ModuleLoader__.load({
2483
4710
  .dsh-ig-batch-btn-exit:hover{background:rgba(255,255,255,0.1);color:#fff}
2484
4711
 
2485
4712
  /* Batch Delete Confirmation Modal */
2486
- .dsh-ig-modal-backdrop{position:fixed;inset:0;background:rgba(0,0,0,0.65);backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);z-index:99995;display:flex;align-items:center;justify-content:center;padding:16px;animation:dsh-ig-fade .15s ease-out}
4713
+ .dsh-ig-modal-backdrop{position:fixed;inset:0;background:rgba(0,0,0,0.65);backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);z-index:100005;display:flex;align-items:center;justify-content:center;padding:16px;animation:dsh-ig-fade .15s ease-out}
2487
4714
  .dsh-ig-modal-box{width:100%;max-width:440px;background:var(--dsw-alias-bg-layer-1,#1c1e24);border:1px solid var(--dsw-alias-border-subtle,rgba(255,255,255,0.12));border-radius:12px;padding:22px;box-sizing:border-box;box-shadow:0 20px 50px rgba(0,0,0,0.45);color:var(--dsw-alias-label-primary,#fff);animation:dsh-ig-scale-up .15s ease-out}
2488
4715
  @keyframes dsh-ig-scale-up{from{transform:scale(0.95);opacity:0}to{transform:scale(1);opacity:1}}
2489
4716
  .dsh-ig-modal-header{display:flex;align-items:flex-start;gap:12px;margin-bottom:12px}
@@ -2499,9 +4726,26 @@ window.__ModuleLoader__.load({
2499
4726
  .dsh-ig-modal-btn-cancel:hover{background:var(--dsw-alias-bg-layer-3,rgba(255,255,255,0.08))}
2500
4727
  .dsh-ig-modal-btn-danger{background:#dc2626;border:1px solid #dc2626;color:#fff}
2501
4728
  .dsh-ig-modal-btn-danger:hover{background:#b91c1c;border-color:#b91c1c}
4729
+ .dsh-ig-modal-btn-primary{background:linear-gradient(135deg,#3b82f6,#2563eb);border:1px solid #2563eb;color:#fff;display:inline-flex;align-items:center;gap:6px}
4730
+ .dsh-ig-modal-btn-primary:hover:not(:disabled){background:linear-gradient(135deg,#2563eb,#1d4ed8)}
4731
+ .dsh-ig-modal-btn-primary:disabled{opacity:0.5;cursor:not-allowed}
4732
+ .dsh-ig-regenerate-modal-box{max-width:520px}
4733
+ .dsh-ig-regenerate-modal-icon{width:36px;height:36px;border-radius:50%;background:rgba(59,130,246,0.15);color:#3b82f6;display:flex;align-items:center;justify-content:center;flex-shrink:0}
4734
+ .dsh-ig-regenerate-modal-title-wrap{display:flex;flex-direction:column;gap:4px}
4735
+ .dsh-ig-regenerate-modal-meta{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
4736
+ .dsh-ig-regenerate-modal-textarea{width:100%;box-sizing:border-box;background:var(--dsw-alias-bg-layer-2,rgba(0,0,0,0.25));border:1px solid var(--dsw-alias-border-subtle,rgba(255,255,255,0.15));border-radius:8px;padding:10px 12px;font-size:13px;line-height:1.5;color:var(--dsw-alias-label-primary,#fff);resize:vertical;font-family:inherit;outline:none;transition:border-color .15s,box-shadow .15s}
4737
+ .dsh-ig-regenerate-modal-textarea:focus{border-color:#3b82f6;box-shadow:0 0 0 2px rgba(59,130,246,0.2)}
4738
+ .dsh-ig-lightbox-btn-regenerate:hover{color:#60a5fa!important;border-color:rgba(96,165,250,0.5)!important;background:rgba(96,165,250,0.12)!important}
4739
+ .dsh-ig-lightbox-generating-indicator{display:inline-flex;align-items:center;gap:8px;padding:4px 12px;background:rgba(37,99,235,0.2);border:1px solid rgba(59,130,246,0.5);border-radius:20px;font-size:12px;color:#93c5fd;animation:dsh-ig-fade .15s ease-out}
4740
+ .dsh-ig-lightbox-spinner-sm{width:12px;height:12px;border:2px solid rgba(147,197,253,0.3);border-top-color:#93c5fd;border-radius:50%;animation:dsh-ig-spin .8s linear infinite;flex-shrink:0}
4741
+ .dsh-ig-lightbox-abort-btn{appearance:none;background:transparent;border:0;color:#fca5a5;font-size:11.5px;cursor:pointer;padding:0 4px;margin-left:4px;text-decoration:underline;text-underline-offset:2px}
4742
+ .dsh-ig-lightbox-abort-btn:hover{color:#ef4444}
2502
4743
 
2503
- /* Hide floating chat composer when gallery page is active */
4744
+ /* Hide floating chat composer and width handles when gallery page is active */
2504
4745
  [data-conversation-scroll]:has(.dsh-ig-gallery-page) [data-composer-seat]{display:none!important}
4746
+ :has(> [data-conversation-scroll]:has(.dsh-ig-gallery-page)) > [class*="widthHandle"],
4747
+ :has(.dsh-ig-gallery-page) [class*="widthHandle"],
4748
+ .root:has(.dsh-ig-gallery-page) [class*="widthHandle"]{display:none!important}
2505
4749
  `;
2506
4750
  /** Required browser services. */
2507
4751
  const inject = [
@@ -2521,7 +4765,7 @@ window.__ModuleLoader__.load({
2521
4765
  ctx.effect(() => {
2522
4766
  const style = document.createElement("style");
2523
4767
  style.dataset.plugin = "dsh-image-gen";
2524
- style.textContent = STYLE;
4768
+ style.textContent = `${STYLE}\n${STUDIO_STYLE}`;
2525
4769
  document.head.appendChild(style);
2526
4770
  return () => {
2527
4771
  style.remove();
@@ -3106,7 +5350,8 @@ window.__ModuleLoader__.load({
3106
5350
  if (props.promoted && result !== void 0) return (0, react_jsx_runtime.jsx)(PromotedResultNotice, { locale: props.locale });
3107
5351
  return (0, react_jsx_runtime.jsx)(ImageResultCard, {
3108
5352
  result,
3109
- locale: props.locale
5353
+ locale: props.locale,
5354
+ sessionId: props.sessionId
3110
5355
  });
3111
5356
  }
3112
5357
  /** Render modern image artifacts as final conversation output instead of Tool process content. */
@@ -3115,7 +5360,8 @@ window.__ModuleLoader__.load({
3115
5360
  className: "dsh-ig-promoted-results",
3116
5361
  children: props.node.data.results.map((result) => (0, react_jsx_runtime.jsx)(ImageResultCard, {
3117
5362
  result,
3118
- locale: props.locale
5363
+ locale: props.locale,
5364
+ sessionId: props.sessionId
3119
5365
  }, result.attachment.attachmentId))
3120
5366
  });
3121
5367
  }
@@ -3126,14 +5372,31 @@ window.__ModuleLoader__.load({
3126
5372
  children: DICT[lang].resultShown
3127
5373
  });
3128
5374
  }
3129
- function ImageResultCard({ result, locale }) {
5375
+ function ImageResultCard({ result, locale, sessionId }) {
3130
5376
  const attachment = result?.attachment;
5377
+ const originId = attachment?.attachmentId;
5378
+ const [revisionChain, setRevisionChain] = (0, react.useState)(() => loadConversationImageRevisionChain(String(originId ?? "")));
5379
+ const selectedRevision = revisionChain.revisions[revisionChain.currentIndex - 1];
5380
+ const activeAttachment = selectedRevision?.attachment ?? attachment;
5381
+ const activeResult = selectedRevision !== void 0 ? {
5382
+ prompt: selectedRevision.prompt,
5383
+ provider: selectedRevision.provider,
5384
+ model: selectedRevision.model,
5385
+ output: selectedRevision.output,
5386
+ createdAt: selectedRevision.createdAt
5387
+ } : result;
3131
5388
  const savedTo = result?.savedTo;
3132
5389
  const [url, setUrl] = (0, react.useState)();
3133
5390
  const [blob, setBlob] = (0, react.useState)();
3134
5391
  const [error, setError] = (0, react.useState)();
3135
5392
  const [previewOpen, setPreviewOpen] = (0, react.useState)(false);
3136
5393
  const [toast, setToast] = (0, react.useState)();
5394
+ const [regenerateOpen, setRegenerateOpen] = (0, react.useState)(false);
5395
+ const [regeneratePrompt, setRegeneratePrompt] = (0, react.useState)("");
5396
+ const [regenerateError, setRegenerateError] = (0, react.useState)();
5397
+ const [isRegenerating, setIsRegenerating] = (0, react.useState)(false);
5398
+ const regenerateControllerRef = (0, react.useRef)();
5399
+ const regenerateTextareaRef = (0, react.useRef)(null);
3137
5400
  const lang = usePluginLanguage(locale);
3138
5401
  const t = (keyName, params) => {
3139
5402
  let text = (lang === "en" ? DICT.en : DICT.zh)[keyName] || DICT.zh[keyName] || keyName;
@@ -3150,9 +5413,36 @@ window.__ModuleLoader__.load({
3150
5413
  model: result.model,
3151
5414
  output: result.output,
3152
5415
  ...result.savedTo ? { savedTo: result.savedTo } : {},
3153
- ...result.seed !== void 0 ? { seed: result.seed } : {}
5416
+ ...result.seed !== void 0 ? { seed: result.seed } : {},
5417
+ ...sessionId ? { sessionId } : {}
3154
5418
  });
3155
5419
  }, [result?.attachment.attachmentId]);
5420
+ (0, react.useEffect)(() => {
5421
+ if (!originId) return;
5422
+ regenerateControllerRef.current?.abort();
5423
+ regenerateControllerRef.current = void 0;
5424
+ setIsRegenerating(false);
5425
+ setRegenerateOpen(false);
5426
+ setRevisionChain(loadConversationImageRevisionChain(String(originId)));
5427
+ }, [originId]);
5428
+ (0, react.useEffect)(() => () => {
5429
+ regenerateControllerRef.current?.abort();
5430
+ }, []);
5431
+ (0, react.useEffect)(() => {
5432
+ if (!regenerateOpen || isRegenerating) return;
5433
+ const frame = requestAnimationFrame(() => {
5434
+ regenerateTextareaRef.current?.focus();
5435
+ regenerateTextareaRef.current?.select();
5436
+ });
5437
+ const onKeyDown = (event) => {
5438
+ if (event.key === "Escape") setRegenerateOpen(false);
5439
+ };
5440
+ window.addEventListener("keydown", onKeyDown);
5441
+ return () => {
5442
+ cancelAnimationFrame(frame);
5443
+ window.removeEventListener("keydown", onKeyDown);
5444
+ };
5445
+ }, [regenerateOpen, isRegenerating]);
3156
5446
  (0, react.useEffect)(() => {
3157
5447
  if (!previewOpen) return;
3158
5448
  const onKeyDown = (e) => {
@@ -3163,30 +5453,34 @@ window.__ModuleLoader__.load({
3163
5453
  window.removeEventListener("keydown", onKeyDown);
3164
5454
  };
3165
5455
  }, [previewOpen]);
5456
+ const currentUrlRef = (0, react.useRef)();
3166
5457
  (0, react.useEffect)(() => {
3167
- if (attachment === void 0) return;
3168
- const controller = new AbortController();
3169
- let objectUrl;
3170
- fetch(IMAGE_ROUTE, {
3171
- method: "POST",
3172
- signal: controller.signal,
3173
- headers: { "content-type": "application/json" },
3174
- body: JSON.stringify({ attachment })
3175
- }).then(async (response) => {
3176
- if (!response.ok) throw new Error(t("loadFailed", { status: String(response.status) }));
3177
- const resBlob = await response.blob();
3178
- if (controller.signal.aborted) return;
5458
+ return () => {
5459
+ if (currentUrlRef.current !== void 0) {
5460
+ URL.revokeObjectURL(currentUrlRef.current);
5461
+ currentUrlRef.current = void 0;
5462
+ }
5463
+ };
5464
+ }, []);
5465
+ (0, react.useEffect)(() => {
5466
+ if (activeAttachment === void 0) return;
5467
+ let canceled = false;
5468
+ fetchAttachmentBlob(activeAttachment).then((resBlob) => {
5469
+ if (canceled) return;
3179
5470
  setBlob(resBlob);
3180
- objectUrl = URL.createObjectURL(resBlob);
3181
- setUrl(objectUrl);
5471
+ setError(void 0);
5472
+ const nextUrl = URL.createObjectURL(resBlob);
5473
+ if (currentUrlRef.current !== void 0) URL.revokeObjectURL(currentUrlRef.current);
5474
+ currentUrlRef.current = nextUrl;
5475
+ setUrl(nextUrl);
3182
5476
  }).catch((cause) => {
3183
- if (!controller.signal.aborted) setError(cause instanceof Error ? cause.message : String(cause));
5477
+ if (canceled) return;
5478
+ setError(cause instanceof Error ? cause.message : String(cause));
3184
5479
  });
3185
5480
  return () => {
3186
- controller.abort();
3187
- if (objectUrl !== void 0) URL.revokeObjectURL(objectUrl);
5481
+ canceled = true;
3188
5482
  };
3189
- }, [attachment?.attachmentId, lang]);
5483
+ }, [activeAttachment?.attachmentId]);
3190
5484
  const copy = async (e) => {
3191
5485
  e.stopPropagation();
3192
5486
  if (!blob) return;
@@ -3201,7 +5495,7 @@ window.__ModuleLoader__.load({
3201
5495
  if (!url) return;
3202
5496
  const a = document.createElement("a");
3203
5497
  a.href = url;
3204
- a.download = attachment?.name || `dsh-image-${Date.now()}.png`;
5498
+ a.download = activeAttachment?.name || `dsh-image-${Date.now()}.png`;
3205
5499
  document.body.appendChild(a);
3206
5500
  a.click();
3207
5501
  document.body.removeChild(a);
@@ -3211,6 +5505,93 @@ window.__ModuleLoader__.load({
3211
5505
  if (!url) return;
3212
5506
  window.open(url, "_blank", "noopener,noreferrer");
3213
5507
  };
5508
+ const openRegenerate = (event) => {
5509
+ event.stopPropagation();
5510
+ if (activeResult === void 0 || isRegenerating) return;
5511
+ setRegeneratePrompt(activeResult.prompt);
5512
+ setRegenerateError(void 0);
5513
+ setRegenerateOpen(true);
5514
+ };
5515
+ const cancelRegenerate = () => {
5516
+ regenerateControllerRef.current?.abort();
5517
+ regenerateControllerRef.current = void 0;
5518
+ setIsRegenerating(false);
5519
+ setRegenerateOpen(false);
5520
+ setRegenerateError(void 0);
5521
+ };
5522
+ const regenerate = async () => {
5523
+ if (activeResult === void 0 || !originId || isRegenerating || regeneratePrompt.trim().length === 0) return;
5524
+ setIsRegenerating(true);
5525
+ setRegenerateError(void 0);
5526
+ setRegenerateOpen(false);
5527
+ const controller = new AbortController();
5528
+ regenerateControllerRef.current = controller;
5529
+ try {
5530
+ const request = conversationRegenerateRequest(activeResult, regeneratePrompt, selectedRevision === void 0 ? void 0 : {
5531
+ ratio: selectedRevision.ratio,
5532
+ quality: selectedRevision.quality
5533
+ });
5534
+ const response = await fetch(STUDIO_ROUTE, {
5535
+ method: "POST",
5536
+ credentials: "same-origin",
5537
+ signal: controller.signal,
5538
+ headers: { "content-type": "application/json" },
5539
+ body: JSON.stringify(request)
5540
+ });
5541
+ const payload = await response.json().catch(() => null);
5542
+ if (!response.ok || payload === null || !("attachment" in payload)) throw new Error(payload && "error" in payload && payload.error ? payload.error : t("regenerateFailed"));
5543
+ const revision = {
5544
+ attachment: payload.attachment,
5545
+ prompt: payload.prompt,
5546
+ provider: payload.provider,
5547
+ model: payload.model,
5548
+ output: payload.output,
5549
+ createdAt: payload.createdAt,
5550
+ ratio: request.ratio,
5551
+ quality: request.quality
5552
+ };
5553
+ await saveGalleryItem({
5554
+ id: String(revision.attachment.attachmentId),
5555
+ attachment: revision.attachment,
5556
+ prompt: revision.prompt,
5557
+ provider: revision.provider,
5558
+ model: revision.model,
5559
+ createdAt: revision.createdAt,
5560
+ aspectRatio: revision.ratio,
5561
+ imageSize: revision.quality,
5562
+ output: revision.output,
5563
+ ...savedTo ? { savedTo } : {},
5564
+ ...sessionId ? { sessionId } : {}
5565
+ });
5566
+ const next = appendConversationImageRevision(String(originId), revision);
5567
+ setRevisionChain(next);
5568
+ setToast(t("regenerate"));
5569
+ setTimeout(() => {
5570
+ setToast(void 0);
5571
+ }, 2e3);
5572
+ } catch (cause) {
5573
+ if (!controller.signal.aborted) {
5574
+ const errMsg = cause instanceof Error ? cause.message : String(cause);
5575
+ setError(errMsg);
5576
+ setToast(errMsg);
5577
+ setTimeout(() => {
5578
+ setToast(void 0);
5579
+ }, 3500);
5580
+ }
5581
+ } finally {
5582
+ if (regenerateControllerRef.current === controller) {
5583
+ regenerateControllerRef.current = void 0;
5584
+ setIsRegenerating(false);
5585
+ }
5586
+ }
5587
+ };
5588
+ const selectVersion = (index) => {
5589
+ if (!originId || isRegenerating) return;
5590
+ setRevisionChain(selectConversationImageRevision(String(originId), index));
5591
+ };
5592
+ const canRegenerate = activeResult !== void 0 && CLOUD_IMAGE_PROVIDERS.includes(activeResult.provider) && activeResult.model.trim().length > 0;
5593
+ const versionTotal = revisionChain.revisions.length + 1;
5594
+ const versionCurrent = revisionChain.currentIndex + 1;
3214
5595
  if (attachment === void 0) return (0, react_jsx_runtime.jsx)("div", {
3215
5596
  className: "dsh-ig-loading",
3216
5597
  children: t("generating")
@@ -3241,100 +5622,174 @@ window.__ModuleLoader__.load({
3241
5622
  }) : null,
3242
5623
  url !== void 0 ? (0, react_jsx_runtime.jsxs)("div", {
3243
5624
  className: "dsh-ig-container",
3244
- children: [(0, react_jsx_runtime.jsx)("img", {
3245
- className: "dsh-ig-image",
3246
- src: url,
3247
- alt: attachment.name ?? "Generated image",
3248
- onClick: () => {
3249
- setPreviewOpen(true);
3250
- }
3251
- }), (0, react_jsx_runtime.jsxs)("div", {
3252
- className: "dsh-ig-toolbar",
3253
- children: [
3254
- (0, react_jsx_runtime.jsx)("button", {
3255
- type: "button",
3256
- className: "dsh-ig-tool-btn",
3257
- title: t("copyImg"),
3258
- onClick: (e) => {
3259
- copy(e);
3260
- },
3261
- children: (0, react_jsx_runtime.jsxs)("svg", {
3262
- width: "15",
3263
- height: "15",
3264
- viewBox: "0 0 24 24",
3265
- fill: "none",
3266
- stroke: "currentColor",
3267
- strokeWidth: "2",
3268
- strokeLinecap: "round",
3269
- strokeLinejoin: "round",
3270
- children: [(0, react_jsx_runtime.jsx)("rect", {
3271
- x: "9",
3272
- y: "9",
3273
- width: "13",
3274
- height: "13",
3275
- rx: "2",
3276
- ry: "2"
3277
- }), (0, react_jsx_runtime.jsx)("path", { d: "M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" })]
3278
- })
3279
- }),
3280
- (0, react_jsx_runtime.jsx)("button", {
3281
- type: "button",
3282
- className: "dsh-ig-tool-btn",
3283
- title: t("download"),
3284
- onClick: download,
3285
- children: (0, react_jsx_runtime.jsxs)("svg", {
3286
- width: "15",
3287
- height: "15",
3288
- viewBox: "0 0 24 24",
3289
- fill: "none",
3290
- stroke: "currentColor",
3291
- strokeWidth: "2",
3292
- strokeLinecap: "round",
3293
- strokeLinejoin: "round",
3294
- children: [
3295
- (0, react_jsx_runtime.jsx)("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
3296
- (0, react_jsx_runtime.jsx)("polyline", { points: "7 10 12 15 17 10" }),
3297
- (0, react_jsx_runtime.jsx)("line", {
3298
- x1: "12",
3299
- y1: "15",
3300
- x2: "12",
3301
- y2: "3"
3302
- })
3303
- ]
5625
+ children: [
5626
+ (0, react_jsx_runtime.jsx)("img", {
5627
+ className: "dsh-ig-image",
5628
+ src: url,
5629
+ alt: activeAttachment?.name ?? "Generated image",
5630
+ onClick: () => {
5631
+ if (!isRegenerating) setPreviewOpen(true);
5632
+ }
5633
+ }),
5634
+ isRegenerating ? (0, react_jsx_runtime.jsxs)("div", {
5635
+ className: "dsh-ig-regenerate-overlay",
5636
+ children: [
5637
+ (0, react_jsx_runtime.jsx)("div", { className: "dsh-ig-regenerate-spinner" }),
5638
+ (0, react_jsx_runtime.jsx)("span", {
5639
+ className: "dsh-ig-regenerate-overlay-text",
5640
+ children: t("regenerating")
5641
+ }),
5642
+ (0, react_jsx_runtime.jsx)("button", {
5643
+ type: "button",
5644
+ className: "dsh-ig-regenerate-overlay-cancel",
5645
+ onClick: cancelRegenerate,
5646
+ children: t("cancel")
3304
5647
  })
5648
+ ]
5649
+ }) : null,
5650
+ (0, react_jsx_runtime.jsxs)("div", {
5651
+ className: "dsh-ig-toolbar",
5652
+ children: [
5653
+ canRegenerate ? (0, react_jsx_runtime.jsx)("button", {
5654
+ type: "button",
5655
+ className: "dsh-ig-tool-btn",
5656
+ disabled: isRegenerating,
5657
+ title: isRegenerating ? t("regenerating") : t("regenerate"),
5658
+ onClick: openRegenerate,
5659
+ children: (0, react_jsx_runtime.jsxs)("svg", {
5660
+ width: "15",
5661
+ height: "15",
5662
+ viewBox: "0 0 24 24",
5663
+ fill: "none",
5664
+ stroke: "currentColor",
5665
+ strokeWidth: "2",
5666
+ strokeLinecap: "round",
5667
+ strokeLinejoin: "round",
5668
+ children: [(0, react_jsx_runtime.jsx)("path", { d: "M20 11a8.1 8.1 0 0 0-15.5-2M4 4v5h5" }), (0, react_jsx_runtime.jsx)("path", { d: "M4 13a8.1 8.1 0 0 0 15.5 2M20 20v-5h-5" })]
5669
+ })
5670
+ }) : null,
5671
+ (0, react_jsx_runtime.jsx)("button", {
5672
+ type: "button",
5673
+ className: "dsh-ig-tool-btn",
5674
+ title: t("copyImg"),
5675
+ onClick: (e) => {
5676
+ copy(e);
5677
+ },
5678
+ children: (0, react_jsx_runtime.jsxs)("svg", {
5679
+ width: "15",
5680
+ height: "15",
5681
+ viewBox: "0 0 24 24",
5682
+ fill: "none",
5683
+ stroke: "currentColor",
5684
+ strokeWidth: "2",
5685
+ strokeLinecap: "round",
5686
+ strokeLinejoin: "round",
5687
+ children: [(0, react_jsx_runtime.jsx)("rect", {
5688
+ x: "9",
5689
+ y: "9",
5690
+ width: "13",
5691
+ height: "13",
5692
+ rx: "2",
5693
+ ry: "2"
5694
+ }), (0, react_jsx_runtime.jsx)("path", { d: "M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" })]
5695
+ })
5696
+ }),
5697
+ (0, react_jsx_runtime.jsx)("button", {
5698
+ type: "button",
5699
+ className: "dsh-ig-tool-btn",
5700
+ title: t("download"),
5701
+ onClick: download,
5702
+ children: (0, react_jsx_runtime.jsxs)("svg", {
5703
+ width: "15",
5704
+ height: "15",
5705
+ viewBox: "0 0 24 24",
5706
+ fill: "none",
5707
+ stroke: "currentColor",
5708
+ strokeWidth: "2",
5709
+ strokeLinecap: "round",
5710
+ strokeLinejoin: "round",
5711
+ children: [
5712
+ (0, react_jsx_runtime.jsx)("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
5713
+ (0, react_jsx_runtime.jsx)("polyline", { points: "7 10 12 15 17 10" }),
5714
+ (0, react_jsx_runtime.jsx)("line", {
5715
+ x1: "12",
5716
+ y1: "15",
5717
+ x2: "12",
5718
+ y2: "3"
5719
+ })
5720
+ ]
5721
+ })
5722
+ }),
5723
+ (0, react_jsx_runtime.jsx)("button", {
5724
+ type: "button",
5725
+ className: "dsh-ig-tool-btn",
5726
+ title: t("openNewTab"),
5727
+ onClick: openNewTab,
5728
+ children: (0, react_jsx_runtime.jsxs)("svg", {
5729
+ width: "15",
5730
+ height: "15",
5731
+ viewBox: "0 0 24 24",
5732
+ fill: "none",
5733
+ stroke: "currentColor",
5734
+ strokeWidth: "2",
5735
+ strokeLinecap: "round",
5736
+ strokeLinejoin: "round",
5737
+ children: [
5738
+ (0, react_jsx_runtime.jsx)("path", { d: "M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" }),
5739
+ (0, react_jsx_runtime.jsx)("polyline", { points: "15 3 21 3 21 9" }),
5740
+ (0, react_jsx_runtime.jsx)("line", {
5741
+ x1: "10",
5742
+ y1: "14",
5743
+ x2: "21",
5744
+ y2: "3"
5745
+ })
5746
+ ]
5747
+ })
5748
+ }),
5749
+ toast ? (0, react_jsx_runtime.jsx)("div", {
5750
+ className: "dsh-ig-toast",
5751
+ children: toast
5752
+ }) : null
5753
+ ]
5754
+ }),
5755
+ versionTotal > 1 ? (0, react_jsx_runtime.jsxs)("div", {
5756
+ className: "dsh-ig-version-nav",
5757
+ "aria-label": t("versionLabel", {
5758
+ current: String(versionCurrent),
5759
+ total: String(versionTotal)
3305
5760
  }),
3306
- (0, react_jsx_runtime.jsx)("button", {
3307
- type: "button",
3308
- className: "dsh-ig-tool-btn",
3309
- title: t("openNewTab"),
3310
- onClick: openNewTab,
3311
- children: (0, react_jsx_runtime.jsxs)("svg", {
3312
- width: "15",
3313
- height: "15",
3314
- viewBox: "0 0 24 24",
3315
- fill: "none",
3316
- stroke: "currentColor",
3317
- strokeWidth: "2",
3318
- strokeLinecap: "round",
3319
- strokeLinejoin: "round",
5761
+ children: [
5762
+ (0, react_jsx_runtime.jsx)("button", {
5763
+ type: "button",
5764
+ title: t("versionPrevious"),
5765
+ disabled: revisionChain.currentIndex === 0 || isRegenerating,
5766
+ onClick: (event) => {
5767
+ event.stopPropagation();
5768
+ selectVersion(revisionChain.currentIndex - 1);
5769
+ },
5770
+ children: "‹"
5771
+ }),
5772
+ (0, react_jsx_runtime.jsxs)("span", {
5773
+ className: "dsh-ig-version-count",
3320
5774
  children: [
3321
- (0, react_jsx_runtime.jsx)("path", { d: "M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" }),
3322
- (0, react_jsx_runtime.jsx)("polyline", { points: "15 3 21 3 21 9" }),
3323
- (0, react_jsx_runtime.jsx)("line", {
3324
- x1: "10",
3325
- y1: "14",
3326
- x2: "21",
3327
- y2: "3"
3328
- })
5775
+ versionCurrent,
5776
+ "/",
5777
+ versionTotal
3329
5778
  ]
5779
+ }),
5780
+ (0, react_jsx_runtime.jsx)("button", {
5781
+ type: "button",
5782
+ title: t("versionNext"),
5783
+ disabled: revisionChain.currentIndex >= revisionChain.revisions.length || isRegenerating,
5784
+ onClick: (event) => {
5785
+ event.stopPropagation();
5786
+ selectVersion(revisionChain.currentIndex + 1);
5787
+ },
5788
+ children: "›"
3330
5789
  })
3331
- }),
3332
- toast ? (0, react_jsx_runtime.jsx)("div", {
3333
- className: "dsh-ig-toast",
3334
- children: toast
3335
- }) : null
3336
- ]
3337
- })]
5790
+ ]
5791
+ }) : null
5792
+ ]
3338
5793
  }) : null,
3339
5794
  previewOpen && url !== void 0 ? (0, react_jsx_runtime.jsx)("div", {
3340
5795
  className: "dsh-ig-lightbox-backdrop",
@@ -3349,9 +5804,63 @@ window.__ModuleLoader__.load({
3349
5804
  children: (0, react_jsx_runtime.jsx)("img", {
3350
5805
  className: "dsh-ig-lightbox-img",
3351
5806
  src: url,
3352
- alt: attachment.name ?? "Generated image preview"
5807
+ alt: activeAttachment?.name ?? "Generated image preview"
3353
5808
  })
3354
5809
  })
5810
+ }) : null,
5811
+ regenerateOpen && activeResult !== void 0 ? (0, react_jsx_runtime.jsx)("div", {
5812
+ className: "dsh-ig-regenerate-backdrop",
5813
+ onMouseDown: (event) => {
5814
+ if (event.target === event.currentTarget && !isRegenerating) setRegenerateOpen(false);
5815
+ },
5816
+ children: (0, react_jsx_runtime.jsxs)("div", {
5817
+ className: "dsh-ig-regenerate-dialog",
5818
+ role: "dialog",
5819
+ "aria-modal": "true",
5820
+ "aria-labelledby": `dsh-ig-regenerate-${String(originId)}`,
5821
+ children: [
5822
+ (0, react_jsx_runtime.jsx)("h3", {
5823
+ id: `dsh-ig-regenerate-${String(originId)}`,
5824
+ children: t("regenerateTitle")
5825
+ }),
5826
+ (0, react_jsx_runtime.jsx)("p", { children: t("regenerateHint") }),
5827
+ (0, react_jsx_runtime.jsxs)("label", { children: [(0, react_jsx_runtime.jsx)("span", { children: t("prompt") }), (0, react_jsx_runtime.jsx)("textarea", {
5828
+ ref: regenerateTextareaRef,
5829
+ value: regeneratePrompt,
5830
+ maxLength: 2e3,
5831
+ disabled: isRegenerating,
5832
+ onChange: (event) => setRegeneratePrompt(event.target.value),
5833
+ onKeyDown: (event) => {
5834
+ if (event.key === "Enter" && (event.ctrlKey || event.metaKey)) {
5835
+ event.preventDefault();
5836
+ regenerate();
5837
+ }
5838
+ }
5839
+ })] }),
5840
+ regenerateError ? (0, react_jsx_runtime.jsx)("p", {
5841
+ className: "dsh-ig-regenerate-error",
5842
+ role: "alert",
5843
+ children: regenerateError
5844
+ }) : null,
5845
+ (0, react_jsx_runtime.jsxs)("div", {
5846
+ className: "dsh-ig-regenerate-actions",
5847
+ children: [(0, react_jsx_runtime.jsx)("button", {
5848
+ type: "button",
5849
+ className: "dsh-ig-regenerate-cancel",
5850
+ onClick: cancelRegenerate,
5851
+ children: t("cancel")
5852
+ }), (0, react_jsx_runtime.jsx)("button", {
5853
+ type: "button",
5854
+ className: "dsh-ig-regenerate-confirm",
5855
+ disabled: isRegenerating || regeneratePrompt.trim().length === 0,
5856
+ onClick: () => {
5857
+ regenerate();
5858
+ },
5859
+ children: t("confirmRegenerate")
5860
+ })]
5861
+ })
5862
+ ]
5863
+ })
3355
5864
  }) : null
3356
5865
  ]
3357
5866
  });