dsh-image-gen 0.2.5 → 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. */
@@ -210,19 +212,33 @@ window.__ModuleLoader__.load({
210
212
  /**
211
213
  * Save or update a gallery record by attachmentId.
212
214
  * Skipped if the item was previously deleted (tombstoned).
215
+ * Preserves existing isFavorite, tags, and original createdAt on re-renders.
213
216
  */
214
217
  async function saveGalleryItem(item) {
215
218
  try {
216
219
  const db = await getDB();
217
220
  if ((await loadTombstones(db)).has(item.id)) return;
218
- const record = {
219
- ...item,
220
- createdAt: item.createdAt ?? Date.now()
221
- };
222
221
  await new Promise((resolve, reject) => {
223
- const req = db.transaction(STORE_NAME, "readwrite").objectStore(STORE_NAME).put(record);
224
- req.onsuccess = () => resolve();
225
- 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);
226
242
  });
227
243
  notifyListeners();
228
244
  } catch (err) {
@@ -253,52 +269,537 @@ window.__ModuleLoader__.load({
253
269
  }
254
270
  }
255
271
  /**
256
- * 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).
257
274
  */
258
- async function deleteGalleryItem(id) {
275
+ async function toggleFavoriteGalleryItem(id) {
259
276
  try {
260
277
  const db = await getDB();
261
- (await loadTombstones(db)).add(id);
262
- await new Promise((resolve, reject) => {
263
- const tx = db.transaction([STORE_NAME, TOMBSTONE_STORE], "readwrite");
264
- const store = tx.objectStore(STORE_NAME);
265
- const tombstoneStore = tx.objectStore(TOMBSTONE_STORE);
266
- store.delete(id);
267
- tombstoneStore.put({
268
- id,
269
- deletedAt: Date.now()
270
- });
271
- tx.oncomplete = () => resolve();
272
- 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);
273
294
  });
274
295
  notifyListeners();
296
+ return newStatus;
275
297
  } catch (err) {
276
- 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;
277
300
  }
278
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
+ }]]);
279
766
  //#endregion
280
767
  //#region lib/types/client/gallery-view.js
281
768
  /**
282
- * Native Workspace Gallery View Component for DSH `conversation.view` slot.
283
- * 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.
284
772
  */
