dsh-image-gen 0.2.6 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -7,13 +7,23 @@ 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";
16
+ /** Browser route used for saving generated images to workspace on demand. */
17
+ const SAVE_WORKSPACE_ROUTE = "/plugins/dsh-image-gen/save-workspace";
15
18
  /** Namespace persisted through DSH Settings. */
16
19
  const IMAGE_GENERATION_NAMESPACE = "image-generation";
20
+ /** Providers supported by the first browser workbench release. */
21
+ const CLOUD_IMAGE_PROVIDERS = [
22
+ "google",
23
+ "openai",
24
+ "seedream",
25
+ "dashscope"
26
+ ];
17
27
  /** Default endpoints and base URLs. */
18
28
  const DEFAULT_GOOGLE_ENDPOINT = "https://generativelanguage.googleapis.com/v1beta/interactions";
19
29
  const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";
@@ -300,6 +310,29 @@ window.__ModuleLoader__.load({
300
310
  }
301
311
  }
302
312
  /**
313
+ * Delete a single gallery record by ID and record a tombstone.
314
+ */
315
+ async function deleteGalleryItem(id) {
316
+ const db = await getDB();
317
+ const tombstones = await loadTombstones(db);
318
+ await new Promise((resolve, reject) => {
319
+ const tx = db.transaction([STORE_NAME, TOMBSTONE_STORE], "readwrite");
320
+ const store = tx.objectStore(STORE_NAME);
321
+ const tombstoneStore = tx.objectStore(TOMBSTONE_STORE);
322
+ store.delete(id);
323
+ tombstoneStore.put({
324
+ id,
325
+ deletedAt: Date.now()
326
+ });
327
+ tx.oncomplete = () => {
328
+ tombstones.add(id);
329
+ resolve();
330
+ };
331
+ tx.onerror = () => reject(tx.error);
332
+ });
333
+ notifyListeners();
334
+ }
335
+ /**
303
336
  * Bulk delete multiple gallery records by IDs and record tombstones in a single transaction.
304
337
  */
305
338
  async function bulkDeleteGalleryItems(ids) {
@@ -327,6 +360,30 @@ window.__ModuleLoader__.load({
327
360
  });
328
361
  notifyListeners();
329
362
  }
363
+ /**
364
+ * Standardize path format across Windows and POSIX (lowercase, forward slashes, no trailing slash).
365
+ */
366
+ function normalizeWorkspacePath(rawPath) {
367
+ return rawPath.replace(/\\/g, "/").toLowerCase().replace(/\/+$/, "");
368
+ }
369
+ /**
370
+ * Determine if a gallery item belongs to the given workspace context.
371
+ * Compares workspaceId, sessionId membership, workspacePath, and savedTo file path prefix.
372
+ */
373
+ function isItemInWorkspace(item, workspace) {
374
+ if (!workspace || !workspace.workspaceId && !workspace.path && (!workspace.sessionIds || workspace.sessionIds.length === 0)) return true;
375
+ if (item.workspaceId && workspace.workspaceId && item.workspaceId === workspace.workspaceId) return true;
376
+ if (item.sessionId && Array.isArray(workspace.sessionIds) && workspace.sessionIds.includes(item.sessionId)) return true;
377
+ if (item.workspacePath && workspace.path) {
378
+ if (normalizeWorkspacePath(item.workspacePath) === normalizeWorkspacePath(workspace.path)) return true;
379
+ }
380
+ if (item.savedTo && workspace.path) {
381
+ const normSaved = normalizeWorkspacePath(item.savedTo);
382
+ const normWs = normalizeWorkspacePath(workspace.path);
383
+ if (normSaved === normWs || normSaved.startsWith(normWs + "/")) return true;
384
+ }
385
+ return false;
386
+ }
330
387
  //#endregion
331
388
  //#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
389
  /**
@@ -456,6 +513,26 @@ window.__ModuleLoader__.load({
456
513
  * This source code is licensed under the ISC license.
457
514
  * See the LICENSE file in the root directory of this source tree.
458
515
  */
