dsh-image-gen 0.2.4 → 0.2.6

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
@@ -10,6 +10,8 @@ window.__ModuleLoader__.load({
10
10
  /** Values shared by the Host and browser Bundle faces. */
11
11
  /** Browser route used by the generated-image card. */
12
12
  const IMAGE_ROUTE = "/plugins/dsh-image-gen/image";
13
+ /** Browser route used for deleting generated images and workspace files. */
14
+ const DELETE_ROUTE = "/plugins/dsh-image-gen/delete";
13
15
  /** Namespace persisted through DSH Settings. */
14
16
  const IMAGE_GENERATION_NAMESPACE = "image-generation";
15
17
  /** Default endpoints and base URLs. */
@@ -19,12 +21,61 @@ window.__ModuleLoader__.load({
19
21
  const DEFAULT_DASHSCOPE_ENDPOINT = "https://dashscope.aliyuncs.com/api/v1";
20
22
  const DEFAULT_COMFYUI_BASE_URL = "http://127.0.0.1:8188";
21
23
  const DEFAULT_COMFYUI_TIMEOUT_MS = 3e5;
24
+ const DEFAULT_COMFYUI_WORKFLOW_LABEL = "API workflow";
25
+ /** Default model names. */
26
+ const DEFAULT_GOOGLE_MODEL = "gemini-3.1-flash-image";
27
+ const DEFAULT_OPENAI_MODEL = "gpt-image-2";
28
+ const DEFAULT_SEEDREAM_MODEL = "doubao-seedream-5-0-260128";
29
+ const DEFAULT_DASHSCOPE_MODEL = "qwen-image-3.0";
30
+ /** Named workflows, falling back to the legacy single-workflow fields when the list is empty. */
31
+ function resolveComfyUIWorkflows(source) {
32
+ const named = [];
33
+ for (const entry of source.comfyuiWorkflows ?? []) {
34
+ const name = typeof entry?.name === "string" ? entry.name.trim() : "";
35
+ const json = typeof entry?.json === "string" ? entry.json : "";
36
+ if (name.length > 0 && json.trim().length > 0) {
37
+ const presetPrompt = typeof entry.presetPrompt === "string" ? entry.presetPrompt.trim() : "";
38
+ named.push(presetPrompt.length > 0 ? {
39
+ name,
40
+ json,
41
+ presetPrompt
42
+ } : {
43
+ name,
44
+ json
45
+ });
46
+ }
47
+ }
48
+ if (named.length > 0) return named;
49
+ const legacyJson = typeof source.comfyuiWorkflowJson === "string" ? source.comfyuiWorkflowJson : "";
50
+ if (legacyJson.trim().length === 0) return [];
51
+ const legacyName = typeof source.comfyuiWorkflowName === "string" ? source.comfyuiWorkflowName.trim() : "";
52
+ return [{
53
+ name: legacyName.length > 0 ? legacyName : DEFAULT_COMFYUI_WORKFLOW_LABEL,
54
+ json: legacyJson
55
+ }];
56
+ }
57
+ /** The workflow ComfyUI calls use by default: the configured active name, else the first entry. */
58
+ function activeComfyUIWorkflow(source) {
59
+ const workflows = resolveComfyUIWorkflows(source);
60
+ if (workflows.length === 0) return void 0;
61
+ const activeName = typeof source.comfyuiActiveWorkflow === "string" ? source.comfyuiActiveWorkflow.trim() : "";
62
+ return workflows.find((workflow) => workflow.name === activeName) ?? workflows[0];
63
+ }
64
+ /** Derive a workflow label that does not collide with the given existing names. */
65
+ function uniqueComfyUIWorkflowName(name, existing) {
66
+ const base = name.trim().length > 0 ? name.trim() : DEFAULT_COMFYUI_WORKFLOW_LABEL;
67
+ if (!existing.includes(base)) return base;
68
+ for (let index = 2;; index += 1) {
69
+ const candidate = `${base} (${index})`;
70
+ if (!existing.includes(candidate)) return candidate;
71
+ }
72
+ }
22
73
  const DEFAULT_MODELS = {
23
- google: "gemini-3.1-flash-image",
24
- openai: "gpt-image-2",
25
- seedream: "doubao-seedream-5-0-260128",
26
- dashscope: "qwen-image-3.0",
27
- comfyui: "API workflow"
74
+ google: DEFAULT_GOOGLE_MODEL,
75
+ openai: DEFAULT_OPENAI_MODEL,
76
+ seedream: DEFAULT_SEEDREAM_MODEL,
77
+ dashscope: DEFAULT_DASHSCOPE_MODEL,
78
+ comfyui: DEFAULT_COMFYUI_WORKFLOW_LABEL
28
79
  };
29
80
  const DEFAULT_BASE_URLS = {
30
81
  google: DEFAULT_GOOGLE_ENDPOINT,
@@ -37,9 +88,16 @@ window.__ModuleLoader__.load({
37
88
  //#region lib/types/comfyui-workflow.js
38
89
  /** Pure ComfyUI API-workflow validation and placeholder injection. */
39
90
  const COMFYUI_PROMPT_PLACEHOLDER = "{{prompt}}";
91
+ const COMFYUI_IMAGE_PLACEHOLDER = "{{image}}";
92
+ /** Legacy single-percent placeholders from early releases, still accepted. */
93
+ const LEGACY_PROMPT_PLACEHOLDER = "%prompt%";
94
+ const LEGACY_IMAGE_PLACEHOLDER = "%image%";
95
+ /** LoadImage-style input key that receives the uploaded source image name. */
96
+ const IMAGE_INPUT_KEY = "image";
40
97
  /** Validate imported JSON without exposing workflow graph details to callers. */
41
98
  function validateComfyUIWorkflowJson(workflowJson) {
42
- parseWorkflow(workflowJson);
99
+ const imageInputs = countImagePlaceholders(parseWorkflow(workflowJson));
100
+ if (imageInputs > 1) throw new Error(`ComfyUI workflow must contain at most one ${COMFYUI_IMAGE_PLACEHOLDER} image input; found ${String(imageInputs)}`);
43
101
  }
44
102
  function parseWorkflow(workflowJson) {
45
103
  if (workflowJson.trim().length === 0) throw new Error("Import a ComfyUI API workflow JSON file in Settings before generating");
@@ -54,11 +112,23 @@ window.__ModuleLoader__.load({
54
112
  if (!Object.values(value).some((node) => {
55
113
  const inputs = record$1(node)?.inputs;
56
114
  return inputs !== void 0 && containsPromptPlaceholder(inputs);
57
- })) throw new Error(`ComfyUI workflow must contain ${COMFYUI_PROMPT_PLACEHOLDER} in a text input`);
115
+ })) throw new Error(`ComfyUI workflow must contain ${COMFYUI_PROMPT_PLACEHOLDER} (or ${LEGACY_PROMPT_PLACEHOLDER}) in a text input`);
58
116
  return value;
59
117
  }
118
+ /** Count `inputs.image` fields that are exactly an image placeholder. */
119
+ function countImagePlaceholders(workflow) {
120
+ let count = 0;
121
+ for (const node of Object.values(workflow)) {
122
+ const inputs = record$1(record$1(node)?.inputs);
123
+ if (inputs !== void 0 && isImagePlaceholder(inputs[IMAGE_INPUT_KEY])) count += 1;
124
+ }
125
+ return count;
126
+ }
127
+ function isImagePlaceholder(value) {
128
+ return value === "{{image}}" || value === LEGACY_IMAGE_PLACEHOLDER;
129
+ }
60
130
  function containsPromptPlaceholder(value) {
61
- if (typeof value === "string") return value.includes(COMFYUI_PROMPT_PLACEHOLDER);
131
+ if (typeof value === "string") return value.includes("{{prompt}}") || value.includes(LEGACY_PROMPT_PLACEHOLDER);
62
132
  if (Array.isArray(value)) return value.some(containsPromptPlaceholder);
63
133
  return isRecord(value) && Object.values(value).some(containsPromptPlaceholder);
64
134
  }
@@ -142,19 +212,33 @@ window.__ModuleLoader__.load({
142
212
  /**
143
213
  * Save or update a gallery record by attachmentId.
144
214
  * Skipped if the item was previously deleted (tombstoned).
215
+ * Preserves existing isFavorite, tags, and original createdAt on re-renders.
145
216
  */
146
217
  async function saveGalleryItem(item) {
147
218
  try {
148
219
  const db = await getDB();
149
220
  if ((await loadTombstones(db)).has(item.id)) return;
150
- const record = {
151
- ...item,
152
- createdAt: item.createdAt ?? Date.now()
153
- };
154
221
  await new Promise((resolve, reject) => {
155
- const req = db.transaction(STORE_NAME, "readwrite").objectStore(STORE_NAME).put(record);
156
- req.onsuccess = () => resolve();
157
- req.onerror = () => reject(req.error);
222
+ const store = db.transaction(STORE_NAME, "readwrite").objectStore(STORE_NAME);
223
+ const getReq = store.get(item.id);
224
+ getReq.onsuccess = () => {
225
+ const existing = getReq.result;
226
+ const fav = item.isFavorite !== void 0 ? item.isFavorite : existing?.isFavorite;
227
+ const userTags = item.tags !== void 0 ? item.tags : existing?.tags;
228
+ const savedPath = item.savedTo !== void 0 ? item.savedTo : existing?.savedTo;
229
+ const record = {
230
+ ...existing,
231
+ ...item,
232
+ createdAt: existing?.createdAt ?? item.createdAt ?? Date.now(),
233
+ ...fav !== void 0 ? { isFavorite: fav } : {},
234
+ ...userTags !== void 0 ? { tags: userTags } : {},
235
+ ...savedPath !== void 0 ? { savedTo: savedPath } : {}
236
+ };
237
+ const putReq = store.put(record);
238
+ putReq.onsuccess = () => resolve();
239
+ putReq.onerror = () => reject(putReq.error);
240
+ };
241
+ getReq.onerror = () => reject(getReq.error);
158
242
  });
159
243
  notifyListeners();
160
244
  } catch (err) {
@@ -185,51 +269,537 @@ window.__ModuleLoader__.load({
185
269
  }
186
270
  }
187
271
  /**
188
- * Delete a single gallery record by ID and record a tombstone.
272
+ * Toggle favorite status of a gallery item.
273
+ * Returns the new favorite status (true if favorited, false if unfavorited).
189
274
  */
190
- async function deleteGalleryItem(id) {
275
+ async function toggleFavoriteGalleryItem(id) {
191
276
  try {
192
277
  const db = await getDB();
193
- (await loadTombstones(db)).add(id);
194
- await new Promise((resolve, reject) => {
195
- const tx = db.transaction([STORE_NAME, TOMBSTONE_STORE], "readwrite");
196
- const store = tx.objectStore(STORE_NAME);
197
- const tombstoneStore = tx.objectStore(TOMBSTONE_STORE);
198
- store.delete(id);
199
- tombstoneStore.put({
200
- id,
201
- deletedAt: Date.now()
202
- });
203
- tx.oncomplete = () => resolve();
204
- tx.onerror = () => reject(tx.error);
278
+ const newStatus = await new Promise((resolve, reject) => {
279
+ const store = db.transaction(STORE_NAME, "readwrite").objectStore(STORE_NAME);
280
+ const getReq = store.get(id);
281
+ getReq.onsuccess = () => {
282
+ const item = getReq.result;
283
+ if (!item) {
284
+ resolve(false);
285
+ return;
286
+ }
287
+ const nextFavorite = !item.isFavorite;
288
+ item.isFavorite = nextFavorite;
289
+ const putReq = store.put(item);
290
+ putReq.onsuccess = () => resolve(nextFavorite);
291
+ putReq.onerror = () => reject(putReq.error);
292
+ };
293
+ getReq.onerror = () => reject(getReq.error);
205
294
  });
206
295
  notifyListeners();
296
+ return newStatus;
207
297
  } catch (err) {
208
- console.warn("[dsh-image-gen] Failed to delete gallery item from IndexedDB:", err);
298
+ console.warn("[dsh-image-gen] Failed to toggle favorite item in IndexedDB:", err);
299
+ return false;
209
300
  }
210
301
  }
302
+ /**
303
+ * Bulk delete multiple gallery records by IDs and record tombstones in a single transaction.
304
+ */
305
+ async function bulkDeleteGalleryItems(ids) {
306
+ if (ids.length === 0) return;
307
+ const db = await getDB();
308
+ const tombstones = await loadTombstones(db);
309
+ const now = Date.now();
310
+ await new Promise((resolve, reject) => {
311
+ const tx = db.transaction([STORE_NAME, TOMBSTONE_STORE], "readwrite");
312
+ const store = tx.objectStore(STORE_NAME);
313
+ const tombstoneStore = tx.objectStore(TOMBSTONE_STORE);
314
+ for (const id of ids) {
315
+ store.delete(id);
316
+ tombstoneStore.put({
317
+ id,
318
+ deletedAt: now
319
+ });
320
+ }
321
+ tx.oncomplete = () => {
322
+ for (const id of ids) tombstones.add(id);
323
+ resolve();
324
+ };
325
+ tx.onerror = () => reject(tx.error ?? /* @__PURE__ */ new Error("IndexedDB transaction failed"));
326
+ tx.onabort = () => reject(tx.error ?? /* @__PURE__ */ new Error("IndexedDB transaction aborted"));
327
+ });
328
+ notifyListeners();
329
+ }
330
+ //#endregion
331
+ //#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
+ /**
333
+ * @license lucide-react v1.38.0 - ISC
334
+ *
335
+ * This source code is licensed under the ISC license.
336
+ * See the LICENSE file in the root directory of this source tree.
337
+ */
338
+ const mergeClasses = (...classes) => classes.filter((className, index, array) => {
339
+ return Boolean(className) && className.trim() !== "" && array.indexOf(className) === index;
340
+ }).join(" ").trim();
341
+ //#endregion
342
+ //#region node_modules/.pnpm/lucide-react@1.38.0_react@18.3.1/node_modules/lucide-react/dist/esm/shared/src/utils/toKebabCase.mjs
343
+ /**
344
+ * @license lucide-react v1.38.0 - ISC
345
+ *
346
+ * This source code is licensed under the ISC license.
347
+ * See the LICENSE file in the root directory of this source tree.
348
+ */
349
+ const toKebabCase = (string) => string.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
350
+ //#endregion
351
+ //#region node_modules/.pnpm/lucide-react@1.38.0_react@18.3.1/node_modules/lucide-react/dist/esm/shared/src/utils/toCamelCase.mjs
352
+ /**
353
+ * @license lucide-react v1.38.0 - ISC
354
+ *
355
+ * This source code is licensed under the ISC license.
356
+ * See the LICENSE file in the root directory of this source tree.
357
+ */
358
+ const toCamelCase = (string) => string.replace(/^([A-Z])|[\s-_]+(\w)/g, (match, p1, p2) => p2 ? p2.toUpperCase() : p1.toLowerCase());
359
+ //#endregion
360
+ //#region node_modules/.pnpm/lucide-react@1.38.0_react@18.3.1/node_modules/lucide-react/dist/esm/shared/src/utils/toPascalCase.mjs
361
+ /**
362
+ * @license lucide-react v1.38.0 - ISC
363
+ *
364
+ * This source code is licensed under the ISC license.
365
+ * See the LICENSE file in the root directory of this source tree.
366
+ */
367
+ const toPascalCase = (string) => {
368
+ const camelCase = toCamelCase(string);
369
+ return camelCase.charAt(0).toUpperCase() + camelCase.slice(1);
370
+ };
371
+ //#endregion
372
+ //#region node_modules/.pnpm/lucide-react@1.38.0_react@18.3.1/node_modules/lucide-react/dist/esm/defaultAttributes.mjs
373
+ /**
374
+ * @license lucide-react v1.38.0 - ISC
375
+ *
376
+ * This source code is licensed under the ISC license.
377
+ * See the LICENSE file in the root directory of this source tree.
378
+ */
379
+ var defaultAttributes = {
380
+ xmlns: "http://www.w3.org/2000/svg",
381
+ width: 24,
382
+ height: 24,
383
+ viewBox: "0 0 24 24",
384
+ fill: "none",
385
+ stroke: "currentColor",
386
+ strokeWidth: 2,
387
+ strokeLinecap: "round",
388
+ strokeLinejoin: "round"
389
+ };
390
+ //#endregion
391
+ //#region node_modules/.pnpm/lucide-react@1.38.0_react@18.3.1/node_modules/lucide-react/dist/esm/shared/src/utils/hasA11yProp.mjs
392
+ /**
393
+ * @license lucide-react v1.38.0 - ISC
394
+ *
395
+ * This source code is licensed under the ISC license.
396
+ * See the LICENSE file in the root directory of this source tree.
397
+ */
398
+ const hasA11yProp = (props) => {
399
+ for (const prop in props) if (prop.startsWith("aria-") || prop === "role" || prop === "title") return true;
400
+ return false;
401
+ };
402
+ //#endregion
403
+ //#region node_modules/.pnpm/lucide-react@1.38.0_react@18.3.1/node_modules/lucide-react/dist/esm/context.mjs
404
+ /**
405
+ * @license lucide-react v1.38.0 - ISC
406
+ *
407
+ * This source code is licensed under the ISC license.
408
+ * See the LICENSE file in the root directory of this source tree.
409
+ */
410
+ const LucideContext = (0, react.createContext)({});
411
+ const useLucideContext = () => (0, react.useContext)(LucideContext);
412
+ //#endregion
413
+ //#region node_modules/.pnpm/lucide-react@1.38.0_react@18.3.1/node_modules/lucide-react/dist/esm/Icon.mjs
414
+ /**
415
+ * @license lucide-react v1.38.0 - ISC
416
+ *
417
+ * This source code is licensed under the ISC license.
418
+ * See the LICENSE file in the root directory of this source tree.
419
+ */
420
+ const Icon = (0, react.forwardRef)(({ color, size, strokeWidth, absoluteStrokeWidth, className = "", children, iconNode, ...rest }, ref) => {
421
+ const { size: contextSize = 24, strokeWidth: contextStrokeWidth = 2, absoluteStrokeWidth: contextAbsoluteStrokeWidth = false, color: contextColor = "currentColor", className: contextClass = "" } = useLucideContext() ?? {};
422
+ const calculatedStrokeWidth = absoluteStrokeWidth ?? contextAbsoluteStrokeWidth ? Number(strokeWidth ?? contextStrokeWidth) * 24 / Number(size ?? contextSize) : strokeWidth ?? contextStrokeWidth;
423
+ return (0, react.createElement)("svg", {
424
+ ref,
425
+ ...defaultAttributes,
426
+ width: size ?? contextSize ?? defaultAttributes.width,
427
+ height: size ?? contextSize ?? defaultAttributes.height,
428
+ stroke: color ?? contextColor,
429
+ strokeWidth: calculatedStrokeWidth,
430
+ className: mergeClasses("lucide", contextClass, className),
431
+ ...!children && !hasA11yProp(rest) && { "aria-hidden": "true" },
432
+ ...rest
433
+ }, [...iconNode.map(([tag, attrs]) => (0, react.createElement)(tag, attrs)), ...Array.isArray(children) ? children : [children]]);
434
+ });
435
+ //#endregion
436
+ //#region node_modules/.pnpm/lucide-react@1.38.0_react@18.3.1/node_modules/lucide-react/dist/esm/createLucideIcon.mjs
437
+ /**
438
+ * @license lucide-react v1.38.0 - ISC
439
+ *
440
+ * This source code is licensed under the ISC license.
441
+ * See the LICENSE file in the root directory of this source tree.
442
+ */
443
+ const createLucideIcon = (iconName, iconNode) => {
444
+ const Component = (0, react.forwardRef)(({ className, ...props }, ref) => (0, react.createElement)(Icon, {
445
+ ref,
446
+ iconNode,
447
+ className: mergeClasses(`lucide-${toKebabCase(toPascalCase(iconName))}`, `lucide-${iconName}`, className),
448
+ ...props
449
+ }));
450
+ Component.displayName = toPascalCase(iconName);
451
+ return Component;
452
+ };
453
+ /**
454
+ * @license lucide-react v1.38.0 - ISC
455
+ *
456
+ * This source code is licensed under the ISC license.
457
+ * See the LICENSE file in the root directory of this source tree.
458
+ */
459
+ const Check = createLucideIcon("check", [["path", {
460
+ d: "M20 6 9 17l-5-5",
461
+ key: "1gmf2c"
462
+ }]]);
463
+ /**
464
+ * @license lucide-react v1.38.0 - ISC
465
+ *
466
+ * This source code is licensed under the ISC license.
467
+ * See the LICENSE file in the root directory of this source tree.
468
+ */
469
+ const ChevronLeft = createLucideIcon("chevron-left", [["path", {
470
+ d: "m15 18-6-6 6-6",
471
+ key: "1wnfg3"
472
+ }]]);
473
+ /**
474
+ * @license lucide-react v1.38.0 - ISC
475
+ *
476
+ * This source code is licensed under the ISC license.
477
+ * See the LICENSE file in the root directory of this source tree.
478
+ */
479
+ const ChevronRight = createLucideIcon("chevron-right", [["path", {
480
+ d: "m9 18 6-6-6-6",
481
+ key: "mthhwq"
482
+ }]]);
483
+ /**
484
+ * @license lucide-react v1.38.0 - ISC
485
+ *
486
+ * This source code is licensed under the ISC license.
487
+ * See the LICENSE file in the root directory of this source tree.
488
+ */
489
+ const Copy = createLucideIcon("copy", [["rect", {
490
+ width: "14",
491
+ height: "14",
492
+ x: "8",
493
+ y: "8",
494
+ rx: "2",
495
+ ry: "2",
496
+ key: "17jyea"
497
+ }], ["path", {
498
+ d: "M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2",
499
+ key: "zix9uf"
500
+ }]]);
501
+ /**
502
+ * @license lucide-react v1.38.0 - ISC
503
+ *
504
+ * This source code is licensed under the ISC license.
505
+ * See the LICENSE file in the root directory of this source tree.
506
+ */
507
+ const Download = createLucideIcon("download", [
508
+ ["path", {
509
+ d: "M12 15V3",
510
+ key: "m9g1x1"
511
+ }],
512
+ ["path", {
513
+ d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",
514
+ key: "ih7n3h"
515
+ }],
516
+ ["path", {
517
+ d: "m7 10 5 5 5-5",
518
+ key: "brsn70"
519
+ }]
520
+ ]);
521
+ /**
522
+ * @license lucide-react v1.38.0 - ISC
523
+ *
524
+ * This source code is licensed under the ISC license.
525
+ * See the LICENSE file in the root directory of this source tree.
526
+ */
527
+ const FileText = createLucideIcon("file-text", [
528
+ ["path", {
529
+ 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",
530
+ key: "1oefj6"
531
+ }],
532
+ ["path", {
533
+ d: "M14 2v5a1 1 0 0 0 1 1h5",
534
+ key: "wfsgrz"
535
+ }],
536
+ ["path", {
537
+ d: "M10 9H8",
538
+ key: "b1mrlr"
539
+ }],
540
+ ["path", {
541
+ d: "M16 13H8",
542
+ key: "t4e002"
543
+ }],
544
+ ["path", {
545
+ d: "M16 17H8",
546
+ key: "z1uh3a"
547
+ }]
548
+ ]);
549
+ /**
550
+ * @license lucide-react v1.38.0 - ISC
551
+ *
552
+ * This source code is licensed under the ISC license.
553
+ * See the LICENSE file in the root directory of this source tree.
554
+ */
555
+ const GitCompare = createLucideIcon("git-compare", [
556
+ ["circle", {
557
+ cx: "18",
558
+ cy: "18",
559
+ r: "3",
560
+ key: "1xkwt0"
561
+ }],
562
+ ["circle", {
563
+ cx: "6",
564
+ cy: "6",
565
+ r: "3",
566
+ key: "1lh9wr"
567
+ }],
568
+ ["path", {
569
+ d: "M13 6h3a2 2 0 0 1 2 2v7",
570
+ key: "1yeb86"
571
+ }],
572
+ ["path", {
573
+ d: "M11 18H8a2 2 0 0 1-2-2V9",
574
+ key: "19pyzm"
575
+ }]
576
+ ]);
577
+ /**
578
+ * @license lucide-react v1.38.0 - ISC
579
+ *
580
+ * This source code is licensed under the ISC license.
581
+ * See the LICENSE file in the root directory of this source tree.
582
+ */
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
+ const Image$1 = createLucideIcon("image", [
594
+ ["rect", {
595
+ width: "18",
596
+ height: "18",
597
+ x: "3",
598
+ y: "3",
599
+ rx: "2",
600
+ ry: "2",
601
+ key: "1m3agn"
602
+ }],
603
+ ["circle", {
604
+ cx: "9",
605
+ cy: "9",
606
+ r: "2",
607
+ key: "af1f0g"
608
+ }],
609
+ ["path", {
610
+ d: "m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21",
611
+ key: "1xmnt7"
612
+ }]
613
+ ]);
614
+ /**
615
+ * @license lucide-react v1.38.0 - ISC
616
+ *
617
+ * This source code is licensed under the ISC license.
618
+ * See the LICENSE file in the root directory of this source tree.
619
+ */
620
+ const ListTodo = createLucideIcon("list-todo", [
621
+ ["path", {
622
+ d: "M13 5h8",
623
+ key: "a7qcls"
624
+ }],
625
+ ["path", {
626
+ d: "M13 12h8",
627
+ key: "h98zly"
628
+ }],
629
+ ["path", {
630
+ d: "M13 19h8",
631
+ key: "c3s6r1"
632
+ }],
633
+ ["path", {
634
+ d: "m3 17 2 2 4-4",
635
+ key: "1jhpwq"
636
+ }],
637
+ ["rect", {
638
+ x: "3",
639
+ y: "4",
640
+ width: "6",
641
+ height: "6",
642
+ rx: "1",
643
+ key: "cif1o7"
644
+ }]
645
+ ]);
646
+ /**
647
+ * @license lucide-react v1.38.0 - ISC
648
+ *
649
+ * This source code is licensed under the ISC license.
650
+ * See the LICENSE file in the root directory of this source tree.
651
+ */
652
+ const Search = createLucideIcon("search", [["path", {
653
+ d: "m21 21-4.34-4.34",
654
+ key: "14j7rj"
655
+ }], ["circle", {
656
+ cx: "11",
657
+ cy: "11",
658
+ r: "8",
659
+ key: "4ej97u"
660
+ }]]);
661
+ /**
662
+ * @license lucide-react v1.38.0 - ISC
663
+ *
664
+ * This source code is licensed under the ISC license.
665
+ * See the LICENSE file in the root directory of this source tree.
666
+ */
667
+ const SlidersHorizontal = createLucideIcon("sliders-horizontal", [
668
+ ["path", {
669
+ d: "M10 5H3",
670
+ key: "1qgfaw"
671
+ }],
672
+ ["path", {
673
+ d: "M12 19H3",
674
+ key: "yhmn1j"
675
+ }],
676
+ ["path", {
677
+ d: "M14 3v4",
678
+ key: "1sua03"
679
+ }],
680
+ ["path", {
681
+ d: "M16 17v4",
682
+ key: "1q0r14"
683
+ }],
684
+ ["path", {
685
+ d: "M21 12h-9",
686
+ key: "1o4lsq"
687
+ }],
688
+ ["path", {
689
+ d: "M21 19h-5",
690
+ key: "1rlt1p"
691
+ }],
692
+ ["path", {
693
+ d: "M21 5h-7",
694
+ key: "1oszz2"
695
+ }],
696
+ ["path", {
697
+ d: "M8 10v4",
698
+ key: "tgpxqk"
699
+ }],
700
+ ["path", {
701
+ d: "M8 12H3",
702
+ key: "a7s4jb"
703
+ }]
704
+ ]);
705
+ /**
706
+ * @license lucide-react v1.38.0 - ISC
707
+ *
708
+ * This source code is licensed under the ISC license.
709
+ * See the LICENSE file in the root directory of this source tree.
710
+ */
711
+ const Trash2 = createLucideIcon("trash-2", [
712
+ ["path", {
713
+ d: "M10 11v6",
714
+ key: "nco0om"
715
+ }],
716
+ ["path", {
717
+ d: "M14 11v6",
718
+ key: "outv1u"
719
+ }],
720
+ ["path", {
721
+ d: "M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6",
722
+ key: "miytrc"
723
+ }],
724
+ ["path", {
725
+ d: "M3 6h18",
726
+ key: "d0wm0j"
727
+ }],
728
+ ["path", {
729
+ d: "M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2",
730
+ key: "e791ji"
731
+ }]
732
+ ]);
733
+ /**
734
+ * @license lucide-react v1.38.0 - ISC
735
+ *
736
+ * This source code is licensed under the ISC license.
737
+ * See the LICENSE file in the root directory of this source tree.
738
+ */
739
+ const TriangleAlert = createLucideIcon("triangle-alert", [
740
+ ["path", {
741
+ d: "m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3",
742
+ key: "wmoenq"
743
+ }],
744
+ ["path", {
745
+ d: "M12 9v4",
746
+ key: "juzpu7"
747
+ }],
748
+ ["path", {
749
+ d: "M12 17h.01",
750
+ key: "p32p05"
751
+ }]
752
+ ]);
753
+ /**
754
+ * @license lucide-react v1.38.0 - ISC
755
+ *
756
+ * This source code is licensed under the ISC license.
757
+ * See the LICENSE file in the root directory of this source tree.
758
+ */
759
+ const X = createLucideIcon("x", [["path", {
760
+ d: "M18 6 6 18",
761
+ key: "1bl5f8"
762
+ }], ["path", {
763
+ d: "m6 6 12 12",
764
+ key: "d8bk6v"
765
+ }]]);
211
766
  //#endregion
212
767
  //#region lib/types/client/gallery-view.js
213
768
  /**
214
- * Native Workspace Gallery View Component for DSH `conversation.view` slot.
215
- * Fully i18n-reactive (Chinese & English).
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.
216
772
  */
217
773
  const DICT$1 = {
218
774
  zh: {
219
- totalCount: "共 {count} 张生成图片",
220
- searchPlaceholder: "搜索 Prompt 关键词…",
221
- filterAll: "全部厂商",
775
+ tabGallery: "图库",
776
+ tabStudio: "工作台",
777
+ tabFavorites: "收藏",
778
+ tabCompare: "对比",
779
+ tabTasks: "任务",
780
+ filterAllProviders: "全部提供商",
222
781
  filterGoogle: "Google Gemini",
223
782
  filterOpenAI: "OpenAI / 中转站",
224
783
  filterSeedream: "字节 Seedream",
225
784
  filterDashScope: "阿里 DashScope",
785
+ filterComfyUI: "本地 ComfyUI",
786
+ filterAllModels: "全部模型",
787
+ filterAllRatios: "全部比例",
788
+ searchPlaceholder: "搜索 Prompt、标签…",
789
+ sortNewest: "最新生成",
790
+ sortOldest: "最早生成",
791
+ totalCount: "共 {count} 张生成图片",
226
792
  emptyTitle: "暂无生图记录",
227
793
  emptyDesc: "在对话中让 Agent 生图后,生成的图片会自动收录到这里。",
794
+ favEmptyTitle: "暂无收藏图片",
795
+ favEmptyDesc: "在图库中点击卡片右下角的 ♡ 按钮,即可将喜爱的图片收录到这里。",
228
796
  noMatchTitle: "未找到匹配结果",
229
- noMatchDesc: "尝试更换搜索关键词或选择其他厂商。",
797
+ noMatchDesc: "尝试更换搜索关键词或调整筛选条件。",
230
798
  copiedPrompt: "已复制 Prompt",
231
799
  copiedImage: "已复制图片",
232
800
  copyFailed: "复制失败",
801
+ favoriteAdded: "已添加到收藏",
802
+ favoriteRemoved: "已取消收藏",
233
803
  preview: "查看大图",
234
804
  download: "下载图片",
235
805
  copyImg: "复制图片",
@@ -239,23 +809,69 @@ window.__ModuleLoader__.load({
239
809
  deleted: "已从画廊删除",
240
810
  model: "模型",
241
811
  prompt: "Prompt",
242
- close: "关闭 (Esc)"
812
+ close: "关闭 (Esc)",
813
+ prevImage: "上一张 (←)",
814
+ nextImage: "下一张 (→)",
815
+ manage: "批量删除",
816
+ exitManage: "退出选择",
817
+ selectedCount: "已选 {n} 项",
818
+ selectAll: "全选",
819
+ invertSelect: "反选",
820
+ clearSelect: "清空",
821
+ batchDelete: "批量删除",
822
+ batchDeleteTitle: "确认批量删除选中的 {count} 张图片?",
823
+ batchDeleteTitleSingle: "确认从画廊中删除这张图片?",
824
+ batchDeleteDesc: "将从画廊历史记录中移除所选图片。",
825
+ deleteWorkspaceFilesOpt: "同时清理工作区磁盘物理文件(不可恢复)",
826
+ cancel: "取消",
827
+ confirmBatchDelete: "确认删除 ({count})",
828
+ confirmDeleteSingle: "确认删除",
829
+ batchDeletedToast: "已删除 {count} 张图片",
830
+ batchDeletedWithFilesToast: "已删除 {count} 张图片,并清理了 {files} 个工作区文件",
831
+ deleteFailedFileLocked: "已删除 {count} 张图片,但有 {files} 个文件因被系统占用未能删除",
832
+ deleteFailedDatabase: "删除失败:本地数据库操作异常",
833
+ justNow: "刚刚",
834
+ minutesAgo: "{n} 分钟前",
835
+ hoursAgo: "{n} 小时前",
836
+ daysAgo: "{n} 天前",
837
+ studioTitle: "AI 图像工作台 (Studio)",
838
+ studioDesc: "工作台模块正在紧锣密鼓开发中。在此你将体验大图精修、变体生成 (Variations)、参数重调并一键将生成结果无缝插回 DSH 正在进行的对话。",
839
+ studioTip: "💡 提示:目前你可以在“图库”中点击任意图片,在弹窗中进行查看、复制与下载。",
840
+ compareTitle: "多模型横向对比 (Compare)",
841
+ compareDesc: "支持单个 Prompt 一键同时调度 Gemini、Seedream、DashScope 及本地 ComfyUI 模型并排生成,直观横评画质与细节。",
842
+ tasksTitle: "异步任务队列 (Tasks)",
843
+ tasksDesc: "集中管理后台批量生图、多模型并发生成与本地 ComfyUI 耗时任务。支持状态追踪、失败重试与执行耗时分析。",
844
+ comingSoonBadge: "即将推出"
243
845
  },
244
846
  en: {
245
- totalCount: "{count} images total",
246
- searchPlaceholder: "Search prompt keywords…",
247
- filterAll: "All Providers",
847
+ tabGallery: "Gallery",
848
+ tabStudio: "Studio",
849
+ tabFavorites: "Favorites",
850
+ tabCompare: "Compare",
851
+ tabTasks: "Tasks",
852
+ filterAllProviders: "All Providers",
248
853
  filterGoogle: "Google Gemini",
249
854
  filterOpenAI: "OpenAI / Relay",
250
855
  filterSeedream: "ByteDance Seedream",
251
856
  filterDashScope: "Aliyun DashScope",
857
+ filterComfyUI: "Local ComfyUI",
858
+ filterAllModels: "All Models",
859
+ filterAllRatios: "All Ratios",
860
+ searchPlaceholder: "Search prompt, tags…",
861
+ sortNewest: "Newest first",
862
+ sortOldest: "Oldest first",
863
+ totalCount: "{count} images total",
252
864
  emptyTitle: "No images generated yet",
253
865
  emptyDesc: "Images generated during conversations will automatically appear here.",
866
+ favEmptyTitle: "No favorite images yet",
867
+ favEmptyDesc: "Click the ♡ button on any card in the gallery to collect your favorite images here.",
254
868
  noMatchTitle: "No matching images",
255
- noMatchDesc: "Try a different search keyword or provider filter.",
869
+ noMatchDesc: "Try a different search keyword or adjust filter criteria.",
256
870
  copiedPrompt: "Prompt copied",
257
871
  copiedImage: "Image copied",
258
872
  copyFailed: "Copy failed",
873
+ favoriteAdded: "Added to favorites",
874
+ favoriteRemoved: "Removed from favorites",
259
875
  preview: "Full Preview",
260
876
  download: "Download",
261
877
  copyImg: "Copy Image",
@@ -265,17 +881,123 @@ window.__ModuleLoader__.load({
265
881
  deleted: "Deleted from gallery",
266
882
  model: "Model",
267
883
  prompt: "Prompt",
268
- close: "Close (Esc)"
884
+ close: "Close (Esc)",
885
+ prevImage: "Previous (←)",
886
+ nextImage: "Next (→)",
887
+ manage: "Batch Delete",
888
+ exitManage: "Done",
889
+ selectedCount: "{n} selected",
890
+ selectAll: "Select All",
891
+ invertSelect: "Invert",
892
+ clearSelect: "Clear",
893
+ batchDelete: "Batch Delete",
894
+ batchDeleteTitle: "Delete {count} selected images?",
895
+ batchDeleteTitleSingle: "Delete this image from gallery?",
896
+ batchDeleteDesc: "These images will be removed from your gallery history.",
897
+ deleteWorkspaceFilesOpt: "Also delete files from workspace disk (cannot be undone)",
898
+ cancel: "Cancel",
899
+ confirmBatchDelete: "Delete ({count})",
900
+ confirmDeleteSingle: "Delete",
901
+ batchDeletedToast: "Deleted {count} images",
902
+ batchDeletedWithFilesToast: "Deleted {count} images and cleaned {files} workspace files",
903
+ deleteFailedFileLocked: "Deleted {count} images, but {files} files could not be deleted (busy/locked)",
904
+ deleteFailedDatabase: "Failed to delete: local database error",
905
+ justNow: "Just now",
906
+ minutesAgo: "{n}m ago",
907
+ hoursAgo: "{n}h ago",
908
+ daysAgo: "{n}d ago",
909
+ studioTitle: "AI Image Studio",
910
+ studioDesc: "Studio workbench is under active development. Fine-tune prompts, generate variations (2x/4x), and inject images directly into DSH chat.",
911
+ studioTip: "💡 Tip: You can currently click any image in the Gallery to preview, copy, or download it.",
912
+ compareTitle: "Model Comparison (Compare)",
913
+ compareDesc: "Side-by-side multi-model benchmarking coming soon. Test Gemini, Seedream, DashScope, and ComfyUI with a single prompt.",
914
+ tasksTitle: "Task Queue (Tasks)",
915
+ tasksDesc: "Centralized view for batch generation, asynchronous ComfyUI runs, live progress tracking, and retry controls.",
916
+ comingSoonBadge: "Coming Soon"
269
917
  }
270
918
  };
919
+ /** Format human-readable relative time */
920
+ function formatRelativeTime(timestamp, t) {
921
+ const diff = Date.now() - timestamp;
922
+ if (diff < 6e4) return t("justNow");
923
+ if (diff < 36e5) {
924
+ const mins = Math.max(1, Math.floor(diff / 6e4));
925
+ return t("minutesAgo", { n: String(mins) });
926
+ }
927
+ if (diff < 864e5) {
928
+ const hours = Math.floor(diff / 36e5);
929
+ return t("hoursAgo", { n: String(hours) });
930
+ }
931
+ if (diff < 2592e6) {
932
+ const days = Math.floor(diff / 864e5);
933
+ return t("daysAgo", { n: String(days) });
934
+ }
935
+ const d = new Date(timestamp);
936
+ return `${d.getFullYear()}/${d.getMonth() + 1}/${d.getDate()}`;
937
+ }
938
+ /** Extract standard aspect ratio for precise filtering */
939
+ function getItemRatio(item) {
940
+ if (item.aspectRatio && item.aspectRatio !== "custom") return item.aspectRatio;
941
+ if (item.output) {
942
+ const ratioMatch = item.output.match(/\b(1:1|16:9|9:16|4:3|3:4|3:2|2:3)\b/);
943
+ if (ratioMatch?.[1]) return ratioMatch[1];
944
+ const dimMatch = item.output.match(/(\d{3,4})\s*[×x*]\s*(\d{3,4})/);
945
+ if (dimMatch?.[1] && dimMatch[2]) {
946
+ const w = parseInt(dimMatch[1], 10);
947
+ const h = parseInt(dimMatch[2], 10);
948
+ if (w === h) return "1:1";
949
+ const approx = w / h;
950
+ if (Math.abs(approx - 16 / 9) < .05) return "16:9";
951
+ if (Math.abs(approx - 9 / 16) < .05) return "9:16";
952
+ if (Math.abs(approx - 4 / 3) < .05) return "4:3";
953
+ if (Math.abs(approx - 3 / 4) < .05) return "3:4";
954
+ if (Math.abs(approx - 3 / 2) < .05) return "3:2";
955
+ if (Math.abs(approx - 2 / 3) < .05) return "2:3";
956
+ }
957
+ }
958
+ return "1:1";
959
+ }
960
+ /** Format aspect ratio or dimensions for card metadata (e.g., 1024×1024 or 16:9) */
961
+ function formatCardMeta(item) {
962
+ if (item.output) {
963
+ const dimMatch = item.output.match(/(\d{3,4})\s*[×x*]\s*(\d{3,4})/);
964
+ if (dimMatch?.[1] && dimMatch[2]) return `${dimMatch[1]}×${dimMatch[2]}`;
965
+ }
966
+ if (item.aspectRatio && item.aspectRatio !== "custom") return item.aspectRatio;
967
+ if (item.output) {
968
+ const ratioMatch = item.output.match(/\b(1:1|16:9|9:16|4:3|3:4|3:2|2:3)\b/);
969
+ if (ratioMatch?.[1]) return ratioMatch[1];
970
+ }
971
+ if (item.imageSize) return item.imageSize;
972
+ return "1:1";
973
+ }
271
974
  const GalleryViewTab = ({ locale }) => {
975
+ const [activeTab, setActiveTab] = (0, react.useState)("gallery");
272
976
  const [items, setItems] = (0, react.useState)([]);
273
977
  const [search, setSearch] = (0, react.useState)("");
274
978
  const [selectedProvider, setSelectedProvider] = (0, react.useState)("all");
979
+ const [selectedRatio, setSelectedRatio] = (0, react.useState)("all");
980
+ const [sortBy, setSortBy] = (0, react.useState)("newest");
275
981
  const [previewItem, setPreviewItem] = (0, react.useState)(null);
276
982
  const [previewUrl, setPreviewUrl] = (0, react.useState)(null);
277
983
  const [previewBlob, setPreviewBlob] = (0, react.useState)(null);
278
984
  const [toast, setToast] = (0, react.useState)(null);
985
+ const [isManageMode, setIsManageMode] = (0, react.useState)(false);
986
+ const [selectedIds, setSelectedIds] = (0, react.useState)(/* @__PURE__ */ new Set());
987
+ const lastSelectedIndexRef = (0, react.useRef)(null);
988
+ const [showDeleteModal, setShowDeleteModal] = (0, react.useState)(false);
989
+ const [deleteWorkspaceFiles, setDeleteWorkspaceFiles] = (0, react.useState)(true);
990
+ const [isDeleting, setIsDeleting] = (0, react.useState)(false);
991
+ (0, react.useEffect)(() => {
992
+ setSelectedIds(/* @__PURE__ */ new Set());
993
+ lastSelectedIndexRef.current = null;
994
+ }, [
995
+ activeTab,
996
+ search,
997
+ selectedProvider,
998
+ selectedRatio,
999
+ sortBy
1000
+ ]);
279
1001
  const [lang, setLang] = (0, react.useState)(() => {
280
1002
  return (locale?.getSnapshot?.()?.active)?.startsWith("en") ? "en" : "zh";
281
1003
  });
@@ -322,108 +1044,431 @@ window.__ModuleLoader__.load({
322
1044
  unsubscribe();
323
1045
  };
324
1046
  }, []);
325
- (0, react.useEffect)(() => {
326
- if (!previewItem) return;
327
- const onKeyDown = (e) => {
328
- if (e.key === "Escape") {
329
- setPreviewItem(null);
330
- setPreviewUrl(null);
331
- setPreviewBlob(null);
332
- }
333
- };
334
- window.addEventListener("keydown", onKeyDown);
335
- return () => window.removeEventListener("keydown", onKeyDown);
336
- }, [previewItem]);
1047
+ const [previewLoading, setPreviewLoading] = (0, react.useState)(false);
1048
+ const blobCache = (0, react.useMemo)(() => /* @__PURE__ */ new Map(), []);
1049
+ const abortControllerRef = (0, react.useRef)(null);
337
1050
  const filteredItems = (0, react.useMemo)(() => {
338
1051
  return items.filter((item) => {
1052
+ if (activeTab === "favorites" && !item.isFavorite) return false;
339
1053
  if (selectedProvider !== "all" && item.provider !== selectedProvider) return false;
1054
+ if (selectedRatio !== "all") {
1055
+ if (getItemRatio(item) !== selectedRatio) return false;
1056
+ }
340
1057
  if (search.trim().length > 0) {
341
1058
  const q = search.trim().toLowerCase();
342
1059
  const matchPrompt = item.prompt?.toLowerCase().includes(q);
343
1060
  const matchModel = item.model?.toLowerCase().includes(q);
344
- if (!matchPrompt && !matchModel) return false;
1061
+ const matchProvider = item.provider?.toLowerCase().includes(q);
1062
+ const matchTags = item.tags?.some((t) => t.toLowerCase().includes(q));
1063
+ if (!matchPrompt && !matchModel && !matchProvider && !matchTags) return false;
345
1064
  }
346
1065
  return true;
1066
+ }).sort((a, b) => {
1067
+ if (sortBy === "oldest") return a.createdAt - b.createdAt;
1068
+ return b.createdAt - a.createdAt;
347
1069
  });
348
1070
  }, [
349
1071
  items,
1072
+ activeTab,
1073
+ selectedProvider,
1074
+ selectedRatio,
350
1075
  search,
351
- selectedProvider
1076
+ sortBy
352
1077
  ]);
1078
+ const currentPreviewIndex = (0, react.useMemo)(() => {
1079
+ if (!previewItem) return -1;
1080
+ return filteredItems.findIndex((i) => i.id === previewItem.id);
1081
+ }, [previewItem, filteredItems]);
1082
+ const hasPrev = currentPreviewIndex > 0;
1083
+ const hasNext = currentPreviewIndex >= 0 && currentPreviewIndex < filteredItems.length - 1;
1084
+ const openPreviewItem = (item, blob) => {
1085
+ if (abortControllerRef.current) {
1086
+ abortControllerRef.current.abort();
1087
+ abortControllerRef.current = null;
1088
+ }
1089
+ if (previewUrl) URL.revokeObjectURL(previewUrl);
1090
+ setPreviewItem(item);
1091
+ if (blob) {
1092
+ blobCache.set(item.id, blob);
1093
+ const url = URL.createObjectURL(blob);
1094
+ setPreviewBlob(blob);
1095
+ setPreviewUrl(url);
1096
+ setPreviewLoading(false);
1097
+ return;
1098
+ }
1099
+ const cached = blobCache.get(item.id);
1100
+ if (cached) {
1101
+ const url = URL.createObjectURL(cached);
1102
+ setPreviewBlob(cached);
1103
+ setPreviewUrl(url);
1104
+ setPreviewLoading(false);
1105
+ return;
1106
+ }
1107
+ setPreviewLoading(true);
1108
+ setPreviewBlob(null);
1109
+ setPreviewUrl(null);
1110
+ const controller = new AbortController();
1111
+ 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) => {
1121
+ if (controller.signal.aborted) return;
1122
+ blobCache.set(item.id, fetchedBlob);
1123
+ const url = URL.createObjectURL(fetchedBlob);
1124
+ setPreviewBlob(fetchedBlob);
1125
+ setPreviewUrl(url);
1126
+ setPreviewLoading(false);
1127
+ }).catch((err) => {
1128
+ if (controller.signal.aborted) return;
1129
+ setPreviewLoading(false);
1130
+ showToast(t("copyFailed"));
1131
+ });
1132
+ };
1133
+ const handleClosePreview = () => {
1134
+ if (abortControllerRef.current) {
1135
+ abortControllerRef.current.abort();
1136
+ abortControllerRef.current = null;
1137
+ }
1138
+ if (previewUrl) URL.revokeObjectURL(previewUrl);
1139
+ setPreviewItem(null);
1140
+ setPreviewUrl(null);
1141
+ setPreviewBlob(null);
1142
+ setPreviewLoading(false);
1143
+ };
1144
+ const goToPrev = () => {
1145
+ if (currentPreviewIndex > 0) {
1146
+ const prevItem = filteredItems[currentPreviewIndex - 1];
1147
+ if (prevItem) openPreviewItem(prevItem);
1148
+ }
1149
+ };
1150
+ const goToNext = () => {
1151
+ if (currentPreviewIndex >= 0 && currentPreviewIndex < filteredItems.length - 1) {
1152
+ const nextItem = filteredItems[currentPreviewIndex + 1];
1153
+ if (nextItem) openPreviewItem(nextItem);
1154
+ }
1155
+ };
1156
+ (0, react.useEffect)(() => {
1157
+ return () => {
1158
+ if (abortControllerRef.current) abortControllerRef.current.abort();
1159
+ if (previewUrl) URL.revokeObjectURL(previewUrl);
1160
+ };
1161
+ }, [previewUrl]);
1162
+ (0, react.useEffect)(() => {
1163
+ const onKeyDown = (e) => {
1164
+ if (["INPUT", "TEXTAREA"].includes(e.target?.tagName)) return;
1165
+ if (showDeleteModal) {
1166
+ if (e.key === "Escape" && !isDeleting) {
1167
+ e.preventDefault();
1168
+ setShowDeleteModal(false);
1169
+ if (!isManageMode) setSelectedIds(/* @__PURE__ */ new Set());
1170
+ }
1171
+ return;
1172
+ }
1173
+ if (previewItem) {
1174
+ if (e.key === "Escape") handleClosePreview();
1175
+ else if (e.key === "ArrowLeft" || e.key === "Left") {
1176
+ e.preventDefault();
1177
+ goToPrev();
1178
+ } else if (e.key === "ArrowRight" || e.key === "Right") {
1179
+ e.preventDefault();
1180
+ goToNext();
1181
+ }
1182
+ }
1183
+ };
1184
+ window.addEventListener("keydown", onKeyDown);
1185
+ return () => window.removeEventListener("keydown", onKeyDown);
1186
+ }, [
1187
+ showDeleteModal,
1188
+ isDeleting,
1189
+ isManageMode,
1190
+ previewItem,
1191
+ currentPreviewIndex,
1192
+ filteredItems
1193
+ ]);
1194
+ const handleCardClick = (item, index, e, blob) => {
1195
+ if (e.shiftKey || isManageMode) {
1196
+ e.stopPropagation();
1197
+ if (!isManageMode) setIsManageMode(true);
1198
+ if (e.shiftKey && lastSelectedIndexRef.current !== null) {
1199
+ const from = Math.min(lastSelectedIndexRef.current, index);
1200
+ const to = Math.max(lastSelectedIndexRef.current, index);
1201
+ const rangeItems = filteredItems.slice(from, to + 1);
1202
+ setSelectedIds((prev) => {
1203
+ const next = new Set(prev);
1204
+ for (const r of rangeItems) next.add(r.id);
1205
+ return next;
1206
+ });
1207
+ } else {
1208
+ setSelectedIds((prev) => {
1209
+ const next = new Set(prev);
1210
+ if (next.has(item.id)) next.delete(item.id);
1211
+ else next.add(item.id);
1212
+ return next;
1213
+ });
1214
+ lastSelectedIndexRef.current = index;
1215
+ }
1216
+ return;
1217
+ }
1218
+ if (blob) openPreviewItem(item, blob);
1219
+ };
1220
+ const handleToggleSelect = (id, index, e) => {
1221
+ e.stopPropagation();
1222
+ if (!isManageMode) setIsManageMode(true);
1223
+ if (e.shiftKey && lastSelectedIndexRef.current !== null) {
1224
+ const from = Math.min(lastSelectedIndexRef.current, index);
1225
+ const to = Math.max(lastSelectedIndexRef.current, index);
1226
+ const rangeItems = filteredItems.slice(from, to + 1);
1227
+ setSelectedIds((prev) => {
1228
+ const next = new Set(prev);
1229
+ for (const r of rangeItems) next.add(r.id);
1230
+ return next;
1231
+ });
1232
+ } else {
1233
+ setSelectedIds((prev) => {
1234
+ const next = new Set(prev);
1235
+ if (next.has(id)) next.delete(id);
1236
+ else next.add(id);
1237
+ return next;
1238
+ });
1239
+ lastSelectedIndexRef.current = index;
1240
+ }
1241
+ };
1242
+ const handleSelectAll = () => {
1243
+ setSelectedIds(new Set(filteredItems.map((i) => i.id)));
1244
+ };
1245
+ const handleInvertSelect = () => {
1246
+ setSelectedIds((prev) => {
1247
+ const next = /* @__PURE__ */ new Set();
1248
+ for (const item of filteredItems) if (!prev.has(item.id)) next.add(item.id);
1249
+ return next;
1250
+ });
1251
+ };
1252
+ const handleClearSelect = () => {
1253
+ setSelectedIds(/* @__PURE__ */ new Set());
1254
+ lastSelectedIndexRef.current = null;
1255
+ };
1256
+ const handleExitManage = () => {
1257
+ setIsManageMode(false);
1258
+ setSelectedIds(/* @__PURE__ */ new Set());
1259
+ lastSelectedIndexRef.current = null;
1260
+ };
1261
+ const requestSingleDelete = (item, e) => {
1262
+ if (e) e.stopPropagation();
1263
+ setSelectedIds(/* @__PURE__ */ new Set([item.id]));
1264
+ setShowDeleteModal(true);
1265
+ };
1266
+ const handleConfirmBatchDelete = async () => {
1267
+ if (selectedIds.size === 0 || isDeleting) return;
1268
+ setIsDeleting(true);
1269
+ const idsToDelete = Array.from(selectedIds);
1270
+ const itemsToDelete = items.filter((i) => selectedIds.has(i.id));
1271
+ 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
+ let deletedFiles = 0;
1274
+ let failedFilesCount = 0;
1275
+ if (deleteWorkspaceFiles && (pathsToDelete.length > 0 || fallbackAttachmentIds.length > 0)) try {
1276
+ const res = await fetch(DELETE_ROUTE, {
1277
+ method: "POST",
1278
+ headers: { "content-type": "application/json" },
1279
+ body: JSON.stringify({
1280
+ paths: pathsToDelete,
1281
+ attachmentIds: fallbackAttachmentIds
1282
+ })
1283
+ });
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;
1288
+ }
1289
+ } catch (err) {
1290
+ console.warn("[dsh-image-gen] Workspace file deletion failed:", err);
1291
+ }
1292
+ try {
1293
+ await bulkDeleteGalleryItems(idsToDelete);
1294
+ } catch (err) {
1295
+ setIsDeleting(false);
1296
+ showToast(t("deleteFailedDatabase"));
1297
+ return;
1298
+ }
1299
+ if (previewItem && selectedIds.has(previewItem.id)) handleClosePreview();
1300
+ setIsDeleting(false);
1301
+ setShowDeleteModal(false);
1302
+ if (isManageMode) handleExitManage();
1303
+ else {
1304
+ setSelectedIds(/* @__PURE__ */ new Set());
1305
+ lastSelectedIndexRef.current = null;
1306
+ }
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", {
1312
+ count: String(idsToDelete.length),
1313
+ files: String(deletedFiles)
1314
+ }));
1315
+ else showToast(t("batchDeletedToast", { count: String(idsToDelete.length) }));
1316
+ };
353
1317
  return (0, react_jsx_runtime.jsxs)("div", {
354
1318
  className: "dsh-ig-gallery-page",
355
1319
  children: [
356
1320
  (0, react_jsx_runtime.jsxs)("header", {
357
- className: "dsh-ig-gallery-page-header",
358
- children: [(0, react_jsx_runtime.jsx)("div", {
359
- className: "dsh-ig-gallery-page-title-row",
360
- children: (0, react_jsx_runtime.jsx)("span", {
361
- className: "dsh-ig-gallery-page-count",
362
- children: t("totalCount", { count: String(items.length) })
1321
+ className: "dsh-ig-studio-tabs-bar",
1322
+ children: [
1323
+ (0, react_jsx_runtime.jsxs)("button", {
1324
+ type: "button",
1325
+ className: `dsh-ig-studio-tab-btn ${activeTab === "gallery" ? "is-active" : ""}`,
1326
+ onClick: () => setActiveTab("gallery"),
1327
+ children: [(0, react_jsx_runtime.jsx)(Image$1, { size: 15 }), (0, react_jsx_runtime.jsx)("span", { children: t("tabGallery") })]
1328
+ }),
1329
+ (0, react_jsx_runtime.jsxs)("button", {
1330
+ type: "button",
1331
+ className: `dsh-ig-studio-tab-btn ${activeTab === "studio" ? "is-active" : ""}`,
1332
+ onClick: () => setActiveTab("studio"),
1333
+ children: [(0, react_jsx_runtime.jsx)(SlidersHorizontal, { size: 15 }), (0, react_jsx_runtime.jsx)("span", { children: t("tabStudio") })]
1334
+ }),
1335
+ (0, react_jsx_runtime.jsxs)("button", {
1336
+ type: "button",
1337
+ className: `dsh-ig-studio-tab-btn ${activeTab === "favorites" ? "is-active" : ""}`,
1338
+ onClick: () => setActiveTab("favorites"),
1339
+ 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") })]
363
1352
  })
364
- }), (0, react_jsx_runtime.jsxs)("div", {
365
- className: "dsh-ig-gallery-page-tools",
366
- children: [(0, react_jsx_runtime.jsxs)("div", {
367
- className: "dsh-ig-gallery-search-wrap",
368
- children: [(0, react_jsx_runtime.jsxs)("svg", {
369
- className: "dsh-ig-gallery-search-icon",
370
- width: "14",
371
- height: "14",
372
- viewBox: "0 0 24 24",
373
- fill: "none",
374
- stroke: "currentColor",
375
- strokeWidth: "2",
376
- strokeLinecap: "round",
377
- strokeLinejoin: "round",
378
- children: [(0, react_jsx_runtime.jsx)("circle", {
379
- cx: "11",
380
- cy: "11",
381
- r: "8"
382
- }), (0, react_jsx_runtime.jsx)("line", {
383
- x1: "21",
384
- y1: "21",
385
- x2: "16.65",
386
- y2: "16.65"
1353
+ ]
1354
+ }),
1355
+ (activeTab === "gallery" || activeTab === "favorites") && (0, react_jsx_runtime.jsxs)("div", {
1356
+ className: "dsh-ig-studio-toolbar",
1357
+ children: [(0, react_jsx_runtime.jsxs)("div", {
1358
+ className: "dsh-ig-studio-toolbar-left",
1359
+ children: [
1360
+ (0, react_jsx_runtime.jsxs)("select", {
1361
+ className: "dsh-ig-studio-select",
1362
+ value: selectedProvider,
1363
+ onChange: (e) => setSelectedProvider(e.target.value),
1364
+ children: [
1365
+ (0, react_jsx_runtime.jsx)("option", {
1366
+ value: "all",
1367
+ children: t("filterAllProviders")
1368
+ }),
1369
+ (0, react_jsx_runtime.jsx)("option", {
1370
+ value: "google",
1371
+ children: t("filterGoogle")
1372
+ }),
1373
+ (0, react_jsx_runtime.jsx)("option", {
1374
+ value: "openai",
1375
+ children: t("filterOpenAI")
1376
+ }),
1377
+ (0, react_jsx_runtime.jsx)("option", {
1378
+ value: "seedream",
1379
+ children: t("filterSeedream")
1380
+ }),
1381
+ (0, react_jsx_runtime.jsx)("option", {
1382
+ value: "dashscope",
1383
+ children: t("filterDashScope")
1384
+ }),
1385
+ (0, react_jsx_runtime.jsx)("option", {
1386
+ value: "comfyui",
1387
+ children: t("filterComfyUI")
1388
+ })
1389
+ ]
1390
+ }),
1391
+ (0, react_jsx_runtime.jsxs)("select", {
1392
+ className: "dsh-ig-studio-select",
1393
+ value: selectedRatio,
1394
+ onChange: (e) => setSelectedRatio(e.target.value),
1395
+ children: [
1396
+ (0, react_jsx_runtime.jsx)("option", {
1397
+ value: "all",
1398
+ children: t("filterAllRatios")
1399
+ }),
1400
+ (0, react_jsx_runtime.jsx)("option", {
1401
+ value: "1:1",
1402
+ children: "1:1"
1403
+ }),
1404
+ (0, react_jsx_runtime.jsx)("option", {
1405
+ value: "16:9",
1406
+ children: "16:9"
1407
+ }),
1408
+ (0, react_jsx_runtime.jsx)("option", {
1409
+ value: "9:16",
1410
+ children: "9:16"
1411
+ }),
1412
+ (0, react_jsx_runtime.jsx)("option", {
1413
+ value: "4:3",
1414
+ children: "4:3"
1415
+ }),
1416
+ (0, react_jsx_runtime.jsx)("option", {
1417
+ value: "3:4",
1418
+ children: "3:4"
1419
+ }),
1420
+ (0, react_jsx_runtime.jsx)("option", {
1421
+ value: "3:2",
1422
+ children: "3:2"
1423
+ }),
1424
+ (0, react_jsx_runtime.jsx)("option", {
1425
+ value: "2:3",
1426
+ children: "2:3"
1427
+ })
1428
+ ]
1429
+ }),
1430
+ (0, react_jsx_runtime.jsxs)("div", {
1431
+ className: "dsh-ig-studio-search-wrap",
1432
+ children: [(0, react_jsx_runtime.jsx)(Search, {
1433
+ className: "dsh-ig-studio-search-icon",
1434
+ size: 14
1435
+ }), (0, react_jsx_runtime.jsx)("input", {
1436
+ type: "text",
1437
+ className: "dsh-ig-studio-search-input",
1438
+ placeholder: t("searchPlaceholder"),
1439
+ value: search,
1440
+ onChange: (e) => setSearch(e.target.value)
387
1441
  })]
388
- }), (0, react_jsx_runtime.jsx)("input", {
389
- type: "text",
390
- className: "dsh-ig-gallery-search-input",
391
- placeholder: t("searchPlaceholder"),
392
- value: search,
393
- onChange: (e) => setSearch(e.target.value)
1442
+ })
1443
+ ]
1444
+ }), (0, react_jsx_runtime.jsxs)("div", {
1445
+ className: "dsh-ig-studio-toolbar-right",
1446
+ children: [(0, react_jsx_runtime.jsxs)("select", {
1447
+ className: "dsh-ig-studio-select dsh-ig-studio-select-sort",
1448
+ value: sortBy,
1449
+ onChange: (e) => setSortBy(e.target.value),
1450
+ children: [(0, react_jsx_runtime.jsx)("option", {
1451
+ value: "newest",
1452
+ children: t("sortNewest")
1453
+ }), (0, react_jsx_runtime.jsx)("option", {
1454
+ value: "oldest",
1455
+ children: t("sortOldest")
394
1456
  })]
395
- }), (0, react_jsx_runtime.jsxs)("select", {
396
- className: "dsh-ig-gallery-select",
397
- value: selectedProvider,
398
- onChange: (e) => setSelectedProvider(e.target.value),
399
- children: [
400
- (0, react_jsx_runtime.jsx)("option", {
401
- value: "all",
402
- children: t("filterAll")
403
- }),
404
- (0, react_jsx_runtime.jsx)("option", {
405
- value: "google",
406
- children: t("filterGoogle")
407
- }),
408
- (0, react_jsx_runtime.jsx)("option", {
409
- value: "openai",
410
- children: t("filterOpenAI")
411
- }),
412
- (0, react_jsx_runtime.jsx)("option", {
413
- value: "seedream",
414
- children: t("filterSeedream")
415
- }),
416
- (0, react_jsx_runtime.jsx)("option", {
417
- value: "dashscope",
418
- children: t("filterDashScope")
419
- })
420
- ]
1457
+ }), (0, react_jsx_runtime.jsxs)("button", {
1458
+ type: "button",
1459
+ className: `dsh-ig-studio-btn dsh-ig-studio-btn-danger ${isManageMode ? "is-active" : ""}`,
1460
+ title: isManageMode ? t("exitManage") : t("manage"),
1461
+ onClick: () => {
1462
+ if (isManageMode) handleExitManage();
1463
+ else setIsManageMode(true);
1464
+ },
1465
+ children: [(0, react_jsx_runtime.jsx)(Trash2, { size: 13 }), (0, react_jsx_runtime.jsx)("span", { children: isManageMode ? t("exitManage") : t("manage") })]
421
1466
  })]
422
1467
  })]
423
1468
  }),
424
1469
  (0, react_jsx_runtime.jsx)("div", {
425
1470
  className: "dsh-ig-gallery-page-body",
426
- children: items.length === 0 ? (0, react_jsx_runtime.jsxs)("div", {
1471
+ children: activeTab === "gallery" || activeTab === "favorites" ? items.length === 0 ? (0, react_jsx_runtime.jsxs)("div", {
427
1472
  className: "dsh-ig-gallery-empty",
428
1473
  children: [
429
1474
  (0, react_jsx_runtime.jsx)("div", {
@@ -439,6 +1484,22 @@ window.__ModuleLoader__.load({
439
1484
  children: t("emptyDesc")
440
1485
  })
441
1486
  ]
1487
+ }) : activeTab === "favorites" && filteredItems.length === 0 && search === "" && selectedProvider === "all" && selectedRatio === "all" ? (0, react_jsx_runtime.jsxs)("div", {
1488
+ className: "dsh-ig-gallery-empty",
1489
+ children: [
1490
+ (0, react_jsx_runtime.jsx)("div", {
1491
+ className: "dsh-ig-gallery-empty-icon",
1492
+ children: "🤍"
1493
+ }),
1494
+ (0, react_jsx_runtime.jsx)("div", {
1495
+ className: "dsh-ig-gallery-empty-title",
1496
+ children: t("favEmptyTitle")
1497
+ }),
1498
+ (0, react_jsx_runtime.jsx)("div", {
1499
+ className: "dsh-ig-gallery-empty-desc",
1500
+ children: t("favEmptyDesc")
1501
+ })
1502
+ ]
442
1503
  }) : filteredItems.length === 0 ? (0, react_jsx_runtime.jsxs)("div", {
443
1504
  className: "dsh-ig-gallery-empty",
444
1505
  children: [
@@ -457,82 +1518,212 @@ window.__ModuleLoader__.load({
457
1518
  ]
458
1519
  }) : (0, react_jsx_runtime.jsx)("div", {
459
1520
  className: "dsh-ig-gallery-grid",
460
- children: filteredItems.map((item) => (0, react_jsx_runtime.jsx)(GalleryCard, {
1521
+ children: filteredItems.map((item, idx) => (0, react_jsx_runtime.jsx)(GalleryCard, {
461
1522
  item,
1523
+ index: idx,
1524
+ isManageMode,
1525
+ isSelected: selectedIds.has(item.id),
462
1526
  t,
463
- onPreview: (url, blob) => {
464
- setPreviewItem(item);
465
- setPreviewUrl(url);
466
- setPreviewBlob(blob);
467
- },
1527
+ onClick: (e, blob) => handleCardClick(item, idx, e, blob),
1528
+ onToggleSelect: (e) => handleToggleSelect(item.id, idx, e),
1529
+ onRequestDelete: () => requestSingleDelete(item),
1530
+ onPreview: (blob) => openPreviewItem(item, blob),
1531
+ onBlobLoaded: (b) => blobCache.set(item.id, b),
468
1532
  onToast: showToast
469
1533
  }, 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")
470
1550
  })
471
1551
  }),
472
1552
  toast && (0, react_jsx_runtime.jsx)("div", {
473
1553
  className: "dsh-ig-gallery-page-toast",
474
1554
  children: toast
475
1555
  }),
476
- previewItem && previewUrl && (0, react_jsx_runtime.jsxs)("div", {
477
- className: "dsh-ig-lightbox-backdrop",
1556
+ (isManageMode || selectedIds.size > 0) && (0, react_jsx_runtime.jsxs)("div", {
1557
+ className: "dsh-ig-batch-bar",
1558
+ children: [(0, react_jsx_runtime.jsxs)("div", {
1559
+ className: "dsh-ig-batch-bar-left",
1560
+ children: [
1561
+ (0, react_jsx_runtime.jsx)("span", {
1562
+ className: "dsh-ig-batch-counter",
1563
+ children: t("selectedCount", { n: String(selectedIds.size) })
1564
+ }),
1565
+ (0, react_jsx_runtime.jsx)("button", {
1566
+ type: "button",
1567
+ className: "dsh-ig-batch-btn",
1568
+ onClick: handleSelectAll,
1569
+ children: t("selectAll")
1570
+ }),
1571
+ (0, react_jsx_runtime.jsx)("button", {
1572
+ type: "button",
1573
+ className: "dsh-ig-batch-btn",
1574
+ onClick: handleInvertSelect,
1575
+ children: t("invertSelect")
1576
+ }),
1577
+ selectedIds.size > 0 && (0, react_jsx_runtime.jsx)("button", {
1578
+ type: "button",
1579
+ className: "dsh-ig-batch-btn",
1580
+ onClick: handleClearSelect,
1581
+ children: t("clearSelect")
1582
+ })
1583
+ ]
1584
+ }), (0, react_jsx_runtime.jsxs)("div", {
1585
+ className: "dsh-ig-batch-bar-right",
1586
+ children: [(0, react_jsx_runtime.jsxs)("button", {
1587
+ type: "button",
1588
+ className: "dsh-ig-batch-btn dsh-ig-batch-btn-danger",
1589
+ disabled: selectedIds.size === 0,
1590
+ onClick: () => setShowDeleteModal(true),
1591
+ children: [(0, react_jsx_runtime.jsx)(Trash2, { size: 13 }), (0, react_jsx_runtime.jsxs)("span", { children: [t("batchDelete"), selectedIds.size > 0 ? ` (${selectedIds.size})` : ""] })]
1592
+ }), (0, react_jsx_runtime.jsx)("button", {
1593
+ type: "button",
1594
+ className: "dsh-ig-batch-btn dsh-ig-batch-btn-exit",
1595
+ onClick: handleExitManage,
1596
+ children: t("exitManage")
1597
+ })]
1598
+ })]
1599
+ }),
1600
+ showDeleteModal && (0, react_jsx_runtime.jsx)("div", {
1601
+ className: "dsh-ig-modal-backdrop",
478
1602
  onClick: () => {
479
- setPreviewItem(null);
480
- setPreviewUrl(null);
481
- setPreviewBlob(null);
1603
+ if (!isDeleting) {
1604
+ setShowDeleteModal(false);
1605
+ if (!isManageMode) setSelectedIds(/* @__PURE__ */ new Set());
1606
+ }
482
1607
  },
1608
+ children: (0, react_jsx_runtime.jsxs)("div", {
1609
+ className: "dsh-ig-modal-box",
1610
+ onClick: (e) => e.stopPropagation(),
1611
+ children: [
1612
+ (0, react_jsx_runtime.jsxs)("div", {
1613
+ className: "dsh-ig-modal-header",
1614
+ children: [(0, react_jsx_runtime.jsx)("div", {
1615
+ className: "dsh-ig-modal-icon-danger",
1616
+ children: (0, react_jsx_runtime.jsx)(TriangleAlert, { size: 20 })
1617
+ }), (0, react_jsx_runtime.jsx)("div", {
1618
+ className: "dsh-ig-modal-title",
1619
+ children: selectedIds.size === 1 ? t("batchDeleteTitleSingle") : t("batchDeleteTitle", { count: String(selectedIds.size) })
1620
+ })]
1621
+ }),
1622
+ (0, react_jsx_runtime.jsxs)("div", {
1623
+ className: "dsh-ig-modal-body",
1624
+ children: [(0, react_jsx_runtime.jsx)("p", {
1625
+ className: "dsh-ig-modal-desc",
1626
+ children: t("batchDeleteDesc")
1627
+ }), (0, react_jsx_runtime.jsxs)("label", {
1628
+ className: "dsh-ig-modal-checkbox-label",
1629
+ children: [(0, react_jsx_runtime.jsx)("input", {
1630
+ type: "checkbox",
1631
+ checked: deleteWorkspaceFiles,
1632
+ onChange: (e) => setDeleteWorkspaceFiles(e.target.checked),
1633
+ disabled: isDeleting
1634
+ }), (0, react_jsx_runtime.jsx)("span", { children: t("deleteWorkspaceFilesOpt") })]
1635
+ })]
1636
+ }),
1637
+ (0, react_jsx_runtime.jsxs)("div", {
1638
+ className: "dsh-ig-modal-footer",
1639
+ children: [(0, react_jsx_runtime.jsx)("button", {
1640
+ type: "button",
1641
+ className: "dsh-ig-modal-btn dsh-ig-modal-btn-cancel",
1642
+ onClick: () => {
1643
+ setShowDeleteModal(false);
1644
+ if (!isManageMode) setSelectedIds(/* @__PURE__ */ new Set());
1645
+ },
1646
+ disabled: isDeleting,
1647
+ children: t("cancel")
1648
+ }), (0, react_jsx_runtime.jsx)("button", {
1649
+ type: "button",
1650
+ className: "dsh-ig-modal-btn dsh-ig-modal-btn-danger",
1651
+ onClick: handleConfirmBatchDelete,
1652
+ disabled: isDeleting,
1653
+ children: isDeleting ? "..." : selectedIds.size === 1 ? t("confirmDeleteSingle") : t("confirmBatchDelete", { count: String(selectedIds.size) })
1654
+ })]
1655
+ })
1656
+ ]
1657
+ })
1658
+ }),
1659
+ previewItem && (0, react_jsx_runtime.jsxs)("div", {
1660
+ className: "dsh-ig-lightbox-backdrop",
1661
+ onClick: handleClosePreview,
483
1662
  children: [
484
1663
  (0, react_jsx_runtime.jsxs)("div", {
485
1664
  className: "dsh-ig-lightbox-topbar",
486
1665
  onClick: (e) => e.stopPropagation(),
487
1666
  children: [(0, react_jsx_runtime.jsxs)("div", {
488
1667
  className: "dsh-ig-lightbox-meta",
489
- children: [(0, react_jsx_runtime.jsx)("span", {
490
- className: "dsh-ig-tag",
491
- children: previewItem.provider
492
- }), previewItem.model ? (0, react_jsx_runtime.jsx)("span", {
493
- className: "dsh-ig-tag dsh-ig-tag-model",
494
- children: previewItem.model
495
- }) : null]
1668
+ children: [
1669
+ (0, react_jsx_runtime.jsx)("span", {
1670
+ className: "dsh-ig-tag",
1671
+ children: previewItem.provider
1672
+ }),
1673
+ previewItem.model ? (0, react_jsx_runtime.jsx)("span", {
1674
+ className: "dsh-ig-tag dsh-ig-tag-model",
1675
+ children: previewItem.model
1676
+ }) : null,
1677
+ currentPreviewIndex >= 0 && (0, react_jsx_runtime.jsxs)("span", {
1678
+ className: "dsh-ig-lightbox-counter",
1679
+ children: [
1680
+ currentPreviewIndex + 1,
1681
+ " / ",
1682
+ filteredItems.length
1683
+ ]
1684
+ })
1685
+ ]
496
1686
  }), (0, react_jsx_runtime.jsx)("button", {
497
1687
  type: "button",
498
1688
  className: "dsh-ig-lightbox-close-btn",
499
1689
  title: t("close"),
500
- onClick: () => {
501
- setPreviewItem(null);
502
- setPreviewUrl(null);
503
- setPreviewBlob(null);
504
- },
505
- children: (0, react_jsx_runtime.jsxs)("svg", {
506
- width: "18",
507
- height: "18",
508
- viewBox: "0 0 24 24",
509
- fill: "none",
510
- stroke: "currentColor",
511
- strokeWidth: "2",
512
- strokeLinecap: "round",
513
- strokeLinejoin: "round",
514
- children: [(0, react_jsx_runtime.jsx)("line", {
515
- x1: "18",
516
- y1: "6",
517
- x2: "6",
518
- y2: "18"
519
- }), (0, react_jsx_runtime.jsx)("line", {
520
- x1: "6",
521
- y1: "6",
522
- x2: "18",
523
- y2: "18"
524
- })]
525
- })
1690
+ onClick: handleClosePreview,
1691
+ children: (0, react_jsx_runtime.jsx)(X, { size: 18 })
526
1692
  })]
527
1693
  }),
1694
+ (0, react_jsx_runtime.jsx)("button", {
1695
+ type: "button",
1696
+ className: "dsh-ig-lightbox-nav-btn dsh-ig-lightbox-nav-prev",
1697
+ title: t("prevImage"),
1698
+ disabled: !hasPrev,
1699
+ onClick: (e) => {
1700
+ e.stopPropagation();
1701
+ goToPrev();
1702
+ },
1703
+ children: (0, react_jsx_runtime.jsx)(ChevronLeft, { size: 24 })
1704
+ }),
1705
+ (0, react_jsx_runtime.jsx)("button", {
1706
+ type: "button",
1707
+ className: "dsh-ig-lightbox-nav-btn dsh-ig-lightbox-nav-next",
1708
+ title: t("nextImage"),
1709
+ disabled: !hasNext,
1710
+ onClick: (e) => {
1711
+ e.stopPropagation();
1712
+ goToNext();
1713
+ },
1714
+ children: (0, react_jsx_runtime.jsx)(ChevronRight, { size: 24 })
1715
+ }),
528
1716
  (0, react_jsx_runtime.jsx)("div", {
529
1717
  className: "dsh-ig-lightbox-img-wrap",
530
1718
  onClick: (e) => e.stopPropagation(),
531
- children: (0, react_jsx_runtime.jsx)("img", {
1719
+ children: previewLoading ? (0, react_jsx_runtime.jsx)("div", {
1720
+ className: "dsh-ig-lightbox-loading",
1721
+ children: (0, react_jsx_runtime.jsx)("div", { className: "dsh-ig-lightbox-spinner" })
1722
+ }) : previewUrl ? (0, react_jsx_runtime.jsx)("img", {
532
1723
  className: "dsh-ig-lightbox-img",
533
1724
  src: previewUrl,
534
1725
  alt: previewItem.prompt
535
- })
1726
+ }) : null
536
1727
  }),
537
1728
  (0, react_jsx_runtime.jsxs)("div", {
538
1729
  className: "dsh-ig-lightbox-bottombar",
@@ -552,24 +1743,7 @@ window.__ModuleLoader__.load({
552
1743
  await navigator.clipboard.writeText(previewItem.prompt);
553
1744
  showToast(t("copiedPrompt"));
554
1745
  },
555
- children: [(0, react_jsx_runtime.jsxs)("svg", {
556
- width: "14",
557
- height: "14",
558
- viewBox: "0 0 24 24",
559
- fill: "none",
560
- stroke: "currentColor",
561
- strokeWidth: "2",
562
- strokeLinecap: "round",
563
- strokeLinejoin: "round",
564
- children: [(0, react_jsx_runtime.jsx)("path", { 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" }), (0, react_jsx_runtime.jsx)("rect", {
565
- x: "8",
566
- y: "2",
567
- width: "8",
568
- height: "4",
569
- rx: "1",
570
- ry: "1"
571
- })]
572
- }), (0, react_jsx_runtime.jsx)("span", { children: t("copyPpt") })]
1746
+ children: [(0, react_jsx_runtime.jsx)(FileText, { size: 14 }), (0, react_jsx_runtime.jsx)("span", { children: t("copyPpt") })]
573
1747
  }),
574
1748
  (0, react_jsx_runtime.jsxs)("button", {
575
1749
  type: "button",
@@ -580,30 +1754,14 @@ window.__ModuleLoader__.load({
580
1754
  const ok = await copyImageBlob(previewBlob);
581
1755
  showToast(ok ? t("copiedImage") : t("copyFailed"));
582
1756
  },
583
- children: [(0, react_jsx_runtime.jsxs)("svg", {
584
- width: "14",
585
- height: "14",
586
- viewBox: "0 0 24 24",
587
- fill: "none",
588
- stroke: "currentColor",
589
- strokeWidth: "2",
590
- strokeLinecap: "round",
591
- strokeLinejoin: "round",
592
- children: [(0, react_jsx_runtime.jsx)("rect", {
593
- x: "9",
594
- y: "9",
595
- width: "13",
596
- height: "13",
597
- rx: "2",
598
- ry: "2"
599
- }), (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" })]
600
- }), (0, react_jsx_runtime.jsx)("span", { children: t("copyImg") })]
1757
+ children: [(0, react_jsx_runtime.jsx)(Copy, { size: 14 }), (0, react_jsx_runtime.jsx)("span", { children: t("copyImg") })]
601
1758
  }),
602
1759
  (0, react_jsx_runtime.jsxs)("button", {
603
1760
  type: "button",
604
1761
  className: "dsh-ig-lightbox-btn",
605
1762
  title: t("download"),
606
1763
  onClick: () => {
1764
+ if (!previewUrl) return;
607
1765
  const a = document.createElement("a");
608
1766
  a.href = previewUrl;
609
1767
  a.download = `dsh-${previewItem.provider}-${previewItem.id}.png`;
@@ -611,65 +1769,16 @@ window.__ModuleLoader__.load({
611
1769
  a.click();
612
1770
  document.body.removeChild(a);
613
1771
  },
614
- children: [(0, react_jsx_runtime.jsxs)("svg", {
615
- width: "14",
616
- height: "14",
617
- viewBox: "0 0 24 24",
618
- fill: "none",
619
- stroke: "currentColor",
620
- strokeWidth: "2",
621
- strokeLinecap: "round",
622
- strokeLinejoin: "round",
623
- children: [
624
- (0, react_jsx_runtime.jsx)("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
625
- (0, react_jsx_runtime.jsx)("polyline", { points: "7 10 12 15 17 10" }),
626
- (0, react_jsx_runtime.jsx)("line", {
627
- x1: "12",
628
- y1: "15",
629
- x2: "12",
630
- y2: "3"
631
- })
632
- ]
633
- }), (0, react_jsx_runtime.jsx)("span", { children: t("download") })]
1772
+ children: [(0, react_jsx_runtime.jsx)(Download, { size: 14 }), (0, react_jsx_runtime.jsx)("span", { children: t("download") })]
634
1773
  }),
635
1774
  (0, react_jsx_runtime.jsxs)("button", {
636
1775
  type: "button",
637
1776
  className: "dsh-ig-lightbox-btn dsh-ig-lightbox-btn-danger",
638
1777
  title: t("delete"),
639
- onClick: async () => {
640
- if (!window.confirm(t("confirmDelete"))) return;
641
- await deleteGalleryItem(previewItem.id);
642
- setPreviewItem(null);
643
- setPreviewUrl(null);
644
- setPreviewBlob(null);
645
- showToast(t("deleted"));
1778
+ onClick: () => {
1779
+ if (previewItem) requestSingleDelete(previewItem);
646
1780
  },
647
- children: [(0, react_jsx_runtime.jsxs)("svg", {
648
- width: "14",
649
- height: "14",
650
- viewBox: "0 0 24 24",
651
- fill: "none",
652
- stroke: "currentColor",
653
- strokeWidth: "2",
654
- strokeLinecap: "round",
655
- strokeLinejoin: "round",
656
- children: [
657
- (0, react_jsx_runtime.jsx)("polyline", { points: "3 6 5 6 21 6" }),
658
- (0, react_jsx_runtime.jsx)("path", { d: "M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" }),
659
- (0, react_jsx_runtime.jsx)("line", {
660
- x1: "10",
661
- y1: "11",
662
- x2: "10",
663
- y2: "17"
664
- }),
665
- (0, react_jsx_runtime.jsx)("line", {
666
- x1: "14",
667
- y1: "11",
668
- x2: "14",
669
- y2: "17"
670
- })
671
- ]
672
- }), (0, react_jsx_runtime.jsx)("span", { children: t("delete") })]
1781
+ children: [(0, react_jsx_runtime.jsx)(Trash2, { size: 14 }), (0, react_jsx_runtime.jsx)("span", { children: t("delete") })]
673
1782
  })
674
1783
  ]
675
1784
  })]
@@ -679,7 +1788,7 @@ window.__ModuleLoader__.load({
679
1788
  ]
680
1789
  });
681
1790
  };
682
- const GalleryCard = ({ item, t, onPreview, onToast }) => {
1791
+ const GalleryCard = ({ item, index: _index, isManageMode, isSelected, t, onClick, onToggleSelect, onRequestDelete, onPreview: _onPreview, onBlobLoaded, onToast }) => {
683
1792
  const [url, setUrl] = (0, react.useState)();
684
1793
  const [blob, setBlob] = (0, react.useState)();
685
1794
  const [loading, setLoading] = (0, react.useState)(true);
@@ -697,6 +1806,7 @@ window.__ModuleLoader__.load({
697
1806
  const resBlob = await response.blob();
698
1807
  if (controller.signal.aborted) return;
699
1808
  setBlob(resBlob);
1809
+ onBlobLoaded?.(resBlob);
700
1810
  objectUrl = URL.createObjectURL(resBlob);
701
1811
  setUrl(objectUrl);
702
1812
  setLoading(false);
@@ -735,14 +1845,27 @@ window.__ModuleLoader__.load({
735
1845
  a.click();
736
1846
  document.body.removeChild(a);
737
1847
  };
1848
+ const toggleFavorite = async (e) => {
1849
+ e.stopPropagation();
1850
+ onToast(await toggleFavoriteGalleryItem(item.id) ? t("favoriteAdded") : t("favoriteRemoved"));
1851
+ };
1852
+ const badgeLabel = item.model || item.provider;
738
1853
  return (0, react_jsx_runtime.jsxs)("div", {
739
- className: "dsh-ig-gallery-card",
740
- onClick: () => {
741
- if (url && blob) onPreview(url, blob);
742
- },
1854
+ className: `dsh-ig-gallery-card ${isSelected ? "is-selected" : ""} ${isManageMode ? "is-manage-mode" : ""}`,
1855
+ onClick: (e) => onClick(e, blob),
743
1856
  children: [(0, react_jsx_runtime.jsxs)("div", {
744
1857
  className: "dsh-ig-gallery-card-media",
745
1858
  children: [
1859
+ (0, react_jsx_runtime.jsx)("button", {
1860
+ type: "button",
1861
+ className: `dsh-ig-card-checkbox ${isSelected ? "is-checked" : ""}`,
1862
+ title: isSelected ? t("clearSelect") : t("manage"),
1863
+ onClick: onToggleSelect,
1864
+ children: isSelected && (0, react_jsx_runtime.jsx)(Check, {
1865
+ size: 11,
1866
+ strokeWidth: 3
1867
+ })
1868
+ }),
746
1869
  loading && (0, react_jsx_runtime.jsx)("div", {
747
1870
  className: "dsh-ig-gallery-card-loading",
748
1871
  children: "..."
@@ -767,50 +1890,14 @@ window.__ModuleLoader__.load({
767
1890
  onClick: (e) => {
768
1891
  copyImage(e);
769
1892
  },
770
- children: (0, react_jsx_runtime.jsxs)("svg", {
771
- width: "14",
772
- height: "14",
773
- viewBox: "0 0 24 24",
774
- fill: "none",
775
- stroke: "currentColor",
776
- strokeWidth: "2",
777
- strokeLinecap: "round",
778
- strokeLinejoin: "round",
779
- children: [(0, react_jsx_runtime.jsx)("rect", {
780
- x: "9",
781
- y: "9",
782
- width: "13",
783
- height: "13",
784
- rx: "2",
785
- ry: "2"
786
- }), (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" })]
787
- })
1893
+ children: (0, react_jsx_runtime.jsx)(Copy, { size: 13 })
788
1894
  }),
789
1895
  (0, react_jsx_runtime.jsx)("button", {
790
1896
  type: "button",
791
1897
  className: "dsh-ig-tool-btn",
792
1898
  title: t("download"),
793
1899
  onClick: downloadImage,
794
- children: (0, react_jsx_runtime.jsxs)("svg", {
795
- width: "14",
796
- height: "14",
797
- viewBox: "0 0 24 24",
798
- fill: "none",
799
- stroke: "currentColor",
800
- strokeWidth: "2",
801
- strokeLinecap: "round",
802
- strokeLinejoin: "round",
803
- children: [
804
- (0, react_jsx_runtime.jsx)("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
805
- (0, react_jsx_runtime.jsx)("polyline", { points: "7 10 12 15 17 10" }),
806
- (0, react_jsx_runtime.jsx)("line", {
807
- x1: "12",
808
- y1: "15",
809
- x2: "12",
810
- y2: "3"
811
- })
812
- ]
813
- })
1900
+ children: (0, react_jsx_runtime.jsx)(Download, { size: 13 })
814
1901
  }),
815
1902
  (0, react_jsx_runtime.jsx)("button", {
816
1903
  type: "button",
@@ -819,84 +1906,95 @@ window.__ModuleLoader__.load({
819
1906
  onClick: (e) => {
820
1907
  copyPrompt(e);
821
1908
  },
822
- children: (0, react_jsx_runtime.jsxs)("svg", {
823
- width: "14",
824
- height: "14",
825
- viewBox: "0 0 24 24",
826
- fill: "none",
827
- stroke: "currentColor",
828
- strokeWidth: "2",
829
- strokeLinecap: "round",
830
- strokeLinejoin: "round",
831
- children: [(0, react_jsx_runtime.jsx)("path", { 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" }), (0, react_jsx_runtime.jsx)("rect", {
832
- x: "8",
833
- y: "2",
834
- width: "8",
835
- height: "4",
836
- rx: "1",
837
- ry: "1"
838
- })]
839
- })
1909
+ children: (0, react_jsx_runtime.jsx)(FileText, { size: 13 })
840
1910
  }),
841
1911
  (0, react_jsx_runtime.jsx)("button", {
842
1912
  type: "button",
843
1913
  className: "dsh-ig-tool-btn dsh-ig-tool-btn-danger",
844
1914
  title: t("delete"),
845
- onClick: async (e) => {
1915
+ onClick: (e) => {
846
1916
  e.stopPropagation();
847
- if (!window.confirm(t("confirmDelete"))) return;
848
- await deleteGalleryItem(item.id);
849
- onToast(t("deleted"));
1917
+ onRequestDelete();
850
1918
  },
851
- children: (0, react_jsx_runtime.jsxs)("svg", {
852
- width: "14",
853
- height: "14",
854
- viewBox: "0 0 24 24",
855
- fill: "none",
856
- stroke: "currentColor",
857
- strokeWidth: "2",
858
- strokeLinecap: "round",
859
- strokeLinejoin: "round",
860
- children: [
861
- (0, react_jsx_runtime.jsx)("polyline", { points: "3 6 5 6 21 6" }),
862
- (0, react_jsx_runtime.jsx)("path", { d: "M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" }),
863
- (0, react_jsx_runtime.jsx)("line", {
864
- x1: "10",
865
- y1: "11",
866
- x2: "10",
867
- y2: "17"
868
- }),
869
- (0, react_jsx_runtime.jsx)("line", {
870
- x1: "14",
871
- y1: "11",
872
- x2: "14",
873
- y2: "17"
874
- })
875
- ]
876
- })
1919
+ children: (0, react_jsx_runtime.jsx)(Trash2, { size: 13 })
877
1920
  })
878
1921
  ]
879
1922
  })
880
1923
  ]
881
1924
  }), (0, react_jsx_runtime.jsxs)("div", {
882
1925
  className: "dsh-ig-gallery-card-meta",
883
- children: [(0, react_jsx_runtime.jsxs)("div", {
884
- className: "dsh-ig-gallery-card-header",
885
- children: [(0, react_jsx_runtime.jsx)("span", {
886
- className: "dsh-ig-tag",
887
- children: item.provider
888
- }), item.model ? (0, react_jsx_runtime.jsx)("span", {
889
- className: "dsh-ig-tag dsh-ig-tag-model",
890
- children: item.model
891
- }) : null]
892
- }), (0, react_jsx_runtime.jsx)("p", {
893
- className: "dsh-ig-gallery-card-prompt",
894
- title: item.prompt,
895
- children: item.prompt
896
- })]
1926
+ children: [
1927
+ (0, react_jsx_runtime.jsx)("div", {
1928
+ className: "dsh-ig-card-badge-row",
1929
+ children: (0, react_jsx_runtime.jsx)("span", {
1930
+ className: "dsh-ig-card-badge",
1931
+ title: badgeLabel,
1932
+ children: badgeLabel
1933
+ })
1934
+ }),
1935
+ (0, react_jsx_runtime.jsx)("div", {
1936
+ className: "dsh-ig-gallery-card-prompt-line",
1937
+ title: item.prompt,
1938
+ children: item.prompt
1939
+ }),
1940
+ (0, react_jsx_runtime.jsxs)("div", {
1941
+ className: "dsh-ig-card-footer-row",
1942
+ children: [(0, react_jsx_runtime.jsxs)("span", {
1943
+ className: "dsh-ig-card-meta-text",
1944
+ children: [
1945
+ formatCardMeta(item),
1946
+ " | ",
1947
+ formatRelativeTime(item.createdAt, t)
1948
+ ]
1949
+ }), (0, react_jsx_runtime.jsx)("button", {
1950
+ type: "button",
1951
+ className: `dsh-ig-card-fav-btn ${item.isFavorite ? "is-favorited" : ""}`,
1952
+ title: item.isFavorite ? t("favoriteRemoved") : t("favoriteAdded"),
1953
+ onClick: (e) => {
1954
+ toggleFavorite(e);
1955
+ },
1956
+ children: (0, react_jsx_runtime.jsx)(Heart, {
1957
+ size: 15,
1958
+ fill: item.isFavorite ? "currentColor" : "none"
1959
+ })
1960
+ })]
1961
+ })
1962
+ ]
897
1963
  })]
898
1964
  });
899
1965
  };
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
+ };
900
1998
  async function copyImageBlob(blob) {
901
1999
  try {
902
2000
  if (blob.type === "image/png") {
@@ -943,80 +2041,93 @@ window.__ModuleLoader__.load({
943
2041
  //#endregion
944
2042
  //#region lib/types/client/image-result-node.js
945
2043
  const IMAGE_RESULT_NODE_KIND = "dsh-image-result";
946
- /** One image result row per Turn, re-anchored to Turn end after the final answer settles. */
947
- const imageResultDefinition = {
948
- kind: IMAGE_RESULT_NODE_KIND,
949
- target: "chat",
950
- match: (event) => {
951
- const turn = eventTurn(event);
952
- if (turn === void 0) return null;
953
- if (event.type === "turn/start") return {
954
- id: String(turn),
955
- role: "start"
956
- };
957
- if (event.type === "turn/end") return {
958
- id: String(turn),
959
- role: "update"
960
- };
961
- if (event.type === "assistant/message") return {
962
- id: String(turn),
963
- role: "update"
964
- };
965
- if (event.type === "tool/result" && imageResultFromMeta(event.data.meta) !== void 0) return {
966
- id: String(turn),
967
- role: "update"
968
- };
969
- return null;
970
- },
971
- start: (_context, match) => {
972
- const turn = eventTurn(match.event);
973
- if (match.event.type !== "turn/start" || turn === void 0) throw new Error("dsh-image-result start requires turn/start");
974
- return {
975
- turn,
976
- results: []
977
- };
978
- },
979
- update: (context, match) => {
980
- if (match.event.type === "turn/end") return {
981
- ...context.state,
982
- endSeq: match.event.seq
983
- };
984
- if (match.event.type === "assistant/message") return {
985
- ...context.state,
986
- answerSeq: match.event.seq
987
- };
988
- if (match.event.type !== "tool/result") return context.state;
989
- const result = imageResultFromMeta(match.event.data.meta);
990
- if (result === void 0) return context.state;
991
- if (context.state.results.some((candidate) => candidate.attachment.attachmentId === result.attachment.attachmentId)) return context.state;
992
- return {
993
- ...context.state,
994
- results: [...context.state.results, {
995
- ...result,
996
- seq: match.event.seq
997
- }]
998
- };
999
- },
1000
- buildViewNode: (context) => {
1001
- const state = context.state;
1002
- const last = state?.results.at(-1);
1003
- if (state === void 0 || last === void 0) return null;
1004
- const location = context.matches.at(-1)?.location ?? context.start?.location ?? { kind: "unresolved" };
1005
- return {
1006
- key: context.key,
1007
- kind: IMAGE_RESULT_NODE_KIND,
1008
- id: context.id,
1009
- target: "chat",
1010
- anchorSeq: state.answerSeq ?? state.endSeq ?? last.seq,
1011
- location,
1012
- visibility: "visible",
1013
- data: {
1014
- turn: state.turn,
1015
- results: state.results.map(({ seq: _seq, ...result }) => result)
1016
- }
1017
- };
2044
+ /**
2045
+ * One image result row per Turn, anchored at its own tool/result position while
2046
+ * the Turn runs, then optionally re-anchored beside the final answer once the
2047
+ * Turn closes (only needed under the folding Compact transcript view).
2048
+ */
2049
+ function createImageResultDefinition(options = {}) {
2050
+ return {
2051
+ kind: IMAGE_RESULT_NODE_KIND,
2052
+ target: "chat",
2053
+ match: (event) => {
2054
+ const turn = eventTurn(event);
2055
+ if (turn === void 0) return null;
2056
+ if (event.type === "turn/start") return {
2057
+ id: String(turn),
2058
+ role: "start"
2059
+ };
2060
+ if (event.type === "turn/end") return {
2061
+ id: String(turn),
2062
+ role: "update"
2063
+ };
2064
+ if (event.type === "assistant/message") return {
2065
+ id: String(turn),
2066
+ role: "update"
2067
+ };
2068
+ if (event.type === "tool/result" && imageResultFromMeta(event.data.meta) !== void 0) return {
2069
+ id: String(turn),
2070
+ role: "update"
2071
+ };
2072
+ return null;
2073
+ },
2074
+ start: (_context, match) => {
2075
+ const turn = eventTurn(match.event);
2076
+ if (match.event.type !== "turn/start" || turn === void 0) throw new Error("dsh-image-result start requires turn/start");
2077
+ return {
2078
+ turn,
2079
+ results: []
2080
+ };
2081
+ },
2082
+ update: (context, match) => {
2083
+ if (match.event.type === "turn/end") return {
2084
+ ...context.state,
2085
+ endSeq: match.event.seq
2086
+ };
2087
+ if (match.event.type === "assistant/message") return {
2088
+ ...context.state,
2089
+ answerSeq: match.event.seq
2090
+ };
2091
+ if (match.event.type !== "tool/result") return context.state;
2092
+ const result = imageResultFromMeta(match.event.data.meta);
2093
+ if (result === void 0) return context.state;
2094
+ if (context.state.results.some((candidate) => candidate.attachment.attachmentId === result.attachment.attachmentId)) return context.state;
2095
+ return {
2096
+ ...context.state,
2097
+ results: [...context.state.results, {
2098
+ ...result,
2099
+ seq: match.event.seq
2100
+ }]
2101
+ };
2102
+ },
2103
+ buildViewNode: (context) => {
2104
+ const state = context.state;
2105
+ const last = state?.results.at(-1);
2106
+ if (state === void 0 || last === void 0) return null;
2107
+ const location = context.matches.at(-1)?.location ?? context.start?.location ?? { kind: "unresolved" };
2108
+ return {
2109
+ key: context.key,
2110
+ kind: IMAGE_RESULT_NODE_KIND,
2111
+ id: context.id,
2112
+ target: "chat",
2113
+ anchorSeq: state.endSeq !== void 0 && compactTranscript(options) ? state.answerSeq ?? state.endSeq ?? last.seq : last.seq,
2114
+ location,
2115
+ visibility: "visible",
2116
+ data: {
2117
+ turn: state.turn,
2118
+ results: state.results.map(({ seq: _seq, ...result }) => result)
2119
+ }
2120
+ };
2121
+ }
2122
+ };
2123
+ }
2124
+ function compactTranscript(options) {
2125
+ try {
2126
+ return options.isCompactTranscript?.() ?? true;
2127
+ } catch {
2128
+ return true;
1018
2129
  }
1019
- };
2130
+ }
1020
2131
  /** Parse the plugin-owned durable presentation metadata from a Tool result event. */
1021
2132
  function imageResultFromMeta(value) {
1022
2133
  const meta = record(value);
@@ -1029,7 +2140,8 @@ window.__ModuleLoader__.load({
1029
2140
  provider: stringValue(meta.provider, "google"),
1030
2141
  model: stringValue(meta.model, ""),
1031
2142
  output: stringValue(meta.output, ""),
1032
- ...typeof meta.savedTo === "string" ? { savedTo: meta.savedTo } : {}
2143
+ ...typeof meta.savedTo === "string" ? { savedTo: meta.savedTo } : {},
2144
+ ...typeof meta.seed === "number" ? { seed: meta.seed } : {}
1033
2145
  };
1034
2146
  }
1035
2147
  function eventTurn(event) {
@@ -1096,13 +2208,18 @@ window.__ModuleLoader__.load({
1096
2208
  endpointHintDashScope: "阿里云百炼 DashScope 官方接口地址。",
1097
2209
  endpointHintComfyUI: "正在运行且 DSH Host 可以访问的 ComfyUI 地址,默认使用本机 8188 端口。",
1098
2210
  model: "模型",
1099
- workflow: "API Workflow",
2211
+ workflow: "API Workflow 工作流",
1100
2212
  workflowImport: "导入 JSON 文件",
1101
- workflowReplace: "替换 JSON 文件",
1102
2213
  workflowMissing: "尚未导入工作流",
1103
2214
  workflowImported: "已导入 {name}",
1104
- workflowHint: "请从 ComfyUI 导出 API Format JSON,并在提示词输入位置写入 {{prompt}};可用 {{seed}} 作为随机种子。",
2215
+ workflowHint: "从 ComfyUI 导出 API Format JSON,在提示词输入写入 {{prompt}},种子可用 {{seed}};图生图工作流在 LoadImage 的 image 输入写入 {{image}}(仅一次)。可导入多个工作流,Agent 也能在调用时按名称指定。",
1105
2216
  workflowTooLarge: "工作流文件不能超过 5 MB。",
2217
+ workflowActiveTitle: "设为当前使用的工作流",
2218
+ workflowRemove: "删除",
2219
+ workflowPresetPlaceholder: "预设提示词,留空则只用对话内容",
2220
+ workflowPresetTitle: "预设提示词:每次调用此工作流时自动加在用户提示词前面。",
2221
+ workflowNameRequired: "工作流名称不能为空。",
2222
+ workflowDuplicateName: "工作流名称不能重复。",
1106
2223
  timeout: "生成超时(秒)",
1107
2224
  timeoutHint: "包括提交、等待和下载图片;默认 300 秒。",
1108
2225
  saveToWorkspace: "保存到工作区",
@@ -1148,13 +2265,18 @@ window.__ModuleLoader__.load({
1148
2265
  endpointHintDashScope: "Official Aliyun DashScope endpoint.",
1149
2266
  endpointHintComfyUI: "A running ComfyUI server reachable by the DSH Host; the default points to port 8188 on this computer.",
1150
2267
  model: "Model",
1151
- workflow: "API Workflow",
2268
+ workflow: "API Workflows",
1152
2269
  workflowImport: "Import JSON file",
1153
- workflowReplace: "Replace JSON file",
1154
2270
  workflowMissing: "No workflow imported",
1155
2271
  workflowImported: "Imported {name}",
1156
- workflowHint: "Export an API Format JSON from ComfyUI and place {{prompt}} in its prompt input. {{seed}} is available for a random seed.",
2272
+ workflowHint: "Export an API Format JSON from ComfyUI and place {{prompt}} in its prompt input; {{seed}} is available for a random seed. For image editing put {{image}} (exactly once) in the LoadImage image input. Import as many workflows as you need; the Agent can also pick one by name.",
1157
2273
  workflowTooLarge: "Workflow files must be no larger than 5 MB.",
2274
+ workflowActiveTitle: "Make this the active workflow",
2275
+ workflowRemove: "Remove",
2276
+ workflowPresetPlaceholder: "Preset prompt (optional)",
2277
+ workflowPresetTitle: "Preset prompt: automatically prepended to the user prompt on every call of this workflow.",
2278
+ workflowNameRequired: "Workflow names cannot be empty.",
2279
+ workflowDuplicateName: "Workflow names must be unique.",
1158
2280
  timeout: "Generation timeout (seconds)",
1159
2281
  timeoutHint: "Covers submission, waiting, and image download; defaults to 300 seconds.",
1160
2282
  saveToWorkspace: "Save to workspace",
@@ -1203,9 +2325,16 @@ window.__ModuleLoader__.load({
1203
2325
  .dsh-ig-file-button:hover{background:var(--dsw-alias-bg-layer-2,#edf0f3);border-color:var(--dsw-alias-label-dimmed,#9ca3af)}
1204
2326
  .dsh-ig-file-button:focus-within{outline:2px solid var(--dsw-alias-brand-primary,#4c78ff);outline-offset:2px}
1205
2327
  .dsh-ig-file-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-secondary,inherit);font-size:12px}
2328
+ .dsh-ig-workflow-list{list-style:none;margin:0;padding:0;display:grid;gap:8px}
2329
+ .dsh-ig-workflow-row{display:flex;flex-direction:column;gap:6px;padding:8px;border:1px solid var(--dsw-alias-border-l2,#eee);border-radius:8px}
2330
+ .dsh-ig-workflow-main{display:flex;align-items:center;gap:8px}
2331
+ .dsh-ig-workflow-active{display:inline-flex;align-items:center;cursor:pointer;flex:none}
2332
+ .dsh-ig-workflow-active input[type=radio]{width:15px;height:15px;accent-color:var(--dsw-alias-brand-primary,#4c78ff);margin:0;cursor:pointer}
2333
+ .dsh-ig-workflow-name{flex:1;min-width:0}
1206
2334
  .dsh-ig-btn-reset{appearance:none;border:1px solid var(--dsw-alias-border-l2,#d7dbe0);border-radius:8px;padding:7px 12px;background:var(--dsw-alias-bg-layer-3,#f9fafb);color:var(--dsw-alias-label-secondary,inherit);font:inherit;font-size:13px;cursor:pointer;white-space:nowrap;transition:background .15s,border-color .15s}
1207
2335
  .dsh-ig-btn-reset:hover{background:var(--dsw-alias-bg-layer-2,#edf0f3);border-color:var(--dsw-alias-label-dimmed,#9ca3af)}
1208
2336
  .dsh-ig-hint,.dsh-ig-status{margin:0;color:var(--dsw-alias-label-tertiary,#7b818b);font-size:12px;line-height:1.4}
2337
+ .dsh-ig-status-error{color:var(--dsw-alias-label-error,#d33);font-weight:500}
1209
2338
  .dsh-ig-actions{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-top:16px;padding-top:12px;border-top:1px solid var(--dsw-alias-border-l2,#eee)}
1210
2339
  .dsh-ig-check-row{display:flex;align-items:center;gap:8px;cursor:pointer}
1211
2340
  .dsh-ig-check-row input[type=checkbox]{width:15px;height:15px;accent-color:var(--dsw-alias-brand-primary,#4c78ff);margin:0}
@@ -1228,58 +2357,149 @@ window.__ModuleLoader__.load({
1228
2357
  .dsh-ig-error{color:var(--dsw-alias-label-error,#d33);font-size:13px}
1229
2358
  .dsh-ig-loading{color:var(--dsw-alias-label-tertiary,#7b818b);font-size:13px}
1230
2359
 
1231
- /* Native Workspace Gallery View (Renders seamlessly inside DSH Session View) */
2360
+ /* Native Workspace Gallery & Studio View (Renders seamlessly inside DSH Session View) */
1232
2361
  .dsh-ig-gallery-page{width:100%;height:100%;background:var(--dsw-alias-bg-layer-1,#ffffff);display:flex;flex-direction:column;overflow:hidden;flex:1}
1233
- .dsh-ig-gallery-page-header{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:12px 28px;border-bottom:1px solid var(--dsw-alias-border-l2,#e5e7eb);background:var(--dsw-alias-bg-layer-1,#ffffff);flex-shrink:0}
1234
- .dsh-ig-gallery-page-title-row{display:flex;align-items:center;gap:12px}
1235
- .dsh-ig-gallery-page-count{font-size:13px;font-weight:500;color:var(--dsw-alias-label-secondary,#4b5563);background:var(--dsw-alias-bg-layer-3,#f3f4f6);padding:3px 10px;border-radius:20px}
1236
- .dsh-ig-gallery-page-tools{display:flex;align-items:center;gap:12px}
1237
- .dsh-ig-gallery-search-wrap{position:relative;display:flex;align-items:center}
1238
- .dsh-ig-gallery-search-icon{position:absolute;left:10px;color:var(--dsw-alias-label-tertiary,#9ca3af);pointer-events:none}
1239
- .dsh-ig-gallery-search-input{padding:6px 12px 6px 32px;font-size:13px;border:1px solid var(--dsw-alias-border-l2,#d7dbe0);border-radius:8px;background:var(--dsw-alias-bg-layer-2,#fff);color:inherit;outline:none;width:200px;transition:border-color .15s,width .2s}
1240
- .dsh-ig-gallery-search-input:focus{border-color:var(--dsw-alias-brand-primary,#4c78ff);width:240px}
1241
- .dsh-ig-gallery-select{padding:6px 12px;font-size:13px;border:1px solid var(--dsw-alias-border-l2,#d7dbe0);border-radius:8px;background:var(--dsw-alias-bg-layer-2,#fff);color:inherit;outline:none;cursor:pointer}
1242
- .dsh-ig-gallery-page-body{flex:1;overflow-y:auto;padding:24px 28px}
1243
- .dsh-ig-gallery-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:20px}
1244
- .dsh-ig-gallery-card{background:var(--dsw-alias-bg-layer-2,#fff);border:1px solid var(--dsw-alias-border-l2,#e5e7eb);border-radius:12px;overflow:hidden;display:flex;flex-direction:column;cursor:pointer;transition:transform .18s ease,box-shadow .18s ease,border-color .18s ease}
1245
- .dsh-ig-gallery-card:hover{transform:translateY(-2px);box-shadow:0 8px 24px rgba(0,0,0,0.06);border-color:var(--dsw-alias-border-l1,#cfd4dc)}
1246
- .dsh-ig-gallery-card-media{position:relative;width:100%;aspect-ratio:1/1;background:#f3f4f6;overflow:hidden;display:flex;align-items:center;justify-content:center}
1247
- .dsh-ig-gallery-card-img{width:100%;height:100%;object-fit:cover;transition:transform .2s}
2362
+
2363
+ /* 1. Top Navigation Tab Bar */
2364
+ .dsh-ig-studio-tabs-bar{display:flex;align-items:center;gap:6px;padding:6px 24px;border-bottom:1px solid var(--dsw-alias-border-l2,#e5e7eb);background:var(--dsw-alias-bg-layer-1,#ffffff);flex-shrink:0}
2365
+ .dsh-ig-studio-tab-btn{appearance:none;-webkit-appearance:none;border:0;background:transparent;display:inline-flex;align-items:center;gap:7px;padding:7px 14px;font-size:13px;font-weight:500;color:var(--dsw-alias-label-secondary,#64748b);cursor:pointer;border-radius:6px;transition:color .15s ease,background-color .15s ease}
2366
+ .dsh-ig-studio-tab-btn:hover{color:var(--dsw-alias-label-primary,#0f172a);background:var(--dsw-alias-bg-layer-2,#f1f5f9)}
2367
+ .dsh-ig-studio-tab-btn.is-active{color:var(--dsw-alias-brand-primary,#2563eb);font-weight:600;background:rgba(37,99,235,0.08)}
2368
+
2369
+ /* 2. Secondary Filter & Search Toolbar */
2370
+ .dsh-ig-studio-toolbar{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 24px;background:var(--dsw-alias-bg-layer-1,#ffffff);border-bottom:1px solid var(--dsw-alias-border-l2,#f1f5f9);flex-shrink:0;flex-wrap:wrap}
2371
+ .dsh-ig-studio-toolbar-left{display:flex;align-items:center;gap:10px;flex-wrap:wrap;flex:1;min-width:0}
2372
+ .dsh-ig-studio-toolbar-right{display:flex;align-items:center;gap:10px;flex-shrink:0}
2373
+
2374
+ /* Modern Custom Select (Removes OS default arrows & ugly borders) */
2375
+ .dsh-ig-studio-select{appearance:none;-webkit-appearance:none;-moz-appearance:none;height:32px;line-height:30px;padding:0 28px 0 12px;font-size:12.5px;color:var(--dsw-alias-label-primary,#334155);background-color:var(--dsw-alias-bg-layer-2,#ffffff);background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 9px center;border:1px solid var(--dsw-alias-border-l2,#e2e8f0);border-radius:6px;outline:none;cursor:pointer;box-sizing:border-box;transition:border-color .15s ease,box-shadow .15s ease,background-color .15s ease}
2376
+ .dsh-ig-studio-select:hover{border-color:var(--dsw-alias-border-l1,#cbd5e1);background-color:var(--dsw-alias-bg-layer-1,#f8fafc)}
2377
+ .dsh-ig-studio-select:focus{border-color:var(--dsw-alias-brand-primary,#3b82f6);box-shadow:0 0 0 2px rgba(59,130,246,0.15)}
2378
+ .dsh-ig-studio-select-sort{font-weight:500}
2379
+
2380
+ /* Unified Search Input */
2381
+ .dsh-ig-studio-search-wrap{position:relative;display:flex;align-items:center;min-width:190px;max-width:320px;flex:1}
2382
+ .dsh-ig-studio-search-icon{position:absolute;left:10px;color:var(--dsw-alias-label-tertiary,#94a3b8);pointer-events:none}
2383
+ .dsh-ig-studio-search-input{width:100%;height:32px;line-height:30px;padding:0 12px 0 32px;font-size:12.5px;border:1px solid var(--dsw-alias-border-l2,#e2e8f0);border-radius:6px;background-color:var(--dsw-alias-bg-layer-2,#ffffff);color:inherit;outline:none;box-sizing:border-box;transition:border-color .15s ease,box-shadow .15s ease}
2384
+ .dsh-ig-studio-search-input:hover{border-color:var(--dsw-alias-border-l1,#cbd5e1)}
2385
+ .dsh-ig-studio-search-input:focus{border-color:var(--dsw-alias-brand-primary,#3b82f6);box-shadow:0 0 0 2px rgba(59,130,246,0.15)}
2386
+ .dsh-ig-studio-search-input::placeholder{color:var(--dsw-alias-label-tertiary,#94a3b8)}
2387
+
2388
+ /* 3. Grid & Responsive Cards */
2389
+ .dsh-ig-gallery-page-body{flex:1;overflow-y:auto;padding:20px 24px}
2390
+ .dsh-ig-gallery-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:18px}
2391
+ .dsh-ig-gallery-card{background:var(--dsw-alias-bg-layer-2,#ffffff);border:1px solid var(--dsw-alias-border-l2,#e2e8f0);border-radius:10px;overflow:hidden;display:flex;flex-direction:column;cursor:pointer;transition:transform .18s ease,box-shadow .18s ease,border-color .18s ease}
2392
+ .dsh-ig-gallery-card:hover{transform:translateY(-2px);box-shadow:0 10px 20px -5px rgba(0,0,0,0.06),0 4px 6px -2px rgba(0,0,0,0.03);border-color:var(--dsw-alias-border-l1,#cbd5e1)}
2393
+ .dsh-ig-gallery-card-media{position:relative;width:100%;aspect-ratio:1/1;background:#f1f5f9;overflow:hidden;display:flex;align-items:center;justify-content:center}
2394
+ .dsh-ig-gallery-card-img{width:100%;height:100%;object-fit:cover;transition:transform .2s ease}
1248
2395
  .dsh-ig-gallery-card:hover .dsh-ig-gallery-card-img{transform:scale(1.03)}
1249
- .dsh-ig-gallery-card-loading{font-size:12px;color:#9ca3af}
2396
+ .dsh-ig-gallery-card-loading{font-size:12px;color:#94a3b8}
1250
2397
  .dsh-ig-gallery-card-error{font-size:12px;color:#ef4444;padding:8px;text-align:center}
1251
2398
 
1252
- /* Floating Action Toolbar (Matches Chat Image Toolbar) */
2399
+ /* Floating Action Toolbar on Card Hover */
1253
2400
  .dsh-ig-gallery-card:hover .dsh-ig-card-toolbar{opacity:1;pointer-events:auto}
1254
- .dsh-ig-card-toolbar{position:absolute;top:8px;left:8px;display:flex;align-items:center;gap:4px;padding:3px 5px;border-radius:8px;background:rgba(0,0,0,0.68);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);opacity:0;pointer-events:none;transition:opacity .18s ease;z-index:10;line-height:1}
2401
+ .dsh-ig-card-toolbar{position:absolute;top:6px;left:6px;display:flex;align-items:center;gap:3px;padding:3px 5px;border-radius:6px;background:rgba(15,23,42,0.72);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);opacity:0;pointer-events:none;transition:opacity .18s ease;z-index:10;line-height:1}
1255
2402
 
1256
- .dsh-ig-gallery-card-meta{padding:12px 14px;display:flex;flex-direction:column;gap:6px;background:var(--dsw-alias-bg-layer-2,#fff);flex:1}
1257
- .dsh-ig-gallery-card-header{display:flex;align-items:center;justify-content:space-between;font-size:11px}
1258
- .dsh-ig-tag{display:inline-block;padding:2px 6px;border-radius:4px;background:var(--dsw-alias-bg-layer-3,#edf0f3);color:var(--dsw-alias-label-secondary,inherit);font-weight:500;text-transform:uppercase;font-size:10px}
1259
- .dsh-ig-tag-model{background:rgba(76,120,255,0.1);color:#4c78ff}
1260
- .dsh-ig-gallery-card-prompt{margin:0;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-primary,inherit);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;word-break:break-word}
1261
- .dsh-ig-gallery-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;min-height:360px;text-align:center;color:var(--dsw-alias-label-tertiary,#7b818b)}
1262
- .dsh-ig-gallery-empty-icon{font-size:48px;margin-bottom:12px}
1263
- .dsh-ig-gallery-empty-title{font-size:16px;font-weight:600;color:var(--dsw-alias-label-primary,inherit);margin-bottom:6px}
2403
+ /* Card Bottom Metadata */
2404
+ .dsh-ig-gallery-card-meta{padding:10px 12px;display:flex;flex-direction:column;gap:5px;background:var(--dsw-alias-bg-layer-2,#ffffff);flex:1}
2405
+ .dsh-ig-card-badge-row{display:flex;align-items:center}
2406
+ .dsh-ig-card-badge{display:inline-block;padding:2px 6px;border-radius:4px;background:var(--dsw-alias-bg-layer-3,#f1f5f9);color:var(--dsw-alias-label-secondary,#475569);font-size:11px;font-weight:500;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
2407
+ .dsh-ig-gallery-card-prompt-line{font-size:12.5px;font-weight:500;color:var(--dsw-alias-label-primary,#1e293b);line-height:1.4;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
2408
+ .dsh-ig-card-footer-row{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:2px}
2409
+ .dsh-ig-card-meta-text{font-size:11px;color:var(--dsw-alias-label-tertiary,#94a3b8);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
2410
+ .dsh-ig-card-fav-btn{appearance:none;border:0;background:transparent;color:var(--dsw-alias-label-tertiary,#94a3b8);padding:2px;border-radius:4px;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;transition:color .15s,transform .15s;flex-shrink:0}
2411
+ .dsh-ig-card-fav-btn:hover{color:#ef4444;transform:scale(1.15)}
2412
+ .dsh-ig-card-fav-btn.is-favorited{color:#ef4444}
2413
+
2414
+ /* Placeholders for Upcoming Routes */
2415
+ .dsh-ig-placeholder-view{display:flex;align-items:center;justify-content:center;min-height:360px;height:100%;padding:24px}
2416
+ .dsh-ig-placeholder-card{max-width:500px;width:100%;text-align:center;padding:36px 28px;background:var(--dsw-alias-bg-layer-2,#ffffff);border:1px dashed var(--dsw-alias-border-l2,#e2e8f0);border-radius:14px;display:flex;flex-direction:column;align-items:center;gap:12px}
2417
+ .dsh-ig-placeholder-icon{font-size:40px;line-height:1}
2418
+ .dsh-ig-placeholder-header{display:flex;align-items:center;gap:8px;justify-content:center}
2419
+ .dsh-ig-placeholder-title{font-size:16px;font-weight:600;color:var(--dsw-alias-label-primary,inherit);margin:0}
2420
+ .dsh-ig-placeholder-badge{font-size:11px;font-weight:500;background:rgba(37,99,235,0.1);color:#2563eb;padding:2px 8px;border-radius:12px}
2421
+ .dsh-ig-placeholder-desc{font-size:13px;line-height:1.6;color:var(--dsw-alias-label-secondary,#64748b);margin:0}
2422
+ .dsh-ig-placeholder-tip{margin-top:6px;padding:8px 12px;font-size:12px;background:var(--dsw-alias-bg-layer-3,#f8fafc);border-radius:8px;color:var(--dsw-alias-label-tertiary,#64748b);text-align:left}
2423
+
2424
+ /* Empty State */
2425
+ .dsh-ig-gallery-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;min-height:360px;text-align:center;color:var(--dsw-alias-label-tertiary,#94a3b8)}
2426
+ .dsh-ig-gallery-empty-icon{font-size:44px;margin-bottom:10px}
2427
+ .dsh-ig-gallery-empty-title{font-size:15px;font-weight:600;color:var(--dsw-alias-label-primary,inherit);margin-bottom:4px}
1264
2428
  .dsh-ig-gallery-empty-desc{font-size:13px;max-width:360px;line-height:1.5}
1265
2429
 
1266
2430
  /* Pure Centered Lightbox */
1267
2431
  .dsh-ig-lightbox-backdrop{position:fixed;inset:0;z-index:99999;background:rgba(0,0,0,0.88);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);display:flex;flex-direction:column;align-items:center;justify-content:center;padding:20px;box-sizing:border-box;cursor:zoom-out;animation:dsh-ig-fade .15s ease-out}
1268
2432
  .dsh-ig-lightbox-topbar{position:absolute;top:20px;left:24px;right:24px;display:flex;align-items:center;justify-content:space-between;z-index:10;pointer-events:none}
1269
2433
  .dsh-ig-lightbox-meta{display:flex;align-items:center;gap:8px;pointer-events:auto}
2434
+ .dsh-ig-tag{display:inline-block;padding:2px 6px;border-radius:4px;background:var(--dsw-alias-bg-layer-3,#edf0f3);color:var(--dsw-alias-label-secondary,inherit);font-weight:500;text-transform:uppercase;font-size:10px}
2435
+ .dsh-ig-tag-model{background:rgba(76,120,255,0.1);color:#4c78ff}
1270
2436
  .dsh-ig-lightbox-close-btn{appearance:none;border:0;background:rgba(255,255,255,0.15);color:#fff;border-radius:50%;width:34px;height:34px;font-size:16px;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;transition:background .15s;pointer-events:auto}
1271
2437
  .dsh-ig-lightbox-close-btn:hover{background:rgba(255,255,255,0.3)}
1272
2438
  .dsh-ig-lightbox-img-wrap{max-width:86vw;max-height:78vh;display:flex;align-items:center;justify-content:center;cursor:default}
1273
2439
  .dsh-ig-lightbox-img{max-width:100%;max-height:78vh;object-fit:contain;border-radius:8px;box-shadow:0 24px 60px rgba(0,0,0,0.7);user-select:none}
1274
2440
  .dsh-ig-lightbox-bottombar{position:absolute;bottom:24px;left:50%;transform:translateX(-50%);max-width:min(90vw,640px);background:rgba(20,22,26,0.85);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border:1px solid rgba(255,255,255,0.15);border-radius:14px;padding:10px 16px;display:flex;flex-direction:column;gap:8px;color:#fff;box-shadow:0 16px 40px rgba(0,0,0,0.5);cursor:default}
1275
2441
  .dsh-ig-lightbox-prompt-text{font-size:13px;line-height:1.4;color:rgba(255,255,255,0.92);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;word-break:break-word}
1276
- .dsh-ig-lightbox-actions{display:flex;align-items:center;gap:8px;justify-content:flex-end;border-top:1px solid rgba(255,255,255,0.1);padding-top:8px}
2442
+ .dsh-ig-lightbox-counter{display:inline-flex;align-items:center;padding:2px 8px;border-radius:4px;background:rgba(255,255,255,0.15);color:#fff;font-size:11.5px;font-weight:500;font-variant-numeric:tabular-nums}
2443
+ .dsh-ig-lightbox-nav-btn{position:fixed;top:50%;transform:translateY(-50%);width:44px;height:44px;border-radius:50%;border:1px solid rgba(255,255,255,0.18);background:rgba(255,255,255,0.12);color:#fff;display:flex;align-items:center;justify-content:center;cursor:pointer;z-index:100;backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);transition:background-color .15s ease,transform .15s ease,opacity .15s ease;outline:none}
2444
+ .dsh-ig-lightbox-nav-btn:hover:not(:disabled){background:rgba(255,255,255,0.28);transform:translateY(-50%) scale(1.08)}
2445
+ .dsh-ig-lightbox-nav-btn:disabled{opacity:0.2;cursor:not-allowed;pointer-events:none}
2446
+ .dsh-ig-lightbox-nav-prev{left:24px}
2447
+ .dsh-ig-lightbox-nav-next{right:24px}
2448
+ .dsh-ig-lightbox-loading{display:flex;align-items:center;justify-content:center;min-width:180px;min-height:180px}
2449
+ .dsh-ig-lightbox-spinner{width:36px;height:36px;border:3px solid rgba(255,255,255,0.2);border-top-color:#fff;border-radius:50%;animation:dsh-ig-spin .8s linear infinite}
2450
+ @keyframes dsh-ig-spin{to{transform:rotate(360deg)}}
1277
2451
  .dsh-ig-lightbox-btn{appearance:none;border:1px solid rgba(255,255,255,0.18);background:rgba(255,255,255,0.08);color:#fff;border-radius:6px;padding:5px 10px;font-size:12px;cursor:pointer;display:inline-flex;align-items:center;gap:5px;transition:background .15s,border-color .15s,color .15s}
1278
2452
  .dsh-ig-lightbox-btn:hover{background:rgba(255,255,255,0.22)}
1279
2453
  .dsh-ig-lightbox-btn-danger{border-color:rgba(239,68,68,0.4);color:#fca5a5}
1280
2454
  .dsh-ig-lightbox-btn-danger:hover{background:rgba(239,68,68,0.35)!important;color:#fff!important;border-color:rgba(239,68,68,0.7)!important}
1281
2455
  .dsh-ig-gallery-page-toast{position:fixed;bottom:24px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,0.85);color:#fff;padding:6px 14px;border-radius:8px;font-size:13px;z-index:99999;animation:dsh-ig-fade .15s}
1282
2456
 
2457
+ /* Card selection and checkbox */
2458
+ .dsh-ig-gallery-card.is-selected{box-shadow:0 0 0 2px var(--dsw-alias-brand-primary,#2563eb);border-color:transparent}
2459
+ .dsh-ig-card-checkbox{position:absolute;top:8px;left:8px;width:22px;height:22px;border-radius:50%;border:1.5px solid rgba(255,255,255,0.7);background:rgba(0,0,0,0.35);backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);color:#fff;display:inline-flex;align-items:center;justify-content:center;cursor:pointer;z-index:5;opacity:0;transition:opacity .15s ease,background-color .15s ease,border-color .15s ease;padding:0;outline:none}
2460
+ .dsh-ig-gallery-card:hover .dsh-ig-card-checkbox,.dsh-ig-gallery-card.is-manage-mode .dsh-ig-card-checkbox,.dsh-ig-card-checkbox.is-checked{opacity:1}
2461
+ .dsh-ig-card-checkbox.is-checked{background:var(--dsw-alias-brand-primary,#2563eb);border-color:var(--dsw-alias-brand-primary,#2563eb)}
2462
+
2463
+ /* Studio button in toolbar */
2464
+ .dsh-ig-studio-btn{display:inline-flex;align-items:center;gap:6px;height:32px;padding:0 12px;box-sizing:border-box;border-radius:6px;border:1px solid var(--dsw-alias-border-subtle,rgba(0,0,0,0.12));background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-primary,inherit);font-size:12.5px;font-weight:500;cursor:pointer;transition:border-color .15s,background .15s,color .15s}
2465
+ .dsh-ig-studio-btn:hover{background:var(--dsw-alias-bg-layer-3,#f3f4f6);border-color:var(--dsw-alias-border-default,rgba(0,0,0,0.2))}
2466
+ .dsh-ig-studio-btn.is-active{background:var(--dsw-alias-brand-primary,#2563eb);border-color:var(--dsw-alias-brand-primary,#2563eb);color:#fff}
2467
+ .dsh-ig-studio-btn-danger{color:#ef4444;border-color:rgba(239,68,68,0.35);background:rgba(239,68,68,0.06)}
2468
+ .dsh-ig-studio-btn-danger:hover{background:rgba(239,68,68,0.14);border-color:rgba(239,68,68,0.6);color:#dc2626}
2469
+ .dsh-ig-studio-btn-danger.is-active{background:#dc2626;border-color:#dc2626;color:#fff}
2470
+
2471
+ /* Floating Batch Action Bar */
2472
+ .dsh-ig-batch-bar{position:fixed;bottom:24px;left:50%;transform:translateX(-50%);display:flex;align-items:center;gap:16px;background:rgba(20,24,32,0.92);backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);border:1px solid rgba(255,255,255,0.18);border-radius:40px;padding:8px 16px;box-shadow:0 16px 40px rgba(0,0,0,0.5);z-index:99990;animation:dsh-ig-slide-up .2s cubic-bezier(0.16,1,0.3,1);color:#fff}
2473
+ @keyframes dsh-ig-slide-up{from{transform:translate(-50%,20px);opacity:0}to{transform:translate(-50%,0);opacity:1}}
2474
+ .dsh-ig-batch-bar-left{display:flex;align-items:center;gap:10px}
2475
+ .dsh-ig-batch-bar-right{display:flex;align-items:center;gap:8px;border-left:1px solid rgba(255,255,255,0.15);padding-left:12px}
2476
+ .dsh-ig-batch-counter{font-size:13px;font-weight:600;color:rgba(255,255,255,0.95);margin-right:4px}
2477
+ .dsh-ig-batch-btn{appearance:none;border:1px solid rgba(255,255,255,0.15);background:rgba(255,255,255,0.08);color:#fff;border-radius:20px;padding:5px 12px;font-size:12px;cursor:pointer;display:inline-flex;align-items:center;gap:5px;transition:background .15s,border-color .15s,color .15s}
2478
+ .dsh-ig-batch-btn:hover:not(:disabled){background:rgba(255,255,255,0.2)}
2479
+ .dsh-ig-batch-btn:disabled{opacity:0.4;cursor:not-allowed}
2480
+ .dsh-ig-batch-btn-danger{background:rgba(239,68,68,0.2);border-color:rgba(239,68,68,0.5);color:#fca5a5}
2481
+ .dsh-ig-batch-btn-danger:hover:not(:disabled){background:rgba(239,68,68,0.4)!important;border-color:rgba(239,68,68,0.8)!important;color:#fff!important}
2482
+ .dsh-ig-batch-btn-exit{border-color:transparent;background:transparent;color:rgba(255,255,255,0.7)}
2483
+ .dsh-ig-batch-btn-exit:hover{background:rgba(255,255,255,0.1);color:#fff}
2484
+
2485
+ /* 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}
2487
+ .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
+ @keyframes dsh-ig-scale-up{from{transform:scale(0.95);opacity:0}to{transform:scale(1);opacity:1}}
2489
+ .dsh-ig-modal-header{display:flex;align-items:flex-start;gap:12px;margin-bottom:12px}
2490
+ .dsh-ig-modal-icon-danger{width:36px;height:36px;border-radius:50%;background:rgba(239,68,68,0.12);color:#ef4444;display:flex;align-items:center;justify-content:center;flex-shrink:0}
2491
+ .dsh-ig-modal-title{font-size:16px;font-weight:600;line-height:1.4}
2492
+ .dsh-ig-modal-body{margin-bottom:20px;padding-left:48px}
2493
+ .dsh-ig-modal-desc{font-size:13.5px;color:var(--dsw-alias-label-secondary,rgba(255,255,255,0.7));margin:0 0 14px 0;line-height:1.5}
2494
+ .dsh-ig-modal-checkbox-label{display:flex;align-items:center;gap:8px;font-size:12.5px;color:var(--dsw-alias-label-primary,inherit);cursor:pointer;user-select:none}
2495
+ .dsh-ig-modal-checkbox-label input{margin:0;cursor:pointer;width:15px;height:15px}
2496
+ .dsh-ig-modal-footer{display:flex;align-items:center;justify-content:flex-end;gap:10px}
2497
+ .dsh-ig-modal-btn{height:34px;padding:0 14px;border-radius:6px;font-size:13px;font-weight:500;cursor:pointer;transition:background .15s,border-color .15s,color .15s;outline:none}
2498
+ .dsh-ig-modal-btn-cancel{background:transparent;border:1px solid var(--dsw-alias-border-subtle,rgba(255,255,255,0.2));color:var(--dsw-alias-label-primary,inherit)}
2499
+ .dsh-ig-modal-btn-cancel:hover{background:var(--dsw-alias-bg-layer-3,rgba(255,255,255,0.08))}
2500
+ .dsh-ig-modal-btn-danger{background:#dc2626;border:1px solid #dc2626;color:#fff}
2501
+ .dsh-ig-modal-btn-danger:hover{background:#b91c1c;border-color:#b91c1c}
2502
+
1283
2503
  /* Hide floating chat composer when gallery page is active */
1284
2504
  [data-conversation-scroll]:has(.dsh-ig-gallery-page) [data-composer-seat]{display:none!important}
1285
2505
  `;
@@ -1294,6 +2514,8 @@ window.__ModuleLoader__.load({
1294
2514
  /** Mount the settings card, generated-image card, and native conversation gallery view. */
1295
2515
  function apply(ctx) {
1296
2516
  const scope = ctx.settingsScope.bind({ namespace: IMAGE_GENERATION_NAMESPACE });
2517
+ const chatScope = ctx.settingsScope.bind({ namespace: "ui-chat" });
2518
+ const isCompactTranscript = () => chatScope.getSnapshot().value?.transcriptView !== "normal";
1297
2519
  const locale = ctx.get("locale");
1298
2520
  const promotion = { enabled: false };
1299
2521
  ctx.effect(() => {
@@ -1311,7 +2533,7 @@ window.__ModuleLoader__.load({
1311
2533
  if (uiConversation === void 0) throw new Error("dsh-image-gen: uiConversation has an incompatible interface");
1312
2534
  promotion.enabled = true;
1313
2535
  const ownerRegister = owner.slots.register.bind(owner.slots);
1314
- owner.effect(() => uiConversation.events.register(imageResultDefinition), "dsh-image-gen: promoted image result node");
2536
+ owner.effect(() => uiConversation.events.register(createImageResultDefinition({ isCompactTranscript })), "dsh-image-gen: promoted image result node");
1315
2537
  owner.slots.inject("conversation.chat.node", () => ownerRegister({
1316
2538
  name: "conversation.chat.node",
1317
2539
  key: IMAGE_RESULT_NODE_KIND,
@@ -1409,8 +2631,8 @@ window.__ModuleLoader__.load({
1409
2631
  const [provider, setProvider] = (0, react.useState)("google");
1410
2632
  const [model, setModel] = (0, react.useState)("");
1411
2633
  const [baseURL, setBaseURL] = (0, react.useState)("");
1412
- const [workflowJson, setWorkflowJson] = (0, react.useState)("");
1413
- const [workflowName, setWorkflowName] = (0, react.useState)("");
2634
+ const [workflows, setWorkflows] = (0, react.useState)([]);
2635
+ const [activeWorkflow, setActiveWorkflow] = (0, react.useState)("");
1414
2636
  const [timeoutSeconds, setTimeoutSeconds] = (0, react.useState)(DEFAULT_COMFYUI_TIMEOUT_MS / 1e3);
1415
2637
  const [saveToWorkspace, setSaveToWorkspace] = (0, react.useState)(true);
1416
2638
  const [workspaceFolder, setWorkspaceFolder] = (0, react.useState)("dsh-image-gen");
@@ -1418,6 +2640,15 @@ window.__ModuleLoader__.load({
1418
2640
  const [configured, setConfigured] = (0, react.useState)();
1419
2641
  const [saving, setSaving] = (0, react.useState)(false);
1420
2642
  const [message, setMessage] = (0, react.useState)("");
2643
+ const [messageIsError, setMessageIsError] = (0, react.useState)(false);
2644
+ const reportMessage = (text) => {
2645
+ setMessage(text);
2646
+ setMessageIsError(false);
2647
+ };
2648
+ const reportError = (text) => {
2649
+ setMessage(text);
2650
+ setMessageIsError(true);
2651
+ };
1421
2652
  (0, react.useEffect)(() => props.scope.subscribe(() => {
1422
2653
  setSnapshot(props.scope.getSnapshot());
1423
2654
  }), [props.scope]);
@@ -1444,8 +2675,8 @@ window.__ModuleLoader__.load({
1444
2675
  setProvider(next);
1445
2676
  setModel(modelOf(next, value));
1446
2677
  setBaseURL(baseURLOf(next, value));
1447
- setWorkflowJson(value?.comfyuiWorkflowJson ?? "");
1448
- setWorkflowName(value?.comfyuiWorkflowName ?? "");
2678
+ setWorkflows(resolveComfyUIWorkflows(value ?? {}));
2679
+ setActiveWorkflow(activeComfyUIWorkflow(value ?? {})?.name ?? "");
1449
2680
  setTimeoutSeconds(Math.max(1, Math.round((value?.comfyuiTimeoutMs ?? 3e5) / 1e3)));
1450
2681
  setSaveToWorkspace(value?.saveToWorkspace ?? true);
1451
2682
  setWorkspaceFolder(value?.workspaceFolder ?? "dsh-image-gen");
@@ -1473,10 +2704,22 @@ window.__ModuleLoader__.load({
1473
2704
  try {
1474
2705
  await props.scope.set("provider", provider);
1475
2706
  if (provider === "comfyui") {
1476
- validateComfyUIWorkflowJson(workflowJson);
2707
+ const entries = workflows.map((entry) => ({
2708
+ name: entry.name.trim(),
2709
+ json: entry.json,
2710
+ presetPrompt: (entry.presetPrompt ?? "").trim()
2711
+ }));
2712
+ for (const entry of entries) {
2713
+ if (entry.name.length === 0) throw new Error(t("workflowNameRequired"));
2714
+ validateComfyUIWorkflowJson(entry.json);
2715
+ }
2716
+ if (new Set(entries.map((entry) => entry.name)).size !== entries.length) throw new Error(t("workflowDuplicateName"));
2717
+ const activeEntry = entries.find((entry) => entry.name === activeWorkflow) ?? entries[0];
1477
2718
  await props.scope.set("comfyuiBaseURL", baseURL);
1478
- await props.scope.set("comfyuiWorkflowJson", workflowJson);
1479
- await props.scope.set("comfyuiWorkflowName", workflowName);
2719
+ await props.scope.set("comfyuiWorkflows", entries);
2720
+ await props.scope.set("comfyuiActiveWorkflow", activeEntry === void 0 ? "" : activeEntry.name);
2721
+ await props.scope.set("comfyuiWorkflowJson", activeEntry === void 0 ? "" : activeEntry.json);
2722
+ await props.scope.set("comfyuiWorkflowName", activeEntry === void 0 ? "" : activeEntry.name);
1480
2723
  await props.scope.set("comfyuiTimeoutMs", Math.max(1, Math.round(timeoutSeconds)) * 1e3);
1481
2724
  } else {
1482
2725
  await props.scope.set(provider === "google" ? "googleModel" : provider === "openai" ? "openaiModel" : provider === "seedream" ? "seedreamModel" : "dashscopeModel", model);
@@ -1492,31 +2735,58 @@ window.__ModuleLoader__.load({
1492
2735
  setKey("");
1493
2736
  setConfigured(true);
1494
2737
  }
1495
- setMessage(t("saved"));
2738
+ reportMessage(t("saved"));
1496
2739
  } catch (cause) {
1497
- setMessage(cause instanceof Error ? cause.message : String(cause));
2740
+ reportError(cause instanceof Error ? cause.message : String(cause));
1498
2741
  } finally {
1499
2742
  setSaving(false);
1500
2743
  }
1501
2744
  };
1502
2745
  const keyStatus = configured === void 0 ? t("checkingKey") : configured ? t("keyConfigured") : t("keyNotConfigured");
1503
- const workflowStatus = workflowName.length > 0 ? t("workflowImported", { name: workflowName }) : t("workflowMissing");
2746
+ const workflowStatus = activeWorkflow.length > 0 ? t("workflowImported", { name: activeWorkflow }) : t("workflowMissing");
1504
2747
  const importWorkflow = async (event) => {
1505
2748
  const file = event.target.files?.[0];
1506
2749
  event.target.value = "";
1507
2750
  if (file === void 0) return;
1508
- setMessage("");
2751
+ reportMessage("");
1509
2752
  try {
1510
2753
  if (file.size > 5242880) throw new Error(t("workflowTooLarge"));
1511
2754
  const json = await file.text();
1512
2755
  validateComfyUIWorkflowJson(json);
1513
- setWorkflowJson(json);
1514
- setWorkflowName(file.name);
1515
- setMessage(t("workflowImported", { name: file.name }));
2756
+ const name = uniqueComfyUIWorkflowName(file.name, workflows.map((entry) => entry.name));
2757
+ setWorkflows((current) => [...current, {
2758
+ name,
2759
+ json
2760
+ }]);
2761
+ setActiveWorkflow((current) => current.length > 0 ? current : name);
2762
+ reportMessage(t("workflowImported", { name }));
1516
2763
  } catch (cause) {
1517
- setMessage(cause instanceof Error ? cause.message : String(cause));
2764
+ reportError(cause instanceof Error ? cause.message : String(cause));
1518
2765
  }
1519
2766
  };
2767
+ /** Renaming the active entry keeps the active selection following its new name. */
2768
+ const renameWorkflow = (index, name) => {
2769
+ const previous = workflows[index];
2770
+ setWorkflows((current) => current.map((entry, position) => position === index ? {
2771
+ ...entry,
2772
+ name
2773
+ } : entry));
2774
+ if (previous !== void 0 && previous.name === activeWorkflow) setActiveWorkflow(name);
2775
+ };
2776
+ /** Removing the active entry moves the selection to the first remaining workflow. */
2777
+ const removeWorkflow = (index) => {
2778
+ const previous = workflows[index];
2779
+ const next = workflows.filter((_entry, position) => position !== index);
2780
+ setWorkflows(next);
2781
+ if (previous !== void 0 && previous.name === activeWorkflow) setActiveWorkflow(next[0]?.name ?? "");
2782
+ };
2783
+ /** Editing one entry's preset leaves the rest of the entry untouched. */
2784
+ const setWorkflowPreset = (index, presetPrompt) => {
2785
+ setWorkflows((current) => current.map((entry, position) => position === index ? {
2786
+ ...entry,
2787
+ presetPrompt
2788
+ } : entry));
2789
+ };
1520
2790
  return (0, react_jsx_runtime.jsxs)("li", {
1521
2791
  className: `dsh-ig-card ${open ? "dsh-ig-card-open" : ""}`,
1522
2792
  children: [(0, react_jsx_runtime.jsxs)("button", {
@@ -1689,13 +2959,60 @@ window.__ModuleLoader__.load({
1689
2959
  onChange: (event) => {
1690
2960
  importWorkflow(event);
1691
2961
  }
1692
- }), workflowName.length > 0 ? t("workflowReplace") : t("workflowImport")]
1693
- }), (0, react_jsx_runtime.jsx)("span", {
2962
+ }), t("workflowImport")]
2963
+ }), workflows.length === 0 ? (0, react_jsx_runtime.jsx)("span", {
1694
2964
  className: "dsh-ig-file-name",
1695
- title: workflowName,
1696
- children: workflowName || t("workflowMissing")
1697
- })]
2965
+ children: t("workflowMissing")
2966
+ }) : null]
1698
2967
  }),
2968
+ workflows.length > 0 ? (0, react_jsx_runtime.jsx)("ul", {
2969
+ className: "dsh-ig-workflow-list",
2970
+ children: workflows.map((entry, index) => (0, react_jsx_runtime.jsxs)("li", {
2971
+ className: "dsh-ig-workflow-row",
2972
+ children: [(0, react_jsx_runtime.jsxs)("div", {
2973
+ className: "dsh-ig-workflow-main",
2974
+ children: [
2975
+ (0, react_jsx_runtime.jsx)("label", {
2976
+ className: "dsh-ig-workflow-active",
2977
+ title: t("workflowActiveTitle"),
2978
+ children: (0, react_jsx_runtime.jsx)("input", {
2979
+ type: "radio",
2980
+ name: "dsh-ig-active-workflow",
2981
+ "aria-label": t("workflowActiveTitle"),
2982
+ checked: entry.name === activeWorkflow,
2983
+ onChange: () => {
2984
+ setActiveWorkflow(entry.name);
2985
+ }
2986
+ })
2987
+ }),
2988
+ (0, react_jsx_runtime.jsx)("input", {
2989
+ className: "dsh-ig-input dsh-ig-workflow-name",
2990
+ value: entry.name,
2991
+ title: entry.name,
2992
+ onChange: (event) => {
2993
+ renameWorkflow(index, event.target.value);
2994
+ }
2995
+ }),
2996
+ (0, react_jsx_runtime.jsx)("button", {
2997
+ type: "button",
2998
+ className: "dsh-ig-btn-reset",
2999
+ onClick: () => {
3000
+ removeWorkflow(index);
3001
+ },
3002
+ children: t("workflowRemove")
3003
+ })
3004
+ ]
3005
+ }), (0, react_jsx_runtime.jsx)("input", {
3006
+ className: "dsh-ig-input dsh-ig-workflow-preset",
3007
+ value: entry.presetPrompt ?? "",
3008
+ placeholder: t("workflowPresetPlaceholder"),
3009
+ title: t("workflowPresetTitle"),
3010
+ onChange: (event) => {
3011
+ setWorkflowPreset(index, event.target.value);
3012
+ }
3013
+ })]
3014
+ }, String(index)))
3015
+ }) : null,
1699
3016
  (0, react_jsx_runtime.jsx)("span", {
1700
3017
  className: "dsh-ig-hint",
1701
3018
  children: t("workflowHint")
@@ -1769,13 +3086,13 @@ window.__ModuleLoader__.load({
1769
3086
  (0, react_jsx_runtime.jsxs)("div", {
1770
3087
  className: "dsh-ig-actions",
1771
3088
  children: [(0, react_jsx_runtime.jsx)("p", {
1772
- className: "dsh-ig-status",
3089
+ className: `dsh-ig-status${messageIsError ? " dsh-ig-status-error" : ""}`,
1773
3090
  role: "status",
1774
3091
  children: message || (provider === "comfyui" ? workflowStatus : keyStatus)
1775
3092
  }), (0, react_jsx_runtime.jsx)("button", {
1776
3093
  className: "dsh-ig-save",
1777
3094
  type: "submit",
1778
- disabled: saving || !snapshot.writable || provider === "comfyui" && workflowJson.length === 0,
3095
+ disabled: saving || !snapshot.writable || provider === "comfyui" && workflows.length === 0,
1779
3096
  children: saving ? t("saving") : t("save")
1780
3097
  })]
1781
3098
  })
@@ -1831,7 +3148,9 @@ window.__ModuleLoader__.load({
1831
3148
  prompt: result.prompt,
1832
3149
  provider: result.provider,
1833
3150
  model: result.model,
1834
- output: result.output
3151
+ output: result.output,
3152
+ ...result.savedTo ? { savedTo: result.savedTo } : {},
3153
+ ...result.seed !== void 0 ? { seed: result.seed } : {}
1835
3154
  });
1836
3155
  }, [result?.attachment.attachmentId]);
1837
3156
  (0, react.useEffect)(() => {
@@ -2055,11 +3374,12 @@ window.__ModuleLoader__.load({
2055
3374
  provider: typeof meta?.provider === "string" ? meta.provider : "google",
2056
3375
  model: typeof meta?.model === "string" ? meta.model : "",
2057
3376
  output: typeof meta?.output === "string" ? meta.output : "",
2058
- ...typeof meta?.savedTo === "string" ? { savedTo: meta.savedTo } : {}
3377
+ ...typeof meta?.savedTo === "string" ? { savedTo: meta.savedTo } : {},
3378
+ ...typeof meta?.seed === "number" ? { seed: meta.seed } : {}
2059
3379
  };
2060
3380
  }
2061
3381
  function modelOf(provider, value) {
2062
- const stored = provider === "google" ? value?.googleModel : provider === "openai" ? value?.openaiModel : provider === "seedream" ? value?.seedreamModel : provider === "dashscope" ? value?.dashscopeModel : value?.comfyuiWorkflowName;
3382
+ const stored = provider === "google" ? value?.googleModel : provider === "openai" ? value?.openaiModel : provider === "seedream" ? value?.seedreamModel : provider === "dashscope" ? value?.dashscopeModel : activeComfyUIWorkflow(value ?? {})?.name;
2063
3383
  return typeof stored === "string" && stored.length > 0 ? stored : DEFAULT_MODELS[provider];
2064
3384
  }
2065
3385
  function baseURLOf(provider, value) {