285
773
  const DICT$1 = {
286
774
  zh: {
287
- totalCount: "共 {count} 张生成图片",
288
- searchPlaceholder: "搜索 Prompt 关键词…",
289
- filterAll: "全部厂商",
775
+ tabGallery: "图库",
776
+ tabStudio: "工作台",
777
+ tabFavorites: "收藏",
778
+ tabCompare: "对比",
779
+ tabTasks: "任务",
780
+ filterAllProviders: "全部提供商",
290
781
  filterGoogle: "Google Gemini",
291
782
  filterOpenAI: "OpenAI / 中转站",
292
783
  filterSeedream: "字节 Seedream",
293
784
  filterDashScope: "阿里 DashScope",
294
785
  filterComfyUI: "本地 ComfyUI",
786
+ filterAllModels: "全部模型",
787
+ filterAllRatios: "全部比例",
788
+ searchPlaceholder: "搜索 Prompt、标签…",
789
+ sortNewest: "最新生成",
790
+ sortOldest: "最早生成",
791
+ totalCount: "共 {count} 张生成图片",
295
792
  emptyTitle: "暂无生图记录",
296
793
  emptyDesc: "在对话中让 Agent 生图后,生成的图片会自动收录到这里。",
794
+ favEmptyTitle: "暂无收藏图片",
795
+ favEmptyDesc: "在图库中点击卡片右下角的 ♡ 按钮,即可将喜爱的图片收录到这里。",
297
796
  noMatchTitle: "未找到匹配结果",
298
- noMatchDesc: "尝试更换搜索关键词或选择其他厂商。",
797
+ noMatchDesc: "尝试更换搜索关键词或调整筛选条件。",
299
798
  copiedPrompt: "已复制 Prompt",
300
799
  copiedImage: "已复制图片",
301
800
  copyFailed: "复制失败",
801
+ favoriteAdded: "已添加到收藏",
802
+ favoriteRemoved: "已取消收藏",
302
803
  preview: "查看大图",
303
804
  download: "下载图片",
304
805
  copyImg: "复制图片",
@@ -308,24 +809,69 @@ window.__ModuleLoader__.load({
308
809
  deleted: "已从画廊删除",
309
810
  model: "模型",
310
811
  prompt: "Prompt",
311
- 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: "即将推出"
312
845
  },
313
846
  en: {
314
- totalCount: "{count} images total",
315
- searchPlaceholder: "Search prompt keywords…",
316
- filterAll: "All Providers",
847
+ tabGallery: "Gallery",
848
+ tabStudio: "Studio",
849
+ tabFavorites: "Favorites",
850
+ tabCompare: "Compare",
851
+ tabTasks: "Tasks",
852
+ filterAllProviders: "All Providers",
317
853
  filterGoogle: "Google Gemini",
318
854
  filterOpenAI: "OpenAI / Relay",
319
855
  filterSeedream: "ByteDance Seedream",
320
856
  filterDashScope: "Aliyun DashScope",
321
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",
322
864
  emptyTitle: "No images generated yet",
323
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.",
324
868
  noMatchTitle: "No matching images",
325
- noMatchDesc: "Try a different search keyword or provider filter.",
869
+ noMatchDesc: "Try a different search keyword or adjust filter criteria.",
326
870
  copiedPrompt: "Prompt copied",
327
871
  copiedImage: "Image copied",
328
872
  copyFailed: "Copy failed",
873
+ favoriteAdded: "Added to favorites",
874
+ favoriteRemoved: "Removed from favorites",
329
875
  preview: "Full Preview",
330
876
  download: "Download",
331
877
  copyImg: "Copy Image",
@@ -335,17 +881,123 @@ window.__ModuleLoader__.load({
335
881
  deleted: "Deleted from gallery",
336
882
  model: "Model",
337
883
  prompt: "Prompt",
338
- 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"
339
917
  }
340
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
+ }
341
974
  const GalleryViewTab = ({ locale }) => {
975
+ const [activeTab, setActiveTab] = (0, react.useState)("gallery");
342
976
  const [items, setItems] = (0, react.useState)([]);
343
977
  const [search, setSearch] = (0, react.useState)("");
344
978
  const [selectedProvider, setSelectedProvider] = (0, react.useState)("all");
979
+ const [selectedRatio, setSelectedRatio] = (0, react.useState)("all");
980
+ const [sortBy, setSortBy] = (0, react.useState)("newest");
345
981
  const [previewItem, setPreviewItem] = (0, react.useState)(null);
346
982
  const [previewUrl, setPreviewUrl] = (0, react.useState)(null);
347
983
  const [previewBlob, setPreviewBlob] = (0, react.useState)(null);
348
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
+ ]);
349
1001
  const [lang, setLang] = (0, react.useState)(() => {
350
1002
  return (locale?.getSnapshot?.()?.active)?.startsWith("en") ? "en" : "zh";
351
1003
  });
@@ -392,112 +1044,431 @@ window.__ModuleLoader__.load({
392
1044
  unsubscribe();
393
1045
  };
394
1046
  }, []);
395
- (0, react.useEffect)(() => {
396
- if (!previewItem) return;
397
- const onKeyDown = (e) => {
398
- if (e.key === "Escape") {
399
- setPreviewItem(null);
400
- setPreviewUrl(null);
401
- setPreviewBlob(null);
402
- }
403
- };
404
- window.addEventListener("keydown", onKeyDown);
405
- return () => window.removeEventListener("keydown", onKeyDown);
406
- }, [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);
407
1050
  const filteredItems = (0, react.useMemo)(() => {
408
1051
  return items.filter((item) => {
1052
+ if (activeTab === "favorites" && !item.isFavorite) return false;
409
1053
  if (selectedProvider !== "all" && item.provider !== selectedProvider) return false;
1054
+ if (selectedRatio !== "all") {
1055
+ if (getItemRatio(item) !== selectedRatio) return false;
1056
+ }
410
1057
  if (search.trim().length > 0) {
411
1058
  const q = search.trim().toLowerCase();
412
1059
  const matchPrompt = item.prompt?.toLowerCase().includes(q);
413
1060
  const matchModel = item.model?.toLowerCase().includes(q);
414
- 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;
415
1064
  }
416
1065
  return true;
1066
+ }).sort((a, b) => {
1067
+ if (sortBy === "oldest") return a.createdAt - b.createdAt;
1068
+ return b.createdAt - a.createdAt;
417
1069
  });
418
1070
  }, [
419
1071
  items,
1072
+ activeTab,
1073
+ selectedProvider,
1074
+ selectedRatio,
420
1075
  search,
421
- selectedProvider
1076
+ sortBy
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
422
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
+ };
423
1317
  return (0, react_jsx_runtime.jsxs)("div", {
424
1318
  className: "dsh-ig-gallery-page",
425
1319
  children: [
426
1320
  (0, react_jsx_runtime.jsxs)("header", {
427
- className: "dsh-ig-gallery-page-header",
428
- children: [(0, react_jsx_runtime.jsx)("div", {
429
- className: "dsh-ig-gallery-page-title-row",
430
- children: (0, react_jsx_runtime.jsx)("span", {
431
- className: "dsh-ig-gallery-page-count",
432
- 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") })]
433
1352
  })
434
- }), (0, react_jsx_runtime.jsxs)("div", {
435
- className: "dsh-ig-gallery-page-tools",
436
- children: [(0, react_jsx_runtime.jsxs)("div", {
437
- className: "dsh-ig-gallery-search-wrap",
438
- children: [(0, react_jsx_runtime.jsxs)("svg", {
439
- className: "dsh-ig-gallery-search-icon",
440
- width: "14",
441
- height: "14",
442
- viewBox: "0 0 24 24",
443
- fill: "none",
444
- stroke: "currentColor",
445
- strokeWidth: "2",
446
- strokeLinecap: "round",
447
- strokeLinejoin: "round",
448
- children: [(0, react_jsx_runtime.jsx)("circle", {
449
- cx: "11",
450
- cy: "11",
451
- r: "8"
452
- }), (0, react_jsx_runtime.jsx)("line", {
453
- x1: "21",
454
- y1: "21",
455
- x2: "16.65",
456
- 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)
457
1441
  })]
458
- }), (0, react_jsx_runtime.jsx)("input", {
459
- type: "text",
460
- className: "dsh-ig-gallery-search-input",
461
- placeholder: t("searchPlaceholder"),
462
- value: search,
463
- 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")
464
1456
  })]
465
- }), (0, react_jsx_runtime.jsxs)("select", {
466
- className: "dsh-ig-gallery-select",
467
- value: selectedProvider,
468
- onChange: (e) => setSelectedProvider(e.target.value),
469
- children: [
470
- (0, react_jsx_runtime.jsx)("option", {
471
- value: "all",
472
- children: t("filterAll")
473
- }),
474
- (0, react_jsx_runtime.jsx)("option", {
475
- value: "google",
476
- children: t("filterGoogle")
477
- }),
478
- (0, react_jsx_runtime.jsx)("option", {
479
- value: "openai",
480
- children: t("filterOpenAI")
481
- }),
482
- (0, react_jsx_runtime.jsx)("option", {
483
- value: "seedream",
484
- children: t("filterSeedream")
485
- }),
486
- (0, react_jsx_runtime.jsx)("option", {
487
- value: "dashscope",
488
- children: t("filterDashScope")
489
- }),
490
- (0, react_jsx_runtime.jsx)("option", {
491
- value: "comfyui",
492
- children: t("filterComfyUI")
493
- })
494
- ]
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") })]
495
1466
  })]
496
1467
  })]
497
1468
  }),
498
1469
  (0, react_jsx_runtime.jsx)("div", {
499
1470
  className: "dsh-ig-gallery-page-body",
500
- 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", {
501
1472
  className: "dsh-ig-gallery-empty",
502
1473
  children: [
503
1474
  (0, react_jsx_runtime.jsx)("div", {
@@ -513,6 +1484,22 @@ window.__ModuleLoader__.load({
513
1484
  children: t("emptyDesc")
514
1485
  })
515
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
+ ]
516
1503
  }) : filteredItems.length === 0 ? (0, react_jsx_runtime.jsxs)("div", {
517
1504
  className: "dsh-ig-gallery-empty",
518
1505
  children: [
@@ -531,82 +1518,212 @@ window.__ModuleLoader__.load({
531
1518
  ]
532
1519
  }) : (0, react_jsx_runtime.jsx)("div", {
533
1520
  className: "dsh-ig-gallery-grid",
534
- children: filteredItems.map((item) => (0, react_jsx_runtime.jsx)(GalleryCard, {
1521
+ children: filteredItems.map((item, idx) => (0, react_jsx_runtime.jsx)(GalleryCard, {
535
1522
  item,
1523
+ index: idx,
1524
+ isManageMode,
1525
+ isSelected: selectedIds.has(item.id),
536
1526
  t,
537
- onPreview: (url, blob) => {
538
- setPreviewItem(item);
539
- setPreviewUrl(url);
540
- setPreviewBlob(blob);
541
- },
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),
542
1532
  onToast: showToast
543
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")
544
1550
  })
545
1551
  }),
546
1552
  toast && (0, react_jsx_runtime.jsx)("div", {
547
1553
  className: "dsh-ig-gallery-page-toast",
548
1554
  children: toast
549
1555
  }),
550
- previewItem && previewUrl && (0, react_jsx_runtime.jsxs)("div", {
551
- 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",
552
1602
  onClick: () => {
553
- setPreviewItem(null);
554
- setPreviewUrl(null);
555
- setPreviewBlob(null);
1603
+ if (!isDeleting) {
1604
+ setShowDeleteModal(false);
1605
+ if (!isManageMode) setSelectedIds(/* @__PURE__ */ new Set());
1606
+ }
556
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,
557
1662
  children: [
558
1663
  (0, react_jsx_runtime.jsxs)("div", {
559
1664
  className: "dsh-ig-lightbox-topbar",
560
1665
  onClick: (e) => e.stopPropagation(),
561
1666
  children: [(0, react_jsx_runtime.jsxs)("div", {
562
1667
  className: "dsh-ig-lightbox-meta",
563
- children: [(0, react_jsx_runtime.jsx)("span", {
564
- className: "dsh-ig-tag",
565
- children: previewItem.provider
566
- }), previewItem.model ? (0, react_jsx_runtime.jsx)("span", {
567
- className: "dsh-ig-tag dsh-ig-tag-model",
568
- children: previewItem.model
569
- }) : 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
+ ]
570
1686
  }), (0, react_jsx_runtime.jsx)("button", {
571
1687
  type: "button",
572
1688
  className: "dsh-ig-lightbox-close-btn",
573
1689
  title: t("close"),
574
- onClick: () => {
575
- setPreviewItem(null);
576
- setPreviewUrl(null);
577
- setPreviewBlob(null);
578
- },
579
- children: (0, react_jsx_runtime.jsxs)("svg", {
580
- width: "18",
581
- height: "18",
582
- viewBox: "0 0 24 24",
583
- fill: "none",
584
- stroke: "currentColor",
585
- strokeWidth: "2",
586
- strokeLinecap: "round",
587
- strokeLinejoin: "round",
588
- children: [(0, react_jsx_runtime.jsx)("line", {
589
- x1: "18",
590
- y1: "6",
591
- x2: "6",
592
- y2: "18"
593
- }), (0, react_jsx_runtime.jsx)("line", {
594
- x1: "6",
595
- y1: "6",
596
- x2: "18",
597
- y2: "18"
598
- })]
599
- })
1690
+ onClick: handleClosePreview,
1691
+ children: (0, react_jsx_runtime.jsx)(X, { size: 18 })
600
1692
  })]
601
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
+ }),
602
1716
  (0, react_jsx_runtime.jsx)("div", {
603
1717
  className: "dsh-ig-lightbox-img-wrap",
604
1718
  onClick: (e) => e.stopPropagation(),
605
- 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", {
606
1723
  className: "dsh-ig-lightbox-img",
607
1724
  src: previewUrl,
608
1725
  alt: previewItem.prompt
609
- })
1726
+ }) : null
610
1727
  }),
611
1728
  (0, react_jsx_runtime.jsxs)("div", {
612
1729
  className: "dsh-ig-lightbox-bottombar",
@@ -626,24 +1743,7 @@ window.__ModuleLoader__.load({
626
1743
  await navigator.clipboard.writeText(previewItem.prompt);
627
1744
  showToast(t("copiedPrompt"));
628
1745
  },
629
- children: [(0, react_jsx_runtime.jsxs)("svg", {
630
- width: "14",
631
- height: "14",
632
- viewBox: "0 0 24 24",
633
- fill: "none",
634
- stroke: "currentColor",
635
- strokeWidth: "2",
636
- strokeLinecap: "round",
637
- strokeLinejoin: "round",
638
- 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", {
639
- x: "8",
640
- y: "2",
641
- width: "8",
642
- height: "4",
643
- rx: "1",
644
- ry: "1"
645
- })]
646
- }), (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") })]
647
1747
  }),
648
1748
  (0, react_jsx_runtime.jsxs)("button", {
649
1749
  type: "button",
@@ -654,30 +1754,14 @@ window.__ModuleLoader__.load({
654
1754
  const ok = await copyImageBlob(previewBlob);
655
1755
  showToast(ok ? t("copiedImage") : t("copyFailed"));
656
1756
  },
657
- children: [(0, react_jsx_runtime.jsxs)("svg", {
658
- width: "14",
659
- height: "14",
660
- viewBox: "0 0 24 24",
661
- fill: "none",
662
- stroke: "currentColor",
663
- strokeWidth: "2",
664
- strokeLinecap: "round",
665
- strokeLinejoin: "round",
666
- children: [(0, react_jsx_runtime.jsx)("rect", {
667
- x: "9",
668
- y: "9",
669
- width: "13",
670
- height: "13",
671
- rx: "2",
672
- ry: "2"
673
- }), (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" })]
674
- }), (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") })]
675
1758
  }),
676
1759
  (0, react_jsx_runtime.jsxs)("button", {
677
1760
  type: "button",
678
1761
  className: "dsh-ig-lightbox-btn",
679
1762
  title: t("download"),
680
1763
  onClick: () => {
1764
+ if (!previewUrl) return;
681
1765
  const a = document.createElement("a");
682
1766
  a.href = previewUrl;
683
1767
  a.download = `dsh-${previewItem.provider}-${previewItem.id}.png`;
@@ -685,65 +1769,16 @@ window.__ModuleLoader__.load({
685
1769
  a.click();
686
1770
  document.body.removeChild(a);
687
1771
  },
688
- children: [(0, react_jsx_runtime.jsxs)("svg", {
689
- width: "14",
690
- height: "14",
691
- viewBox: "0 0 24 24",
692
- fill: "none",
693
- stroke: "currentColor",
694
- strokeWidth: "2",
695
- strokeLinecap: "round",
696
- strokeLinejoin: "round",
697
- children: [
698
- (0, react_jsx_runtime.jsx)("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
699
- (0, react_jsx_runtime.jsx)("polyline", { points: "7 10 12 15 17 10" }),
700
- (0, react_jsx_runtime.jsx)("line", {
701
- x1: "12",
702
- y1: "15",
703
- x2: "12",
704
- y2: "3"
705
- })
706
- ]
707
- }), (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") })]
708
1773
  }),
709
1774
  (0, react_jsx_runtime.jsxs)("button", {
710
1775
  type: "button",
711
1776
  className: "dsh-ig-lightbox-btn dsh-ig-lightbox-btn-danger",
712
1777
  title: t("delete"),
713
- onClick: async () => {
714
- if (!window.confirm(t("confirmDelete"))) return;
715
- await deleteGalleryItem(previewItem.id);
716
- setPreviewItem(null);
717
- setPreviewUrl(null);
718
- setPreviewBlob(null);
719
- showToast(t("deleted"));
1778
+ onClick: () => {
1779
+ if (previewItem) requestSingleDelete(previewItem);
720
1780
  },
721
- children: [(0, react_jsx_runtime.jsxs)("svg", {
722
- width: "14",
723
- height: "14",
724
- viewBox: "0 0 24 24",
725
- fill: "none",
726
- stroke: "currentColor",
727
- strokeWidth: "2",
728
- strokeLinecap: "round",
729
- strokeLinejoin: "round",
730
- children: [
731
- (0, react_jsx_runtime.jsx)("polyline", { points: "3 6 5 6 21 6" }),
732
- (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" }),
733
- (0, react_jsx_runtime.jsx)("line", {
734
- x1: "10",
735
- y1: "11",
736
- x2: "10",
737
- y2: "17"
738
- }),
739
- (0, react_jsx_runtime.jsx)("line", {
740
- x1: "14",
741
- y1: "11",
742
- x2: "14",
743
- y2: "17"
744
- })
745
- ]
746
- }), (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") })]
747
1782
  })
748
1783
  ]
749
1784
  })]
@@ -753,7 +1788,7 @@ window.__ModuleLoader__.load({
753
1788
  ]
754
1789
  });
755
1790
  };
756
- const GalleryCard = ({ item, t, onPreview, onToast }) => {
1791
+ const GalleryCard = ({ item, index: _index, isManageMode, isSelected, t, onClick, onToggleSelect, onRequestDelete, onPreview: _onPreview, onBlobLoaded, onToast }) => {
757
1792
  const [url, setUrl] = (0, react.useState)();
758
1793
  const [blob, setBlob] = (0, react.useState)();
759
1794
  const [loading, setLoading] = (0, react.useState)(true);
@@ -771,6 +1806,7 @@ window.__ModuleLoader__.load({
771
1806
  const resBlob = await response.blob();
772
1807
  if (controller.signal.aborted) return;
773
1808
  setBlob(resBlob);
1809
+ onBlobLoaded?.(resBlob);
774
1810
  objectUrl = URL.createObjectURL(resBlob);
775
1811
  setUrl(objectUrl);
776
1812
  setLoading(false);
@@ -809,14 +1845,27 @@ window.__ModuleLoader__.load({
809
1845
  a.click();
810
1846
  document.body.removeChild(a);
811
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;
812
1853
  return (0, react_jsx_runtime.jsxs)("div", {
813
- className: "dsh-ig-gallery-card",
814
- onClick: () => {
815
- if (url && blob) onPreview(url, blob);
816
- },
1854
+ className: `dsh-ig-gallery-card ${isSelected ? "is-selected" : ""} ${isManageMode ? "is-manage-mode" : ""}`,
1855
+ onClick: (e) => onClick(e, blob),
817
1856
  children: [(0, react_jsx_runtime.jsxs)("div", {
818
1857
  className: "dsh-ig-gallery-card-media",
819
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
+ }),
820
1869
  loading && (0, react_jsx_runtime.jsx)("div", {
821
1870
  className: "dsh-ig-gallery-card-loading",
822
1871
  children: "..."
@@ -841,50 +1890,14 @@ window.__ModuleLoader__.load({
841
1890
  onClick: (e) => {
842
1891
  copyImage(e);
843
1892
  },
844
- children: (0, react_jsx_runtime.jsxs)("svg", {
845
- width: "14",
846
- height: "14",
847
- viewBox: "0 0 24 24",
848
- fill: "none",
849
- stroke: "currentColor",
850
- strokeWidth: "2",
851
- strokeLinecap: "round",
852
- strokeLinejoin: "round",
853
- children: [(0, react_jsx_runtime.jsx)("rect", {
854
- x: "9",
855
- y: "9",
856
- width: "13",
857
- height: "13",
858
- rx: "2",
859
- ry: "2"
860
- }), (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" })]
861
- })
1893
+ children: (0, react_jsx_runtime.jsx)(Copy, { size: 13 })
862
1894
  }),
863
1895
  (0, react_jsx_runtime.jsx)("button", {
864
1896
  type: "button",
865
1897
  className: "dsh-ig-tool-btn",
866
1898
  title: t("download"),
867
1899
  onClick: downloadImage,
868
- children: (0, react_jsx_runtime.jsxs)("svg", {
869
- width: "14",
870
- height: "14",
871
- viewBox: "0 0 24 24",
872
- fill: "none",
873
- stroke: "currentColor",
874
- strokeWidth: "2",
875
- strokeLinecap: "round",
876
- strokeLinejoin: "round",
877
- children: [
878
- (0, react_jsx_runtime.jsx)("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
879
- (0, react_jsx_runtime.jsx)("polyline", { points: "7 10 12 15 17 10" }),
880
- (0, react_jsx_runtime.jsx)("line", {
881
- x1: "12",
882
- y1: "15",
883
- x2: "12",
884
- y2: "3"
885
- })
886
- ]
887
- })
1900
+ children: (0, react_jsx_runtime.jsx)(Download, { size: 13 })
888
1901
  }),
889
1902
  (0, react_jsx_runtime.jsx)("button", {
890
1903
  type: "button",
@@ -893,84 +1906,95 @@ window.__ModuleLoader__.load({
893
1906
  onClick: (e) => {
894
1907
  copyPrompt(e);
895
1908
  },
896
- children: (0, react_jsx_runtime.jsxs)("svg", {
897
- width: "14",
898
- height: "14",
899
- viewBox: "0 0 24 24",
900
- fill: "none",
901
- stroke: "currentColor",
902
- strokeWidth: "2",
903
- strokeLinecap: "round",
904
- strokeLinejoin: "round",
905
- 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", {
906
- x: "8",
907
- y: "2",
908
- width: "8",
909
- height: "4",
910
- rx: "1",
911
- ry: "1"
912
- })]
913
- })
1909
+ children: (0, react_jsx_runtime.jsx)(FileText, { size: 13 })
914
1910
  }),
915
1911
  (0, react_jsx_runtime.jsx)("button", {
916
1912
  type: "button",
917
1913
  className: "dsh-ig-tool-btn dsh-ig-tool-btn-danger",
918
1914
  title: t("delete"),
919
- onClick: async (e) => {
1915
+ onClick: (e) => {
920
1916
  e.stopPropagation();
921
- if (!window.confirm(t("confirmDelete"))) return;
922
- await deleteGalleryItem(item.id);
923
- onToast(t("deleted"));
1917
+ onRequestDelete();
924
1918
  },
925
- children: (0, react_jsx_runtime.jsxs)("svg", {
926
- width: "14",
927
- height: "14",
928
- viewBox: "0 0 24 24",
929
- fill: "none",
930
- stroke: "currentColor",
931
- strokeWidth: "2",
932
- strokeLinecap: "round",
933
- strokeLinejoin: "round",
934
- children: [
935
- (0, react_jsx_runtime.jsx)("polyline", { points: "3 6 5 6 21 6" }),
936
- (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" }),
937
- (0, react_jsx_runtime.jsx)("line", {
938
- x1: "10",
939
- y1: "11",
940
- x2: "10",
941
- y2: "17"
942
- }),
943
- (0, react_jsx_runtime.jsx)("line", {
944
- x1: "14",
945
- y1: "11",
946
- x2: "14",
947
- y2: "17"
948
- })
949
- ]
950
- })
1919
+ children: (0, react_jsx_runtime.jsx)(Trash2, { size: 13 })
951
1920
  })
952
1921
  ]
953
1922
  })
954
1923
  ]
955
1924
  }), (0, react_jsx_runtime.jsxs)("div", {
956
1925
  className: "dsh-ig-gallery-card-meta",
957
- children: [(0, react_jsx_runtime.jsxs)("div", {
958
- className: "dsh-ig-gallery-card-header",
959
- children: [(0, react_jsx_runtime.jsx)("span", {
960
- className: "dsh-ig-tag",
961
- children: item.provider
962
- }), item.model ? (0, react_jsx_runtime.jsx)("span", {
963
- className: "dsh-ig-tag dsh-ig-tag-model",
964
- children: item.model
965
- }) : null]
966
- }), (0, react_jsx_runtime.jsx)("p", {
967
- className: "dsh-ig-gallery-card-prompt",
968
- title: item.prompt,
969
- children: item.prompt
970
- })]
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
+ ]
971
1963
  })]
972
1964
  });
973
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
+ };
974
1998
  async function copyImageBlob(blob) {
975
1999
  try {
976
2000
  if (blob.type === "image/png") {
@@ -1333,58 +2357,149 @@ window.__ModuleLoader__.load({
1333
2357
  .dsh-ig-error{color:var(--dsw-alias-label-error,#d33);font-size:13px}
1334
2358
  .dsh-ig-loading{color:var(--dsw-alias-label-tertiary,#7b818b);font-size:13px}
1335
2359
 
1336
- /* Native Workspace Gallery View (Renders seamlessly inside DSH Session View) */
2360
+ /* Native Workspace Gallery & Studio View (Renders seamlessly inside DSH Session View) */
1337
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}
1338
- .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}
1339
- .dsh-ig-gallery-page-title-row{display:flex;align-items:center;gap:12px}
1340
- .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}
1341
- .dsh-ig-gallery-page-tools{display:flex;align-items:center;gap:12px}
1342
- .dsh-ig-gallery-search-wrap{position:relative;display:flex;align-items:center}
1343
- .dsh-ig-gallery-search-icon{position:absolute;left:10px;color:var(--dsw-alias-label-tertiary,#9ca3af);pointer-events:none}
1344
- .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}
1345
- .dsh-ig-gallery-search-input:focus{border-color:var(--dsw-alias-brand-primary,#4c78ff);width:240px}
1346
- .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}
1347
- .dsh-ig-gallery-page-body{flex:1;overflow-y:auto;padding:24px 28px}
1348
- .dsh-ig-gallery-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:20px}
1349
- .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}
1350
- .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)}
1351
- .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}
1352
- .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}
1353
2395
  .dsh-ig-gallery-card:hover .dsh-ig-gallery-card-img{transform:scale(1.03)}
1354
- .dsh-ig-gallery-card-loading{font-size:12px;color:#9ca3af}
2396
+ .dsh-ig-gallery-card-loading{font-size:12px;color:#94a3b8}
1355
2397
  .dsh-ig-gallery-card-error{font-size:12px;color:#ef4444;padding:8px;text-align:center}
1356
2398
 
1357
- /* Floating Action Toolbar (Matches Chat Image Toolbar) */
2399
+ /* Floating Action Toolbar on Card Hover */
1358
2400
  .dsh-ig-gallery-card:hover .dsh-ig-card-toolbar{opacity:1;pointer-events:auto}
1359
- .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}
1360
2402
 
1361
- .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}
1362
- .dsh-ig-gallery-card-header{display:flex;align-items:center;justify-content:space-between;font-size:11px}
1363
- .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}
1364
- .dsh-ig-tag-model{background:rgba(76,120,255,0.1);color:#4c78ff}
1365
- .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}
1366
- .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)}
1367
- .dsh-ig-gallery-empty-icon{font-size:48px;margin-bottom:12px}
1368
- .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}
1369
2428
  .dsh-ig-gallery-empty-desc{font-size:13px;max-width:360px;line-height:1.5}
1370
2429
 
1371
2430
  /* Pure Centered Lightbox */
1372
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}
1373
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}
1374
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}
1375
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}
1376
2437
  .dsh-ig-lightbox-close-btn:hover{background:rgba(255,255,255,0.3)}
1377
2438
  .dsh-ig-lightbox-img-wrap{max-width:86vw;max-height:78vh;display:flex;align-items:center;justify-content:center;cursor:default}
1378
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}
1379
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}
1380
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}
1381
- .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)}}
1382
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}
1383
2452
  .dsh-ig-lightbox-btn:hover{background:rgba(255,255,255,0.22)}
1384
2453
  .dsh-ig-lightbox-btn-danger{border-color:rgba(239,68,68,0.4);color:#fca5a5}
1385
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}
1386
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}
1387
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
+
1388
2503
  /* Hide floating chat composer when gallery page is active */
1389
2504
  [data-conversation-scroll]:has(.dsh-ig-gallery-page) [data-composer-seat]{display:none!important}
1390
2505
  `;
@@ -2034,6 +3149,7 @@ window.__ModuleLoader__.load({
2034
3149
  provider: result.provider,
2035
3150
  model: result.model,
2036
3151
  output: result.output,
3152
+ ...result.savedTo ? { savedTo: result.savedTo } : {},
2037
3153
  ...result.seed !== void 0 ? { seed: result.seed } : {}
2038
3154
  });
2039
3155
  }, [result?.attachment.attachmentId]);