516
+ const BookmarkPlus = createLucideIcon("bookmark-plus", [
517
+ ["path", {
518
+ d: "M12 7v6",
519
+ key: "lw1j43"
520
+ }],
521
+ ["path", {
522
+ d: "M15 10H9",
523
+ key: "o6yqo3"
524
+ }],
525
+ ["path", {
526
+ d: "M17 3a2 2 0 0 1 2 2v15a1 1 0 0 1-1.496.868l-4.512-2.578a2 2 0 0 0-1.984 0l-4.512 2.578A1 1 0 0 1 5 20V5a2 2 0 0 1 2-2z",
527
+ key: "oz39mx"
528
+ }]
529
+ ]);
530
+ /**
531
+ * @license lucide-react v1.38.0 - ISC
532
+ *
533
+ * This source code is licensed under the ISC license.
534
+ * See the LICENSE file in the root directory of this source tree.
535
+ */
459
536
  const Check = createLucideIcon("check", [["path", {
460
537
  d: "M20 6 9 17l-5-5",
461
538
  key: "1gmf2c"
@@ -466,6 +543,16 @@ window.__ModuleLoader__.load({
466
543
  * This source code is licensed under the ISC license.
467
544
  * See the LICENSE file in the root directory of this source tree.
468
545
  */
546
+ const ChevronDown = createLucideIcon("chevron-down", [["path", {
547
+ d: "m6 9 6 6 6-6",
548
+ key: "qrunsl"
549
+ }]]);
550
+ /**
551
+ * @license lucide-react v1.38.0 - ISC
552
+ *
553
+ * This source code is licensed under the ISC license.
554
+ * See the LICENSE file in the root directory of this source tree.
555
+ */
469
556
  const ChevronLeft = createLucideIcon("chevron-left", [["path", {
470
557
  d: "m15 18-6-6 6-6",
471
558
  key: "1wnfg3"
@@ -486,6 +573,24 @@ window.__ModuleLoader__.load({
486
573
  * This source code is licensed under the ISC license.
487
574
  * See the LICENSE file in the root directory of this source tree.
488
575
  */
576
+ const Clipboard = createLucideIcon("clipboard", [["rect", {
577
+ width: "8",
578
+ height: "4",
579
+ x: "8",
580
+ y: "2",
581
+ rx: "1",
582
+ ry: "1",
583
+ key: "tgr4d6"
584
+ }], ["path", {
585
+ 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",
586
+ key: "116196"
587
+ }]]);
588
+ /**
589
+ * @license lucide-react v1.38.0 - ISC
590
+ *
591
+ * This source code is licensed under the ISC license.
592
+ * See the LICENSE file in the root directory of this source tree.
593
+ */
489
594
  const Copy = createLucideIcon("copy", [["rect", {
490
595
  width: "14",
491
596
  height: "14",
@@ -524,6 +629,46 @@ window.__ModuleLoader__.load({
524
629
  * This source code is licensed under the ISC license.
525
630
  * See the LICENSE file in the root directory of this source tree.
526
631
  */
632
+ const Expand = createLucideIcon("expand", [
633
+ ["path", {
634
+ d: "m15 15 6 6",
635
+ key: "1s409w"
636
+ }],
637
+ ["path", {
638
+ d: "m15 9 6-6",
639
+ key: "ko1vev"
640
+ }],
641
+ ["path", {
642
+ d: "M21 16v5h-5",
643
+ key: "1ck2sf"
644
+ }],
645
+ ["path", {
646
+ d: "M21 8V3h-5",
647
+ key: "1qoq8a"
648
+ }],
649
+ ["path", {
650
+ d: "M3 16v5h5",
651
+ key: "1t08am"
652
+ }],
653
+ ["path", {
654
+ d: "m3 21 6-6",
655
+ key: "wwnumi"
656
+ }],
657
+ ["path", {
658
+ d: "M3 8V3h5",
659
+ key: "1ln10m"
660
+ }],
661
+ ["path", {
662
+ d: "M9 9 3 3",
663
+ key: "v551iv"
664
+ }]
665
+ ]);
666
+ /**
667
+ * @license lucide-react v1.38.0 - ISC
668
+ *
669
+ * This source code is licensed under the ISC license.
670
+ * See the LICENSE file in the root directory of this source tree.
671
+ */
527
672
  const FileText = createLucideIcon("file-text", [
528
673
  ["path", {
529
674
  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 +697,38 @@ window.__ModuleLoader__.load({
552
697
  * This source code is licensed under the ISC license.
553
698
  * See the LICENSE file in the root directory of this source tree.
554
699
  */
555
- const GitCompare = createLucideIcon("git-compare", [
556
- ["circle", {
557
- cx: "18",
558
- cy: "18",
559
- r: "3",
560
- key: "1xkwt0"
700
+ const Heart = createLucideIcon("heart", [["path", {
701
+ 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",
702
+ key: "mvr1a0"
703
+ }]]);
704
+ /**
705
+ * @license lucide-react v1.38.0 - ISC
706
+ *
707
+ * This source code is licensed under the ISC license.
708
+ * See the LICENSE file in the root directory of this source tree.
709
+ */
710
+ const ImagePlus = createLucideIcon("image-plus", [
711
+ ["path", {
712
+ d: "M16 5h6",
713
+ key: "1vod17"
561
714
  }],
562
- ["circle", {
563
- cx: "6",
564
- cy: "6",
565
- r: "3",
566
- key: "1lh9wr"
715
+ ["path", {
716
+ d: "M19 2v6",
717
+ key: "4bpg5p"
567
718
  }],
568
719
  ["path", {
569
- d: "M13 6h3a2 2 0 0 1 2 2v7",
570
- key: "1yeb86"
720
+ d: "M21 11.5V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7.5",
721
+ key: "1ue2ih"
571
722
  }],
572
723
  ["path", {
573
- d: "M11 18H8a2 2 0 0 1-2-2V9",
574
- key: "19pyzm"
724
+ d: "m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21",
725
+ key: "1xmnt7"
726
+ }],
727
+ ["circle", {
728
+ cx: "9",
729
+ cy: "9",
730
+ r: "2",
731
+ key: "af1f0g"
575
732
  }]
576
733
  ]);
577
734
  /**
@@ -580,16 +737,6 @@ window.__ModuleLoader__.load({
580
737
  * This source code is licensed under the ISC license.
581
738
  * See the LICENSE file in the root directory of this source tree.
582
739
  */
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
740
  const Image$1 = createLucideIcon("image", [
594
741
  ["rect", {
595
742
  width: "18",
@@ -617,30 +764,106 @@ window.__ModuleLoader__.load({
617
764
  * This source code is licensed under the ISC license.
618
765
  * See the LICENSE file in the root directory of this source tree.
619
766
  */
620
- const ListTodo = createLucideIcon("list-todo", [
767
+ const LoaderCircle = createLucideIcon("loader-circle", [["path", {
768
+ d: "M21 12a9 9 0 1 1-6.219-8.56",
769
+ key: "13zald"
770
+ }]]);
771
+ /**
772
+ * @license lucide-react v1.38.0 - ISC
773
+ *
774
+ * This source code is licensed under the ISC license.
775
+ * See the LICENSE file in the root directory of this source tree.
776
+ */
777
+ const PanelLeftClose = createLucideIcon("panel-left-close", [
778
+ ["rect", {
779
+ width: "18",
780
+ height: "18",
781
+ x: "3",
782
+ y: "3",
783
+ rx: "2",
784
+ key: "afitv7"
785
+ }],
786
+ ["path", {
787
+ d: "M9 3v18",
788
+ key: "fh3hqa"
789
+ }],
790
+ ["path", {
791
+ d: "m16 15-3-3 3-3",
792
+ key: "14y99z"
793
+ }]
794
+ ]);
795
+ /**
796
+ * @license lucide-react v1.38.0 - ISC
797
+ *
798
+ * This source code is licensed under the ISC license.
799
+ * See the LICENSE file in the root directory of this source tree.
800
+ */
801
+ const PanelLeft = createLucideIcon("panel-left", [["rect", {
802
+ width: "18",
803
+ height: "18",
804
+ x: "3",
805
+ y: "3",
806
+ rx: "2",
807
+ key: "afitv7"
808
+ }], ["path", {
809
+ d: "M9 3v18",
810
+ key: "fh3hqa"
811
+ }]]);
812
+ /**
813
+ * @license lucide-react v1.38.0 - ISC
814
+ *
815
+ * This source code is licensed under the ISC license.
816
+ * See the LICENSE file in the root directory of this source tree.
817
+ */
818
+ const PencilLine = createLucideIcon("pencil-line", [
819
+ ["path", {
820
+ d: "M13 21h8",
821
+ key: "1jsn5i"
822
+ }],
621
823
  ["path", {
622
- d: "M13 5h8",
623
- key: "a7qcls"
824
+ d: "m15 5 4 4",
825
+ key: "1mk7zo"
624
826
  }],
625
827
  ["path", {
626
- d: "M13 12h8",
627
- key: "h98zly"
828
+ 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",
829
+ key: "1a8usu"
830
+ }]
831
+ ]);
832
+ /**
833
+ * @license lucide-react v1.38.0 - ISC
834
+ *
835
+ * This source code is licensed under the ISC license.
836
+ * See the LICENSE file in the root directory of this source tree.
837
+ */
838
+ const Plus = createLucideIcon("plus", [["path", {
839
+ d: "M5 12h14",
840
+ key: "1ays0h"
841
+ }], ["path", {
842
+ d: "M12 5v14",
843
+ key: "s699le"
844
+ }]]);
845
+ /**
846
+ * @license lucide-react v1.38.0 - ISC
847
+ *
848
+ * This source code is licensed under the ISC license.
849
+ * See the LICENSE file in the root directory of this source tree.
850
+ */
851
+ const RefreshCw = createLucideIcon("refresh-cw", [
852
+ ["path", {
853
+ d: "M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8",
854
+ key: "v9h5vc"
628
855
  }],
629
856
  ["path", {
630
- d: "M13 19h8",
631
- key: "c3s6r1"
857
+ d: "M21 3v5h-5",
858
+ key: "1q7to0"
632
859
  }],
633
860
  ["path", {
634
- d: "m3 17 2 2 4-4",
635
- key: "1jhpwq"
861
+ d: "M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16",
862
+ key: "3uifl3"
636
863
  }],
637
- ["rect", {
638
- x: "3",
639
- y: "4",
640
- width: "6",
641
- height: "6",
642
- rx: "1",
643
- key: "cif1o7"
864
+ ["path", {
865
+ d: "M8 16H3v5",
866
+ key: "1cv678"
644
867
  }]
645
868
  ]);
646
869
  /**
@@ -708,6 +931,32 @@ window.__ModuleLoader__.load({
708
931
  * This source code is licensed under the ISC license.
709
932
  * See the LICENSE file in the root directory of this source tree.
710
933
  */
934
+ const Sparkles = createLucideIcon("sparkles", [
935
+ ["path", {
936
+ 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",
937
+ key: "1s2grr"
938
+ }],
939
+ ["path", {
940
+ d: "M20 2v4",
941
+ key: "1rf3ol"
942
+ }],
943
+ ["path", {
944
+ d: "M22 4h-4",
945
+ key: "gwowj6"
946
+ }],
947
+ ["circle", {
948
+ cx: "4",
949
+ cy: "20",
950
+ r: "2",
951
+ key: "6kqj1y"
952
+ }]
953
+ ]);
954
+ /**
955
+ * @license lucide-react v1.38.0 - ISC
956
+ *
957
+ * This source code is licensed under the ISC license.
958
+ * See the LICENSE file in the root directory of this source tree.
959
+ */
711
960
  const Trash2 = createLucideIcon("trash-2", [
712
961
  ["path", {
713
962
  d: "M10 11v6",
@@ -756,6 +1005,26 @@ window.__ModuleLoader__.load({
756
1005
  * This source code is licensed under the ISC license.
757
1006
  * See the LICENSE file in the root directory of this source tree.
758
1007
  */
1008
+ const Upload = createLucideIcon("upload", [
1009
+ ["path", {
1010
+ d: "M12 3v12",
1011
+ key: "1x0j5s"
1012
+ }],
1013
+ ["path", {
1014
+ d: "m17 8-5-5-5 5",
1015
+ key: "7q97r8"
1016
+ }],
1017
+ ["path", {
1018
+ d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",
1019
+ key: "ih7n3h"
1020
+ }]
1021
+ ]);
1022
+ /**
1023
+ * @license lucide-react v1.38.0 - ISC
1024
+ *
1025
+ * This source code is licensed under the ISC license.
1026
+ * See the LICENSE file in the root directory of this source tree.
1027
+ */
759
1028
  const X = createLucideIcon("x", [["path", {
760
1029
  d: "M18 6 6 18",
761
1030
  key: "1bl5f8"
@@ -763,34 +1032,1829 @@ window.__ModuleLoader__.load({
763
1032
  d: "m6 6 12 12",
764
1033
  key: "d8bk6v"
765
1034
  }]]);
1035
+ /**
1036
+ * @license lucide-react v1.38.0 - ISC
1037
+ *
1038
+ * This source code is licensed under the ISC license.
1039
+ * See the LICENSE file in the root directory of this source tree.
1040
+ */
1041
+ const ZoomIn = createLucideIcon("zoom-in", [
1042
+ ["circle", {
1043
+ cx: "11",
1044
+ cy: "11",
1045
+ r: "8",
1046
+ key: "4ej97u"
1047
+ }],
1048
+ ["line", {
1049
+ x1: "21",
1050
+ x2: "16.65",
1051
+ y1: "21",
1052
+ y2: "16.65",
1053
+ key: "13gj7c"
1054
+ }],
1055
+ ["line", {
1056
+ x1: "11",
1057
+ x2: "11",
1058
+ y1: "8",
1059
+ y2: "14",
1060
+ key: "1vmskp"
1061
+ }],
1062
+ ["line", {
1063
+ x1: "8",
1064
+ x2: "14",
1065
+ y1: "11",
1066
+ y2: "11",
1067
+ key: "durymu"
1068
+ }]
1069
+ ]);
1070
+ /**
1071
+ * @license lucide-react v1.38.0 - ISC
1072
+ *
1073
+ * This source code is licensed under the ISC license.
1074
+ * See the LICENSE file in the root directory of this source tree.
1075
+ */
1076
+ const ZoomOut = createLucideIcon("zoom-out", [
1077
+ ["circle", {
1078
+ cx: "11",
1079
+ cy: "11",
1080
+ r: "8",
1081
+ key: "4ej97u"
1082
+ }],
1083
+ ["line", {
1084
+ x1: "21",
1085
+ x2: "16.65",
1086
+ y1: "21",
1087
+ y2: "16.65",
1088
+ key: "13gj7c"
1089
+ }],
1090
+ ["line", {
1091
+ x1: "8",
1092
+ x2: "14",
1093
+ y1: "11",
1094
+ y2: "11",
1095
+ key: "durymu"
1096
+ }]
1097
+ ]);
1098
+ let currentCacheBytes = 0;
1099
+ const cache = /* @__PURE__ */ new Map();
1100
+ /**
1101
+ * Fetch or reuse an image Blob by attachment ID.
1102
+ * Refreshes LRU recency on hit and enforces count/byte limits on arrival.
1103
+ */
1104
+ function fetchAttachmentBlob(attachment) {
1105
+ const key = String(attachment.attachmentId);
1106
+ const existing = cache.get(key);
1107
+ if (existing !== void 0) {
1108
+ cache.delete(key);
1109
+ cache.set(key, existing);
1110
+ return existing.promise;
1111
+ }
1112
+ const entry = {
1113
+ promise: Promise.resolve().then(async () => {
1114
+ try {
1115
+ const response = await fetch(IMAGE_ROUTE, {
1116
+ method: "POST",
1117
+ credentials: "same-origin",
1118
+ headers: { "content-type": "application/json" },
1119
+ body: JSON.stringify({ attachment })
1120
+ });
1121
+ if (!response.ok) throw new Error(`Image unavailable (${response.status})`);
1122
+ const blob = await response.blob();
1123
+ if (cache.get(key) === entry) {
1124
+ entry.bytes = blob.size;
1125
+ currentCacheBytes += blob.size;
1126
+ enforceLimits();
1127
+ }
1128
+ return blob;
1129
+ } catch (err) {
1130
+ if (cache.get(key) === entry) cache.delete(key);
1131
+ throw err;
1132
+ }
1133
+ }),
1134
+ bytes: 0
1135
+ };
1136
+ cache.set(key, entry);
1137
+ enforceLimits();
1138
+ return entry.promise;
1139
+ }
1140
+ /** Evict one item from memory cache (e.g. when deleted from gallery). */
1141
+ function evictAttachmentCache(attachmentId) {
1142
+ const key = String(attachmentId);
1143
+ const entry = cache.get(key);
1144
+ if (entry !== void 0) {
1145
+ currentCacheBytes = Math.max(0, currentCacheBytes - entry.bytes);
1146
+ cache.delete(key);
1147
+ }
1148
+ }
1149
+ function enforceLimits() {
1150
+ while (cache.size > 30 || currentCacheBytes > 134217728) {
1151
+ const oldestKey = cache.keys().next().value;
1152
+ if (oldestKey === void 0) break;
1153
+ const oldest = cache.get(oldestKey);
1154
+ if (oldest !== void 0) currentCacheBytes = Math.max(0, currentCacheBytes - oldest.bytes);
1155
+ cache.delete(oldestKey);
1156
+ }
1157
+ }
766
1158
  //#endregion
767
- //#region lib/types/client/gallery-view.js
1159
+ //#region lib/types/client/browser-image-utils.js
768
1160
  /**
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.
1161
+ * Shared browser image and DOM helpers for client views.
1162
+ * Completely decoupled from gallery-view and studio-view to prevent circular dependencies.
772
1163
  */
773
- const DICT$1 = {
774
- zh: {
775
- tabGallery: "图库",
776
- tabStudio: "工作台",
777
- tabFavorites: "收藏",
778
- tabCompare: "对比",
779
- tabTasks: "任务",
780
- filterAllProviders: "全部提供商",
781
- filterGoogle: "Google Gemini",
782
- filterOpenAI: "OpenAI / 中转站",
783
- filterSeedream: "字节 Seedream",
784
- filterDashScope: "阿里 DashScope",
785
- filterComfyUI: "本地 ComfyUI",
786
- filterAllModels: "全部模型",
787
- filterAllRatios: "全部比例",
788
- searchPlaceholder: "搜索 Prompt、标签…",
789
- sortNewest: "最新生成",
790
- sortOldest: "最早生成",
791
- totalCount: "共 {count} 张生成图片",
792
- emptyTitle: "暂无生图记录",
793
- emptyDesc: "在对话中让 Agent 生图后,生成的图片会自动收录到这里。",
1164
+ /** Copy an image Blob to the OS clipboard, converting to PNG if required by the browser. */
1165
+ async function copyImageBlob(blob) {
1166
+ try {
1167
+ if (blob.type === "image/png") {
1168
+ await navigator.clipboard.write([new ClipboardItem({ "image/png": blob })]);
1169
+ return true;
1170
+ }
1171
+ if (typeof createImageBitmap === "function") {
1172
+ const bitmap = await createImageBitmap(blob);
1173
+ try {
1174
+ const canvas = document.createElement("canvas");
1175
+ canvas.width = bitmap.width;
1176
+ canvas.height = bitmap.height;
1177
+ const ctx = canvas.getContext("2d");
1178
+ if (!ctx) return false;
1179
+ ctx.drawImage(bitmap, 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
+ bitmap.close();
1186
+ }
1187
+ }
1188
+ const img = new Image();
1189
+ const url = URL.createObjectURL(blob);
1190
+ try {
1191
+ await new Promise((resolve, reject) => {
1192
+ img.onload = () => resolve();
1193
+ img.onerror = () => reject(/* @__PURE__ */ new Error("Image decode failed"));
1194
+ img.src = url;
1195
+ });
1196
+ const canvas = document.createElement("canvas");
1197
+ canvas.width = img.naturalWidth;
1198
+ canvas.height = img.naturalHeight;
1199
+ const ctx = canvas.getContext("2d");
1200
+ if (!ctx) return false;
1201
+ ctx.drawImage(img, 0, 0);
1202
+ const pngBlob = await new Promise((resolve) => canvas.toBlob(resolve, "image/png"));
1203
+ if (!pngBlob) return false;
1204
+ await navigator.clipboard.write([new ClipboardItem({ "image/png": pngBlob })]);
1205
+ return true;
1206
+ } finally {
1207
+ URL.revokeObjectURL(url);
1208
+ }
1209
+ } catch {
1210
+ return false;
1211
+ }
1212
+ }
1213
+ /** Safely trigger a browser file download by mounting an anchor element into the DOM. */
1214
+ function downloadBlobUrl(url, filename) {
1215
+ const link = document.createElement("a");
1216
+ link.href = url;
1217
+ link.download = filename;
1218
+ document.body.appendChild(link);
1219
+ try {
1220
+ link.click();
1221
+ } finally {
1222
+ document.body.removeChild(link);
1223
+ }
1224
+ }
1225
+ /** Human-readable relative time formatter supporting zh and en. */
1226
+ function formatRelativeTime$1(timestamp, lang) {
1227
+ const diff = Math.max(0, Date.now() - timestamp);
1228
+ if (lang === "zh") {
1229
+ if (diff < 6e4) return "刚刚";
1230
+ if (diff < 36e5) return `${Math.floor(diff / 6e4)} 分钟前`;
1231
+ if (diff < 864e5) return `${Math.floor(diff / 36e5)} 小时前`;
1232
+ if (diff < 2592e6) return `${Math.floor(diff / 864e5)} 天前`;
1233
+ const d = new Date(timestamp);
1234
+ return `${d.getFullYear()}/${d.getMonth() + 1}/${d.getDate()}`;
1235
+ }
1236
+ if (diff < 6e4) return "Just now";
1237
+ if (diff < 36e5) return `${Math.floor(diff / 6e4)}m ago`;
1238
+ if (diff < 864e5) return `${Math.floor(diff / 36e5)}h ago`;
1239
+ if (diff < 2592e6) return `${Math.floor(diff / 864e5)}d ago`;
1240
+ const d = new Date(timestamp);
1241
+ return `${d.getFullYear()}/${d.getMonth() + 1}/${d.getDate()}`;
1242
+ }
1243
+ //#endregion
1244
+ //#region lib/types/client/studio-view.js
1245
+ const COPY = {
1246
+ zh: {
1247
+ title: "云端生图工作台",
1248
+ configured: "API 已配置",
1249
+ unconfigured: "未配置",
1250
+ recent: "最近生成",
1251
+ empty: "暂无生成历史",
1252
+ generate: "文生图",
1253
+ edit: "图生图",
1254
+ details: "图片详情",
1255
+ reference: "参考图",
1256
+ optional: "选填",
1257
+ upload: "点击或拖拽图片到此处",
1258
+ uploadHint: "支持 JPG / PNG / WebP / GIF,最大 10MB(最多 5 张)",
1259
+ prompt: "提示词 Prompt",
1260
+ clear: "清空",
1261
+ promptPlaceholder: "描述主体、构图、风格、光线与需要出现的文字…(支持 Ctrl+Enter 快捷生成)",
1262
+ provider: "Provider",
1263
+ model: "Model",
1264
+ ratio: "比例",
1265
+ quality: "清晰度",
1266
+ start: "开始生成",
1267
+ generating: "正在生成…",
1268
+ cancelGenerate: "取消生成",
1269
+ count: "生成数量",
1270
+ countUnit: "{n} 张",
1271
+ partialSuccess: "已生成 {success} 张图片,{failed} 张失败",
1272
+ generatingCount: "正在生成(共 {count} 张)…",
1273
+ batchResult: "本次生成(共 {count} 张)",
1274
+ saveToGallery: "收藏进画廊",
1275
+ savedToGallery: "已收藏进画廊",
1276
+ inGallery: "已在画廊",
1277
+ retry: "重新加载",
1278
+ configLoadFailed: "工作台配置加载失败,请检查服务后重试。",
1279
+ noProvider: "请先在设置中配置至少一个云端图像 Provider 的 API Key。",
1280
+ selectConfigured: "该 Provider 尚未配置,请先到设置中配置 API Key。",
1281
+ needPrompt: "请输入提示词",
1282
+ needReference: "请先添加至少一张参考图",
1283
+ result: "本次结果",
1284
+ continueEdit: "继续编辑(垫图)",
1285
+ regenerate: "再次生成",
1286
+ copy: "复制",
1287
+ copied: "已复制到剪贴板",
1288
+ copyFailed: "复制失败",
1289
+ download: "下载",
1290
+ remove: "删除",
1291
+ fit: "适应窗口",
1292
+ loading: "正在读取图片…",
1293
+ generationFailed: "生成失败",
1294
+ selectHistory: "从左侧选择一张图片,或在右侧开始新的生成。",
1295
+ created: "生成时间",
1296
+ elapsed: "耗时",
1297
+ dimensions: "尺寸",
1298
+ output: "输出参数",
1299
+ closeReference: "移除参考图",
1300
+ uploadInvalid: "请选择有效的图片文件(最大 10MB)",
1301
+ imageLoadFailed: "图片读取失败",
1302
+ fullscreen: "大图全屏",
1303
+ close: "关闭",
1304
+ copyPpt: "复制 Prompt",
1305
+ copiedPrompt: "已复制 Prompt",
1306
+ copiedImage: "已复制图片",
1307
+ favorite: "收藏",
1308
+ favorited: "已收藏",
1309
+ favoriteAdded: "已添加到收藏",
1310
+ favoriteRemoved: "已取消收藏",
1311
+ loadMore: "加载更多 ({n})",
1312
+ deleteModalTitle: "从图库删除",
1313
+ deleteModalDesc: "确定从图库中删除这张图片吗?(原聊天记录不会受影响)",
1314
+ deleteWorkspaceFilesLabel: "同时删除工作区本地文件",
1315
+ confirm: "确定删除",
1316
+ cancel: "取消",
1317
+ deletedToast: "已从图库删除",
1318
+ referencesCount: "参考图 ({current}/{max})",
1319
+ addMoreRef: "+ 添加",
1320
+ maxReferencesExceeded: "最多支持添加 {max} 张参考图",
1321
+ newGeneration: "新建生成",
1322
+ new: "新建",
1323
+ configuredCount: "API 已配置 · {count}",
1324
+ unconfiguredStatus: "API 未配置",
1325
+ providerStatus: "云端提供商与模型状态",
1326
+ collapseSidebar: "折叠最近生成",
1327
+ expandSidebar: "展开最近生成"
1328
+ },
1329
+ en: {
1330
+ title: "Cloud Image Studio",
1331
+ configured: "API configured",
1332
+ unconfigured: "Not configured",
1333
+ recent: "Recent generations",
1334
+ empty: "No generated images yet",
1335
+ generate: "Text to image",
1336
+ edit: "Image to image",
1337
+ details: "Image details",
1338
+ reference: "Reference image",
1339
+ optional: "optional",
1340
+ upload: "Click or drop images here",
1341
+ uploadHint: "JPG / PNG / WebP / GIF, up to 10MB (max 5)",
1342
+ prompt: "Prompt",
1343
+ clear: "Clear",
1344
+ promptPlaceholder: "Describe the subject, composition, style, lighting, and exact text… (Ctrl+Enter to generate)",
1345
+ provider: "Provider",
1346
+ model: "Model",
1347
+ ratio: "Aspect ratio",
1348
+ quality: "Quality",
1349
+ start: "Generate",
1350
+ generating: "Generating…",
1351
+ cancelGenerate: "Cancel",
1352
+ count: "Number of images",
1353
+ countUnit: "{n}",
1354
+ partialSuccess: "Generated {success} images, {failed} failed",
1355
+ generatingCount: "Generating ({count} images)…",
1356
+ batchResult: "Generated {count} images",
1357
+ saveToGallery: "Save to Gallery",
1358
+ savedToGallery: "Saved to Gallery",
1359
+ inGallery: "In Gallery",
1360
+ retry: "Retry",
1361
+ configLoadFailed: "Failed to load studio configuration.",
1362
+ noProvider: "Configure an API key for at least one cloud image provider in Settings.",
1363
+ selectConfigured: "This provider is not configured. Add its API key in Settings first.",
1364
+ needPrompt: "Enter a prompt",
1365
+ needReference: "Add at least one reference image first",
1366
+ result: "Current result",
1367
+ continueEdit: "Continue editing",
1368
+ regenerate: "Generate again",
1369
+ copy: "Copy",
1370
+ copied: "Copied to clipboard",
1371
+ copyFailed: "Copy failed",
1372
+ download: "Download",
1373
+ remove: "Delete",
1374
+ fit: "Fit",
1375
+ loading: "Loading image…",
1376
+ generationFailed: "Generation failed",
1377
+ selectHistory: "Select an image on the left, or start a new generation on the right.",
1378
+ created: "Created",
1379
+ elapsed: "Elapsed",
1380
+ dimensions: "Dimensions",
1381
+ output: "Output",
1382
+ closeReference: "Remove reference",
1383
+ uploadInvalid: "Choose a valid image file up to 10MB.",
1384
+ imageLoadFailed: "Could not load image",
1385
+ fullscreen: "Fullscreen",
1386
+ close: "Close",
1387
+ copyPpt: "Copy Prompt",
1388
+ copiedPrompt: "Prompt copied",
1389
+ copiedImage: "Image copied",
1390
+ favorite: "Favorite",
1391
+ favorited: "Favorited",
1392
+ favoriteAdded: "Added to favorites",
1393
+ favoriteRemoved: "Removed from favorites",
1394
+ loadMore: "Load more ({n})",
1395
+ deleteModalTitle: "Delete from gallery",
1396
+ deleteModalDesc: "Remove this image from the local gallery? (Chat history remains unaffected)",
1397
+ deleteWorkspaceFilesLabel: "Also delete local workspace files",
1398
+ confirm: "Delete",
1399
+ cancel: "Cancel",
1400
+ deletedToast: "Deleted from gallery",
1401
+ referencesCount: "References ({current}/{max})",
1402
+ addMoreRef: "+ Add",
1403
+ maxReferencesExceeded: "Up to {max} reference images allowed",
1404
+ newGeneration: "New generation",
1405
+ new: "New",
1406
+ configuredCount: "API configured · {count}",
1407
+ unconfiguredStatus: "Not configured",
1408
+ providerStatus: "Provider & Model Status",
1409
+ collapseSidebar: "Collapse sidebar",
1410
+ expandSidebar: "Expand recent list"
1411
+ }
1412
+ };
1413
+ const StudioView = ({ locale, workspace }) => {
1414
+ const [lang, setLang] = (0, react.useState)(() => locale?.getSnapshot?.().active?.startsWith("en") ? "en" : "zh");
1415
+ const [config, setConfig] = (0, react.useState)(null);
1416
+ const [configLoading, setConfigLoading] = (0, react.useState)(true);
1417
+ const [configError, setConfigError] = (0, react.useState)(null);
1418
+ const [items, setItems] = (0, react.useState)([]);
1419
+ const [sidebarCollapsed, setSidebarCollapsed] = (0, react.useState)(false);
1420
+ const [visibleLimit, setVisibleLimit] = (0, react.useState)(30);
1421
+ const [selected, setSelected] = (0, react.useState)(null);
1422
+ const [mode, setMode] = (0, react.useState)("generate");
1423
+ const [panelTab, setPanelTab] = (0, react.useState)("generate");
1424
+ const [provider, setProvider] = (0, react.useState)("google");
1425
+ const [model, setModel] = (0, react.useState)("");
1426
+ const [ratio, setRatio] = (0, react.useState)("1:1");
1427
+ const [quality, setQuality] = (0, react.useState)("1K");
1428
+ const [prompt, setPrompt] = (0, react.useState)("");
1429
+ const [references, setReferences] = (0, react.useState)([]);
1430
+ const [count, setCount] = (0, react.useState)(1);
1431
+ const [currentBatch, setCurrentBatch] = (0, react.useState)(null);
1432
+ const [isGenerating, setIsGenerating] = (0, react.useState)(false);
1433
+ const [error, setError] = (0, react.useState)(null);
1434
+ const [notice, setNotice] = (0, react.useState)(null);
1435
+ const [zoom, setZoom] = (0, react.useState)(100);
1436
+ const [fit, setFit] = (0, react.useState)(true);
1437
+ const [offset, setOffset] = (0, react.useState)({
1438
+ x: 0,
1439
+ y: 0
1440
+ });
1441
+ const [isDragging, setIsDragging] = (0, react.useState)(false);
1442
+ const dragStartRef = (0, react.useRef)(null);
1443
+ const hasDraggedRef = (0, react.useRef)(false);
1444
+ const [dragging, setDragging] = (0, react.useState)(false);
1445
+ const [lightboxOpen, setLightboxOpen] = (0, react.useState)(false);
1446
+ const [showDeleteModal, setShowDeleteModal] = (0, react.useState)(false);
1447
+ const [deleteWorkspaceFiles, setDeleteWorkspaceFiles] = (0, react.useState)(true);
1448
+ const requestRef = (0, react.useRef)(null);
1449
+ const canvasRef = (0, react.useRef)(null);
1450
+ const fileInputRef = (0, react.useRef)(null);
1451
+ const image = useAttachmentImage(selected?.attachment);
1452
+ const maxReferences = provider === "dashscope" ? 3 : 5;
1453
+ const referencesRef = (0, react.useRef)(references);
1454
+ referencesRef.current = references;
1455
+ const t = (key, values) => {
1456
+ let text = COPY[lang][key];
1457
+ for (const [name, value] of Object.entries(values ?? {})) text = text.replace(`{${name}}`, value);
1458
+ return text;
1459
+ };
1460
+ (0, react.useEffect)(() => locale?.subscribe?.(() => setLang(locale.getSnapshot().active?.startsWith("en") ? "en" : "zh")), [locale]);
1461
+ const loadConfig = (0, react.useCallback)(async () => {
1462
+ setConfigLoading(true);
1463
+ setConfigError(null);
1464
+ const controller = new AbortController();
1465
+ try {
1466
+ const response = await fetch(STUDIO_ROUTE, {
1467
+ signal: controller.signal,
1468
+ credentials: "same-origin"
1469
+ });
1470
+ const payload = await response.json();
1471
+ if (!response.ok || !("providers" in payload)) throw new Error("error" in payload && payload.error ? payload.error : "Studio unavailable");
1472
+ setConfig(payload);
1473
+ const initial = payload.providers.find((item) => item.provider === payload.activeProvider) ?? payload.providers[0];
1474
+ if (initial !== void 0) applyProvider(initial);
1475
+ } catch (fetchError) {
1476
+ if (!controller.signal.aborted) setConfigError(messageOf(fetchError));
1477
+ } finally {
1478
+ setConfigLoading(false);
1479
+ }
1480
+ }, []);
1481
+ (0, react.useEffect)(() => {
1482
+ loadConfig();
1483
+ }, [loadConfig]);
1484
+ (0, react.useEffect)(() => {
1485
+ let mounted = true;
1486
+ const load = () => void getGalleryItems().then((next) => {
1487
+ if (!mounted) return;
1488
+ setItems(next);
1489
+ setSelected((current) => current === null ? null : next.find((item) => item.id === current.id) ?? null);
1490
+ });
1491
+ load();
1492
+ const unsubscribe = subscribeGallery(load);
1493
+ return () => {
1494
+ mounted = false;
1495
+ unsubscribe();
1496
+ };
1497
+ }, []);
1498
+ (0, react.useEffect)(() => {
1499
+ return () => {
1500
+ requestRef.current?.abort();
1501
+ };
1502
+ }, []);
1503
+ (0, react.useEffect)(() => {
1504
+ return () => {
1505
+ for (const item of referencesRef.current) if (item.previewUrl.startsWith("blob:")) URL.revokeObjectURL(item.previewUrl);
1506
+ };
1507
+ }, []);
1508
+ (0, react.useEffect)(() => {
1509
+ const handleKeyDown = (e) => {
1510
+ if (e.key === "Escape" && lightboxOpen) setLightboxOpen(false);
1511
+ };
1512
+ window.addEventListener("keydown", handleKeyDown);
1513
+ return () => window.removeEventListener("keydown", handleKeyDown);
1514
+ }, [lightboxOpen]);
1515
+ (0, react.useEffect)(() => {
1516
+ const canvasEl = canvasRef.current;
1517
+ if (!canvasEl) return;
1518
+ const onWheel = (e) => {
1519
+ if (selected === null && (!currentBatch || currentBatch.length === 0)) return;
1520
+ e.preventDefault();
1521
+ const delta = e.deltaY < 0 ? 15 : -15;
1522
+ setFit(false);
1523
+ setZoom((prev) => Math.min(500, Math.max(25, prev + delta)));
1524
+ };
1525
+ canvasEl.addEventListener("wheel", onWheel, { passive: false });
1526
+ return () => canvasEl.removeEventListener("wheel", onWheel);
1527
+ }, [selected, currentBatch]);
1528
+ (0, react.useEffect)(() => {
1529
+ const handleMouseMove = (e) => {
1530
+ if (!dragStartRef.current) return;
1531
+ const dx = e.clientX - dragStartRef.current.x;
1532
+ const dy = e.clientY - dragStartRef.current.y;
1533
+ if (Math.abs(dx) > 3 || Math.abs(dy) > 3) {
1534
+ hasDraggedRef.current = true;
1535
+ setFit(false);
1536
+ }
1537
+ setOffset({
1538
+ x: dragStartRef.current.startX + dx,
1539
+ y: dragStartRef.current.startY + dy
1540
+ });
1541
+ };
1542
+ const handleMouseUp = () => {
1543
+ dragStartRef.current = null;
1544
+ setIsDragging(false);
1545
+ };
1546
+ window.addEventListener("mousemove", handleMouseMove);
1547
+ window.addEventListener("mouseup", handleMouseUp);
1548
+ return () => {
1549
+ window.removeEventListener("mousemove", handleMouseMove);
1550
+ window.removeEventListener("mouseup", handleMouseUp);
1551
+ };
1552
+ }, []);
1553
+ const activeProfile = (0, react.useMemo)(() => config?.providers.find((item) => item.provider === provider), [config, provider]);
1554
+ const configuredCount = config?.providers.filter((item) => item.configured).length ?? 0;
1555
+ const displayItems = (0, react.useMemo)(() => items.slice(0, visibleLimit), [items, visibleLimit]);
1556
+ const applyProvider = (profile) => {
1557
+ setProvider(profile.provider);
1558
+ setModel(profile.model);
1559
+ setRatio(profile.defaultRatio);
1560
+ setQuality(profile.defaultQuality);
1561
+ setError(null);
1562
+ };
1563
+ const changeProvider = (value) => {
1564
+ const profile = config?.providers.find((item) => item.provider === value);
1565
+ if (profile !== void 0) {
1566
+ applyProvider(profile);
1567
+ const providerMax = value === "dashscope" ? 3 : 5;
1568
+ if (referencesRef.current.length > providerMax) {
1569
+ const keep = referencesRef.current.slice(0, providerMax);
1570
+ const overflow = referencesRef.current.slice(providerMax);
1571
+ for (const item of overflow) if (item.previewUrl.startsWith("blob:")) URL.revokeObjectURL(item.previewUrl);
1572
+ setReferences(keep);
1573
+ referencesRef.current = keep;
1574
+ setError(t("maxReferencesExceeded", { max: String(providerMax) }));
1575
+ }
1576
+ }
1577
+ };
1578
+ const resetFit = () => {
1579
+ setFit(true);
1580
+ setZoom(100);
1581
+ setOffset({
1582
+ x: 0,
1583
+ y: 0
1584
+ });
1585
+ };
1586
+ const selectItem = (item) => {
1587
+ setCurrentBatch(null);
1588
+ setSelected(item);
1589
+ setPanelTab("details");
1590
+ resetFit();
1591
+ };
1592
+ const startNew = () => {
1593
+ setCurrentBatch(null);
1594
+ setSelected(null);
1595
+ setPanelTab("generate");
1596
+ resetFit();
1597
+ };
1598
+ const handleCanvasMouseDown = (e) => {
1599
+ if (e.button !== 0 && e.button !== 1) return;
1600
+ if (selected === null && (!currentBatch || currentBatch.length === 0)) return;
1601
+ setIsDragging(true);
1602
+ hasDraggedRef.current = false;
1603
+ dragStartRef.current = {
1604
+ x: e.clientX,
1605
+ y: e.clientY,
1606
+ startX: offset.x,
1607
+ startY: offset.y
1608
+ };
1609
+ };
1610
+ const handleImageClick = () => {};
1611
+ const addReferenceFiles = (fileList) => {
1612
+ if (!fileList || fileList.length === 0) return;
1613
+ const files = Array.from(fileList);
1614
+ const validFiles = [];
1615
+ for (const file of files) {
1616
+ if (![
1617
+ "image/jpeg",
1618
+ "image/png",
1619
+ "image/webp",
1620
+ "image/gif"
1621
+ ].includes(file.type) || file.size > 10485760) {
1622
+ setError(t("uploadInvalid"));
1623
+ continue;
1624
+ }
1625
+ validFiles.push(file);
1626
+ }
1627
+ if (validFiles.length === 0) return;
1628
+ const currentLen = referencesRef.current.length;
1629
+ const available = maxReferences - currentLen;
1630
+ if (available <= 0) {
1631
+ setError(t("maxReferencesExceeded", { max: String(maxReferences) }));
1632
+ return;
1633
+ }
1634
+ const toAdd = validFiles.slice(0, available);
1635
+ if (validFiles.length > available) setError(t("maxReferencesExceeded", { max: String(maxReferences) }));
1636
+ else setError(null);
1637
+ const newItems = toAdd.map((file, idx) => ({
1638
+ id: `upload-${Date.now()}-${idx}-${Math.random().toString(36).slice(2, 7)}`,
1639
+ file,
1640
+ previewUrl: URL.createObjectURL(file)
1641
+ }));
1642
+ setReferences((prev) => [...prev, ...newItems]);
1643
+ };
1644
+ const removeReference = (id) => {
1645
+ const target = referencesRef.current.find((item) => item.id === id);
1646
+ if (target && target.previewUrl.startsWith("blob:")) URL.revokeObjectURL(target.previewUrl);
1647
+ setReferences((prev) => prev.filter((item) => item.id !== id));
1648
+ };
1649
+ const clearAllReferences = () => {
1650
+ for (const item of referencesRef.current) if (item.previewUrl.startsWith("blob:")) URL.revokeObjectURL(item.previewUrl);
1651
+ setReferences([]);
1652
+ };
1653
+ const continueEdit = async () => {
1654
+ if (selected === null) return;
1655
+ const targetItem = selected;
1656
+ const targetAttId = targetItem.attachment.attachmentId;
1657
+ const targetMax = targetItem.provider === "dashscope" ? 3 : 5;
1658
+ if (referencesRef.current.some((r) => r.attachment?.attachmentId === targetAttId)) {
1659
+ setMode("edit");
1660
+ setPanelTab("generate");
1661
+ changeProvider(targetItem.provider);
1662
+ return;
1663
+ }
1664
+ if (referencesRef.current.length >= targetMax) {
1665
+ setError(t("maxReferencesExceeded", { max: String(targetMax) }));
1666
+ return;
1667
+ }
1668
+ try {
1669
+ const blob = image.blob ?? await fetchAttachmentBlob(targetItem.attachment);
1670
+ if (referencesRef.current.some((r) => r.attachment?.attachmentId === targetAttId)) {
1671
+ setMode("edit");
1672
+ setPanelTab("generate");
1673
+ changeProvider(targetItem.provider);
1674
+ return;
1675
+ }
1676
+ if (referencesRef.current.length >= targetMax) {
1677
+ setError(t("maxReferencesExceeded", { max: String(targetMax) }));
1678
+ return;
1679
+ }
1680
+ const independentUrl = URL.createObjectURL(blob);
1681
+ const newItem = {
1682
+ id: `item-${targetItem.id}-${Date.now()}`,
1683
+ attachment: targetItem.attachment,
1684
+ previewUrl: independentUrl
1685
+ };
1686
+ const nextList = [...referencesRef.current, newItem];
1687
+ setReferences(nextList);
1688
+ referencesRef.current = nextList;
1689
+ setError(null);
1690
+ setMode("edit");
1691
+ setPanelTab("generate");
1692
+ changeProvider(targetItem.provider);
1693
+ } catch {
1694
+ setError(t("imageLoadFailed"));
1695
+ }
1696
+ };
1697
+ const cancelGeneration = () => {
1698
+ if (requestRef.current) {
1699
+ requestRef.current.abort();
1700
+ requestRef.current = null;
1701
+ setIsGenerating(false);
1702
+ }
1703
+ };
1704
+ const submit = async () => {
1705
+ if (isGenerating || activeProfile === void 0) return;
1706
+ if (prompt.trim().length === 0) return setError(t("needPrompt"));
1707
+ if (!activeProfile.configured) return setError(t("selectConfigured"));
1708
+ if (mode === "edit" && references.length === 0) return setError(t("needReference"));
1709
+ setIsGenerating(true);
1710
+ setError(null);
1711
+ const controller = new AbortController();
1712
+ requestRef.current = controller;
1713
+ try {
1714
+ const referencesPayload = mode === "edit" && references.length > 0 ? await Promise.all(references.map(async (ref) => {
1715
+ if (ref.attachment !== void 0) return { attachment: ref.attachment };
1716
+ if (ref.file !== void 0) return {
1717
+ mediaType: ref.file.type,
1718
+ data: await fileToBase64(ref.file),
1719
+ ...ref.file.name ? { name: ref.file.name } : {}
1720
+ };
1721
+ throw new Error("无效参考图");
1722
+ })) : void 0;
1723
+ const response = await fetch(STUDIO_ROUTE, {
1724
+ method: "POST",
1725
+ credentials: "same-origin",
1726
+ signal: controller.signal,
1727
+ headers: { "content-type": "application/json" },
1728
+ body: JSON.stringify({
1729
+ mode,
1730
+ provider,
1731
+ model,
1732
+ prompt: prompt.trim(),
1733
+ ratio,
1734
+ quality,
1735
+ ...count > 1 ? { count } : {},
1736
+ ...workspace?.path ? { workspaceRoot: workspace.path } : {},
1737
+ ...referencesPayload === void 0 ? {} : { references: referencesPayload }
1738
+ })
1739
+ });
1740
+ const payload = await response.json();
1741
+ if (!response.ok || !("attachment" in payload)) throw new Error("error" in payload && payload.error ? payload.error : t("generationFailed"));
1742
+ const generatedList = Array.isArray(payload.items) && payload.items.length > 0 ? payload.items : [{
1743
+ attachment: payload.attachment,
1744
+ output: payload.output,
1745
+ ...payload.savedTo ? { savedTo: payload.savedTo } : {}
1746
+ }];
1747
+ const galleryEntries = generatedList.map((gen, idx) => ({
1748
+ id: String(gen.attachment.attachmentId || `${payload.createdAt}-${idx}`),
1749
+ attachment: gen.attachment,
1750
+ prompt: payload.prompt,
1751
+ provider: payload.provider,
1752
+ model: payload.model,
1753
+ createdAt: payload.createdAt + idx,
1754
+ aspectRatio: ratio,
1755
+ imageSize: quality,
1756
+ output: gen.output,
1757
+ ...gen.savedTo ? { savedTo: gen.savedTo } : {},
1758
+ ...workspace?.path ? { workspacePath: workspace.path } : {},
1759
+ ...workspace?.workspaceId ? { workspaceId: workspace.workspaceId } : {}
1760
+ }));
1761
+ if (galleryEntries.length > 1) {
1762
+ setCurrentBatch(galleryEntries);
1763
+ setSelected(galleryEntries[0]);
1764
+ } else if (galleryEntries.length === 1) {
1765
+ setCurrentBatch(null);
1766
+ setSelected(galleryEntries[0]);
1767
+ }
1768
+ setPanelTab("details");
1769
+ resetFit();
1770
+ if (payload.failedCount && payload.failedCount > 0) flash(t("partialSuccess", {
1771
+ success: String(generatedList.length),
1772
+ failed: String(payload.failedCount)
1773
+ }));
1774
+ } catch (submitError) {
1775
+ if (controller.signal.aborted) return;
1776
+ setError(messageOf(submitError));
1777
+ } finally {
1778
+ if (requestRef.current === controller) {
1779
+ requestRef.current = null;
1780
+ setIsGenerating(false);
1781
+ }
1782
+ }
1783
+ };
1784
+ const isSelectedInGallery = (0, react.useMemo)(() => {
1785
+ if (selected === null) return false;
1786
+ return items.some((item) => item.id === selected.id);
1787
+ }, [items, selected]);
1788
+ const handleSaveToGallery = async () => {
1789
+ if (selected === null || isSelectedInGallery) return;
1790
+ let savedTo = selected.savedTo;
1791
+ const targetRoot = workspace?.path || config?.workspaceRoot;
1792
+ if (!savedTo) try {
1793
+ const res = await fetch(SAVE_WORKSPACE_ROUTE, {
1794
+ method: "POST",
1795
+ credentials: "same-origin",
1796
+ headers: { "content-type": "application/json" },
1797
+ body: JSON.stringify({
1798
+ attachment: selected.attachment,
1799
+ ...targetRoot ? { workspaceRoot: targetRoot } : {}
1800
+ })
1801
+ });
1802
+ if (res.ok) {
1803
+ const data = await res.json();
1804
+ if (data.savedTo) savedTo = data.savedTo;
1805
+ }
1806
+ } catch (e) {
1807
+ console.warn("Failed to save to workspace:", e);
1808
+ }
1809
+ const updatedItem = {
1810
+ ...selected,
1811
+ ...savedTo ? { savedTo } : {},
1812
+ ...targetRoot ? { workspacePath: targetRoot } : {},
1813
+ ...workspace?.workspaceId ? { workspaceId: workspace.workspaceId } : {}
1814
+ };
1815
+ await saveGalleryItem(updatedItem);
1816
+ setSelected(updatedItem);
1817
+ if (currentBatch) setCurrentBatch((prev) => prev ? prev.map((i) => i.id === updatedItem.id ? updatedItem : i) : prev);
1818
+ flash(t("savedToGallery"));
1819
+ };
1820
+ const handleConfirmDelete = async () => {
1821
+ if (selected === null) return;
1822
+ const idToDelete = selected.id;
1823
+ const attachmentId = selected.attachment.attachmentId;
1824
+ const savedPath = selected.savedTo;
1825
+ setShowDeleteModal(false);
1826
+ try {
1827
+ if (deleteWorkspaceFiles && typeof savedPath === "string" && savedPath.trim().length > 0) {
1828
+ const res = await fetch(DELETE_ROUTE, {
1829
+ method: "POST",
1830
+ credentials: "same-origin",
1831
+ headers: { "content-type": "application/json" },
1832
+ body: JSON.stringify({ paths: [savedPath] })
1833
+ });
1834
+ if (!res.ok) throw new Error(`删除请求网络异常 (${res.status})`);
1835
+ const data = await res.json().catch(() => null);
1836
+ if (!data || data.ok === false || Array.isArray(data.failedFiles) && data.failedFiles.length > 0) {
1837
+ const reason = data?.failedFiles?.[0]?.error || "工作区文件删除失败";
1838
+ throw new Error(reason);
1839
+ }
1840
+ }
1841
+ if (isSelectedInGallery) await deleteGalleryItem(idToDelete);
1842
+ evictAttachmentCache(attachmentId);
1843
+ if (currentBatch) {
1844
+ const remaining = currentBatch.filter((i) => i.id !== idToDelete);
1845
+ if (remaining.length > 1) {
1846
+ setCurrentBatch(remaining);
1847
+ setSelected(remaining[0]);
1848
+ } else if (remaining.length === 1) {
1849
+ setCurrentBatch(null);
1850
+ setSelected(remaining[0]);
1851
+ } else {
1852
+ setCurrentBatch(null);
1853
+ setSelected(null);
1854
+ }
1855
+ } else setSelected(null);
1856
+ flash(t("deletedToast"));
1857
+ } catch (delError) {
1858
+ setError(messageOf(delError));
1859
+ }
1860
+ };
1861
+ const copySelected = async () => {
1862
+ if (image.blob === null) return;
1863
+ const ok = await copyImageBlob(image.blob);
1864
+ flash(ok ? t("copiedImage") : t("copyFailed"));
1865
+ };
1866
+ const downloadSelected = () => {
1867
+ if (image.url === null || selected === null) return;
1868
+ downloadBlobUrl(image.url, `dsh-${selected.provider}-${selected.id.slice(-8)}.${extension(selected.attachment.mediaType)}`);
1869
+ };
1870
+ const toggleFavorite = async () => {
1871
+ if (selected === null) return;
1872
+ const targetId = selected.id;
1873
+ if (isSelectedInGallery) {
1874
+ const nextFav = await toggleFavoriteGalleryItem(targetId);
1875
+ setSelected((curr) => curr !== null && curr.id === targetId ? {
1876
+ ...curr,
1877
+ isFavorite: nextFav
1878
+ } : curr);
1879
+ if (currentBatch) setCurrentBatch((prev) => prev ? prev.map((i) => i.id === targetId ? {
1880
+ ...i,
1881
+ isFavorite: nextFav
1882
+ } : i) : null);
1883
+ flash(nextFav ? t("favoriteAdded") : t("favoriteRemoved"));
1884
+ } else {
1885
+ const nextFav = !selected.isFavorite;
1886
+ setSelected((curr) => curr !== null && curr.id === targetId ? {
1887
+ ...curr,
1888
+ isFavorite: nextFav
1889
+ } : curr);
1890
+ if (currentBatch) setCurrentBatch((prev) => prev ? prev.map((i) => i.id === targetId ? {
1891
+ ...i,
1892
+ isFavorite: nextFav
1893
+ } : i) : null);
1894
+ flash(nextFav ? t("favoriteAdded") : t("favoriteRemoved"));
1895
+ }
1896
+ };
1897
+ const stepLightbox = (delta) => {
1898
+ if (!currentBatch || currentBatch.length <= 1 || selected === null) return;
1899
+ const currIdx = currentBatch.findIndex((i) => i.id === selected.id);
1900
+ if (currIdx === -1) return;
1901
+ const nextIdx = (currIdx + delta + currentBatch.length) % currentBatch.length;
1902
+ setSelected(currentBatch[nextIdx]);
1903
+ };
1904
+ (0, react.useEffect)(() => {
1905
+ if (!lightboxOpen || !currentBatch || currentBatch.length <= 1) return;
1906
+ const onKeyDown = (e) => {
1907
+ if (e.key === "ArrowLeft") stepLightbox(-1);
1908
+ else if (e.key === "ArrowRight") stepLightbox(1);
1909
+ };
1910
+ window.addEventListener("keydown", onKeyDown);
1911
+ return () => window.removeEventListener("keydown", onKeyDown);
1912
+ }, [
1913
+ lightboxOpen,
1914
+ currentBatch,
1915
+ selected
1916
+ ]);
1917
+ const flash = (message) => {
1918
+ setNotice(message);
1919
+ window.setTimeout(() => setNotice(null), 2200);
1920
+ };
1921
+ return (0, react_jsx_runtime.jsxs)("section", {
1922
+ className: "dsh-ig-workbench",
1923
+ "aria-label": t("title"),
1924
+ children: [
1925
+ (0, react_jsx_runtime.jsxs)("div", {
1926
+ className: `dsh-ig-workbench-grid ${sidebarCollapsed ? "is-sidebar-collapsed" : ""}`,
1927
+ children: [
1928
+ !sidebarCollapsed && (0, react_jsx_runtime.jsxs)("aside", {
1929
+ className: "dsh-ig-recent-panel",
1930
+ children: [(0, react_jsx_runtime.jsxs)("div", {
1931
+ className: "dsh-ig-panel-title",
1932
+ children: [(0, react_jsx_runtime.jsxs)("div", {
1933
+ style: {
1934
+ display: "flex",
1935
+ alignItems: "center",
1936
+ gap: "8px"
1937
+ },
1938
+ children: [(0, react_jsx_runtime.jsx)("span", { children: t("recent") }), (0, react_jsx_runtime.jsx)("span", {
1939
+ className: "dsh-ig-count-badge",
1940
+ children: items.length
1941
+ })]
1942
+ }), (0, react_jsx_runtime.jsx)("button", {
1943
+ type: "button",
1944
+ className: "dsh-ig-collapse-btn",
1945
+ onClick: () => setSidebarCollapsed(true),
1946
+ title: t("collapseSidebar"),
1947
+ children: (0, react_jsx_runtime.jsx)(PanelLeftClose, { size: 15 })
1948
+ })]
1949
+ }), (0, react_jsx_runtime.jsxs)("div", {
1950
+ className: "dsh-ig-recent-scroll",
1951
+ children: [items.length === 0 ? (0, react_jsx_runtime.jsxs)("div", {
1952
+ className: "dsh-ig-recent-empty",
1953
+ children: [(0, react_jsx_runtime.jsx)(ImagePlus, { size: 22 }), (0, react_jsx_runtime.jsx)("span", { children: t("empty") })]
1954
+ }) : displayItems.map((item) => (0, react_jsx_runtime.jsx)(RecentItem, {
1955
+ item,
1956
+ active: selected?.id === item.id,
1957
+ lang,
1958
+ onClick: () => selectItem(item)
1959
+ }, item.id)), items.length > visibleLimit && (0, react_jsx_runtime.jsx)("button", {
1960
+ type: "button",
1961
+ className: "dsh-ig-load-more",
1962
+ onClick: () => setVisibleLimit((l) => l + 30),
1963
+ children: t("loadMore", { n: String(items.length - visibleLimit) })
1964
+ })]
1965
+ })]
1966
+ }),
1967
+ (0, react_jsx_runtime.jsxs)("main", {
1968
+ className: "dsh-ig-canvas-column",
1969
+ children: [
1970
+ (0, react_jsx_runtime.jsxs)("div", {
1971
+ className: "dsh-ig-canvas-toolbar",
1972
+ children: [(0, react_jsx_runtime.jsxs)("div", {
1973
+ className: "dsh-ig-canvas-toolbar-left",
1974
+ children: [sidebarCollapsed && (0, react_jsx_runtime.jsxs)("button", {
1975
+ type: "button",
1976
+ onClick: () => setSidebarCollapsed(false),
1977
+ title: t("expandSidebar"),
1978
+ children: [(0, react_jsx_runtime.jsx)(PanelLeft, { size: 14 }), (0, react_jsx_runtime.jsx)("span", { children: t("recent") })]
1979
+ }), (0, react_jsx_runtime.jsxs)("div", {
1980
+ className: "dsh-ig-status-badge-wrap",
1981
+ children: [(0, react_jsx_runtime.jsxs)("div", {
1982
+ className: "dsh-ig-status-badge",
1983
+ children: [
1984
+ (0, react_jsx_runtime.jsx)("span", { className: `dsh-ig-status-dot ${configuredCount > 0 ? "is-ready" : "is-muted"}` }),
1985
+ (0, react_jsx_runtime.jsx)("span", {
1986
+ className: "dsh-ig-status-text",
1987
+ children: configuredCount > 0 ? t("configuredCount", { count: String(configuredCount) }) : t("unconfiguredStatus")
1988
+ }),
1989
+ (0, react_jsx_runtime.jsx)(ChevronDown, {
1990
+ size: 11,
1991
+ className: "dsh-ig-status-arrow"
1992
+ })
1993
+ ]
1994
+ }), config !== null && (0, react_jsx_runtime.jsxs)("div", {
1995
+ className: "dsh-ig-status-popover",
1996
+ children: [(0, react_jsx_runtime.jsx)("div", {
1997
+ className: "dsh-ig-status-popover-title",
1998
+ children: t("providerStatus")
1999
+ }), (0, react_jsx_runtime.jsx)("ul", {
2000
+ className: "dsh-ig-status-list",
2001
+ children: config.providers.map((p) => (0, react_jsx_runtime.jsxs)("li", {
2002
+ className: p.configured ? "is-configured" : "is-missing",
2003
+ children: [(0, react_jsx_runtime.jsxs)("div", {
2004
+ className: "dsh-ig-status-item-name",
2005
+ children: [(0, react_jsx_runtime.jsx)("strong", { children: p.label }), (0, react_jsx_runtime.jsx)("small", { children: p.model })]
2006
+ }), (0, react_jsx_runtime.jsx)("span", {
2007
+ className: "dsh-ig-status-item-tag",
2008
+ children: p.configured ? t("configured") : t("unconfigured")
2009
+ })]
2010
+ }, p.provider))
2011
+ })]
2012
+ })]
2013
+ })]
2014
+ }), (0, react_jsx_runtime.jsxs)("div", {
2015
+ className: "dsh-ig-canvas-toolbar-right",
2016
+ children: [
2017
+ selected !== null && (0, react_jsx_runtime.jsxs)("button", {
2018
+ type: "button",
2019
+ onClick: startNew,
2020
+ title: t("newGeneration"),
2021
+ children: [(0, react_jsx_runtime.jsx)(Plus, { size: 13 }), (0, react_jsx_runtime.jsx)("span", { children: t("newGeneration") })]
2022
+ }),
2023
+ (0, react_jsx_runtime.jsxs)("button", {
2024
+ type: "button",
2025
+ className: fit ? "is-active" : "",
2026
+ onClick: resetFit,
2027
+ children: [
2028
+ t("fit"),
2029
+ " ",
2030
+ (0, react_jsx_runtime.jsx)(ChevronDown, { size: 13 })
2031
+ ]
2032
+ }),
2033
+ (0, react_jsx_runtime.jsx)("button", {
2034
+ type: "button",
2035
+ onClick: () => {
2036
+ setFit(false);
2037
+ setZoom((value) => Math.max(25, value - 25));
2038
+ },
2039
+ title: "Zoom out",
2040
+ children: (0, react_jsx_runtime.jsx)(ZoomOut, { size: 16 })
2041
+ }),
2042
+ (0, react_jsx_runtime.jsx)("span", {
2043
+ className: "dsh-ig-zoom-display",
2044
+ children: fit ? "AUTO" : `${Math.round(zoom)}%`
2045
+ }),
2046
+ (0, react_jsx_runtime.jsx)("button", {
2047
+ type: "button",
2048
+ onClick: () => {
2049
+ setFit(false);
2050
+ setZoom((value) => Math.min(500, value + 25));
2051
+ },
2052
+ title: "Zoom in",
2053
+ children: (0, react_jsx_runtime.jsx)(ZoomIn, { size: 16 })
2054
+ }),
2055
+ (0, react_jsx_runtime.jsx)("button", {
2056
+ type: "button",
2057
+ onClick: () => setLightboxOpen(true),
2058
+ title: t("fullscreen"),
2059
+ disabled: selected === null,
2060
+ children: (0, react_jsx_runtime.jsx)(Expand, { size: 16 })
2061
+ })
2062
+ ]
2063
+ })]
2064
+ }),
2065
+ (0, react_jsx_runtime.jsx)("div", {
2066
+ className: `dsh-ig-canvas ${isDragging ? "is-dragging" : ""}`,
2067
+ ref: canvasRef,
2068
+ onMouseDown: handleCanvasMouseDown,
2069
+ onDoubleClick: resetFit,
2070
+ children: isGenerating ? (0, react_jsx_runtime.jsxs)("div", {
2071
+ className: "dsh-ig-generating-state",
2072
+ children: [
2073
+ (0, react_jsx_runtime.jsx)("div", {
2074
+ className: "dsh-ig-generation-orbit",
2075
+ children: (0, react_jsx_runtime.jsx)(Sparkles, { size: 28 })
2076
+ }),
2077
+ (0, react_jsx_runtime.jsx)("strong", { children: count > 1 ? t("generatingCount", { count: String(count) }) : t("generating") }),
2078
+ (0, react_jsx_runtime.jsxs)("span", { children: [
2079
+ activeProfile?.label,
2080
+ " · ",
2081
+ model
2082
+ ] })
2083
+ ]
2084
+ }) : currentBatch && currentBatch.length > 1 ? (0, react_jsx_runtime.jsx)("div", {
2085
+ className: "dsh-ig-canvas-cluster",
2086
+ "data-count": currentBatch.length,
2087
+ style: {
2088
+ transform: `translate(${offset.x}px, ${offset.y}px) scale(${zoom / 100})`,
2089
+ transformOrigin: "center center",
2090
+ transition: isDragging ? "none" : "transform 0.08s ease-out"
2091
+ },
2092
+ children: currentBatch.map((batchItem, index) => (0, react_jsx_runtime.jsx)(BatchCanvasItem, {
2093
+ item: batchItem,
2094
+ index,
2095
+ isSelected: selected?.id === batchItem.id,
2096
+ onSelect: () => {
2097
+ if (!hasDraggedRef.current) setSelected(batchItem);
2098
+ }
2099
+ }, batchItem.id))
2100
+ }) : selected === null ? (0, react_jsx_runtime.jsxs)("div", {
2101
+ className: "dsh-ig-canvas-empty",
2102
+ children: [(0, react_jsx_runtime.jsx)(ImagePlus, { size: 36 }), (0, react_jsx_runtime.jsx)("span", { children: t("selectHistory") })]
2103
+ }) : image.loading ? (0, react_jsx_runtime.jsxs)("div", {
2104
+ className: "dsh-ig-canvas-empty",
2105
+ children: [(0, react_jsx_runtime.jsx)(LoaderCircle, {
2106
+ className: "dsh-ig-spin",
2107
+ size: 28
2108
+ }), (0, react_jsx_runtime.jsx)("span", { children: t("loading") })]
2109
+ }) : image.url !== null ? (0, react_jsx_runtime.jsx)("img", {
2110
+ src: image.url,
2111
+ alt: selected.prompt,
2112
+ draggable: false,
2113
+ onClick: handleImageClick,
2114
+ style: {
2115
+ transform: `translate(${offset.x}px, ${offset.y}px) scale(${zoom / 100})`,
2116
+ transformOrigin: "center center",
2117
+ transition: isDragging ? "none" : "transform 0.08s ease-out"
2118
+ }
2119
+ }) : (0, react_jsx_runtime.jsxs)("div", {
2120
+ className: "dsh-ig-canvas-empty",
2121
+ children: [(0, react_jsx_runtime.jsx)(ImagePlus, { size: 32 }), (0, react_jsx_runtime.jsx)("span", { children: t("imageLoadFailed") })]
2122
+ })
2123
+ }),
2124
+ selected !== null && (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2125
+ (0, react_jsx_runtime.jsxs)("div", {
2126
+ className: "dsh-ig-result-strip",
2127
+ children: [(0, react_jsx_runtime.jsx)("span", { children: t("result") }), (0, react_jsx_runtime.jsxs)("div", { children: [(0, react_jsx_runtime.jsxs)("button", {
2128
+ type: "button",
2129
+ onClick: () => void continueEdit(),
2130
+ children: [(0, react_jsx_runtime.jsx)(PencilLine, { size: 15 }), t("continueEdit")]
2131
+ }), (0, react_jsx_runtime.jsxs)("button", {
2132
+ type: "button",
2133
+ onClick: () => {
2134
+ setMode("generate");
2135
+ setPanelTab("generate");
2136
+ setPrompt(selected.prompt);
2137
+ },
2138
+ children: [(0, react_jsx_runtime.jsx)(RefreshCw, { size: 15 }), t("regenerate")]
2139
+ })] })]
2140
+ }),
2141
+ (0, react_jsx_runtime.jsxs)("div", {
2142
+ className: "dsh-ig-result-actions",
2143
+ children: [(0, react_jsx_runtime.jsx)("p", { children: selected.prompt }), (0, react_jsx_runtime.jsxs)("div", { children: [
2144
+ (0, react_jsx_runtime.jsxs)("button", {
2145
+ type: "button",
2146
+ className: `dsh-ig-save-gallery-btn ${isSelectedInGallery ? "is-saved" : ""}`,
2147
+ onClick: () => void handleSaveToGallery(),
2148
+ title: isSelectedInGallery ? t("inGallery") : t("saveToGallery"),
2149
+ disabled: isSelectedInGallery,
2150
+ children: [isSelectedInGallery ? (0, react_jsx_runtime.jsx)(Check, { size: 15 }) : (0, react_jsx_runtime.jsx)(BookmarkPlus, { size: 15 }), (0, react_jsx_runtime.jsx)("span", { children: isSelectedInGallery ? t("inGallery") : t("saveToGallery") })]
2151
+ }),
2152
+ (0, react_jsx_runtime.jsxs)("button", {
2153
+ type: "button",
2154
+ onClick: () => void toggleFavorite(),
2155
+ title: selected.isFavorite ? t("favorited") : t("favorite"),
2156
+ children: [(0, react_jsx_runtime.jsx)(Heart, {
2157
+ size: 15,
2158
+ fill: selected.isFavorite ? "#ef4444" : "none",
2159
+ color: selected.isFavorite ? "#ef4444" : "currentColor"
2160
+ }), (0, react_jsx_runtime.jsx)("span", { children: selected.isFavorite ? t("favorited") : t("favorite") })]
2161
+ }),
2162
+ (0, react_jsx_runtime.jsxs)("button", {
2163
+ type: "button",
2164
+ onClick: () => void copySelected(),
2165
+ children: [(0, react_jsx_runtime.jsx)(Clipboard, { size: 15 }), (0, react_jsx_runtime.jsx)("span", { children: t("copy") })]
2166
+ }),
2167
+ (0, react_jsx_runtime.jsxs)("button", {
2168
+ type: "button",
2169
+ onClick: downloadSelected,
2170
+ children: [(0, react_jsx_runtime.jsx)(Download, { size: 15 }), (0, react_jsx_runtime.jsx)("span", { children: t("download") })]
2171
+ }),
2172
+ (0, react_jsx_runtime.jsxs)("button", {
2173
+ type: "button",
2174
+ onClick: () => setShowDeleteModal(true),
2175
+ children: [(0, react_jsx_runtime.jsx)(Trash2, { size: 15 }), (0, react_jsx_runtime.jsx)("span", { children: t("remove") })]
2176
+ })
2177
+ ] })]
2178
+ }),
2179
+ (0, react_jsx_runtime.jsxs)("div", {
2180
+ className: "dsh-ig-result-meta",
2181
+ children: [
2182
+ (0, react_jsx_runtime.jsx)("span", { children: formatRelativeTime$1(selected.createdAt, lang) }),
2183
+ (0, react_jsx_runtime.jsx)("span", { children: selected.provider }),
2184
+ (0, react_jsx_runtime.jsx)("span", { children: selected.model }),
2185
+ (0, react_jsx_runtime.jsx)("span", { children: selected.attachment.width && selected.attachment.height ? `${selected.attachment.width} × ${selected.attachment.height}` : "—" })
2186
+ ]
2187
+ })
2188
+ ] })
2189
+ ]
2190
+ }),
2191
+ (0, react_jsx_runtime.jsxs)("aside", {
2192
+ className: "dsh-ig-generate-panel",
2193
+ children: [(0, react_jsx_runtime.jsxs)("div", {
2194
+ className: "dsh-ig-panel-tabs",
2195
+ children: [(0, react_jsx_runtime.jsx)("button", {
2196
+ type: "button",
2197
+ className: panelTab === "generate" ? "is-active" : "",
2198
+ onClick: () => setPanelTab("generate"),
2199
+ children: t("generate")
2200
+ }), (0, react_jsx_runtime.jsx)("button", {
2201
+ type: "button",
2202
+ className: panelTab === "details" ? "is-active" : "",
2203
+ onClick: () => setPanelTab("details"),
2204
+ children: t("details")
2205
+ })]
2206
+ }), panelTab === "details" ? (0, react_jsx_runtime.jsx)(DetailsPanel, {
2207
+ item: selected,
2208
+ t
2209
+ }) : (0, react_jsx_runtime.jsxs)("div", {
2210
+ className: "dsh-ig-generator-form",
2211
+ children: [
2212
+ (0, react_jsx_runtime.jsxs)("div", {
2213
+ className: "dsh-ig-mode-switch",
2214
+ children: [(0, react_jsx_runtime.jsxs)("button", {
2215
+ type: "button",
2216
+ className: mode === "generate" ? "is-active" : "",
2217
+ onClick: () => setMode("generate"),
2218
+ children: [(0, react_jsx_runtime.jsx)(Sparkles, { size: 15 }), t("generate")]
2219
+ }), (0, react_jsx_runtime.jsxs)("button", {
2220
+ type: "button",
2221
+ className: mode === "edit" ? "is-active" : "",
2222
+ onClick: () => setMode("edit"),
2223
+ children: [(0, react_jsx_runtime.jsx)(ImagePlus, { size: 15 }), t("edit")]
2224
+ })]
2225
+ }),
2226
+ mode === "edit" && (0, react_jsx_runtime.jsxs)("div", {
2227
+ className: "dsh-ig-field",
2228
+ children: [
2229
+ (0, react_jsx_runtime.jsxs)("div", {
2230
+ className: "dsh-ig-field-label",
2231
+ children: [(0, react_jsx_runtime.jsxs)("label", { children: [
2232
+ t("referencesCount", {
2233
+ current: String(references.length),
2234
+ max: String(maxReferences)
2235
+ }),
2236
+ " ",
2237
+ (0, react_jsx_runtime.jsx)("b", { children: "*" })
2238
+ ] }), references.length > 0 && (0, react_jsx_runtime.jsx)("button", {
2239
+ type: "button",
2240
+ onClick: clearAllReferences,
2241
+ children: t("clear")
2242
+ })]
2243
+ }),
2244
+ references.length === 0 ? (0, react_jsx_runtime.jsxs)("button", {
2245
+ type: "button",
2246
+ className: `dsh-ig-upload ${dragging ? "is-dragging" : ""}`,
2247
+ onClick: () => fileInputRef.current?.click(),
2248
+ onDragOver: (event) => {
2249
+ event.preventDefault();
2250
+ setDragging(true);
2251
+ },
2252
+ onDragLeave: () => setDragging(false),
2253
+ onDrop: (event) => {
2254
+ event.preventDefault();
2255
+ setDragging(false);
2256
+ addReferenceFiles(event.dataTransfer.files);
2257
+ },
2258
+ children: [
2259
+ (0, react_jsx_runtime.jsx)(Upload, { size: 20 }),
2260
+ (0, react_jsx_runtime.jsx)("strong", { children: t("upload") }),
2261
+ (0, react_jsx_runtime.jsx)("small", { children: t("uploadHint") })
2262
+ ]
2263
+ }) : (0, react_jsx_runtime.jsxs)("div", {
2264
+ className: "dsh-ig-reference-grid",
2265
+ children: [references.map((item, index) => (0, react_jsx_runtime.jsxs)("div", {
2266
+ className: "dsh-ig-reference-card",
2267
+ children: [(0, react_jsx_runtime.jsx)("img", {
2268
+ src: item.previewUrl,
2269
+ alt: `Ref ${index + 1}`
2270
+ }), (0, react_jsx_runtime.jsx)("button", {
2271
+ type: "button",
2272
+ title: t("closeReference"),
2273
+ onClick: () => removeReference(item.id),
2274
+ children: (0, react_jsx_runtime.jsx)(X, { size: 12 })
2275
+ })]
2276
+ }, item.id)), references.length < maxReferences && (0, react_jsx_runtime.jsxs)("button", {
2277
+ type: "button",
2278
+ className: "dsh-ig-reference-add",
2279
+ onClick: () => fileInputRef.current?.click(),
2280
+ title: t("upload"),
2281
+ children: [(0, react_jsx_runtime.jsx)(Upload, { size: 16 }), (0, react_jsx_runtime.jsx)("span", { children: t("addMoreRef") })]
2282
+ })]
2283
+ }),
2284
+ (0, react_jsx_runtime.jsx)("input", {
2285
+ ref: fileInputRef,
2286
+ type: "file",
2287
+ multiple: true,
2288
+ accept: "image/png,image/jpeg,image/webp,image/gif",
2289
+ hidden: true,
2290
+ onChange: (event) => {
2291
+ addReferenceFiles(event.target.files);
2292
+ event.target.value = "";
2293
+ }
2294
+ })
2295
+ ]
2296
+ }),
2297
+ (0, react_jsx_runtime.jsxs)("div", {
2298
+ className: "dsh-ig-field",
2299
+ children: [
2300
+ (0, react_jsx_runtime.jsxs)("div", {
2301
+ className: "dsh-ig-field-label",
2302
+ children: [(0, react_jsx_runtime.jsxs)("label", {
2303
+ htmlFor: "dsh-ig-prompt",
2304
+ children: [
2305
+ t("prompt"),
2306
+ " ",
2307
+ (0, react_jsx_runtime.jsx)("b", { children: "*" })
2308
+ ]
2309
+ }), (0, react_jsx_runtime.jsx)("button", {
2310
+ type: "button",
2311
+ onClick: () => setPrompt(""),
2312
+ children: t("clear")
2313
+ })]
2314
+ }),
2315
+ (0, react_jsx_runtime.jsx)("textarea", {
2316
+ id: "dsh-ig-prompt",
2317
+ value: prompt,
2318
+ maxLength: 2e3,
2319
+ onChange: (event) => setPrompt(event.target.value),
2320
+ onKeyDown: (event) => {
2321
+ if (event.nativeEvent.isComposing) return;
2322
+ if ((event.ctrlKey || event.metaKey) && event.key === "Enter") {
2323
+ event.preventDefault();
2324
+ if (!isGenerating && config !== null && prompt.trim().length > 0) submit();
2325
+ }
2326
+ },
2327
+ placeholder: t("promptPlaceholder")
2328
+ }),
2329
+ (0, react_jsx_runtime.jsxs)("small", { children: [prompt.length, "/2000"] })
2330
+ ]
2331
+ }),
2332
+ config === null ? configLoading ? (0, react_jsx_runtime.jsx)("div", {
2333
+ className: "dsh-ig-form-loading",
2334
+ children: (0, react_jsx_runtime.jsx)(LoaderCircle, {
2335
+ className: "dsh-ig-spin",
2336
+ size: 18
2337
+ })
2338
+ }) : (0, react_jsx_runtime.jsxs)("div", {
2339
+ className: "dsh-ig-config-error",
2340
+ children: [(0, react_jsx_runtime.jsx)("p", { children: configError ?? t("configLoadFailed") }), (0, react_jsx_runtime.jsxs)("button", {
2341
+ type: "button",
2342
+ onClick: () => void loadConfig(),
2343
+ children: [(0, react_jsx_runtime.jsx)(RefreshCw, { size: 13 }), (0, react_jsx_runtime.jsx)("span", { children: t("retry") })]
2344
+ })]
2345
+ }) : (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2346
+ (0, react_jsx_runtime.jsxs)("div", {
2347
+ className: "dsh-ig-field-grid",
2348
+ children: [(0, react_jsx_runtime.jsx)(FieldSelect, {
2349
+ label: t("provider"),
2350
+ value: provider,
2351
+ onChange: changeProvider,
2352
+ options: config.providers.map((item) => ({
2353
+ value: item.provider,
2354
+ label: `${item.label}${item.configured ? "" : ` · ${t("unconfigured")}`}`
2355
+ }))
2356
+ }), (0, react_jsx_runtime.jsx)(FieldSelect, {
2357
+ label: t("model"),
2358
+ value: model,
2359
+ onChange: setModel,
2360
+ options: activeProfile === void 0 ? [] : [{
2361
+ value: activeProfile.model,
2362
+ label: activeProfile.model
2363
+ }]
2364
+ })]
2365
+ }),
2366
+ (0, react_jsx_runtime.jsxs)("div", {
2367
+ className: "dsh-ig-field-grid",
2368
+ children: [(0, react_jsx_runtime.jsx)(FieldSelect, {
2369
+ label: t("ratio"),
2370
+ value: ratio,
2371
+ onChange: setRatio,
2372
+ options: activeProfile?.ratioOptions ?? []
2373
+ }), (0, react_jsx_runtime.jsx)(FieldSelect, {
2374
+ label: t("quality"),
2375
+ value: quality,
2376
+ onChange: setQuality,
2377
+ options: activeProfile?.qualityOptions ?? []
2378
+ })]
2379
+ }),
2380
+ (0, react_jsx_runtime.jsxs)("div", {
2381
+ className: "dsh-ig-field",
2382
+ children: [(0, react_jsx_runtime.jsx)("label", { children: t("count") }), (0, react_jsx_runtime.jsx)("div", {
2383
+ className: "dsh-ig-count-row",
2384
+ children: [
2385
+ 1,
2386
+ 2,
2387
+ 3,
2388
+ 4
2389
+ ].map((option) => (0, react_jsx_runtime.jsx)("button", {
2390
+ type: "button",
2391
+ className: `dsh-ig-count-pill ${count === option ? "is-active" : ""}`,
2392
+ onClick: () => setCount(option),
2393
+ children: t("countUnit", { n: String(option) })
2394
+ }, option))
2395
+ })]
2396
+ })
2397
+ ] }),
2398
+ configuredCount === 0 && config !== null && (0, react_jsx_runtime.jsx)("div", {
2399
+ className: "dsh-ig-inline-note",
2400
+ children: t("noProvider")
2401
+ }),
2402
+ error !== null && (0, react_jsx_runtime.jsx)("div", {
2403
+ className: "dsh-ig-form-error",
2404
+ children: error
2405
+ }),
2406
+ isGenerating ? (0, react_jsx_runtime.jsxs)("button", {
2407
+ type: "button",
2408
+ className: "dsh-ig-generate-button is-cancel",
2409
+ onClick: cancelGeneration,
2410
+ children: [(0, react_jsx_runtime.jsx)(LoaderCircle, {
2411
+ className: "dsh-ig-spin",
2412
+ size: 17
2413
+ }), (0, react_jsx_runtime.jsx)("span", { children: t("cancelGenerate") })]
2414
+ }) : (0, react_jsx_runtime.jsxs)("button", {
2415
+ type: "button",
2416
+ className: "dsh-ig-generate-button",
2417
+ disabled: config === null,
2418
+ onClick: () => void submit(),
2419
+ children: [(0, react_jsx_runtime.jsx)(Sparkles, { size: 17 }), (0, react_jsx_runtime.jsxs)("span", { children: [t("start"), count > 1 ? ` (${count})` : ""] })]
2420
+ })
2421
+ ]
2422
+ })]
2423
+ })
2424
+ ]
2425
+ }),
2426
+ lightboxOpen && selected !== null && (0, react_jsx_runtime.jsxs)("div", {
2427
+ className: "dsh-ig-lightbox-backdrop",
2428
+ onClick: () => setLightboxOpen(false),
2429
+ children: [
2430
+ (0, react_jsx_runtime.jsxs)("div", {
2431
+ className: "dsh-ig-lightbox-topbar",
2432
+ children: [(0, react_jsx_runtime.jsxs)("div", {
2433
+ className: "dsh-ig-lightbox-meta",
2434
+ children: [
2435
+ (0, react_jsx_runtime.jsx)("span", {
2436
+ className: "dsh-ig-tag",
2437
+ children: selected.provider
2438
+ }),
2439
+ (0, react_jsx_runtime.jsx)("span", {
2440
+ className: "dsh-ig-tag dsh-ig-tag-model",
2441
+ children: selected.model
2442
+ }),
2443
+ currentBatch && currentBatch.length > 1 && (0, react_jsx_runtime.jsxs)("span", {
2444
+ className: "dsh-ig-tag",
2445
+ children: [
2446
+ currentBatch.findIndex((i) => i.id === selected.id) + 1,
2447
+ " / ",
2448
+ currentBatch.length
2449
+ ]
2450
+ }),
2451
+ selected.attachment.width && selected.attachment.height ? (0, react_jsx_runtime.jsxs)("span", {
2452
+ className: "dsh-ig-tag",
2453
+ children: [
2454
+ selected.attachment.width,
2455
+ " × ",
2456
+ selected.attachment.height
2457
+ ]
2458
+ }) : null
2459
+ ]
2460
+ }), (0, react_jsx_runtime.jsx)("button", {
2461
+ type: "button",
2462
+ className: "dsh-ig-lightbox-close-btn",
2463
+ title: t("close"),
2464
+ onClick: () => setLightboxOpen(false),
2465
+ children: (0, react_jsx_runtime.jsx)(X, { size: 18 })
2466
+ })]
2467
+ }),
2468
+ currentBatch && currentBatch.length > 1 && (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsx)("button", {
2469
+ type: "button",
2470
+ className: "dsh-ig-lightbox-nav is-prev",
2471
+ onClick: (e) => {
2472
+ e.stopPropagation();
2473
+ stepLightbox(-1);
2474
+ },
2475
+ title: "上一张",
2476
+ children: (0, react_jsx_runtime.jsx)(ChevronLeft, { size: 24 })
2477
+ }), (0, react_jsx_runtime.jsx)("button", {
2478
+ type: "button",
2479
+ className: "dsh-ig-lightbox-nav is-next",
2480
+ onClick: (e) => {
2481
+ e.stopPropagation();
2482
+ stepLightbox(1);
2483
+ },
2484
+ title: "下一张",
2485
+ children: (0, react_jsx_runtime.jsx)(ChevronRight, { size: 24 })
2486
+ })] }),
2487
+ (0, react_jsx_runtime.jsx)("div", {
2488
+ className: "dsh-ig-lightbox-img-wrap",
2489
+ onClick: (e) => e.stopPropagation(),
2490
+ children: image.url !== null ? (0, react_jsx_runtime.jsx)("img", {
2491
+ className: "dsh-ig-lightbox-img",
2492
+ src: image.url,
2493
+ alt: selected.prompt
2494
+ }) : (0, react_jsx_runtime.jsx)("div", {
2495
+ className: "dsh-ig-lightbox-loading",
2496
+ children: (0, react_jsx_runtime.jsx)("div", { className: "dsh-ig-lightbox-spinner" })
2497
+ })
2498
+ }),
2499
+ (0, react_jsx_runtime.jsxs)("div", {
2500
+ className: "dsh-ig-lightbox-bottombar",
2501
+ onClick: (e) => e.stopPropagation(),
2502
+ children: [(0, react_jsx_runtime.jsx)("div", {
2503
+ className: "dsh-ig-lightbox-prompt-text",
2504
+ title: selected.prompt,
2505
+ children: selected.prompt
2506
+ }), (0, react_jsx_runtime.jsxs)("div", {
2507
+ className: "dsh-ig-lightbox-actions",
2508
+ children: [
2509
+ (0, react_jsx_runtime.jsxs)("button", {
2510
+ type: "button",
2511
+ className: "dsh-ig-lightbox-btn",
2512
+ title: t("copyPpt"),
2513
+ onClick: async () => {
2514
+ await navigator.clipboard.writeText(selected.prompt);
2515
+ flash(t("copiedPrompt"));
2516
+ },
2517
+ children: [(0, react_jsx_runtime.jsx)(FileText, { size: 14 }), (0, react_jsx_runtime.jsx)("span", { children: t("copyPpt") })]
2518
+ }),
2519
+ (0, react_jsx_runtime.jsxs)("button", {
2520
+ type: "button",
2521
+ className: "dsh-ig-lightbox-btn",
2522
+ title: t("copy"),
2523
+ onClick: async () => {
2524
+ if (!image.blob) return;
2525
+ const ok = await copyImageBlob(image.blob);
2526
+ flash(ok ? t("copiedImage") : t("copyFailed"));
2527
+ },
2528
+ children: [(0, react_jsx_runtime.jsx)(Copy, { size: 14 }), (0, react_jsx_runtime.jsx)("span", { children: t("copy") })]
2529
+ }),
2530
+ (0, react_jsx_runtime.jsxs)("button", {
2531
+ type: "button",
2532
+ className: "dsh-ig-lightbox-btn",
2533
+ title: t("download"),
2534
+ onClick: downloadSelected,
2535
+ children: [(0, react_jsx_runtime.jsx)(Download, { size: 14 }), (0, react_jsx_runtime.jsx)("span", { children: t("download") })]
2536
+ }),
2537
+ (0, react_jsx_runtime.jsxs)("button", {
2538
+ type: "button",
2539
+ className: `dsh-ig-lightbox-btn ${isSelectedInGallery ? "is-saved" : ""}`,
2540
+ title: isSelectedInGallery ? t("inGallery") : t("saveToGallery"),
2541
+ onClick: () => void handleSaveToGallery(),
2542
+ disabled: isSelectedInGallery,
2543
+ children: [isSelectedInGallery ? (0, react_jsx_runtime.jsx)(Check, { size: 14 }) : (0, react_jsx_runtime.jsx)(BookmarkPlus, { size: 14 }), (0, react_jsx_runtime.jsx)("span", { children: isSelectedInGallery ? t("inGallery") : t("saveToGallery") })]
2544
+ }),
2545
+ (0, react_jsx_runtime.jsxs)("button", {
2546
+ type: "button",
2547
+ className: "dsh-ig-lightbox-btn",
2548
+ title: selected.isFavorite ? t("favorited") : t("favorite"),
2549
+ onClick: () => void toggleFavorite(),
2550
+ children: [(0, react_jsx_runtime.jsx)(Heart, {
2551
+ size: 14,
2552
+ fill: selected.isFavorite ? "#ef4444" : "none",
2553
+ color: selected.isFavorite ? "#ef4444" : "currentColor"
2554
+ }), (0, react_jsx_runtime.jsx)("span", { children: selected.isFavorite ? t("favorited") : t("favorite") })]
2555
+ })
2556
+ ]
2557
+ })]
2558
+ })
2559
+ ]
2560
+ }),
2561
+ showDeleteModal && selected !== null && (0, react_jsx_runtime.jsx)("div", {
2562
+ className: "dsh-ig-workbench-modal-backdrop",
2563
+ onClick: () => setShowDeleteModal(false),
2564
+ children: (0, react_jsx_runtime.jsxs)("div", {
2565
+ className: "dsh-ig-workbench-modal-box",
2566
+ onClick: (e) => e.stopPropagation(),
2567
+ children: [
2568
+ (0, react_jsx_runtime.jsxs)("div", {
2569
+ className: "dsh-ig-workbench-modal-header",
2570
+ children: [(0, react_jsx_runtime.jsx)(TriangleAlert, {
2571
+ size: 20,
2572
+ color: "#dc2626"
2573
+ }), (0, react_jsx_runtime.jsx)("strong", { children: t("deleteModalTitle") })]
2574
+ }),
2575
+ (0, react_jsx_runtime.jsx)("p", {
2576
+ className: "dsh-ig-workbench-modal-desc",
2577
+ children: t("deleteModalDesc")
2578
+ }),
2579
+ Boolean(selected.savedTo) && (0, react_jsx_runtime.jsxs)("label", {
2580
+ className: "dsh-ig-workbench-modal-option",
2581
+ children: [(0, react_jsx_runtime.jsx)("input", {
2582
+ type: "checkbox",
2583
+ checked: deleteWorkspaceFiles,
2584
+ onChange: (e) => setDeleteWorkspaceFiles(e.target.checked)
2585
+ }), (0, react_jsx_runtime.jsx)("span", { children: t("deleteWorkspaceFilesLabel") })]
2586
+ }),
2587
+ (0, react_jsx_runtime.jsxs)("div", {
2588
+ className: "dsh-ig-workbench-modal-actions",
2589
+ children: [(0, react_jsx_runtime.jsx)("button", {
2590
+ type: "button",
2591
+ className: "dsh-ig-workbench-modal-cancel",
2592
+ onClick: () => setShowDeleteModal(false),
2593
+ children: t("cancel")
2594
+ }), (0, react_jsx_runtime.jsx)("button", {
2595
+ type: "button",
2596
+ className: "dsh-ig-workbench-modal-danger",
2597
+ onClick: () => void handleConfirmDelete(),
2598
+ children: t("confirm")
2599
+ })]
2600
+ })
2601
+ ]
2602
+ })
2603
+ }),
2604
+ notice !== null && (0, react_jsx_runtime.jsx)("div", {
2605
+ className: "dsh-ig-workbench-toast",
2606
+ children: notice
2607
+ })
2608
+ ]
2609
+ });
2610
+ };
2611
+ const BatchCanvasItem = ({ item, index, isSelected, onSelect }) => {
2612
+ const image = useAttachmentImage(item.attachment, true);
2613
+ return (0, react_jsx_runtime.jsxs)("div", {
2614
+ className: `dsh-ig-canvas-item ${isSelected ? "is-selected" : ""}`,
2615
+ onClick: onSelect,
2616
+ children: [(0, react_jsx_runtime.jsxs)("span", {
2617
+ className: "dsh-ig-canvas-badge",
2618
+ children: ["#", index + 1]
2619
+ }), image.url !== null ? (0, react_jsx_runtime.jsx)("img", {
2620
+ src: image.url,
2621
+ alt: item.prompt,
2622
+ draggable: false
2623
+ }) : image.loading ? (0, react_jsx_runtime.jsx)("div", {
2624
+ className: "dsh-ig-canvas-item-loading",
2625
+ children: (0, react_jsx_runtime.jsx)(LoaderCircle, {
2626
+ className: "dsh-ig-spin",
2627
+ size: 24
2628
+ })
2629
+ }) : (0, react_jsx_runtime.jsx)("div", {
2630
+ className: "dsh-ig-canvas-item-loading",
2631
+ children: (0, react_jsx_runtime.jsx)(ImagePlus, { size: 24 })
2632
+ })]
2633
+ });
2634
+ };
2635
+ const FieldSelect = ({ label, value, options, onChange }) => (0, react_jsx_runtime.jsxs)("label", {
2636
+ className: "dsh-ig-field-select",
2637
+ children: [(0, react_jsx_runtime.jsx)("span", { children: label }), (0, react_jsx_runtime.jsx)("select", {
2638
+ value,
2639
+ onChange: (event) => onChange(event.target.value),
2640
+ children: options.map((option) => (0, react_jsx_runtime.jsx)("option", {
2641
+ value: option.value,
2642
+ children: option.label
2643
+ }, option.value))
2644
+ })]
2645
+ });
2646
+ const DetailsPanel = ({ item, t }) => item === null ? (0, react_jsx_runtime.jsxs)("div", {
2647
+ className: "dsh-ig-details-empty",
2648
+ children: [(0, react_jsx_runtime.jsx)(ImagePlus, { size: 28 }), (0, react_jsx_runtime.jsx)("span", { children: t("selectHistory") })]
2649
+ }) : (0, react_jsx_runtime.jsxs)("dl", {
2650
+ className: "dsh-ig-details",
2651
+ children: [
2652
+ (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 })] }),
2653
+ (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 })] }),
2654
+ (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 })] }),
2655
+ (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}` : "—" })] }),
2656
+ (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 ?? "—" })] }),
2657
+ (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() })] })
2658
+ ]
2659
+ });
2660
+ const RecentItem = ({ item, active, lang, onClick }) => {
2661
+ const [setRef, inView] = useInView();
2662
+ const image = useAttachmentImage(item.attachment, inView);
2663
+ return (0, react_jsx_runtime.jsxs)("button", {
2664
+ ref: setRef,
2665
+ type: "button",
2666
+ className: `dsh-ig-recent-item ${active ? "is-active" : ""}`,
2667
+ onClick,
2668
+ children: [(0, react_jsx_runtime.jsx)("div", {
2669
+ className: "dsh-ig-recent-thumb",
2670
+ children: image.url !== null ? (0, react_jsx_runtime.jsx)("img", {
2671
+ src: image.url,
2672
+ alt: "",
2673
+ loading: "lazy"
2674
+ }) : (0, react_jsx_runtime.jsx)(ImagePlus, { size: 18 })
2675
+ }), (0, react_jsx_runtime.jsxs)("div", { children: [
2676
+ (0, react_jsx_runtime.jsx)("strong", { children: item.prompt }),
2677
+ (0, react_jsx_runtime.jsx)("span", { children: formatRelativeTime$1(item.createdAt, lang) }),
2678
+ (0, react_jsx_runtime.jsxs)("small", { children: [
2679
+ item.provider,
2680
+ " · ",
2681
+ item.model
2682
+ ] }),
2683
+ (0, react_jsx_runtime.jsx)("small", { children: item.attachment.width && item.attachment.height ? `${item.attachment.width} × ${item.attachment.height}` : "—" })
2684
+ ] })]
2685
+ });
2686
+ };
2687
+ function useInView() {
2688
+ const [inView, setInView] = (0, react.useState)(false);
2689
+ const observerRef = (0, react.useRef)(null);
2690
+ (0, react.useEffect)(() => {
2691
+ return () => {
2692
+ observerRef.current?.disconnect();
2693
+ };
2694
+ }, []);
2695
+ return [(0, react.useCallback)((el) => {
2696
+ observerRef.current?.disconnect();
2697
+ if (!el || inView) return;
2698
+ if (typeof IntersectionObserver === "undefined") {
2699
+ setInView(true);
2700
+ return;
2701
+ }
2702
+ const observer = new IntersectionObserver(([entry]) => {
2703
+ if (entry?.isIntersecting) {
2704
+ setInView(true);
2705
+ observer.disconnect();
2706
+ }
2707
+ }, { rootMargin: "120px" });
2708
+ observerRef.current = observer;
2709
+ observer.observe(el);
2710
+ }, [inView]), inView];
2711
+ }
2712
+ function useAttachmentImage(attachment, shouldLoad = true) {
2713
+ const [value, setValue] = (0, react.useState)({
2714
+ url: null,
2715
+ blob: null,
2716
+ loading: false
2717
+ });
2718
+ (0, react.useEffect)(() => {
2719
+ if (attachment === void 0 || !shouldLoad) {
2720
+ setValue({
2721
+ url: null,
2722
+ blob: null,
2723
+ loading: false
2724
+ });
2725
+ return;
2726
+ }
2727
+ let cancelled = false;
2728
+ let objectUrl = null;
2729
+ setValue({
2730
+ url: null,
2731
+ blob: null,
2732
+ loading: true
2733
+ });
2734
+ fetchAttachmentBlob(attachment).then((blob) => {
2735
+ if (cancelled) return;
2736
+ objectUrl = URL.createObjectURL(blob);
2737
+ setValue({
2738
+ url: objectUrl,
2739
+ blob,
2740
+ loading: false
2741
+ });
2742
+ }).catch(() => {
2743
+ if (!cancelled) setValue({
2744
+ url: null,
2745
+ blob: null,
2746
+ loading: false
2747
+ });
2748
+ });
2749
+ return () => {
2750
+ cancelled = true;
2751
+ if (objectUrl !== null) URL.revokeObjectURL(objectUrl);
2752
+ };
2753
+ }, [attachment?.attachmentId, shouldLoad]);
2754
+ return value;
2755
+ }
2756
+ function fileToBase64(file) {
2757
+ return new Promise((resolve, reject) => {
2758
+ const reader = new FileReader();
2759
+ reader.onload = () => {
2760
+ const result = reader.result;
2761
+ const commaIndex = result.indexOf(",");
2762
+ resolve(commaIndex >= 0 ? result.slice(commaIndex + 1) : result);
2763
+ };
2764
+ reader.onerror = () => reject(reader.error ?? /* @__PURE__ */ new Error("Failed to read image file"));
2765
+ reader.readAsDataURL(file);
2766
+ });
2767
+ }
2768
+ function extension(mediaType) {
2769
+ return mediaType === "image/jpeg" ? "jpg" : mediaType.split("/")[1] ?? "png";
2770
+ }
2771
+ function messageOf(error) {
2772
+ return error instanceof Error ? error.message : String(error);
2773
+ }
2774
+ //#endregion
2775
+ //#region lib/types/client/conversation-regenerate.js
2776
+ /** Build the workbench request that reproduces a conversation image with an edited prompt. */
2777
+ function conversationRegenerateRequest(image, prompt, remembered) {
2778
+ if (!CLOUD_IMAGE_PROVIDERS.includes(image.provider)) throw new Error("当前图片使用的 Provider 暂不支持重新生成");
2779
+ const provider = image.provider;
2780
+ const settings = remembered ?? outputSettings(provider, image.output);
2781
+ return {
2782
+ mode: "generate",
2783
+ provider,
2784
+ model: image.model,
2785
+ prompt: prompt.trim(),
2786
+ ratio: settings.ratio,
2787
+ quality: settings.quality
2788
+ };
2789
+ }
2790
+ function outputSettings(provider, output) {
2791
+ const normalized = (output ?? "").trim();
2792
+ if (provider === "google") {
2793
+ const [rawRatio, rawQuality] = normalized.split(",").map((value) => value.trim());
2794
+ return {
2795
+ ratio: isRatio(rawRatio) ? rawRatio : "1:1",
2796
+ quality: rawQuality === "1K" || rawQuality === "2K" || rawQuality === "4K" ? rawQuality : "1K"
2797
+ };
2798
+ }
2799
+ if (provider === "seedream") return {
2800
+ ratio: "auto",
2801
+ quality: normalized === "1K" || normalized === "4K" ? normalized : "2K"
2802
+ };
2803
+ if (provider === "openai") return {
2804
+ ratio: ratioFromSize(normalized, "x"),
2805
+ quality: "standard"
2806
+ };
2807
+ return {
2808
+ ratio: ratioFromSize(normalized, "*"),
2809
+ quality: "standard"
2810
+ };
2811
+ }
2812
+ function ratioFromSize(size, separator) {
2813
+ return (separator === "x" ? {
2814
+ "1024x1024": "1:1",
2815
+ "1536x1024": "3:2",
2816
+ "1024x1536": "2:3"
2817
+ } : {
2818
+ "1024*1024": "1:1",
2819
+ "1536*1024": "3:2",
2820
+ "1024*1536": "2:3",
2821
+ "1664*928": "16:9",
2822
+ "928*1664": "9:16"
2823
+ })[size] ?? "1:1";
2824
+ }
2825
+ function isRatio(value) {
2826
+ return value === "1:1" || value === "3:2" || value === "2:3" || value === "4:3" || value === "3:4" || value === "16:9" || value === "9:16";
2827
+ }
2828
+ //#endregion
2829
+ //#region lib/types/client/gallery-view.js
2830
+ /**
2831
+ * Native Workspace Gallery & Studio View Component for DSH `conversation.view` slot.
2832
+ * Fully i18n-reactive (Chinese & English) with modular tabs, multi-dimensional filters,
2833
+ * responsive image grid, and placeholder routes.
2834
+ */
2835
+ const DICT$1 = {
2836
+ zh: {
2837
+ tabGallery: "图库",
2838
+ tabStudio: "工作台",
2839
+ tabFavorites: "收藏",
2840
+ tabCompare: "对比",
2841
+ tabTasks: "任务",
2842
+ filterAllProviders: "全部提供商",
2843
+ filterGoogle: "Google Gemini",
2844
+ filterOpenAI: "OpenAI / 中转站",
2845
+ filterSeedream: "字节 Seedream",
2846
+ filterDashScope: "阿里 DashScope",
2847
+ filterComfyUI: "本地 ComfyUI",
2848
+ filterAllModels: "全部模型",
2849
+ filterAllRatios: "全部比例",
2850
+ searchPlaceholder: "搜索 Prompt、标签…",
2851
+ filterCurrentWorkspace: "仅看此工作区",
2852
+ filterCurrentWorkspaceHint: "只展示属于当前工作区的生图记录与物理文件",
2853
+ sortNewest: "最新生成",
2854
+ sortOldest: "最早生成",
2855
+ totalCount: "共 {count} 张生成图片",
2856
+ emptyTitle: "暂无生图记录",
2857
+ emptyDesc: "在对话中让 Agent 生图后,生成的图片会自动收录到这里。",
794
2858
  favEmptyTitle: "暂无收藏图片",
795
2859
  favEmptyDesc: "在图库中点击卡片右下角的 ♡ 按钮,即可将喜爱的图片收录到这里。",
796
2860
  noMatchTitle: "未找到匹配结果",
@@ -804,6 +2868,13 @@ window.__ModuleLoader__.load({
804
2868
  download: "下载图片",
805
2869
  copyImg: "复制图片",
806
2870
  copyPpt: "复制 Prompt",
2871
+ regenerate: "重新生成",
2872
+ regenerateTitle: "重新生成图片",
2873
+ regenerateHint: "可按需修改 Prompt,基于当前模型和比例在画廊生成一张新图片。",
2874
+ confirmRegenerate: "开始生成",
2875
+ regenerating: "正在重新生成…",
2876
+ regenerateSuccess: "已生成新图片并收录到画廊",
2877
+ regenerateFailed: "重新生成失败",
807
2878
  delete: "从画廊删除",
808
2879
  confirmDelete: "确定要从画廊中删除这张图片吗?(不会影响原聊天记录)",
809
2880
  deleted: "已从画廊删除",
@@ -858,6 +2929,8 @@ window.__ModuleLoader__.load({
858
2929
  filterAllModels: "All Models",
859
2930
  filterAllRatios: "All Ratios",
860
2931
  searchPlaceholder: "Search prompt, tags…",
2932
+ filterCurrentWorkspace: "Current workspace only",
2933
+ filterCurrentWorkspaceHint: "Only show images belonging to the active workspace",
861
2934
  sortNewest: "Newest first",
862
2935
  sortOldest: "Oldest first",
863
2936
  totalCount: "{count} images total",
@@ -876,6 +2949,13 @@ window.__ModuleLoader__.load({
876
2949
  download: "Download",
877
2950
  copyImg: "Copy Image",
878
2951
  copyPpt: "Copy Prompt",
2952
+ regenerate: "Regenerate",
2953
+ regenerateTitle: "Regenerate Image",
2954
+ regenerateHint: "Edit prompt if needed. A new image will be generated and added to the gallery.",
2955
+ confirmRegenerate: "Generate",
2956
+ regenerating: "Regenerating…",
2957
+ regenerateSuccess: "New image generated and added to gallery",
2958
+ regenerateFailed: "Regeneration failed",
879
2959
  delete: "Delete from gallery",
880
2960
  confirmDelete: "Are you sure you want to remove this image from the gallery? (Chat history will not be affected)",
881
2961
  deleted: "Deleted from gallery",
@@ -971,17 +3051,80 @@ window.__ModuleLoader__.load({
971
3051
  if (item.imageSize) return item.imageSize;
972
3052
  return "1:1";
973
3053
  }
974
- const GalleryViewTab = ({ locale }) => {
3054
+ const GalleryViewTab = (props) => {
3055
+ const { locale, sessionId, useSessions, useWorkspaces } = props;
975
3056
  const [activeTab, setActiveTab] = (0, react.useState)("gallery");
976
3057
  const [items, setItems] = (0, react.useState)([]);
977
3058
  const [search, setSearch] = (0, react.useState)("");
978
3059
  const [selectedProvider, setSelectedProvider] = (0, react.useState)("all");
979
3060
  const [selectedRatio, setSelectedRatio] = (0, react.useState)("all");
980
3061
  const [sortBy, setSortBy] = (0, react.useState)("newest");
3062
+ const [onlyCurrentWorkspace, setOnlyCurrentWorkspace] = (0, react.useState)(() => {
3063
+ try {
3064
+ const stored = localStorage.getItem("dsh-ig-only-current-workspace");
3065
+ return stored === null ? true : stored === "true";
3066
+ } catch {
3067
+ return true;
3068
+ }
3069
+ });
3070
+ const [serverWorkspaces, setServerWorkspaces] = (0, react.useState)([]);
3071
+ const [serverActiveRoot, setServerActiveRoot] = (0, react.useState)(null);
3072
+ (0, react.useEffect)(() => {
3073
+ try {
3074
+ localStorage.setItem("dsh-ig-only-current-workspace", String(onlyCurrentWorkspace));
3075
+ } catch {}
3076
+ }, [onlyCurrentWorkspace]);
3077
+ (0, react.useEffect)(() => {
3078
+ let mounted = true;
3079
+ fetch(STUDIO_ROUTE).then((res) => res.ok ? res.json() : null).then((data) => {
3080
+ if (!mounted || !data) return;
3081
+ if (Array.isArray(data.workspaces)) setServerWorkspaces(data.workspaces);
3082
+ if (typeof data.workspaceRoot === "string") setServerActiveRoot(data.workspaceRoot);
3083
+ }).catch(() => {});
3084
+ return () => {
3085
+ mounted = false;
3086
+ };
3087
+ }, []);
3088
+ const liveWorkspaces = useWorkspaces ? useWorkspaces((s) => s?.items) : null;
3089
+ const liveSessions = useSessions ? useSessions((s) => s) : null;
3090
+ const currentSessionId = sessionId || liveSessions?.current;
3091
+ const activeWorkspace = (0, react.useMemo)(() => {
3092
+ const allWs = Array.isArray(liveWorkspaces) && liveWorkspaces.length > 0 ? liveWorkspaces : serverWorkspaces;
3093
+ if (allWs.length > 0) {
3094
+ if (currentSessionId) {
3095
+ const matched = allWs.find((ws) => Array.isArray(ws.sessionIds) && ws.sessionIds.includes(currentSessionId));
3096
+ if (matched) return matched;
3097
+ }
3098
+ if (serverActiveRoot) {
3099
+ const normActive = serverActiveRoot.replace(/\\/g, "/").toLowerCase();
3100
+ const matched = allWs.find((ws) => ws.path && ws.path.replace(/\\/g, "/").toLowerCase() === normActive);
3101
+ if (matched) return matched;
3102
+ }
3103
+ return allWs[0];
3104
+ }
3105
+ if (serverActiveRoot) return {
3106
+ workspaceId: "default",
3107
+ path: serverActiveRoot,
3108
+ title: "Workspace",
3109
+ sessionIds: []
3110
+ };
3111
+ return null;
3112
+ }, [
3113
+ liveWorkspaces,
3114
+ serverWorkspaces,
3115
+ currentSessionId,
3116
+ serverActiveRoot
3117
+ ]);
981
3118
  const [previewItem, setPreviewItem] = (0, react.useState)(null);
982
3119
  const [previewUrl, setPreviewUrl] = (0, react.useState)(null);
3120
+ const previewUrlRef = (0, react.useRef)(null);
3121
+ previewUrlRef.current = previewUrl;
983
3122
  const [previewBlob, setPreviewBlob] = (0, react.useState)(null);
984
3123
  const [toast, setToast] = (0, react.useState)(null);
3124
+ const [showRegenerateModal, setShowRegenerateModal] = (0, react.useState)(false);
3125
+ const [regeneratePrompt, setRegeneratePrompt] = (0, react.useState)("");
3126
+ const [isRegenerating, setIsRegenerating] = (0, react.useState)(false);
3127
+ const regenerateControllerRef = (0, react.useRef)(null);
985
3128
  const [isManageMode, setIsManageMode] = (0, react.useState)(false);
986
3129
  const [selectedIds, setSelectedIds] = (0, react.useState)(/* @__PURE__ */ new Set());
987
3130
  const lastSelectedIndexRef = (0, react.useRef)(null);
@@ -996,7 +3139,8 @@ window.__ModuleLoader__.load({
996
3139
  search,
997
3140
  selectedProvider,
998
3141
  selectedRatio,
999
- sortBy
3142
+ sortBy,
3143
+ onlyCurrentWorkspace
1000
3144
  ]);
1001
3145
  const [lang, setLang] = (0, react.useState)(() => {
1002
3146
  return (locale?.getSnapshot?.()?.active)?.startsWith("en") ? "en" : "zh";
@@ -1062,6 +3206,9 @@ window.__ModuleLoader__.load({
1062
3206
  const matchTags = item.tags?.some((t) => t.toLowerCase().includes(q));
1063
3207
  if (!matchPrompt && !matchModel && !matchProvider && !matchTags) return false;
1064
3208
  }
3209
+ if (onlyCurrentWorkspace && activeWorkspace) {
3210
+ if (!isItemInWorkspace(item, activeWorkspace)) return false;
3211
+ }
1065
3212
  return true;
1066
3213
  }).sort((a, b) => {
1067
3214
  if (sortBy === "oldest") return a.createdAt - b.createdAt;
@@ -1073,8 +3220,14 @@ window.__ModuleLoader__.load({
1073
3220
  selectedProvider,
1074
3221
  selectedRatio,
1075
3222
  search,
1076
- sortBy
3223
+ sortBy,
3224
+ onlyCurrentWorkspace,
3225
+ activeWorkspace
1077
3226
  ]);
3227
+ const workspaceItemsCount = (0, react.useMemo)(() => {
3228
+ if (!activeWorkspace) return items.length;
3229
+ return items.filter((item) => isItemInWorkspace(item, activeWorkspace)).length;
3230
+ }, [items, activeWorkspace]);
1078
3231
  const currentPreviewIndex = (0, react.useMemo)(() => {
1079
3232
  if (!previewItem) return -1;
1080
3233
  return filteredItems.findIndex((i) => i.id === previewItem.id);
@@ -1086,11 +3239,15 @@ window.__ModuleLoader__.load({
1086
3239
  abortControllerRef.current.abort();
1087
3240
  abortControllerRef.current = null;
1088
3241
  }
1089
- if (previewUrl) URL.revokeObjectURL(previewUrl);
3242
+ if (previewUrlRef.current) {
3243
+ URL.revokeObjectURL(previewUrlRef.current);
3244
+ previewUrlRef.current = null;
3245
+ }
1090
3246
  setPreviewItem(item);
1091
3247
  if (blob) {
1092
3248
  blobCache.set(item.id, blob);
1093
3249
  const url = URL.createObjectURL(blob);
3250
+ previewUrlRef.current = url;
1094
3251
  setPreviewBlob(blob);
1095
3252
  setPreviewUrl(url);
1096
3253
  setPreviewLoading(false);
@@ -1099,6 +3256,7 @@ window.__ModuleLoader__.load({
1099
3256
  const cached = blobCache.get(item.id);
1100
3257
  if (cached) {
1101
3258
  const url = URL.createObjectURL(cached);
3259
+ previewUrlRef.current = url;
1102
3260
  setPreviewBlob(cached);
1103
3261
  setPreviewUrl(url);
1104
3262
  setPreviewLoading(false);
@@ -1109,18 +3267,11 @@ window.__ModuleLoader__.load({
1109
3267
  setPreviewUrl(null);
1110
3268
  const controller = new AbortController();
1111
3269
  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) => {
3270
+ fetchAttachmentBlob(item.attachment).then((fetchedBlob) => {
1121
3271
  if (controller.signal.aborted) return;
1122
3272
  blobCache.set(item.id, fetchedBlob);
1123
3273
  const url = URL.createObjectURL(fetchedBlob);
3274
+ previewUrlRef.current = url;
1124
3275
  setPreviewBlob(fetchedBlob);
1125
3276
  setPreviewUrl(url);
1126
3277
  setPreviewLoading(false);
@@ -1135,7 +3286,16 @@ window.__ModuleLoader__.load({
1135
3286
  abortControllerRef.current.abort();
1136
3287
  abortControllerRef.current = null;
1137
3288
  }
1138
- if (previewUrl) URL.revokeObjectURL(previewUrl);
3289
+ if (regenerateControllerRef.current) {
3290
+ regenerateControllerRef.current.abort();
3291
+ regenerateControllerRef.current = null;
3292
+ setIsRegenerating(false);
3293
+ }
3294
+ setShowRegenerateModal(false);
3295
+ if (previewUrlRef.current) {
3296
+ URL.revokeObjectURL(previewUrlRef.current);
3297
+ previewUrlRef.current = null;
3298
+ }
1139
3299
  setPreviewItem(null);
1140
3300
  setPreviewUrl(null);
1141
3301
  setPreviewBlob(null);
@@ -1153,15 +3313,86 @@ window.__ModuleLoader__.load({
1153
3313
  if (nextItem) openPreviewItem(nextItem);
1154
3314
  }
1155
3315
  };
3316
+ const handleOpenRegenerate = () => {
3317
+ if (!previewItem || isRegenerating) return;
3318
+ setRegeneratePrompt(previewItem.prompt);
3319
+ setShowRegenerateModal(true);
3320
+ };
3321
+ const handleCancelRegeneratePrompt = () => {
3322
+ setShowRegenerateModal(false);
3323
+ };
3324
+ const handleAbortRegenerating = () => {
3325
+ regenerateControllerRef.current?.abort();
3326
+ regenerateControllerRef.current = null;
3327
+ setIsRegenerating(false);
3328
+ showToast(t("cancel"));
3329
+ };
3330
+ const handleConfirmRegenerate = async () => {
3331
+ if (!previewItem || isRegenerating || regeneratePrompt.trim().length === 0) return;
3332
+ setShowRegenerateModal(false);
3333
+ setIsRegenerating(true);
3334
+ const controller = new AbortController();
3335
+ regenerateControllerRef.current = controller;
3336
+ try {
3337
+ const request = conversationRegenerateRequest(previewItem, regeneratePrompt, previewItem.aspectRatio ? {
3338
+ ratio: previewItem.aspectRatio,
3339
+ quality: previewItem.imageSize || "standard"
3340
+ } : void 0);
3341
+ if (activeWorkspace?.path) request.workspaceRoot = activeWorkspace.path;
3342
+ const response = await fetch(STUDIO_ROUTE, {
3343
+ method: "POST",
3344
+ credentials: "same-origin",
3345
+ signal: controller.signal,
3346
+ headers: { "content-type": "application/json" },
3347
+ body: JSON.stringify(request)
3348
+ });
3349
+ const payload = await response.json().catch(() => null);
3350
+ if (!response.ok || payload === null || !("attachment" in payload)) throw new Error(payload && "error" in payload && payload.error ? payload.error : t("regenerateFailed"));
3351
+ const newItem = {
3352
+ id: String(payload.attachment.attachmentId),
3353
+ attachment: payload.attachment,
3354
+ prompt: payload.prompt,
3355
+ provider: payload.provider,
3356
+ model: payload.model,
3357
+ createdAt: payload.createdAt,
3358
+ aspectRatio: request.ratio,
3359
+ imageSize: request.quality,
3360
+ output: payload.output,
3361
+ ...payload.savedTo ? { savedTo: payload.savedTo } : {},
3362
+ ...activeWorkspace?.path ? { workspacePath: activeWorkspace.path } : {},
3363
+ ...activeWorkspace?.workspaceId ? { workspaceId: activeWorkspace.workspaceId } : {},
3364
+ ...currentSessionId ? { sessionId: currentSessionId } : {}
3365
+ };
3366
+ await saveGalleryItem(newItem);
3367
+ if (activeTab !== "gallery") setActiveTab("gallery");
3368
+ if (search.trim().length > 0) setSearch("");
3369
+ openPreviewItem(newItem);
3370
+ showToast(t("regenerateSuccess"));
3371
+ } catch (err) {
3372
+ if (controller.signal.aborted) return;
3373
+ showToast(err instanceof Error ? err.message : String(err));
3374
+ } finally {
3375
+ setIsRegenerating(false);
3376
+ regenerateControllerRef.current = null;
3377
+ }
3378
+ };
1156
3379
  (0, react.useEffect)(() => {
1157
3380
  return () => {
1158
3381
  if (abortControllerRef.current) abortControllerRef.current.abort();
1159
- if (previewUrl) URL.revokeObjectURL(previewUrl);
3382
+ if (regenerateControllerRef.current) regenerateControllerRef.current.abort();
3383
+ if (previewUrlRef.current) URL.revokeObjectURL(previewUrlRef.current);
1160
3384
  };
1161
- }, [previewUrl]);
3385
+ }, []);
1162
3386
  (0, react.useEffect)(() => {
1163
3387
  const onKeyDown = (e) => {
1164
3388
  if (["INPUT", "TEXTAREA"].includes(e.target?.tagName)) return;
3389
+ if (showRegenerateModal) {
3390
+ if (e.key === "Escape") {
3391
+ e.preventDefault();
3392
+ setShowRegenerateModal(false);
3393
+ }
3394
+ return;
3395
+ }
1165
3396
  if (showDeleteModal) {
1166
3397
  if (e.key === "Escape" && !isDeleting) {
1167
3398
  e.preventDefault();
@@ -1184,6 +3415,7 @@ window.__ModuleLoader__.load({
1184
3415
  window.addEventListener("keydown", onKeyDown);
1185
3416
  return () => window.removeEventListener("keydown", onKeyDown);
1186
3417
  }, [
3418
+ showRegenerateModal,
1187
3419
  showDeleteModal,
1188
3420
  isDeleting,
1189
3421
  isManageMode,
@@ -1263,34 +3495,41 @@ window.__ModuleLoader__.load({
1263
3495
  setSelectedIds(/* @__PURE__ */ new Set([item.id]));
1264
3496
  setShowDeleteModal(true);
1265
3497
  };
3498
+ const hasWorkspaceFilesToDelete = (0, react.useMemo)(() => items.some((i) => selectedIds.has(i.id) && typeof i.savedTo === "string" && i.savedTo.trim().length > 0), [items, selectedIds]);
1266
3499
  const handleConfirmBatchDelete = async () => {
1267
3500
  if (selectedIds.size === 0 || isDeleting) return;
1268
3501
  setIsDeleting(true);
1269
3502
  const idsToDelete = Array.from(selectedIds);
1270
3503
  const itemsToDelete = items.filter((i) => selectedIds.has(i.id));
1271
3504
  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
3505
  let deletedFiles = 0;
1274
- let failedFilesCount = 0;
1275
- if (deleteWorkspaceFiles && (pathsToDelete.length > 0 || fallbackAttachmentIds.length > 0)) try {
3506
+ if (deleteWorkspaceFiles && pathsToDelete.length > 0) try {
1276
3507
  const res = await fetch(DELETE_ROUTE, {
1277
3508
  method: "POST",
1278
3509
  headers: { "content-type": "application/json" },
1279
- body: JSON.stringify({
1280
- paths: pathsToDelete,
1281
- attachmentIds: fallbackAttachmentIds
1282
- })
3510
+ body: JSON.stringify({ paths: pathsToDelete })
1283
3511
  });
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;
3512
+ if (!res.ok) {
3513
+ setIsDeleting(false);
3514
+ showToast(`工作区文件删除失败 (${res.status})`);
3515
+ return;
1288
3516
  }
3517
+ const data = await res.json().catch(() => null);
3518
+ if (!data || data.ok === false || Array.isArray(data.failedFiles) && data.failedFiles.length > 0) {
3519
+ setIsDeleting(false);
3520
+ const reason = data?.failedFiles?.[0]?.error || "工作区文件未能删除";
3521
+ showToast(`删除失败:${reason}`);
3522
+ return;
3523
+ }
3524
+ deletedFiles = data.deletedCount ?? 0;
1289
3525
  } catch (err) {
1290
- console.warn("[dsh-image-gen] Workspace file deletion failed:", err);
3526
+ setIsDeleting(false);
3527
+ showToast(err instanceof Error ? err.message : "工作区文件删除网络异常");
3528
+ return;
1291
3529
  }
1292
3530
  try {
1293
3531
  await bulkDeleteGalleryItems(idsToDelete);
3532
+ for (const item of itemsToDelete) evictAttachmentCache(item.attachment.attachmentId);
1294
3533
  } catch (err) {
1295
3534
  setIsDeleting(false);
1296
3535
  showToast(t("deleteFailedDatabase"));
@@ -1304,11 +3543,7 @@ window.__ModuleLoader__.load({
1304
3543
  setSelectedIds(/* @__PURE__ */ new Set());
1305
3544
  lastSelectedIndexRef.current = null;
1306
3545
  }
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", {
3546
+ if (deletedFiles > 0) showToast(t("batchDeletedWithFilesToast", {
1312
3547
  count: String(idsToDelete.length),
1313
3548
  files: String(deletedFiles)
1314
3549
  }));
@@ -1316,6 +3551,7 @@ window.__ModuleLoader__.load({
1316
3551
  };
1317
3552
  return (0, react_jsx_runtime.jsxs)("div", {
1318
3553
  className: "dsh-ig-gallery-page",
3554
+ "data-conversation-composer-overlay": "",
1319
3555
  children: [
1320
3556
  (0, react_jsx_runtime.jsxs)("header", {
1321
3557
  className: "dsh-ig-studio-tabs-bar",
@@ -1337,18 +3573,6 @@ window.__ModuleLoader__.load({
1337
3573
  className: `dsh-ig-studio-tab-btn ${activeTab === "favorites" ? "is-active" : ""}`,
1338
3574
  onClick: () => setActiveTab("favorites"),
1339
3575
  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
3576
  })
1353
3577
  ]
1354
3578
  }),
@@ -1439,6 +3663,30 @@ window.__ModuleLoader__.load({
1439
3663
  value: search,
1440
3664
  onChange: (e) => setSearch(e.target.value)
1441
3665
  })]
3666
+ }),
3667
+ (0, react_jsx_runtime.jsxs)("label", {
3668
+ className: "dsh-ig-workspace-filter-label",
3669
+ title: t("filterCurrentWorkspaceHint"),
3670
+ children: [
3671
+ (0, react_jsx_runtime.jsx)("input", {
3672
+ type: "checkbox",
3673
+ className: "dsh-ig-workspace-checkbox",
3674
+ checked: onlyCurrentWorkspace,
3675
+ onChange: (e) => setOnlyCurrentWorkspace(e.target.checked)
3676
+ }),
3677
+ (0, react_jsx_runtime.jsx)("span", {
3678
+ className: "dsh-ig-workspace-filter-text",
3679
+ children: t("filterCurrentWorkspace")
3680
+ }),
3681
+ (0, react_jsx_runtime.jsxs)("span", {
3682
+ className: "dsh-ig-workspace-filter-badge",
3683
+ children: [
3684
+ workspaceItemsCount,
3685
+ "/",
3686
+ items.length
3687
+ ]
3688
+ })
3689
+ ]
1442
3690
  })
1443
3691
  ]
1444
3692
  }), (0, react_jsx_runtime.jsxs)("div", {
@@ -1467,7 +3715,7 @@ window.__ModuleLoader__.load({
1467
3715
  })]
1468
3716
  }),
1469
3717
  (0, react_jsx_runtime.jsx)("div", {
1470
- className: "dsh-ig-gallery-page-body",
3718
+ className: `dsh-ig-gallery-page-body ${activeTab === "studio" ? "is-workbench" : ""}`,
1471
3719
  children: activeTab === "gallery" || activeTab === "favorites" ? items.length === 0 ? (0, react_jsx_runtime.jsxs)("div", {
1472
3720
  className: "dsh-ig-gallery-empty",
1473
3721
  children: [
@@ -1531,22 +3779,9 @@ window.__ModuleLoader__.load({
1531
3779
  onBlobLoaded: (b) => blobCache.set(item.id, b),
1532
3780
  onToast: showToast
1533
3781
  }, 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")
3782
+ }) : (0, react_jsx_runtime.jsx)(StudioView, {
3783
+ locale,
3784
+ workspace: activeWorkspace
1550
3785
  })
1551
3786
  }),
1552
3787
  toast && (0, react_jsx_runtime.jsx)("div", {
@@ -1624,7 +3859,7 @@ window.__ModuleLoader__.load({
1624
3859
  children: [(0, react_jsx_runtime.jsx)("p", {
1625
3860
  className: "dsh-ig-modal-desc",
1626
3861
  children: t("batchDeleteDesc")
1627
- }), (0, react_jsx_runtime.jsxs)("label", {
3862
+ }), hasWorkspaceFilesToDelete && (0, react_jsx_runtime.jsxs)("label", {
1628
3863
  className: "dsh-ig-modal-checkbox-label",
1629
3864
  children: [(0, react_jsx_runtime.jsx)("input", {
1630
3865
  type: "checkbox",
@@ -1656,6 +3891,74 @@ window.__ModuleLoader__.load({
1656
3891
  ]
1657
3892
  })
1658
3893
  }),
3894
+ showRegenerateModal && previewItem && (0, react_jsx_runtime.jsx)("div", {
3895
+ className: "dsh-ig-modal-backdrop",
3896
+ onClick: handleCancelRegeneratePrompt,
3897
+ children: (0, react_jsx_runtime.jsxs)("div", {
3898
+ className: "dsh-ig-modal-box dsh-ig-regenerate-modal-box",
3899
+ onClick: (e) => e.stopPropagation(),
3900
+ children: [
3901
+ (0, react_jsx_runtime.jsxs)("div", {
3902
+ className: "dsh-ig-modal-header",
3903
+ children: [(0, react_jsx_runtime.jsx)("div", {
3904
+ className: "dsh-ig-regenerate-modal-icon",
3905
+ children: (0, react_jsx_runtime.jsx)(Sparkles, { size: 18 })
3906
+ }), (0, react_jsx_runtime.jsxs)("div", {
3907
+ className: "dsh-ig-regenerate-modal-title-wrap",
3908
+ children: [(0, react_jsx_runtime.jsx)("div", {
3909
+ className: "dsh-ig-modal-title",
3910
+ children: t("regenerateTitle")
3911
+ }), (0, react_jsx_runtime.jsxs)("div", {
3912
+ className: "dsh-ig-regenerate-modal-meta",
3913
+ children: [
3914
+ (0, react_jsx_runtime.jsx)("span", {
3915
+ className: "dsh-ig-tag",
3916
+ children: previewItem.provider
3917
+ }),
3918
+ previewItem.model && (0, react_jsx_runtime.jsx)("span", {
3919
+ className: "dsh-ig-tag dsh-ig-tag-model",
3920
+ children: previewItem.model
3921
+ }),
3922
+ (0, react_jsx_runtime.jsx)("span", {
3923
+ className: "dsh-ig-tag",
3924
+ children: formatCardMeta(previewItem)
3925
+ })
3926
+ ]
3927
+ })]
3928
+ })]
3929
+ }),
3930
+ (0, react_jsx_runtime.jsxs)("div", {
3931
+ className: "dsh-ig-modal-body",
3932
+ children: [(0, react_jsx_runtime.jsx)("p", {
3933
+ className: "dsh-ig-modal-desc",
3934
+ children: t("regenerateHint")
3935
+ }), (0, react_jsx_runtime.jsx)("textarea", {
3936
+ className: "dsh-ig-regenerate-modal-textarea",
3937
+ value: regeneratePrompt,
3938
+ onChange: (e) => setRegeneratePrompt(e.target.value),
3939
+ placeholder: t("prompt"),
3940
+ rows: 4,
3941
+ autoFocus: true
3942
+ })]
3943
+ }),
3944
+ (0, react_jsx_runtime.jsxs)("div", {
3945
+ className: "dsh-ig-modal-footer",
3946
+ children: [(0, react_jsx_runtime.jsx)("button", {
3947
+ type: "button",
3948
+ className: "dsh-ig-modal-btn dsh-ig-modal-btn-cancel",
3949
+ onClick: handleCancelRegeneratePrompt,
3950
+ children: t("cancel")
3951
+ }), (0, react_jsx_runtime.jsxs)("button", {
3952
+ type: "button",
3953
+ className: "dsh-ig-modal-btn dsh-ig-modal-btn-primary",
3954
+ onClick: handleConfirmRegenerate,
3955
+ disabled: regeneratePrompt.trim().length === 0,
3956
+ children: [(0, react_jsx_runtime.jsx)(Sparkles, { size: 14 }), (0, react_jsx_runtime.jsx)("span", { children: t("confirmRegenerate") })]
3957
+ })]
3958
+ })
3959
+ ]
3960
+ })
3961
+ }),
1659
3962
  previewItem && (0, react_jsx_runtime.jsxs)("div", {
1660
3963
  className: "dsh-ig-lightbox-backdrop",
1661
3964
  onClick: handleClosePreview,
@@ -1771,6 +4074,27 @@ window.__ModuleLoader__.load({
1771
4074
  },
1772
4075
  children: [(0, react_jsx_runtime.jsx)(Download, { size: 14 }), (0, react_jsx_runtime.jsx)("span", { children: t("download") })]
1773
4076
  }),
4077
+ (0, react_jsx_runtime.jsxs)("button", {
4078
+ type: "button",
4079
+ className: "dsh-ig-lightbox-btn dsh-ig-lightbox-btn-regenerate",
4080
+ title: t("regenerate"),
4081
+ disabled: isRegenerating,
4082
+ onClick: handleOpenRegenerate,
4083
+ children: [(0, react_jsx_runtime.jsx)(Sparkles, { size: 14 }), (0, react_jsx_runtime.jsx)("span", { children: t("regenerate") })]
4084
+ }),
4085
+ isRegenerating && (0, react_jsx_runtime.jsxs)("div", {
4086
+ className: "dsh-ig-lightbox-generating-indicator",
4087
+ children: [
4088
+ (0, react_jsx_runtime.jsx)("div", { className: "dsh-ig-lightbox-spinner-sm" }),
4089
+ (0, react_jsx_runtime.jsx)("span", { children: t("regenerating") }),
4090
+ (0, react_jsx_runtime.jsx)("button", {
4091
+ type: "button",
4092
+ className: "dsh-ig-lightbox-abort-btn",
4093
+ onClick: handleAbortRegenerating,
4094
+ children: t("cancel")
4095
+ })
4096
+ ]
4097
+ }),
1774
4098
  (0, react_jsx_runtime.jsxs)("button", {
1775
4099
  type: "button",
1776
4100
  className: "dsh-ig-lightbox-btn dsh-ig-lightbox-btn-danger",
@@ -1963,68 +4287,6 @@ window.__ModuleLoader__.load({
1963
4287
  })]
1964
4288
  });
1965
4289
  };
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
4290
  //#endregion
2029
4291
  //#region lib/types/client/image-ref.js
2030
4292
  /**
@@ -2038,6 +4300,116 @@ window.__ModuleLoader__.load({
2038
4300
  const fromBlockContent = Array.isArray(block.content) ? block.content.find((item) => item.type === "image") : void 0;
2039
4301
  if (fromBlockContent?.type === "image") return fromBlockContent.attachment;
2040
4302
  }
4303
+ //#endregion
4304
+ //#region lib/types/client/studio-style.js
4305
+ const STUDIO_STYLE = `
4306
+ .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}
4307
+ .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%)}
4308
+ .dsh-ig-workbench *{box-sizing:border-box}.dsh-ig-workbench button,.dsh-ig-workbench select,.dsh-ig-workbench textarea{font:inherit;color:inherit}
4309
+ .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}
4310
+ .dsh-ig-workbench-grid.is-sidebar-collapsed{grid-template-columns:minmax(0,1fr) minmax(286px,340px)}
4311
+ .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)}
4312
+ .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}
4313
+ .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}
4314
+ .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}
4315
+ .dsh-ig-collapse-btn:hover{border-color:var(--ig-line);background:var(--ig-soft);color:var(--foreground,#182033)}
4316
+ .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}
4317
+ .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}
4318
+ .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}
4319
+ .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)}
4320
+ .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)}
4321
+ .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)}
4322
+ .dsh-ig-canvas-toolbar-left,.dsh-ig-canvas-toolbar-right{display:flex;align-items:center;gap:6px}
4323
+ .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}
4324
+ .dsh-ig-canvas-toolbar button:hover{border-color:#9bb4f5;background:rgba(47,100,245,.05);color:var(--ig-blue)}
4325
+ .dsh-ig-canvas-toolbar button.is-active{border-color:#8ba8f8;background:rgba(47,100,245,.08);color:var(--ig-blue);font-weight:550}
4326
+ .dsh-ig-zoom-display{font-size:11px;min-width:38px;text-align:center;color:#64748b;user-select:none;font-variant-numeric:tabular-nums}
4327
+ .dsh-ig-status-badge-wrap{position:relative;display:inline-flex;align-items:center}
4328
+ .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}
4329
+ .dsh-ig-status-badge:hover{border-color:#9bb4f5;background:rgba(47,100,245,.04)}
4330
+ .dsh-ig-status-dot{width:7px;height:7px;min-width:7px;max-width:7px;border-radius:50%;flex-shrink:0;box-sizing:border-box}
4331
+ .dsh-ig-status-dot.is-ready{background:#10b981;box-shadow:0 0 0 2px rgba(16,185,129,.2)}
4332
+ .dsh-ig-status-dot.is-muted{background:#ef4444;box-shadow:0 0 0 2px rgba(239,68,68,.2)}
4333
+ .dsh-ig-status-text{font-size:11.5px;font-weight:500;color:var(--foreground,#242f44);line-height:1}
4334
+ .dsh-ig-status-arrow{color:#94a3b8;transition:transform .15s,color .15s}
4335
+ .dsh-ig-status-badge-wrap:hover .dsh-ig-status-arrow{transform:rotate(180deg);color:var(--ig-blue)}
4336
+ .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)}
4337
+ .dsh-ig-status-badge-wrap:hover .dsh-ig-status-popover{opacity:1;pointer-events:auto;transform:translateY(0)}
4338
+ .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}
4339
+ .dsh-ig-status-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:5px}
4340
+ .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}
4341
+ .dsh-ig-status-list li.is-configured{background:rgba(16,185,129,.05);border:1px solid rgba(16,185,129,.12)}
4342
+ .dsh-ig-status-list li.is-missing{background:rgba(239,68,68,.04);border:1px solid rgba(239,68,68,.12);opacity:.75}
4343
+ .dsh-ig-status-item-name{display:flex;flex-direction:column;min-width:0}
4344
+ .dsh-ig-status-item-name strong{font-size:11.5px;font-weight:600;color:var(--foreground,#182033);line-height:16px}
4345
+ .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}
4346
+ .dsh-ig-status-item-tag{font-size:10px;font-weight:600;padding:2px 6px;border-radius:4px;flex-shrink:0}
4347
+ .is-configured .dsh-ig-status-item-tag{color:#059669;background:rgba(16,185,129,.14)}
4348
+ .is-missing .dsh-ig-status-item-tag{color:#dc2626;background:rgba(239,68,68,.14)}
4349
+ .dsh-ig-canvas{position:relative;flex:1 1 0;height:0;min-height:0;display:flex;align-items:center;justify-content:center;overflow:hidden;padding:24px;touch-action:none;user-select:none;cursor:grab;background-image:linear-gradient(45deg,rgba(114,128,156,.035) 25%,transparent 25%),linear-gradient(-45deg,rgba(114,128,156,.035) 25%,transparent 25%),linear-gradient(45deg,transparent 75%,rgba(114,128,156,.035) 75%),linear-gradient(-45deg,transparent 75%,rgba(114,128,156,.035) 75%);background-size:24px 24px;background-position:0 0,0 12px,12px -12px,-12px 0}
4350
+ .dsh-ig-canvas.is-dragging,.dsh-ig-canvas.is-dragging *{cursor:grabbing!important}
4351
+ .dsh-ig-canvas img{display:block;max-width:100%;max-height:100%;width:auto;height:auto;border-radius:10px;box-shadow:0 14px 45px rgba(25,35,63,.18);user-select:none;-webkit-user-drag:none;pointer-events:auto}
4352
+ .dsh-ig-canvas-empty,.dsh-ig-generating-state{display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;gap:12px;color:#929bad;font-size:12px;padding:26px;cursor:default}
4353
+ .dsh-ig-generating-state strong{font-size:15px;color:var(--foreground,#182033)}.dsh-ig-generating-state span{font-size:11px}.dsh-ig-generation-orbit{width:66px;height:66px;border-radius:50%;display:grid;place-items:center;color:var(--ig-blue);background:rgba(47,100,245,.09);box-shadow:0 0 0 10px rgba(47,100,245,.035);animation:dshIgPulse 1.8s ease-in-out infinite}
4354
+ .dsh-ig-result-strip{flex:0 0 auto;min-height:52px;padding:8px 14px;display:flex;align-items:center;justify-content:space-between;gap:12px;border-top:1px solid var(--ig-line)}.dsh-ig-result-strip>span{font-size:12px;font-weight:650}.dsh-ig-result-strip>div,.dsh-ig-result-actions>div{display:flex;gap:7px}.dsh-ig-result-strip button,.dsh-ig-result-actions button{height:32px;display:inline-flex;align-items:center;gap:6px;padding:0 10px;border:1px solid var(--ig-line);border-radius:7px;background:var(--background,#fff);font-size:11px;cursor:pointer}.dsh-ig-result-strip button:hover,.dsh-ig-result-actions button:hover{border-color:#9fb2ed;color:var(--ig-blue)}.dsh-ig-result-actions{flex:0 0 auto;min-height:54px;padding:8px 14px;display:flex;align-items:center;justify-content:space-between;gap:12px;border-top:1px solid var(--ig-line)}.dsh-ig-result-actions p{margin:0;min-width:0;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;font-size:11.5px;line-height:18px;color:#596277}.dsh-ig-result-meta{flex:0 0 auto;min-height:32px;padding:6px 14px;display:flex;gap:13px;align-items:center;flex-wrap:wrap;border-top:1px solid var(--ig-line);font-size:10px;color:#8790a3}.dsh-ig-result-meta span+span:before{content:'·';margin-right:13px}
4355
+ .dsh-ig-generate-panel{height:100%;display:flex;flex-direction:column;border-left:1px solid var(--ig-line);overflow:hidden}.dsh-ig-panel-tabs{height:48px;flex:0 0 48px;display:grid;grid-template-columns:1fr 1fr;border-bottom:1px solid var(--ig-line);padding:0 14px}.dsh-ig-panel-tabs button{position:relative;border:0;background:transparent;font-size:12px;color:#7c8598;cursor:pointer}.dsh-ig-panel-tabs button.is-active{color:var(--ig-blue);font-weight:650}.dsh-ig-panel-tabs button.is-active:after{content:'';position:absolute;left:20%;right:20%;bottom:-1px;height:2px;background:var(--ig-blue);border-radius:2px 2px 0 0}.dsh-ig-generator-form{flex:1 1 0;height:0;padding:15px;display:flex;flex-direction:column;gap:15px;overflow-x:hidden;overflow-y:auto;min-height:0}.dsh-ig-mode-switch{display:grid;grid-template-columns:1fr 1fr;gap:8px}.dsh-ig-mode-switch button{height:39px;border:1px solid var(--ig-line);border-radius:8px;background:var(--background,#fff);display:flex;align-items:center;justify-content:center;gap:7px;font-size:12px;cursor:pointer}.dsh-ig-mode-switch button.is-active{border-color:#789bff;color:var(--ig-blue);background:rgba(47,100,245,.065);box-shadow:inset 0 0 0 1px rgba(47,100,245,.1)}
4356
+ .dsh-ig-field{position:relative;display:flex;flex-direction:column;gap:7px}.dsh-ig-field>label,.dsh-ig-field-label label{font-size:11.5px;font-weight:630}.dsh-ig-field>label span{font-weight:400;color:#929bad}.dsh-ig-field-label{display:flex;align-items:center;justify-content:space-between}.dsh-ig-field-label b{color:#e14d4d}.dsh-ig-field-label button{border:0;background:transparent;color:#798397;font-size:10.5px;cursor:pointer}.dsh-ig-field textarea{width:100%;height:112px;padding:11px 12px 22px;resize:vertical;border:1px solid var(--ig-line);border-radius:9px;background:var(--background,#fff);font-size:12px;line-height:19px;outline:none}.dsh-ig-field textarea:focus{border-color:var(--ig-blue);box-shadow:0 0 0 3px rgba(47,100,245,.09)}.dsh-ig-field>small{position:absolute;right:9px;bottom:7px;color:#9aa2b2;font-size:9.5px}.dsh-ig-field-grid{display:grid;grid-template-columns:1fr 1fr;gap:9px}.dsh-ig-field-select{display:flex;flex-direction:column;gap:6px;min-width:0}.dsh-ig-field-select>span{font-size:11px;font-weight:620}.dsh-ig-field-select select{width:100%;height:36px;padding:0 26px 0 9px;border:1px solid var(--ig-line);border-radius:8px;background:var(--background,#fff);font-size:11px;outline:none;text-overflow:ellipsis}.dsh-ig-field-select select:focus{border-color:var(--ig-blue)}.dsh-ig-count-row{display:grid;grid-template-columns:repeat(4,1fr);gap:6px}.dsh-ig-count-pill{height:34px;border:1px solid var(--ig-line);border-radius:7px;background:var(--background,#fff);font-size:12px;font-weight:600;color:#596277;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .15s}.dsh-ig-count-pill:hover{border-color:#9fb2ed;color:var(--ig-blue)}.dsh-ig-count-pill.is-active{border-color:var(--ig-blue);background:rgba(47,100,245,.08);color:var(--ig-blue);box-shadow:inset 0 0 0 1px var(--ig-blue)}
4357
+ .dsh-ig-upload{height:92px;border:1px dashed #bcc6d9;border-radius:9px;background:rgba(255,255,255,.45);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:5px;color:#7d879b;cursor:pointer}.dsh-ig-upload strong{font-size:11px;font-weight:570}.dsh-ig-upload small{font-size:9.5px}.dsh-ig-upload:hover,.dsh-ig-upload.is-dragging{border-color:var(--ig-blue);background:rgba(47,100,245,.05);color:var(--ig-blue)}
4358
+ .dsh-ig-reference-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(64px,1fr));gap:8px}
4359
+ .dsh-ig-reference-card{position:relative;aspect-ratio:1;border-radius:8px;overflow:hidden;background:#eef1f7;border:1px solid var(--ig-line)}
4360
+ .dsh-ig-reference-card img{width:100%;height:100%;object-fit:cover}
4361
+ .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}
4362
+ .dsh-ig-reference-card button:hover{background:#ef4444}
4363
+ .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}
4364
+ .dsh-ig-reference-add:hover{border-color:var(--ig-blue);background:rgba(47,100,245,.05);color:var(--ig-blue)}
4365
+ .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}
4366
+ .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)}
4367
+ .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}
4368
+ .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}
4369
+ .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)}
4370
+ .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}
4371
+ .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}
4372
+ .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}
4373
+ .dsh-ig-workbench-modal-header{display:flex;align-items:center;gap:10px}
4374
+ .dsh-ig-workbench-modal-header strong{font-size:14px;font-weight:650;color:#b91c1c}
4375
+ .dsh-ig-workbench-modal-desc{font-size:12px;line-height:18px;color:#555f77;margin:0}
4376
+ .dsh-ig-workbench-modal-option{display:flex;align-items:center;gap:8px;font-size:11.5px;color:#4b5563;cursor:pointer}
4377
+ .dsh-ig-workbench-modal-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:4px}
4378
+ .dsh-ig-workbench-modal-actions button{height:32px;padding:0 14px;border-radius:7px;font-size:11.5px;font-weight:600;cursor:pointer}
4379
+ .dsh-ig-workbench-modal-cancel{border:1px solid var(--ig-line);background:var(--background,#fff);color:#6b7280}
4380
+ .dsh-ig-workbench-modal-cancel:hover{background:#f3f4f6}
4381
+ .dsh-ig-workbench-modal-danger{border:0;background:#dc2626;color:#fff}
4382
+ .dsh-ig-workbench-modal-danger:hover{background:#b91c1c}
4383
+ @keyframes dshIgPulse{50%{transform:scale(1.06);box-shadow:0 0 0 16px rgba(47,100,245,.025)}}
4384
+ .dsh-ig-workbench-toast{position:fixed;bottom:28px;left:50%;transform:translateX(-50%);background:rgba(15,23,42,0.88);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);color:#fff;padding:8px 18px;border-radius:10px;font-size:12.5px;font-weight:500;box-shadow:0 12px 32px rgba(0,0,0,0.3);z-index:99999;animation:dshIgFade .15s ease-out;pointer-events:none}
4385
+ @keyframes dshIgFade{from{opacity:0;transform:translate(-50%,6px)}to{opacity:1;transform:translate(-50%,0)}}
4386
+ @media(max-width:1120px){.dsh-ig-workbench-grid{grid-template-columns:190px minmax(360px,1fr) 300px}.dsh-ig-result-actions button span{display:none}}
4387
+ @media(max-width:900px){.dsh-ig-gallery-page-body.is-workbench{height:auto;overflow:auto}.dsh-ig-workbench{height:auto;min-height:100%;overflow:visible;padding:12px}.dsh-ig-workbench-heading{height:auto;padding-bottom:12px;align-items:center}.dsh-ig-provider-quick>span{display:none}.dsh-ig-workbench-grid{height:auto;display:flex;flex:none;flex-direction:column;overflow:visible}.dsh-ig-recent-panel{height:auto;overflow:hidden;border-right:0;border-bottom:1px solid var(--ig-line)}.dsh-ig-recent-scroll{height:auto;display:flex;overflow-x:auto;overflow-y:hidden;padding:8px}.dsh-ig-recent-item{min-width:220px}.dsh-ig-canvas-column{height:auto;min-height:560px}.dsh-ig-generate-panel{height:auto;border-left:0;border-top:1px solid var(--ig-line);min-height:560px}.dsh-ig-generator-form{height:auto;overflow:visible}.dsh-ig-canvas{height:auto;min-height:360px}}
4388
+ @media(max-width:560px){.dsh-ig-workbench-heading>div{display:block}.dsh-ig-workbench-heading h1{font-size:15px}.dsh-ig-provider-quick select{min-width:128px;max-width:150px}.dsh-ig-canvas{padding:12px;min-height:300px}.dsh-ig-result-strip,.dsh-ig-result-actions{align-items:flex-start;flex-direction:column}.dsh-ig-result-strip>div,.dsh-ig-result-actions>div{width:100%}.dsh-ig-result-strip button,.dsh-ig-result-actions button{flex:1;justify-content:center}.dsh-ig-result-actions button span{display:inline}.dsh-ig-field-grid{grid-template-columns:1fr}.dsh-ig-result-meta span+span:before{display:none}}
4389
+ @media(prefers-reduced-motion:reduce){.dsh-ig-workbench *{scroll-behavior:auto!important;animation-duration:.01ms!important;transition-duration:.01ms!important}}
4390
+ .dsh-ig-workspace-filter-label{height:30px;display:inline-flex;align-items:center;gap:6px;padding:0 10px;border:1px solid var(--ig-line,rgba(0,0,0,.1));border-radius:7px;background:var(--background,#fff);font-size:11.5px;color:var(--foreground,#242f44);cursor:pointer;user-select:none;transition:border-color .15s,background .15s}
4391
+ .dsh-ig-workspace-filter-label:hover{border-color:#9bb4f5;background:rgba(47,100,245,.04)}
4392
+ .dsh-ig-workspace-checkbox{cursor:pointer;accent-color:#2f64f5;width:13.5px;height:13.5px;margin:0}
4393
+ .dsh-ig-workspace-filter-text{font-weight:500;white-space:nowrap}
4394
+ .dsh-ig-workspace-filter-badge{font-size:10px;font-weight:600;padding:1px 6px;border-radius:999px;background:color-mix(in srgb,currentColor 7%,transparent);color:#64748b;font-variant-numeric:tabular-nums}
4395
+ .dsh-ig-canvas-cluster{display:grid;gap:16px;align-items:center;justify-content:center;user-select:none;pointer-events:auto;flex-shrink:0}
4396
+ .dsh-ig-canvas-cluster[data-count="2"]{grid-template-columns:repeat(2,1fr);max-width:880px}
4397
+ .dsh-ig-canvas-cluster[data-count="3"],.dsh-ig-canvas-cluster[data-count="4"]{grid-template-columns:repeat(2,1fr);max-width:760px}
4398
+ .dsh-ig-canvas-item{position:relative;display:flex;align-items:center;justify-content:center;border-radius:10px;cursor:pointer;transition:transform .12s ease-out}
4399
+ .dsh-ig-canvas-item img{display:block;max-width:100%;max-height:360px;width:auto;height:auto;object-fit:contain;border-radius:10px;box-shadow:0 10px 30px rgba(25,35,63,.16);user-select:none;-webkit-user-drag:none;pointer-events:auto;transition:box-shadow .15s,outline .15s}
4400
+ .dsh-ig-canvas-item:hover img{box-shadow:0 14px 40px rgba(47,100,245,.2)}
4401
+ .dsh-ig-canvas-item.is-selected img{outline:3px solid var(--ig-blue);outline-offset:2px;box-shadow:0 14px 40px rgba(47,100,245,.35)}
4402
+ .dsh-ig-canvas-badge{position:absolute;top:10px;left:10px;background:rgba(15,23,42,.7);color:#fff;font-size:11px;font-weight:700;padding:2px 7px;border-radius:5px;backdrop-filter:blur(4px);pointer-events:none;transition:background .15s;z-index:2}
4403
+ .dsh-ig-canvas-item.is-selected .dsh-ig-canvas-badge{background:var(--ig-blue)}
4404
+ .dsh-ig-canvas-item-loading{width:260px;height:260px;display:flex;align-items:center;justify-content:center;background:rgba(255,255,255,.45);border:1px solid var(--ig-line);border-radius:10px;color:#929bad}
4405
+ .dsh-ig-lightbox-nav{position:absolute;top:50%;transform:translateY(-50%);width:42px;height:42px;border-radius:50%;border:0;background:rgba(15,23,42,.65);color:#fff;display:grid;place-items:center;cursor:pointer;transition:background .15s,transform .15s;z-index:10;backdrop-filter:blur(4px)}
4406
+ .dsh-ig-lightbox-nav:hover{background:rgba(47,100,245,.85);transform:translateY(-50%) scale(1.08)}
4407
+ .dsh-ig-lightbox-nav.is-prev{left:18px}
4408
+ .dsh-ig-lightbox-nav.is-next{right:18px}
4409
+ .dsh-ig-save-gallery-btn{color:var(--ig-blue)!important;border-color:rgba(47,100,245,.35)!important;background:rgba(47,100,245,.06)!important}
4410
+ .dsh-ig-save-gallery-btn:hover{background:rgba(47,100,245,.12)!important}
4411
+ .dsh-ig-save-gallery-btn.is-saved{color:#64748b!important;border-color:var(--ig-line)!important;background:transparent!important}
4412
+ `;
2041
4413
  //#endregion
2042
4414
  //#region lib/types/client/image-result-node.js
2043
4415
  const IMAGE_RESULT_NODE_KIND = "dsh-image-result";
@@ -2179,6 +4551,75 @@ window.__ModuleLoader__.load({
2179
4551
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
2180
4552
  }
2181
4553
  //#endregion
4554
+ //#region lib/types/client/conversation-image-revisions.js
4555
+ const STORAGE_PREFIX = "dsh_image_gen_conversation_revisions:";
4556
+ /** Read the locally persisted replacement history for one immutable conversation image. */
4557
+ function loadConversationImageRevisionChain(originId) {
4558
+ const empty = () => ({
4559
+ originId,
4560
+ currentIndex: 0,
4561
+ revisions: []
4562
+ });
4563
+ try {
4564
+ const raw = globalThis.localStorage?.getItem(storageKey(originId));
4565
+ if (!raw) return empty();
4566
+ const value = JSON.parse(raw);
4567
+ if (!isChain(value, originId)) return empty();
4568
+ return {
4569
+ originId,
4570
+ currentIndex: Math.min(value.currentIndex, value.revisions.length),
4571
+ revisions: value.revisions
4572
+ };
4573
+ } catch {
4574
+ return empty();
4575
+ }
4576
+ }
4577
+ /** Append a generated version and make it the version shown by the conversation card. */
4578
+ function appendConversationImageRevision(originId, revision) {
4579
+ const revisions = [...loadConversationImageRevisionChain(originId).revisions.filter((item) => item.attachment.attachmentId !== revision.attachment.attachmentId), revision].slice(-20);
4580
+ const next = {
4581
+ originId,
4582
+ currentIndex: revisions.length,
4583
+ revisions
4584
+ };
4585
+ persist(next);
4586
+ return next;
4587
+ }
4588
+ /** Select an earlier or later version without altering the underlying conversation event. */
4589
+ function selectConversationImageRevision(originId, requestedIndex) {
4590
+ const current = loadConversationImageRevisionChain(originId);
4591
+ const currentIndex = Math.max(0, Math.min(Math.trunc(requestedIndex), current.revisions.length));
4592
+ const next = {
4593
+ ...current,
4594
+ currentIndex
4595
+ };
4596
+ persist(next);
4597
+ return next;
4598
+ }
4599
+ function persist(chain) {
4600
+ try {
4601
+ globalThis.localStorage?.setItem(storageKey(chain.originId), JSON.stringify(chain));
4602
+ } catch {}
4603
+ }
4604
+ function storageKey(originId) {
4605
+ return `${STORAGE_PREFIX}${originId}`;
4606
+ }
4607
+ function isChain(value, originId) {
4608
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
4609
+ const candidate = value;
4610
+ return candidate.originId === originId && typeof candidate.currentIndex === "number" && Number.isSafeInteger(candidate.currentIndex) && candidate.currentIndex >= 0 && Array.isArray(candidate.revisions) && candidate.revisions.every(isRevision);
4611
+ }
4612
+ function isRevision(value) {
4613
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
4614
+ const candidate = value;
4615
+ 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";
4616
+ }
4617
+ function isAttachment(value) {
4618
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
4619
+ const candidate = value;
4620
+ 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";
4621
+ }
4622
+ //#endregion
2182
4623
  //#region lib/types/client/index.js
2183
4624
  const KEY_REF = {
2184
4625
  google: "GEMINI_API_KEY",
@@ -2242,7 +4683,18 @@ window.__ModuleLoader__.load({
2242
4683
  download: "下载图片",
2243
4684
  openNewTab: "新标签页打开",
2244
4685
  copiedImage: "已复制图片",
2245
- copyFailed: "复制失败"
4686
+ copyFailed: "复制失败",
4687
+ regenerate: "重新生成",
4688
+ regenerateTitle: "重新生成图片",
4689
+ regenerateHint: "如有需要可微调提示词。生成的新图片将替代当前展示,原图依然可在版本中查看。",
4690
+ prompt: "提示词",
4691
+ cancel: "取消",
4692
+ confirmRegenerate: "确认生成",
4693
+ regenerating: "重新生成中…",
4694
+ regenerateFailed: "重新生成失败",
4695
+ versionPrevious: "上一版本",
4696
+ versionNext: "下一版本",
4697
+ versionLabel: "图片版本 {current}/{total}"
2246
4698
  },
2247
4699
  en: {
2248
4700
  title: "Image Generation",
@@ -2299,7 +4751,18 @@ window.__ModuleLoader__.load({
2299
4751
  download: "Download Image",
2300
4752
  openNewTab: "Open in new tab",
2301
4753
  copiedImage: "Image copied",
2302
- copyFailed: "Copy failed"
4754
+ copyFailed: "Copy failed",
4755
+ regenerate: "Regenerate",
4756
+ regenerateTitle: "Regenerate image",
4757
+ regenerateHint: "Edit the prompt if needed. The new image replaces this view while the original remains available.",
4758
+ prompt: "Prompt",
4759
+ cancel: "Cancel",
4760
+ confirmRegenerate: "Regenerate",
4761
+ regenerating: "Regenerating…",
4762
+ regenerateFailed: "Regeneration failed",
4763
+ versionPrevious: "Previous version",
4764
+ versionNext: "Next version",
4765
+ versionLabel: "Image version {current}/{total}"
2303
4766
  }
2304
4767
  };
2305
4768
  const STYLE = `
@@ -2345,14 +4808,39 @@ window.__ModuleLoader__.load({
2345
4808
  .dsh-ig-result{display:grid;gap:10px;max-width:520px}
2346
4809
  .dsh-ig-promoted-results{display:grid;gap:16px}
2347
4810
  .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}
4811
+ .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}
4812
+ .dsh-ig-container:hover .dsh-ig-toolbar,.dsh-ig-container:focus-within .dsh-ig-toolbar{opacity:1;pointer-events:auto}
4813
+ .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
4814
  .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
4815
  .dsh-ig-tool-btn:hover{background:rgba(255,255,255,0.25)}
2353
4816
  .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}
4817
+ .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
4818
  .dsh-ig-image{display:block;max-width:100%;max-height:520px;border-radius:12px;background:#f2f3f5;cursor:pointer}
4819
+ .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}
4820
+ .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}
4821
+ .dsh-ig-version-nav button:hover:not(:disabled){background:rgba(255,255,255,.2)}
4822
+ .dsh-ig-version-nav button:disabled{opacity:.3;cursor:default}
4823
+ .dsh-ig-version-count{min-width:34px;text-align:center;font-variant-numeric:tabular-nums}
4824
+ .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}
4825
+ .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}
4826
+ .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)}
4827
+ .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}
4828
+ .dsh-ig-regenerate-overlay-cancel:hover{background:rgba(255,255,255,0.3)}
4829
+ @keyframes dsh-ig-spin{to{transform:rotate(360deg)}}
4830
+ .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}
4831
+ .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}
4832
+ .dsh-ig-regenerate-dialog h3{margin:0;font-size:16px;font-weight:650}
4833
+ .dsh-ig-regenerate-dialog p{margin:7px 0 16px;color:var(--dsw-alias-label-tertiary,#737d8f);font-size:12.5px;line-height:1.55}
4834
+ .dsh-ig-regenerate-dialog label{display:grid;gap:7px;font-size:12.5px;font-weight:600}
4835
+ .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}
4836
+ .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)}
4837
+ .dsh-ig-regenerate-error{margin-top:10px!important;color:var(--dsw-alias-label-error,#d33)!important}
4838
+ .dsh-ig-regenerate-actions{display:flex;justify-content:flex-end;gap:9px;margin-top:16px}
4839
+ .dsh-ig-regenerate-actions button{height:35px;padding:0 14px;border-radius:7px;font:inherit;font-size:13px;font-weight:550;cursor:pointer}
4840
+ .dsh-ig-regenerate-cancel{border:1px solid var(--dsw-alias-border-l2,#d7dce5);background:transparent;color:inherit}
4841
+ .dsh-ig-regenerate-confirm{border:1px solid var(--dsw-alias-brand-primary,#3569ed);background:var(--dsw-alias-brand-primary,#3569ed);color:#fff}
4842
+ .dsh-ig-regenerate-actions button:disabled{opacity:.5;cursor:default}
4843
+ @media(hover:none){.dsh-ig-container .dsh-ig-toolbar{opacity:1;pointer-events:auto}}
2356
4844
  @keyframes dsh-ig-fade{from{opacity:0}to{opacity:1}}
2357
4845
  .dsh-ig-error{color:var(--dsw-alias-label-error,#d33);font-size:13px}
2358
4846
  .dsh-ig-loading{color:var(--dsw-alias-label-tertiary,#7b818b);font-size:13px}
@@ -2483,7 +4971,7 @@ window.__ModuleLoader__.load({
2483
4971
  .dsh-ig-batch-btn-exit:hover{background:rgba(255,255,255,0.1);color:#fff}
2484
4972
 
2485
4973
  /* 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}
4974
+ .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
4975
  .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
4976
  @keyframes dsh-ig-scale-up{from{transform:scale(0.95);opacity:0}to{transform:scale(1);opacity:1}}
2489
4977
  .dsh-ig-modal-header{display:flex;align-items:flex-start;gap:12px;margin-bottom:12px}
@@ -2499,9 +4987,26 @@ window.__ModuleLoader__.load({
2499
4987
  .dsh-ig-modal-btn-cancel:hover{background:var(--dsw-alias-bg-layer-3,rgba(255,255,255,0.08))}
2500
4988
  .dsh-ig-modal-btn-danger{background:#dc2626;border:1px solid #dc2626;color:#fff}
2501
4989
  .dsh-ig-modal-btn-danger:hover{background:#b91c1c;border-color:#b91c1c}
4990
+ .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}
4991
+ .dsh-ig-modal-btn-primary:hover:not(:disabled){background:linear-gradient(135deg,#2563eb,#1d4ed8)}
4992
+ .dsh-ig-modal-btn-primary:disabled{opacity:0.5;cursor:not-allowed}
4993
+ .dsh-ig-regenerate-modal-box{max-width:520px}
4994
+ .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}
4995
+ .dsh-ig-regenerate-modal-title-wrap{display:flex;flex-direction:column;gap:4px}
4996
+ .dsh-ig-regenerate-modal-meta{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
4997
+ .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}
4998
+ .dsh-ig-regenerate-modal-textarea:focus{border-color:#3b82f6;box-shadow:0 0 0 2px rgba(59,130,246,0.2)}
4999
+ .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}
5000
+ .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}
5001
+ .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}
5002
+ .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}
5003
+ .dsh-ig-lightbox-abort-btn:hover{color:#ef4444}
2502
5004
 
2503
- /* Hide floating chat composer when gallery page is active */
5005
+ /* Hide floating chat composer and width handles when gallery page is active */
2504
5006
  [data-conversation-scroll]:has(.dsh-ig-gallery-page) [data-composer-seat]{display:none!important}
5007
+ :has(> [data-conversation-scroll]:has(.dsh-ig-gallery-page)) > [class*="widthHandle"],
5008
+ :has(.dsh-ig-gallery-page) [class*="widthHandle"],
5009
+ .root:has(.dsh-ig-gallery-page) [class*="widthHandle"]{display:none!important}
2505
5010
  `;
2506
5011
  /** Required browser services. */
2507
5012
  const inject = [
@@ -2521,7 +5026,7 @@ window.__ModuleLoader__.load({
2521
5026
  ctx.effect(() => {
2522
5027
  const style = document.createElement("style");
2523
5028
  style.dataset.plugin = "dsh-image-gen";
2524
- style.textContent = STYLE;
5029
+ style.textContent = `${STYLE}\n${STUDIO_STYLE}`;
2525
5030
  document.head.appendChild(style);
2526
5031
  return () => {
2527
5032
  style.remove();
@@ -3106,7 +5611,8 @@ window.__ModuleLoader__.load({
3106
5611
  if (props.promoted && result !== void 0) return (0, react_jsx_runtime.jsx)(PromotedResultNotice, { locale: props.locale });
3107
5612
  return (0, react_jsx_runtime.jsx)(ImageResultCard, {
3108
5613
  result,
3109
- locale: props.locale
5614
+ locale: props.locale,
5615
+ sessionId: props.sessionId
3110
5616
  });
3111
5617
  }
3112
5618
  /** Render modern image artifacts as final conversation output instead of Tool process content. */
@@ -3115,7 +5621,8 @@ window.__ModuleLoader__.load({
3115
5621
  className: "dsh-ig-promoted-results",
3116
5622
  children: props.node.data.results.map((result) => (0, react_jsx_runtime.jsx)(ImageResultCard, {
3117
5623
  result,
3118
- locale: props.locale
5624
+ locale: props.locale,
5625
+ sessionId: props.sessionId
3119
5626
  }, result.attachment.attachmentId))
3120
5627
  });
3121
5628
  }
@@ -3126,14 +5633,31 @@ window.__ModuleLoader__.load({
3126
5633
  children: DICT[lang].resultShown
3127
5634
  });
3128
5635
  }
3129
- function ImageResultCard({ result, locale }) {
5636
+ function ImageResultCard({ result, locale, sessionId }) {
3130
5637
  const attachment = result?.attachment;
5638
+ const originId = attachment?.attachmentId;
5639
+ const [revisionChain, setRevisionChain] = (0, react.useState)(() => loadConversationImageRevisionChain(String(originId ?? "")));
5640
+ const selectedRevision = revisionChain.revisions[revisionChain.currentIndex - 1];
5641
+ const activeAttachment = selectedRevision?.attachment ?? attachment;
5642
+ const activeResult = selectedRevision !== void 0 ? {
5643
+ prompt: selectedRevision.prompt,
5644
+ provider: selectedRevision.provider,
5645
+ model: selectedRevision.model,
5646
+ output: selectedRevision.output,
5647
+ createdAt: selectedRevision.createdAt
5648
+ } : result;
3131
5649
  const savedTo = result?.savedTo;
3132
5650
  const [url, setUrl] = (0, react.useState)();
3133
5651
  const [blob, setBlob] = (0, react.useState)();
3134
5652
  const [error, setError] = (0, react.useState)();
3135
5653
  const [previewOpen, setPreviewOpen] = (0, react.useState)(false);
3136
5654
  const [toast, setToast] = (0, react.useState)();
5655
+ const [regenerateOpen, setRegenerateOpen] = (0, react.useState)(false);
5656
+ const [regeneratePrompt, setRegeneratePrompt] = (0, react.useState)("");
5657
+ const [regenerateError, setRegenerateError] = (0, react.useState)();
5658
+ const [isRegenerating, setIsRegenerating] = (0, react.useState)(false);
5659
+ const regenerateControllerRef = (0, react.useRef)();
5660
+ const regenerateTextareaRef = (0, react.useRef)(null);
3137
5661
  const lang = usePluginLanguage(locale);
3138
5662
  const t = (keyName, params) => {
3139
5663
  let text = (lang === "en" ? DICT.en : DICT.zh)[keyName] || DICT.zh[keyName] || keyName;
@@ -3150,9 +5674,36 @@ window.__ModuleLoader__.load({
3150
5674
  model: result.model,
3151
5675
  output: result.output,
3152
5676
  ...result.savedTo ? { savedTo: result.savedTo } : {},
3153
- ...result.seed !== void 0 ? { seed: result.seed } : {}
5677
+ ...result.seed !== void 0 ? { seed: result.seed } : {},
5678
+ ...sessionId ? { sessionId } : {}
3154
5679
  });
3155
5680
  }, [result?.attachment.attachmentId]);
5681
+ (0, react.useEffect)(() => {
5682
+ if (!originId) return;
5683
+ regenerateControllerRef.current?.abort();
5684
+ regenerateControllerRef.current = void 0;
5685
+ setIsRegenerating(false);
5686
+ setRegenerateOpen(false);
5687
+ setRevisionChain(loadConversationImageRevisionChain(String(originId)));
5688
+ }, [originId]);
5689
+ (0, react.useEffect)(() => () => {
5690
+ regenerateControllerRef.current?.abort();
5691
+ }, []);
5692
+ (0, react.useEffect)(() => {
5693
+ if (!regenerateOpen || isRegenerating) return;
5694
+ const frame = requestAnimationFrame(() => {
5695
+ regenerateTextareaRef.current?.focus();
5696
+ regenerateTextareaRef.current?.select();
5697
+ });
5698
+ const onKeyDown = (event) => {
5699
+ if (event.key === "Escape") setRegenerateOpen(false);
5700
+ };
5701
+ window.addEventListener("keydown", onKeyDown);
5702
+ return () => {
5703
+ cancelAnimationFrame(frame);
5704
+ window.removeEventListener("keydown", onKeyDown);
5705
+ };
5706
+ }, [regenerateOpen, isRegenerating]);
3156
5707
  (0, react.useEffect)(() => {
3157
5708
  if (!previewOpen) return;
3158
5709
  const onKeyDown = (e) => {
@@ -3163,30 +5714,34 @@ window.__ModuleLoader__.load({
3163
5714
  window.removeEventListener("keydown", onKeyDown);
3164
5715
  };
3165
5716
  }, [previewOpen]);
5717
+ const currentUrlRef = (0, react.useRef)();
3166
5718
  (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;
5719
+ return () => {
5720
+ if (currentUrlRef.current !== void 0) {
5721
+ URL.revokeObjectURL(currentUrlRef.current);
5722
+ currentUrlRef.current = void 0;
5723
+ }
5724
+ };
5725
+ }, []);
5726
+ (0, react.useEffect)(() => {
5727
+ if (activeAttachment === void 0) return;
5728
+ let canceled = false;
5729
+ fetchAttachmentBlob(activeAttachment).then((resBlob) => {
5730
+ if (canceled) return;
3179
5731
  setBlob(resBlob);
3180
- objectUrl = URL.createObjectURL(resBlob);
3181
- setUrl(objectUrl);
5732
+ setError(void 0);
5733
+ const nextUrl = URL.createObjectURL(resBlob);
5734
+ if (currentUrlRef.current !== void 0) URL.revokeObjectURL(currentUrlRef.current);
5735
+ currentUrlRef.current = nextUrl;
5736
+ setUrl(nextUrl);
3182
5737
  }).catch((cause) => {
3183
- if (!controller.signal.aborted) setError(cause instanceof Error ? cause.message : String(cause));
5738
+ if (canceled) return;
5739
+ setError(cause instanceof Error ? cause.message : String(cause));
3184
5740
  });
3185
5741
  return () => {
3186
- controller.abort();
3187
- if (objectUrl !== void 0) URL.revokeObjectURL(objectUrl);
5742
+ canceled = true;
3188
5743
  };
3189
- }, [attachment?.attachmentId, lang]);
5744
+ }, [activeAttachment?.attachmentId]);
3190
5745
  const copy = async (e) => {
3191
5746
  e.stopPropagation();
3192
5747
  if (!blob) return;
@@ -3201,7 +5756,7 @@ window.__ModuleLoader__.load({
3201
5756
  if (!url) return;
3202
5757
  const a = document.createElement("a");
3203
5758
  a.href = url;
3204
- a.download = attachment?.name || `dsh-image-${Date.now()}.png`;
5759
+ a.download = activeAttachment?.name || `dsh-image-${Date.now()}.png`;
3205
5760
  document.body.appendChild(a);
3206
5761
  a.click();
3207
5762
  document.body.removeChild(a);
@@ -3211,6 +5766,93 @@ window.__ModuleLoader__.load({
3211
5766
  if (!url) return;
3212
5767
  window.open(url, "_blank", "noopener,noreferrer");
3213
5768
  };
5769
+ const openRegenerate = (event) => {
5770
+ event.stopPropagation();
5771
+ if (activeResult === void 0 || isRegenerating) return;
5772
+ setRegeneratePrompt(activeResult.prompt);
5773
+ setRegenerateError(void 0);
5774
+ setRegenerateOpen(true);
5775
+ };
5776
+ const cancelRegenerate = () => {
5777
+ regenerateControllerRef.current?.abort();
5778
+ regenerateControllerRef.current = void 0;
5779
+ setIsRegenerating(false);
5780
+ setRegenerateOpen(false);
5781
+ setRegenerateError(void 0);
5782
+ };
5783
+ const regenerate = async () => {
5784
+ if (activeResult === void 0 || !originId || isRegenerating || regeneratePrompt.trim().length === 0) return;
5785
+ setIsRegenerating(true);
5786
+ setRegenerateError(void 0);
5787
+ setRegenerateOpen(false);
5788
+ const controller = new AbortController();
5789
+ regenerateControllerRef.current = controller;
5790
+ try {
5791
+ const request = conversationRegenerateRequest(activeResult, regeneratePrompt, selectedRevision === void 0 ? void 0 : {
5792
+ ratio: selectedRevision.ratio,
5793
+ quality: selectedRevision.quality
5794
+ });
5795
+ const response = await fetch(STUDIO_ROUTE, {
5796
+ method: "POST",
5797
+ credentials: "same-origin",
5798
+ signal: controller.signal,
5799
+ headers: { "content-type": "application/json" },
5800
+ body: JSON.stringify(request)
5801
+ });
5802
+ const payload = await response.json().catch(() => null);
5803
+ if (!response.ok || payload === null || !("attachment" in payload)) throw new Error(payload && "error" in payload && payload.error ? payload.error : t("regenerateFailed"));
5804
+ const revision = {
5805
+ attachment: payload.attachment,
5806
+ prompt: payload.prompt,
5807
+ provider: payload.provider,
5808
+ model: payload.model,
5809
+ output: payload.output,
5810
+ createdAt: payload.createdAt,
5811
+ ratio: request.ratio,
5812
+ quality: request.quality
5813
+ };
5814
+ await saveGalleryItem({
5815
+ id: String(revision.attachment.attachmentId),
5816
+ attachment: revision.attachment,
5817
+ prompt: revision.prompt,
5818
+ provider: revision.provider,
5819
+ model: revision.model,
5820
+ createdAt: revision.createdAt,
5821
+ aspectRatio: revision.ratio,
5822
+ imageSize: revision.quality,
5823
+ output: revision.output,
5824
+ ...savedTo ? { savedTo } : {},
5825
+ ...sessionId ? { sessionId } : {}
5826
+ });
5827
+ const next = appendConversationImageRevision(String(originId), revision);
5828
+ setRevisionChain(next);
5829
+ setToast(t("regenerate"));
5830
+ setTimeout(() => {
5831
+ setToast(void 0);
5832
+ }, 2e3);
5833
+ } catch (cause) {
5834
+ if (!controller.signal.aborted) {
5835
+ const errMsg = cause instanceof Error ? cause.message : String(cause);
5836
+ setError(errMsg);
5837
+ setToast(errMsg);
5838
+ setTimeout(() => {
5839
+ setToast(void 0);
5840
+ }, 3500);
5841
+ }
5842
+ } finally {
5843
+ if (regenerateControllerRef.current === controller) {
5844
+ regenerateControllerRef.current = void 0;
5845
+ setIsRegenerating(false);
5846
+ }
5847
+ }
5848
+ };
5849
+ const selectVersion = (index) => {
5850
+ if (!originId || isRegenerating) return;
5851
+ setRevisionChain(selectConversationImageRevision(String(originId), index));
5852
+ };
5853
+ const canRegenerate = activeResult !== void 0 && CLOUD_IMAGE_PROVIDERS.includes(activeResult.provider) && activeResult.model.trim().length > 0;
5854
+ const versionTotal = revisionChain.revisions.length + 1;
5855
+ const versionCurrent = revisionChain.currentIndex + 1;
3214
5856
  if (attachment === void 0) return (0, react_jsx_runtime.jsx)("div", {
3215
5857
  className: "dsh-ig-loading",
3216
5858
  children: t("generating")
@@ -3241,100 +5883,174 @@ window.__ModuleLoader__.load({
3241
5883
  }) : null,
3242
5884
  url !== void 0 ? (0, react_jsx_runtime.jsxs)("div", {
3243
5885
  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
- ]
5886
+ children: [
5887
+ (0, react_jsx_runtime.jsx)("img", {
5888
+ className: "dsh-ig-image",
5889
+ src: url,
5890
+ alt: activeAttachment?.name ?? "Generated image",
5891
+ onClick: () => {
5892
+ if (!isRegenerating) setPreviewOpen(true);
5893
+ }
5894
+ }),
5895
+ isRegenerating ? (0, react_jsx_runtime.jsxs)("div", {
5896
+ className: "dsh-ig-regenerate-overlay",
5897
+ children: [
5898
+ (0, react_jsx_runtime.jsx)("div", { className: "dsh-ig-regenerate-spinner" }),
5899
+ (0, react_jsx_runtime.jsx)("span", {
5900
+ className: "dsh-ig-regenerate-overlay-text",
5901
+ children: t("regenerating")
5902
+ }),
5903
+ (0, react_jsx_runtime.jsx)("button", {
5904
+ type: "button",
5905
+ className: "dsh-ig-regenerate-overlay-cancel",
5906
+ onClick: cancelRegenerate,
5907
+ children: t("cancel")
3304
5908
  })
5909
+ ]
5910
+ }) : null,
5911
+ (0, react_jsx_runtime.jsxs)("div", {
5912
+ className: "dsh-ig-toolbar",
5913
+ children: [
5914
+ canRegenerate ? (0, react_jsx_runtime.jsx)("button", {
5915
+ type: "button",
5916
+ className: "dsh-ig-tool-btn",
5917
+ disabled: isRegenerating,
5918
+ title: isRegenerating ? t("regenerating") : t("regenerate"),
5919
+ onClick: openRegenerate,
5920
+ children: (0, react_jsx_runtime.jsxs)("svg", {
5921
+ width: "15",
5922
+ height: "15",
5923
+ viewBox: "0 0 24 24",
5924
+ fill: "none",
5925
+ stroke: "currentColor",
5926
+ strokeWidth: "2",
5927
+ strokeLinecap: "round",
5928
+ strokeLinejoin: "round",
5929
+ 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" })]
5930
+ })
5931
+ }) : null,
5932
+ (0, react_jsx_runtime.jsx)("button", {
5933
+ type: "button",
5934
+ className: "dsh-ig-tool-btn",
5935
+ title: t("copyImg"),
5936
+ onClick: (e) => {
5937
+ copy(e);
5938
+ },
5939
+ children: (0, react_jsx_runtime.jsxs)("svg", {
5940
+ width: "15",
5941
+ height: "15",
5942
+ viewBox: "0 0 24 24",
5943
+ fill: "none",
5944
+ stroke: "currentColor",
5945
+ strokeWidth: "2",
5946
+ strokeLinecap: "round",
5947
+ strokeLinejoin: "round",
5948
+ children: [(0, react_jsx_runtime.jsx)("rect", {
5949
+ x: "9",
5950
+ y: "9",
5951
+ width: "13",
5952
+ height: "13",
5953
+ rx: "2",
5954
+ ry: "2"
5955
+ }), (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" })]
5956
+ })
5957
+ }),
5958
+ (0, react_jsx_runtime.jsx)("button", {
5959
+ type: "button",
5960
+ className: "dsh-ig-tool-btn",
5961
+ title: t("download"),
5962
+ onClick: download,
5963
+ children: (0, react_jsx_runtime.jsxs)("svg", {
5964
+ width: "15",
5965
+ height: "15",
5966
+ viewBox: "0 0 24 24",
5967
+ fill: "none",
5968
+ stroke: "currentColor",
5969
+ strokeWidth: "2",
5970
+ strokeLinecap: "round",
5971
+ strokeLinejoin: "round",
5972
+ children: [
5973
+ (0, react_jsx_runtime.jsx)("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
5974
+ (0, react_jsx_runtime.jsx)("polyline", { points: "7 10 12 15 17 10" }),
5975
+ (0, react_jsx_runtime.jsx)("line", {
5976
+ x1: "12",
5977
+ y1: "15",
5978
+ x2: "12",
5979
+ y2: "3"
5980
+ })
5981
+ ]
5982
+ })
5983
+ }),
5984
+ (0, react_jsx_runtime.jsx)("button", {
5985
+ type: "button",
5986
+ className: "dsh-ig-tool-btn",
5987
+ title: t("openNewTab"),
5988
+ onClick: openNewTab,
5989
+ children: (0, react_jsx_runtime.jsxs)("svg", {
5990
+ width: "15",
5991
+ height: "15",
5992
+ viewBox: "0 0 24 24",
5993
+ fill: "none",
5994
+ stroke: "currentColor",
5995
+ strokeWidth: "2",
5996
+ strokeLinecap: "round",
5997
+ strokeLinejoin: "round",
5998
+ children: [
5999
+ (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" }),
6000
+ (0, react_jsx_runtime.jsx)("polyline", { points: "15 3 21 3 21 9" }),
6001
+ (0, react_jsx_runtime.jsx)("line", {
6002
+ x1: "10",
6003
+ y1: "14",
6004
+ x2: "21",
6005
+ y2: "3"
6006
+ })
6007
+ ]
6008
+ })
6009
+ }),
6010
+ toast ? (0, react_jsx_runtime.jsx)("div", {
6011
+ className: "dsh-ig-toast",
6012
+ children: toast
6013
+ }) : null
6014
+ ]
6015
+ }),
6016
+ versionTotal > 1 ? (0, react_jsx_runtime.jsxs)("div", {
6017
+ className: "dsh-ig-version-nav",
6018
+ "aria-label": t("versionLabel", {
6019
+ current: String(versionCurrent),
6020
+ total: String(versionTotal)
3305
6021
  }),
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",
6022
+ children: [
6023
+ (0, react_jsx_runtime.jsx)("button", {
6024
+ type: "button",
6025
+ title: t("versionPrevious"),
6026
+ disabled: revisionChain.currentIndex === 0 || isRegenerating,
6027
+ onClick: (event) => {
6028
+ event.stopPropagation();
6029
+ selectVersion(revisionChain.currentIndex - 1);
6030
+ },
6031
+ children: "‹"
6032
+ }),
6033
+ (0, react_jsx_runtime.jsxs)("span", {
6034
+ className: "dsh-ig-version-count",
3320
6035
  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
- })
6036
+ versionCurrent,
6037
+ "/",
6038
+ versionTotal
3329
6039
  ]
6040
+ }),
6041
+ (0, react_jsx_runtime.jsx)("button", {
6042
+ type: "button",
6043
+ title: t("versionNext"),
6044
+ disabled: revisionChain.currentIndex >= revisionChain.revisions.length || isRegenerating,
6045
+ onClick: (event) => {
6046
+ event.stopPropagation();
6047
+ selectVersion(revisionChain.currentIndex + 1);
6048
+ },
6049
+ children: "›"
3330
6050
  })
3331
- }),
3332
- toast ? (0, react_jsx_runtime.jsx)("div", {
3333
- className: "dsh-ig-toast",
3334
- children: toast
3335
- }) : null
3336
- ]
3337
- })]
6051
+ ]
6052
+ }) : null
6053
+ ]
3338
6054
  }) : null,
3339
6055
  previewOpen && url !== void 0 ? (0, react_jsx_runtime.jsx)("div", {
3340
6056
  className: "dsh-ig-lightbox-backdrop",
@@ -3349,9 +6065,63 @@ window.__ModuleLoader__.load({
3349
6065
  children: (0, react_jsx_runtime.jsx)("img", {
3350
6066
  className: "dsh-ig-lightbox-img",
3351
6067
  src: url,
3352
- alt: attachment.name ?? "Generated image preview"
6068
+ alt: activeAttachment?.name ?? "Generated image preview"
3353
6069
  })
3354
6070
  })
6071
+ }) : null,
6072
+ regenerateOpen && activeResult !== void 0 ? (0, react_jsx_runtime.jsx)("div", {
6073
+ className: "dsh-ig-regenerate-backdrop",
6074
+ onMouseDown: (event) => {
6075
+ if (event.target === event.currentTarget && !isRegenerating) setRegenerateOpen(false);
6076
+ },
6077
+ children: (0, react_jsx_runtime.jsxs)("div", {
6078
+ className: "dsh-ig-regenerate-dialog",
6079
+ role: "dialog",
6080
+ "aria-modal": "true",
6081
+ "aria-labelledby": `dsh-ig-regenerate-${String(originId)}`,
6082
+ children: [
6083
+ (0, react_jsx_runtime.jsx)("h3", {
6084
+ id: `dsh-ig-regenerate-${String(originId)}`,
6085
+ children: t("regenerateTitle")
6086
+ }),
6087
+ (0, react_jsx_runtime.jsx)("p", { children: t("regenerateHint") }),
6088
+ (0, react_jsx_runtime.jsxs)("label", { children: [(0, react_jsx_runtime.jsx)("span", { children: t("prompt") }), (0, react_jsx_runtime.jsx)("textarea", {
6089
+ ref: regenerateTextareaRef,
6090
+ value: regeneratePrompt,
6091
+ maxLength: 2e3,
6092
+ disabled: isRegenerating,
6093
+ onChange: (event) => setRegeneratePrompt(event.target.value),
6094
+ onKeyDown: (event) => {
6095
+ if (event.key === "Enter" && (event.ctrlKey || event.metaKey)) {
6096
+ event.preventDefault();
6097
+ regenerate();
6098
+ }
6099
+ }
6100
+ })] }),
6101
+ regenerateError ? (0, react_jsx_runtime.jsx)("p", {
6102
+ className: "dsh-ig-regenerate-error",
6103
+ role: "alert",
6104
+ children: regenerateError
6105
+ }) : null,
6106
+ (0, react_jsx_runtime.jsxs)("div", {
6107
+ className: "dsh-ig-regenerate-actions",
6108
+ children: [(0, react_jsx_runtime.jsx)("button", {
6109
+ type: "button",
6110
+ className: "dsh-ig-regenerate-cancel",
6111
+ onClick: cancelRegenerate,
6112
+ children: t("cancel")
6113
+ }), (0, react_jsx_runtime.jsx)("button", {
6114
+ type: "button",
6115
+ className: "dsh-ig-regenerate-confirm",
6116
+ disabled: isRegenerating || regeneratePrompt.trim().length === 0,
6117
+ onClick: () => {
6118
+ regenerate();
6119
+ },
6120
+ children: t("confirmRegenerate")
6121
+ })]
6122
+ })
6123
+ ]
6124
+ })
3355
6125
  }) : null
3356
6126
  ]
3357
6127
  });