dsh-image-gen 0.1.2 → 0.1.4

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
@@ -27,6 +27,724 @@ window.__ModuleLoader__.load({
27
27
  seedream: DEFAULT_SEEDREAM_BASE_URL
28
28
  };
29
29
  //#endregion
30
+ //#region lib/types/client/gallery-store.js
31
+ const DB_NAME = "dsh_image_gen_db";
32
+ const DB_VERSION = 1;
33
+ const STORE_NAME = "gallery_history";
34
+ let dbPromise = null;
35
+ function getDB() {
36
+ if (dbPromise) return dbPromise;
37
+ dbPromise = new Promise((resolve, reject) => {
38
+ if (typeof indexedDB === "undefined") {
39
+ reject(/* @__PURE__ */ new Error("IndexedDB is not supported in this environment."));
40
+ return;
41
+ }
42
+ const request = indexedDB.open(DB_NAME, DB_VERSION);
43
+ request.onupgradeneeded = (event) => {
44
+ const db = event.target.result;
45
+ if (!db.objectStoreNames.contains(STORE_NAME)) db.createObjectStore(STORE_NAME, { keyPath: "id" }).createIndex("createdAt", "createdAt", { unique: false });
46
+ };
47
+ request.onsuccess = () => {
48
+ resolve(request.result);
49
+ };
50
+ request.onerror = () => {
51
+ reject(request.error);
52
+ };
53
+ });
54
+ return dbPromise;
55
+ }
56
+ const listeners = /* @__PURE__ */ new Set();
57
+ function notifyListeners() {
58
+ for (const listener of listeners) try {
59
+ listener();
60
+ } catch (err) {
61
+ console.error("[dsh-image-gen] Gallery listener error:", err);
62
+ }
63
+ }
64
+ /**
65
+ * Subscribe to gallery mutations (insert/delete/clear).
66
+ */
67
+ function subscribeGallery(listener) {
68
+ listeners.add(listener);
69
+ return () => {
70
+ listeners.delete(listener);
71
+ };
72
+ }
73
+ /**
74
+ * Save or update a gallery record by attachmentId.
75
+ */
76
+ async function saveGalleryItem(item) {
77
+ try {
78
+ const db = await getDB();
79
+ const record = {
80
+ ...item,
81
+ createdAt: item.createdAt ?? Date.now()
82
+ };
83
+ await new Promise((resolve, reject) => {
84
+ const req = db.transaction(STORE_NAME, "readwrite").objectStore(STORE_NAME).put(record);
85
+ req.onsuccess = () => resolve();
86
+ req.onerror = () => reject(req.error);
87
+ });
88
+ notifyListeners();
89
+ } catch (err) {
90
+ console.warn("[dsh-image-gen] Failed to save gallery item to IndexedDB:", err);
91
+ }
92
+ }
93
+ /**
94
+ * Retrieve all gallery records sorted by createdAt descending.
95
+ */
96
+ async function getGalleryItems() {
97
+ try {
98
+ const db = await getDB();
99
+ return await new Promise((resolve, reject) => {
100
+ const req = db.transaction(STORE_NAME, "readonly").objectStore(STORE_NAME).index("createdAt").openCursor(null, "prev");
101
+ const items = [];
102
+ req.onsuccess = (event) => {
103
+ const cursor = event.target.result;
104
+ if (cursor) {
105
+ items.push(cursor.value);
106
+ cursor.continue();
107
+ } else resolve(items);
108
+ };
109
+ req.onerror = () => reject(req.error);
110
+ });
111
+ } catch (err) {
112
+ console.warn("[dsh-image-gen] Failed to read gallery items from IndexedDB:", err);
113
+ return [];
114
+ }
115
+ }
116
+ //#endregion
117
+ //#region lib/types/client/gallery-view.js
118
+ /**
119
+ * Native Workspace Gallery View Component for DSH `conversation.view` slot.
120
+ * Fully i18n-reactive (Chinese & English).
121
+ */
122
+ const DICT$1 = {
123
+ zh: {
124
+ totalCount: "共 {count} 张生成图片",
125
+ searchPlaceholder: "搜索 Prompt 关键词…",
126
+ filterAll: "全部厂商",
127
+ filterGoogle: "Google Gemini",
128
+ filterOpenAI: "OpenAI / 中转站",
129
+ filterSeedream: "字节 Seedream",
130
+ emptyTitle: "暂无生图记录",
131
+ emptyDesc: "在对话中让 Agent 生图后,生成的图片会自动收录到这里。",
132
+ noMatchTitle: "未找到匹配结果",
133
+ noMatchDesc: "尝试更换搜索关键词或选择其他厂商。",
134
+ copiedPrompt: "已复制 Prompt",
135
+ copiedImage: "已复制图片",
136
+ copyFailed: "复制失败",
137
+ preview: "查看大图",
138
+ download: "下载图片",
139
+ copyImg: "复制图片",
140
+ copyPpt: "复制 Prompt",
141
+ model: "模型",
142
+ prompt: "Prompt",
143
+ close: "关闭 (Esc)"
144
+ },
145
+ en: {
146
+ totalCount: "{count} images total",
147
+ searchPlaceholder: "Search prompt keywords…",
148
+ filterAll: "All Providers",
149
+ filterGoogle: "Google Gemini",
150
+ filterOpenAI: "OpenAI / Relay",
151
+ filterSeedream: "ByteDance Seedream",
152
+ emptyTitle: "No images generated yet",
153
+ emptyDesc: "Images generated during conversations will automatically appear here.",
154
+ noMatchTitle: "No matching images",
155
+ noMatchDesc: "Try a different search keyword or provider filter.",
156
+ copiedPrompt: "Prompt copied",
157
+ copiedImage: "Image copied",
158
+ copyFailed: "Copy failed",
159
+ preview: "Full Preview",
160
+ download: "Download",
161
+ copyImg: "Copy Image",
162
+ copyPpt: "Copy Prompt",
163
+ model: "Model",
164
+ prompt: "Prompt",
165
+ close: "Close (Esc)"
166
+ }
167
+ };
168
+ const GalleryViewTab = ({ locale }) => {
169
+ const [items, setItems] = (0, react.useState)([]);
170
+ const [search, setSearch] = (0, react.useState)("");
171
+ const [selectedProvider, setSelectedProvider] = (0, react.useState)("all");
172
+ const [previewItem, setPreviewItem] = (0, react.useState)(null);
173
+ const [previewUrl, setPreviewUrl] = (0, react.useState)(null);
174
+ const [previewBlob, setPreviewBlob] = (0, react.useState)(null);
175
+ const [toast, setToast] = (0, react.useState)(null);
176
+ const [lang, setLang] = (0, react.useState)(() => {
177
+ return (locale?.getSnapshot?.()?.active)?.startsWith("en") ? "en" : "zh";
178
+ });
179
+ (0, react.useEffect)(() => {
180
+ if (!locale?.subscribe) return;
181
+ return locale.subscribe(() => {
182
+ const active = locale.getSnapshot?.()?.active;
183
+ setLang(active?.startsWith("en") ? "en" : "zh");
184
+ });
185
+ }, [locale]);
186
+ const dict = lang === "en" ? DICT$1.en : DICT$1.zh;
187
+ const t = (key, params) => {
188
+ let text = dict[key] || DICT$1.zh[key] || key;
189
+ if (params) for (const [k, v] of Object.entries(params)) text = text.replace(`{${k}}`, v);
190
+ return text;
191
+ };
192
+ const showToast = (msg) => {
193
+ setToast(msg);
194
+ setTimeout(() => {
195
+ setToast(null);
196
+ }, 2e3);
197
+ };
198
+ (0, react.useEffect)(() => {
199
+ const seat = document.querySelector("[data-composer-seat]");
200
+ if (seat) {
201
+ const prevDisplay = seat.style.display;
202
+ seat.style.display = "none";
203
+ return () => {
204
+ seat.style.display = prevDisplay;
205
+ };
206
+ }
207
+ }, []);
208
+ (0, react.useEffect)(() => {
209
+ let active = true;
210
+ const load = () => {
211
+ getGalleryItems().then((res) => {
212
+ if (active) setItems(res);
213
+ });
214
+ };
215
+ load();
216
+ const unsubscribe = subscribeGallery(load);
217
+ return () => {
218
+ active = false;
219
+ unsubscribe();
220
+ };
221
+ }, []);
222
+ (0, react.useEffect)(() => {
223
+ if (!previewItem) return;
224
+ const onKeyDown = (e) => {
225
+ if (e.key === "Escape") {
226
+ setPreviewItem(null);
227
+ setPreviewUrl(null);
228
+ setPreviewBlob(null);
229
+ }
230
+ };
231
+ window.addEventListener("keydown", onKeyDown);
232
+ return () => window.removeEventListener("keydown", onKeyDown);
233
+ }, [previewItem]);
234
+ const filteredItems = (0, react.useMemo)(() => {
235
+ return items.filter((item) => {
236
+ if (selectedProvider !== "all" && item.provider !== selectedProvider) return false;
237
+ if (search.trim().length > 0) {
238
+ const q = search.trim().toLowerCase();
239
+ const matchPrompt = item.prompt?.toLowerCase().includes(q);
240
+ const matchModel = item.model?.toLowerCase().includes(q);
241
+ if (!matchPrompt && !matchModel) return false;
242
+ }
243
+ return true;
244
+ });
245
+ }, [
246
+ items,
247
+ search,
248
+ selectedProvider
249
+ ]);
250
+ return (0, react_jsx_runtime.jsxs)("div", {
251
+ className: "dsh-ig-gallery-page",
252
+ children: [
253
+ (0, react_jsx_runtime.jsxs)("header", {
254
+ className: "dsh-ig-gallery-page-header",
255
+ children: [(0, react_jsx_runtime.jsx)("div", {
256
+ className: "dsh-ig-gallery-page-title-row",
257
+ children: (0, react_jsx_runtime.jsx)("span", {
258
+ className: "dsh-ig-gallery-page-count",
259
+ children: t("totalCount", { count: String(items.length) })
260
+ })
261
+ }), (0, react_jsx_runtime.jsxs)("div", {
262
+ className: "dsh-ig-gallery-page-tools",
263
+ children: [(0, react_jsx_runtime.jsxs)("div", {
264
+ className: "dsh-ig-gallery-search-wrap",
265
+ children: [(0, react_jsx_runtime.jsxs)("svg", {
266
+ className: "dsh-ig-gallery-search-icon",
267
+ width: "14",
268
+ height: "14",
269
+ viewBox: "0 0 24 24",
270
+ fill: "none",
271
+ stroke: "currentColor",
272
+ strokeWidth: "2",
273
+ strokeLinecap: "round",
274
+ strokeLinejoin: "round",
275
+ children: [(0, react_jsx_runtime.jsx)("circle", {
276
+ cx: "11",
277
+ cy: "11",
278
+ r: "8"
279
+ }), (0, react_jsx_runtime.jsx)("line", {
280
+ x1: "21",
281
+ y1: "21",
282
+ x2: "16.65",
283
+ y2: "16.65"
284
+ })]
285
+ }), (0, react_jsx_runtime.jsx)("input", {
286
+ type: "text",
287
+ className: "dsh-ig-gallery-search-input",
288
+ placeholder: t("searchPlaceholder"),
289
+ value: search,
290
+ onChange: (e) => setSearch(e.target.value)
291
+ })]
292
+ }), (0, react_jsx_runtime.jsxs)("select", {
293
+ className: "dsh-ig-gallery-select",
294
+ value: selectedProvider,
295
+ onChange: (e) => setSelectedProvider(e.target.value),
296
+ children: [
297
+ (0, react_jsx_runtime.jsx)("option", {
298
+ value: "all",
299
+ children: t("filterAll")
300
+ }),
301
+ (0, react_jsx_runtime.jsx)("option", {
302
+ value: "google",
303
+ children: t("filterGoogle")
304
+ }),
305
+ (0, react_jsx_runtime.jsx)("option", {
306
+ value: "openai",
307
+ children: t("filterOpenAI")
308
+ }),
309
+ (0, react_jsx_runtime.jsx)("option", {
310
+ value: "seedream",
311
+ children: t("filterSeedream")
312
+ })
313
+ ]
314
+ })]
315
+ })]
316
+ }),
317
+ (0, react_jsx_runtime.jsx)("div", {
318
+ className: "dsh-ig-gallery-page-body",
319
+ children: items.length === 0 ? (0, react_jsx_runtime.jsxs)("div", {
320
+ className: "dsh-ig-gallery-empty",
321
+ children: [
322
+ (0, react_jsx_runtime.jsx)("div", {
323
+ className: "dsh-ig-gallery-empty-icon",
324
+ children: "🖼️"
325
+ }),
326
+ (0, react_jsx_runtime.jsx)("div", {
327
+ className: "dsh-ig-gallery-empty-title",
328
+ children: t("emptyTitle")
329
+ }),
330
+ (0, react_jsx_runtime.jsx)("div", {
331
+ className: "dsh-ig-gallery-empty-desc",
332
+ children: t("emptyDesc")
333
+ })
334
+ ]
335
+ }) : filteredItems.length === 0 ? (0, react_jsx_runtime.jsxs)("div", {
336
+ className: "dsh-ig-gallery-empty",
337
+ children: [
338
+ (0, react_jsx_runtime.jsx)("div", {
339
+ className: "dsh-ig-gallery-empty-icon",
340
+ children: "🔍"
341
+ }),
342
+ (0, react_jsx_runtime.jsx)("div", {
343
+ className: "dsh-ig-gallery-empty-title",
344
+ children: t("noMatchTitle")
345
+ }),
346
+ (0, react_jsx_runtime.jsx)("div", {
347
+ className: "dsh-ig-gallery-empty-desc",
348
+ children: t("noMatchDesc")
349
+ })
350
+ ]
351
+ }) : (0, react_jsx_runtime.jsx)("div", {
352
+ className: "dsh-ig-gallery-grid",
353
+ children: filteredItems.map((item) => (0, react_jsx_runtime.jsx)(GalleryCard, {
354
+ item,
355
+ t,
356
+ onPreview: (url, blob) => {
357
+ setPreviewItem(item);
358
+ setPreviewUrl(url);
359
+ setPreviewBlob(blob);
360
+ },
361
+ onToast: showToast
362
+ }, item.id))
363
+ })
364
+ }),
365
+ toast && (0, react_jsx_runtime.jsx)("div", {
366
+ className: "dsh-ig-gallery-page-toast",
367
+ children: toast
368
+ }),
369
+ previewItem && previewUrl && (0, react_jsx_runtime.jsxs)("div", {
370
+ className: "dsh-ig-lightbox-backdrop",
371
+ onClick: () => {
372
+ setPreviewItem(null);
373
+ setPreviewUrl(null);
374
+ setPreviewBlob(null);
375
+ },
376
+ children: [
377
+ (0, react_jsx_runtime.jsxs)("div", {
378
+ className: "dsh-ig-lightbox-topbar",
379
+ onClick: (e) => e.stopPropagation(),
380
+ children: [(0, react_jsx_runtime.jsxs)("div", {
381
+ className: "dsh-ig-lightbox-meta",
382
+ children: [(0, react_jsx_runtime.jsx)("span", {
383
+ className: "dsh-ig-tag",
384
+ children: previewItem.provider
385
+ }), previewItem.model ? (0, react_jsx_runtime.jsx)("span", {
386
+ className: "dsh-ig-tag dsh-ig-tag-model",
387
+ children: previewItem.model
388
+ }) : null]
389
+ }), (0, react_jsx_runtime.jsx)("button", {
390
+ type: "button",
391
+ className: "dsh-ig-lightbox-close-btn",
392
+ title: t("close"),
393
+ onClick: () => {
394
+ setPreviewItem(null);
395
+ setPreviewUrl(null);
396
+ setPreviewBlob(null);
397
+ },
398
+ children: (0, react_jsx_runtime.jsxs)("svg", {
399
+ width: "18",
400
+ height: "18",
401
+ viewBox: "0 0 24 24",
402
+ fill: "none",
403
+ stroke: "currentColor",
404
+ strokeWidth: "2",
405
+ strokeLinecap: "round",
406
+ strokeLinejoin: "round",
407
+ children: [(0, react_jsx_runtime.jsx)("line", {
408
+ x1: "18",
409
+ y1: "6",
410
+ x2: "6",
411
+ y2: "18"
412
+ }), (0, react_jsx_runtime.jsx)("line", {
413
+ x1: "6",
414
+ y1: "6",
415
+ x2: "18",
416
+ y2: "18"
417
+ })]
418
+ })
419
+ })]
420
+ }),
421
+ (0, react_jsx_runtime.jsx)("div", {
422
+ className: "dsh-ig-lightbox-img-wrap",
423
+ onClick: (e) => e.stopPropagation(),
424
+ children: (0, react_jsx_runtime.jsx)("img", {
425
+ className: "dsh-ig-lightbox-img",
426
+ src: previewUrl,
427
+ alt: previewItem.prompt
428
+ })
429
+ }),
430
+ (0, react_jsx_runtime.jsxs)("div", {
431
+ className: "dsh-ig-lightbox-bottombar",
432
+ onClick: (e) => e.stopPropagation(),
433
+ children: [(0, react_jsx_runtime.jsx)("div", {
434
+ className: "dsh-ig-lightbox-prompt-text",
435
+ title: previewItem.prompt,
436
+ children: previewItem.prompt
437
+ }), (0, react_jsx_runtime.jsxs)("div", {
438
+ className: "dsh-ig-lightbox-actions",
439
+ children: [
440
+ (0, react_jsx_runtime.jsxs)("button", {
441
+ type: "button",
442
+ className: "dsh-ig-lightbox-btn",
443
+ title: t("copyPpt"),
444
+ onClick: async () => {
445
+ await navigator.clipboard.writeText(previewItem.prompt);
446
+ showToast(t("copiedPrompt"));
447
+ },
448
+ children: [(0, react_jsx_runtime.jsxs)("svg", {
449
+ width: "14",
450
+ height: "14",
451
+ viewBox: "0 0 24 24",
452
+ fill: "none",
453
+ stroke: "currentColor",
454
+ strokeWidth: "2",
455
+ strokeLinecap: "round",
456
+ strokeLinejoin: "round",
457
+ 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", {
458
+ x: "8",
459
+ y: "2",
460
+ width: "8",
461
+ height: "4",
462
+ rx: "1",
463
+ ry: "1"
464
+ })]
465
+ }), (0, react_jsx_runtime.jsx)("span", { children: t("copyPpt") })]
466
+ }),
467
+ (0, react_jsx_runtime.jsxs)("button", {
468
+ type: "button",
469
+ className: "dsh-ig-lightbox-btn",
470
+ title: t("copyImg"),
471
+ onClick: async () => {
472
+ if (!previewBlob) return;
473
+ const ok = await copyImageBlob(previewBlob);
474
+ showToast(ok ? t("copiedImage") : t("copyFailed"));
475
+ },
476
+ children: [(0, react_jsx_runtime.jsxs)("svg", {
477
+ width: "14",
478
+ height: "14",
479
+ viewBox: "0 0 24 24",
480
+ fill: "none",
481
+ stroke: "currentColor",
482
+ strokeWidth: "2",
483
+ strokeLinecap: "round",
484
+ strokeLinejoin: "round",
485
+ children: [(0, react_jsx_runtime.jsx)("rect", {
486
+ x: "9",
487
+ y: "9",
488
+ width: "13",
489
+ height: "13",
490
+ rx: "2",
491
+ ry: "2"
492
+ }), (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" })]
493
+ }), (0, react_jsx_runtime.jsx)("span", { children: t("copyImg") })]
494
+ }),
495
+ (0, react_jsx_runtime.jsxs)("button", {
496
+ type: "button",
497
+ className: "dsh-ig-lightbox-btn",
498
+ title: t("download"),
499
+ onClick: () => {
500
+ const a = document.createElement("a");
501
+ a.href = previewUrl;
502
+ a.download = `dsh-${previewItem.provider}-${previewItem.id}.png`;
503
+ document.body.appendChild(a);
504
+ a.click();
505
+ document.body.removeChild(a);
506
+ },
507
+ children: [(0, react_jsx_runtime.jsxs)("svg", {
508
+ width: "14",
509
+ height: "14",
510
+ viewBox: "0 0 24 24",
511
+ fill: "none",
512
+ stroke: "currentColor",
513
+ strokeWidth: "2",
514
+ strokeLinecap: "round",
515
+ strokeLinejoin: "round",
516
+ children: [
517
+ (0, react_jsx_runtime.jsx)("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
518
+ (0, react_jsx_runtime.jsx)("polyline", { points: "7 10 12 15 17 10" }),
519
+ (0, react_jsx_runtime.jsx)("line", {
520
+ x1: "12",
521
+ y1: "15",
522
+ x2: "12",
523
+ y2: "3"
524
+ })
525
+ ]
526
+ }), (0, react_jsx_runtime.jsx)("span", { children: t("download") })]
527
+ })
528
+ ]
529
+ })]
530
+ })
531
+ ]
532
+ })
533
+ ]
534
+ });
535
+ };
536
+ const GalleryCard = ({ item, t, onPreview, onToast }) => {
537
+ const [url, setUrl] = (0, react.useState)();
538
+ const [blob, setBlob] = (0, react.useState)();
539
+ const [loading, setLoading] = (0, react.useState)(true);
540
+ const [error, setError] = (0, react.useState)();
541
+ (0, react.useEffect)(() => {
542
+ const controller = new AbortController();
543
+ let objectUrl;
544
+ fetch(IMAGE_ROUTE, {
545
+ method: "POST",
546
+ signal: controller.signal,
547
+ headers: { "content-type": "application/json" },
548
+ body: JSON.stringify({ attachment: item.attachment })
549
+ }).then(async (response) => {
550
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
551
+ const resBlob = await response.blob();
552
+ if (controller.signal.aborted) return;
553
+ setBlob(resBlob);
554
+ objectUrl = URL.createObjectURL(resBlob);
555
+ setUrl(objectUrl);
556
+ setLoading(false);
557
+ }).catch((err) => {
558
+ if (!controller.signal.aborted) {
559
+ setError(err instanceof Error ? err.message : String(err));
560
+ setLoading(false);
561
+ }
562
+ });
563
+ return () => {
564
+ controller.abort();
565
+ if (objectUrl) URL.revokeObjectURL(objectUrl);
566
+ };
567
+ }, [item.attachment]);
568
+ const copyPrompt = async (e) => {
569
+ e.stopPropagation();
570
+ try {
571
+ await navigator.clipboard.writeText(item.prompt);
572
+ onToast(t("copiedPrompt"));
573
+ } catch {
574
+ onToast(t("copyFailed"));
575
+ }
576
+ };
577
+ const copyImage = async (e) => {
578
+ e.stopPropagation();
579
+ if (!blob) return;
580
+ onToast(await copyImageBlob(blob) ? t("copiedImage") : t("copyFailed"));
581
+ };
582
+ const downloadImage = (e) => {
583
+ e.stopPropagation();
584
+ if (!url) return;
585
+ const a = document.createElement("a");
586
+ a.href = url;
587
+ a.download = `dsh-${item.provider}-${item.id}.png`;
588
+ document.body.appendChild(a);
589
+ a.click();
590
+ document.body.removeChild(a);
591
+ };
592
+ return (0, react_jsx_runtime.jsxs)("div", {
593
+ className: "dsh-ig-gallery-card",
594
+ onClick: () => {
595
+ if (url && blob) onPreview(url, blob);
596
+ },
597
+ children: [(0, react_jsx_runtime.jsxs)("div", {
598
+ className: "dsh-ig-gallery-card-media",
599
+ children: [
600
+ loading && (0, react_jsx_runtime.jsx)("div", {
601
+ className: "dsh-ig-gallery-card-loading",
602
+ children: "..."
603
+ }),
604
+ error && (0, react_jsx_runtime.jsxs)("div", {
605
+ className: "dsh-ig-gallery-card-error",
606
+ children: ["⚠️ ", error]
607
+ }),
608
+ url && (0, react_jsx_runtime.jsx)("img", {
609
+ className: "dsh-ig-gallery-card-img",
610
+ src: url,
611
+ alt: item.prompt,
612
+ loading: "lazy"
613
+ }),
614
+ (0, react_jsx_runtime.jsxs)("div", {
615
+ className: "dsh-ig-card-toolbar",
616
+ children: [
617
+ (0, react_jsx_runtime.jsx)("button", {
618
+ type: "button",
619
+ className: "dsh-ig-tool-btn",
620
+ title: t("copyImg"),
621
+ onClick: (e) => {
622
+ copyImage(e);
623
+ },
624
+ children: (0, react_jsx_runtime.jsxs)("svg", {
625
+ width: "14",
626
+ height: "14",
627
+ viewBox: "0 0 24 24",
628
+ fill: "none",
629
+ stroke: "currentColor",
630
+ strokeWidth: "2",
631
+ strokeLinecap: "round",
632
+ strokeLinejoin: "round",
633
+ children: [(0, react_jsx_runtime.jsx)("rect", {
634
+ x: "9",
635
+ y: "9",
636
+ width: "13",
637
+ height: "13",
638
+ rx: "2",
639
+ ry: "2"
640
+ }), (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" })]
641
+ })
642
+ }),
643
+ (0, react_jsx_runtime.jsx)("button", {
644
+ type: "button",
645
+ className: "dsh-ig-tool-btn",
646
+ title: t("download"),
647
+ onClick: downloadImage,
648
+ children: (0, react_jsx_runtime.jsxs)("svg", {
649
+ width: "14",
650
+ height: "14",
651
+ viewBox: "0 0 24 24",
652
+ fill: "none",
653
+ stroke: "currentColor",
654
+ strokeWidth: "2",
655
+ strokeLinecap: "round",
656
+ strokeLinejoin: "round",
657
+ children: [
658
+ (0, react_jsx_runtime.jsx)("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
659
+ (0, react_jsx_runtime.jsx)("polyline", { points: "7 10 12 15 17 10" }),
660
+ (0, react_jsx_runtime.jsx)("line", {
661
+ x1: "12",
662
+ y1: "15",
663
+ x2: "12",
664
+ y2: "3"
665
+ })
666
+ ]
667
+ })
668
+ }),
669
+ (0, react_jsx_runtime.jsx)("button", {
670
+ type: "button",
671
+ className: "dsh-ig-tool-btn",
672
+ title: t("copyPpt"),
673
+ onClick: (e) => {
674
+ copyPrompt(e);
675
+ },
676
+ children: (0, react_jsx_runtime.jsxs)("svg", {
677
+ width: "14",
678
+ height: "14",
679
+ viewBox: "0 0 24 24",
680
+ fill: "none",
681
+ stroke: "currentColor",
682
+ strokeWidth: "2",
683
+ strokeLinecap: "round",
684
+ strokeLinejoin: "round",
685
+ 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", {
686
+ x: "8",
687
+ y: "2",
688
+ width: "8",
689
+ height: "4",
690
+ rx: "1",
691
+ ry: "1"
692
+ })]
693
+ })
694
+ })
695
+ ]
696
+ })
697
+ ]
698
+ }), (0, react_jsx_runtime.jsxs)("div", {
699
+ className: "dsh-ig-gallery-card-meta",
700
+ children: [(0, react_jsx_runtime.jsxs)("div", {
701
+ className: "dsh-ig-gallery-card-header",
702
+ children: [(0, react_jsx_runtime.jsx)("span", {
703
+ className: "dsh-ig-tag",
704
+ children: item.provider
705
+ }), item.model ? (0, react_jsx_runtime.jsx)("span", {
706
+ className: "dsh-ig-tag dsh-ig-tag-model",
707
+ children: item.model
708
+ }) : null]
709
+ }), (0, react_jsx_runtime.jsx)("p", {
710
+ className: "dsh-ig-gallery-card-prompt",
711
+ title: item.prompt,
712
+ children: item.prompt
713
+ })]
714
+ })]
715
+ });
716
+ };
717
+ async function copyImageBlob(blob) {
718
+ try {
719
+ if (blob.type === "image/png") {
720
+ await navigator.clipboard.write([new ClipboardItem({ "image/png": blob })]);
721
+ return true;
722
+ }
723
+ const img = new Image();
724
+ const url = URL.createObjectURL(blob);
725
+ await new Promise((resolve, reject) => {
726
+ img.onload = resolve;
727
+ img.onerror = reject;
728
+ img.src = url;
729
+ });
730
+ const canvas = document.createElement("canvas");
731
+ canvas.width = img.naturalWidth;
732
+ canvas.height = img.naturalHeight;
733
+ const ctx = canvas.getContext("2d");
734
+ if (!ctx) throw new Error("Canvas unavailable");
735
+ ctx.drawImage(img, 0, 0);
736
+ URL.revokeObjectURL(url);
737
+ const pngBlob = await new Promise((res) => {
738
+ canvas.toBlob(res, "image/png");
739
+ });
740
+ if (!pngBlob) throw new Error("Blob conversion failed");
741
+ await navigator.clipboard.write([new ClipboardItem({ "image/png": pngBlob })]);
742
+ return true;
743
+ } catch (_err) {
744
+ return false;
745
+ }
746
+ }
747
+ //#endregion
30
748
  //#region lib/types/client/index.js
31
749
  /** Web settings and generated-image cards contributed by the Bundle. */
32
750
  const KEY_REF = {
@@ -61,7 +779,12 @@ window.__ModuleLoader__.load({
61
779
  generating: "正在生成图片…",
62
780
  loading: "正在加载图片…",
63
781
  loadFailed: "图片读取失败 ({status})",
64
- generatedTitle: "Generated image"
782
+ generatedTitle: "已生成图片",
783
+ copyImg: "复制图片",
784
+ download: "下载图片",
785
+ openNewTab: "新标签页打开",
786
+ copiedImage: "已复制图片",
787
+ copyFailed: "复制失败"
65
788
  },
66
789
  en: {
67
790
  title: "Image Generation",
@@ -89,7 +812,12 @@ window.__ModuleLoader__.load({
89
812
  generating: "Generating image…",
90
813
  loading: "Loading image…",
91
814
  loadFailed: "Failed to load image ({status})",
92
- generatedTitle: "Generated image"
815
+ generatedTitle: "Generated image",
816
+ copyImg: "Copy Image",
817
+ download: "Download Image",
818
+ openNewTab: "Open in new tab",
819
+ copiedImage: "Image copied",
820
+ copyFailed: "Copy failed"
93
821
  }
94
822
  };
95
823
  const STYLE = `
@@ -115,11 +843,73 @@ window.__ModuleLoader__.load({
115
843
  .dsh-ig-actions{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-top:16px;padding-top:12px;border-top:1px solid var(--dsw-alias-border-l2,#eee)}
116
844
  .dsh-ig-save{appearance:none;border:0;border-radius:8px;padding:6px 16px;background:var(--dsw-alias-label-primary,#111827);color:var(--dsw-alias-bg-layer-3,#fff);font:inherit;font-size:13px;font-weight:500;cursor:pointer;transition:opacity .15s}
117
845
  .dsh-ig-save:disabled{opacity:.4;cursor:default}
846
+
118
847
  .dsh-ig-result{display:grid;gap:10px;max-width:520px}
119
848
  .dsh-ig-result-title{font-size:14px;font-weight:600}
120
- .dsh-ig-image{display:block;max-width:100%;max-height:520px;border-radius:12px;background:#f2f3f5}
849
+ .dsh-ig-container{position:relative;display:inline-block;width:fit-content;max-width:100%;justify-self:start;border-radius:12px;overflow:hidden;line-height:0}
850
+ .dsh-ig-container:hover .dsh-ig-toolbar{opacity:1;pointer-events:auto}
851
+ .dsh-ig-toolbar{position:absolute;top:8px;left:8px;display:flex;align-items:center;gap:5px;padding:3px 5px;border-radius:8px;background:rgba(0,0,0,0.65);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);opacity:0;pointer-events:none;transition:opacity .18s ease;z-index:10;line-height:1}
852
+ .dsh-ig-tool-btn{appearance:none;border:0;background:transparent;color:#fff;padding:5px;border-radius:6px;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;transition:background .15s}
853
+ .dsh-ig-tool-btn:hover{background:rgba(255,255,255,0.25)}
854
+ .dsh-ig-toast{position:absolute;top:100%;left:0;margin-top:5px;padding:3px 8px;border-radius:6px;background:rgba(0,0,0,0.85);color:#fff;font-size:11px;white-space:nowrap;pointer-events:none;z-index:20}
855
+ .dsh-ig-image{display:block;max-width:100%;max-height:520px;border-radius:12px;background:#f2f3f5;cursor:pointer}
856
+ @keyframes dsh-ig-fade{from{opacity:0}to{opacity:1}}
121
857
  .dsh-ig-error{color:var(--dsw-alias-label-error,#d33);font-size:13px}
122
- .dsh-ig-loading{color:var(--dsw-alias-label-tertiary,#7b818b);font-size:13px}`;
858
+ .dsh-ig-loading{color:var(--dsw-alias-label-tertiary,#7b818b);font-size:13px}
859
+
860
+ /* Native Workspace Gallery View (Renders seamlessly inside DSH Session View) */
861
+ .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}
862
+ .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}
863
+ .dsh-ig-gallery-page-title-row{display:flex;align-items:center;gap:12px}
864
+ .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}
865
+ .dsh-ig-gallery-page-tools{display:flex;align-items:center;gap:12px}
866
+ .dsh-ig-gallery-search-wrap{position:relative;display:flex;align-items:center}
867
+ .dsh-ig-gallery-search-icon{position:absolute;left:10px;color:var(--dsw-alias-label-tertiary,#9ca3af);pointer-events:none}
868
+ .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}
869
+ .dsh-ig-gallery-search-input:focus{border-color:var(--dsw-alias-brand-primary,#4c78ff);width:240px}
870
+ .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}
871
+ .dsh-ig-gallery-page-body{flex:1;overflow-y:auto;padding:24px 28px}
872
+ .dsh-ig-gallery-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:20px}
873
+ .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}
874
+ .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)}
875
+ .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}
876
+ .dsh-ig-gallery-card-img{width:100%;height:100%;object-fit:cover;transition:transform .2s}
877
+ .dsh-ig-gallery-card:hover .dsh-ig-gallery-card-img{transform:scale(1.03)}
878
+ .dsh-ig-gallery-card-loading{font-size:12px;color:#9ca3af}
879
+ .dsh-ig-gallery-card-error{font-size:12px;color:#ef4444;padding:8px;text-align:center}
880
+
881
+ /* Floating Action Toolbar (Matches Chat Image Toolbar) */
882
+ .dsh-ig-gallery-card:hover .dsh-ig-card-toolbar{opacity:1;pointer-events:auto}
883
+ .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}
884
+
885
+ .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}
886
+ .dsh-ig-gallery-card-header{display:flex;align-items:center;justify-content:space-between;font-size:11px}
887
+ .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}
888
+ .dsh-ig-tag-model{background:rgba(76,120,255,0.1);color:#4c78ff}
889
+ .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}
890
+ .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)}
891
+ .dsh-ig-gallery-empty-icon{font-size:48px;margin-bottom:12px}
892
+ .dsh-ig-gallery-empty-title{font-size:16px;font-weight:600;color:var(--dsw-alias-label-primary,inherit);margin-bottom:6px}
893
+ .dsh-ig-gallery-empty-desc{font-size:13px;max-width:360px;line-height:1.5}
894
+
895
+ /* Pure Centered Lightbox */
896
+ .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}
897
+ .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}
898
+ .dsh-ig-lightbox-meta{display:flex;align-items:center;gap:8px;pointer-events:auto}
899
+ .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}
900
+ .dsh-ig-lightbox-close-btn:hover{background:rgba(255,255,255,0.3)}
901
+ .dsh-ig-lightbox-img-wrap{max-width:86vw;max-height:78vh;display:flex;align-items:center;justify-content:center;cursor:default}
902
+ .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}
903
+ .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}
904
+ .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}
905
+ .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}
906
+ .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}
907
+ .dsh-ig-lightbox-btn:hover{background:rgba(255,255,255,0.22)}
908
+ .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}
909
+
910
+ /* Hide floating chat composer when gallery page is active */
911
+ [data-conversation-scroll]:has(.dsh-ig-gallery-page) [data-composer-seat]{display:none!important}
912
+ `;
123
913
  /** Required browser services. */
124
914
  const inject = [
125
915
  "slots",
@@ -128,7 +918,7 @@ window.__ModuleLoader__.load({
128
918
  "settingsScope",
129
919
  "locale"
130
920
  ];
131
- /** Mount the settings card and generated-image card. */
921
+ /** Mount the settings card, generated-image card, and native conversation gallery view. */
132
922
  function apply(ctx) {
133
923
  const { api } = ctx.get("connection");
134
924
  const scope = ctx.settingsScope.bind({ namespace: IMAGE_GENERATION_NAMESPACE });
@@ -154,16 +944,26 @@ window.__ModuleLoader__.load({
154
944
  locale
155
945
  })
156
946
  }, ImageGenerationSettingsCard));
157
- ctx.slots.inject("tool.call.toolview", () => ctx.slots.register({
947
+ ctx.slots.inject("tool.call.toolview", () => register({
158
948
  name: "tool.call.toolview",
159
- key: "generate_image"
949
+ key: "generate_image",
950
+ inject: () => ({ locale })
160
951
  }, GeneratedImageCard));
952
+ ctx.slots.inject("conversation.view", () => register({
953
+ name: "conversation.view",
954
+ id: "gallery",
955
+ order: 20,
956
+ label: () => {
957
+ return (locale?.getSnapshot?.()?.active)?.startsWith("en") ? "Gallery" : "画廊";
958
+ },
959
+ inject: () => ({ locale })
960
+ }, GalleryViewTab));
161
961
  }
162
962
  /** Edit provider settings and its write-only API credential. */
163
963
  function ImageGenerationSettingsCard(props) {
164
964
  const [open, setOpen] = (0, react.useState)(false);
165
965
  const [snapshot, setSnapshot] = (0, react.useState)(() => props.scope.getSnapshot());
166
- const [lang, setLang] = (0, react.useState)(() => props.locale?.getSnapshot?.()?.active ?? "zh");
966
+ const [lang, setLang] = (0, react.useState)(() => props.locale?.getSnapshot?.()?.active?.startsWith("en") ? "en" : "zh");
167
967
  const [provider, setProvider] = (0, react.useState)("google");
168
968
  const [model, setModel] = (0, react.useState)("");
169
969
  const [baseURL, setBaseURL] = (0, react.useState)("");
@@ -176,7 +976,7 @@ window.__ModuleLoader__.load({
176
976
  }), [props.scope]);
177
977
  (0, react.useEffect)(() => {
178
978
  return props.locale?.subscribe?.(() => {
179
- setLang(props.locale?.getSnapshot?.()?.active ?? "zh");
979
+ setLang(props.locale?.getSnapshot?.()?.active?.startsWith("en") ? "en" : "zh");
180
980
  });
181
981
  }, [props.locale]);
182
982
  const t = (keyName, params) => {
@@ -400,7 +1200,48 @@ window.__ModuleLoader__.load({
400
1200
  function GeneratedImageCard(props) {
401
1201
  const attachment = imageRef(props.block);
402
1202
  const [url, setUrl] = (0, react.useState)();
1203
+ const [blob, setBlob] = (0, react.useState)();
403
1204
  const [error, setError] = (0, react.useState)();
1205
+ const [previewOpen, setPreviewOpen] = (0, react.useState)(false);
1206
+ const [toast, setToast] = (0, react.useState)();
1207
+ const [lang, setLang] = (0, react.useState)(() => props.locale?.getSnapshot?.()?.active?.startsWith("en") ? "en" : "zh");
1208
+ (0, react.useEffect)(() => {
1209
+ return props.locale?.subscribe?.(() => {
1210
+ setLang(props.locale?.getSnapshot?.()?.active?.startsWith("en") ? "en" : "zh");
1211
+ });
1212
+ }, [props.locale]);
1213
+ const t = (keyName, params) => {
1214
+ let text = (lang === "en" ? DICT.en : DICT.zh)[keyName] || DICT.zh[keyName] || keyName;
1215
+ if (params) for (const [k, v] of Object.entries(params)) text = text.replace(`{${k}}`, v);
1216
+ return text;
1217
+ };
1218
+ (0, react.useEffect)(() => {
1219
+ if (attachment === void 0) return;
1220
+ const blockAny = props.block;
1221
+ const meta = blockAny.meta ?? blockAny.resultView?.meta;
1222
+ const prompt = typeof meta?.prompt === "string" ? meta.prompt : blockAny.call?.args?.prompt ?? "Generated Image";
1223
+ const provider = typeof meta?.provider === "string" ? meta.provider : "google";
1224
+ const model = typeof meta?.model === "string" ? meta.model : "";
1225
+ const output = typeof meta?.output === "string" ? meta.output : "";
1226
+ saveGalleryItem({
1227
+ id: attachment.attachmentId,
1228
+ attachment,
1229
+ prompt,
1230
+ provider,
1231
+ model,
1232
+ output
1233
+ });
1234
+ }, [attachment?.attachmentId]);
1235
+ (0, react.useEffect)(() => {
1236
+ if (!previewOpen) return;
1237
+ const onKeyDown = (e) => {
1238
+ if (e.key === "Escape") setPreviewOpen(false);
1239
+ };
1240
+ window.addEventListener("keydown", onKeyDown);
1241
+ return () => {
1242
+ window.removeEventListener("keydown", onKeyDown);
1243
+ };
1244
+ }, [previewOpen]);
404
1245
  (0, react.useEffect)(() => {
405
1246
  if (attachment === void 0) return;
406
1247
  const controller = new AbortController();
@@ -411,10 +1252,11 @@ window.__ModuleLoader__.load({
411
1252
  headers: { "content-type": "application/json" },
412
1253
  body: JSON.stringify({ attachment })
413
1254
  }).then(async (response) => {
414
- if (!response.ok) throw new Error(`图片读取失败 (${String(response.status)})`);
415
- const blob = await response.blob();
1255
+ if (!response.ok) throw new Error(t("loadFailed", { status: String(response.status) }));
1256
+ const resBlob = await response.blob();
416
1257
  if (controller.signal.aborted) return;
417
- objectUrl = URL.createObjectURL(blob);
1258
+ setBlob(resBlob);
1259
+ objectUrl = URL.createObjectURL(resBlob);
418
1260
  setUrl(objectUrl);
419
1261
  }).catch((cause) => {
420
1262
  if (!controller.signal.aborted) setError(cause instanceof Error ? cause.message : String(cause));
@@ -423,18 +1265,42 @@ window.__ModuleLoader__.load({
423
1265
  controller.abort();
424
1266
  if (objectUrl !== void 0) URL.revokeObjectURL(objectUrl);
425
1267
  };
426
- }, [attachment?.attachmentId]);
1268
+ }, [attachment?.attachmentId, lang]);
1269
+ const copy = async (e) => {
1270
+ e.stopPropagation();
1271
+ if (!blob) return;
1272
+ const ok = await copyImageBlob(blob);
1273
+ setToast(ok ? t("copiedImage") : t("copyFailed"));
1274
+ setTimeout(() => {
1275
+ setToast(void 0);
1276
+ }, 2e3);
1277
+ };
1278
+ const download = (e) => {
1279
+ e.stopPropagation();
1280
+ if (!url) return;
1281
+ const a = document.createElement("a");
1282
+ a.href = url;
1283
+ a.download = attachment?.name || `dsh-image-${Date.now()}.png`;
1284
+ document.body.appendChild(a);
1285
+ a.click();
1286
+ document.body.removeChild(a);
1287
+ };
1288
+ const openNewTab = (e) => {
1289
+ e.stopPropagation();
1290
+ if (!url) return;
1291
+ window.open(url, "_blank", "noopener,noreferrer");
1292
+ };
427
1293
  if (attachment === void 0) return (0, react_jsx_runtime.jsx)("div", {
428
1294
  className: "dsh-ig-loading",
429
- children: "正在生成图片…"
1295
+ children: t("generating")
430
1296
  });
431
1297
  return (0, react_jsx_runtime.jsxs)("section", {
432
1298
  className: "dsh-ig-result",
433
- "aria-label": "Generated image",
1299
+ "aria-label": t("generatedTitle"),
434
1300
  children: [
435
1301
  (0, react_jsx_runtime.jsx)("div", {
436
1302
  className: "dsh-ig-result-title",
437
- children: "Generated image"
1303
+ children: t("generatedTitle")
438
1304
  }),
439
1305
  error !== void 0 ? (0, react_jsx_runtime.jsx)("div", {
440
1306
  className: "dsh-ig-error",
@@ -442,12 +1308,121 @@ window.__ModuleLoader__.load({
442
1308
  }) : null,
443
1309
  url === void 0 && error === void 0 ? (0, react_jsx_runtime.jsx)("div", {
444
1310
  className: "dsh-ig-loading",
445
- children: "正在加载图片…"
1311
+ children: t("loading")
446
1312
  }) : null,
447
- url !== void 0 ? (0, react_jsx_runtime.jsx)("img", {
448
- className: "dsh-ig-image",
449
- src: url,
450
- alt: attachment.name ?? "Generated image"
1313
+ url !== void 0 ? (0, react_jsx_runtime.jsxs)("div", {
1314
+ className: "dsh-ig-container",
1315
+ children: [(0, react_jsx_runtime.jsx)("img", {
1316
+ className: "dsh-ig-image",
1317
+ src: url,
1318
+ alt: attachment.name ?? "Generated image",
1319
+ onClick: () => {
1320
+ setPreviewOpen(true);
1321
+ }
1322
+ }), (0, react_jsx_runtime.jsxs)("div", {
1323
+ className: "dsh-ig-toolbar",
1324
+ children: [
1325
+ (0, react_jsx_runtime.jsx)("button", {
1326
+ type: "button",
1327
+ className: "dsh-ig-tool-btn",
1328
+ title: t("copyImg"),
1329
+ onClick: (e) => {
1330
+ copy(e);
1331
+ },
1332
+ children: (0, react_jsx_runtime.jsxs)("svg", {
1333
+ width: "15",
1334
+ height: "15",
1335
+ viewBox: "0 0 24 24",
1336
+ fill: "none",
1337
+ stroke: "currentColor",
1338
+ strokeWidth: "2",
1339
+ strokeLinecap: "round",
1340
+ strokeLinejoin: "round",
1341
+ children: [(0, react_jsx_runtime.jsx)("rect", {
1342
+ x: "9",
1343
+ y: "9",
1344
+ width: "13",
1345
+ height: "13",
1346
+ rx: "2",
1347
+ ry: "2"
1348
+ }), (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" })]
1349
+ })
1350
+ }),
1351
+ (0, react_jsx_runtime.jsx)("button", {
1352
+ type: "button",
1353
+ className: "dsh-ig-tool-btn",
1354
+ title: t("download"),
1355
+ onClick: download,
1356
+ children: (0, react_jsx_runtime.jsxs)("svg", {
1357
+ width: "15",
1358
+ height: "15",
1359
+ viewBox: "0 0 24 24",
1360
+ fill: "none",
1361
+ stroke: "currentColor",
1362
+ strokeWidth: "2",
1363
+ strokeLinecap: "round",
1364
+ strokeLinejoin: "round",
1365
+ children: [
1366
+ (0, react_jsx_runtime.jsx)("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
1367
+ (0, react_jsx_runtime.jsx)("polyline", { points: "7 10 12 15 17 10" }),
1368
+ (0, react_jsx_runtime.jsx)("line", {
1369
+ x1: "12",
1370
+ y1: "15",
1371
+ x2: "12",
1372
+ y2: "3"
1373
+ })
1374
+ ]
1375
+ })
1376
+ }),
1377
+ (0, react_jsx_runtime.jsx)("button", {
1378
+ type: "button",
1379
+ className: "dsh-ig-tool-btn",
1380
+ title: t("openNewTab"),
1381
+ onClick: openNewTab,
1382
+ children: (0, react_jsx_runtime.jsxs)("svg", {
1383
+ width: "15",
1384
+ height: "15",
1385
+ viewBox: "0 0 24 24",
1386
+ fill: "none",
1387
+ stroke: "currentColor",
1388
+ strokeWidth: "2",
1389
+ strokeLinecap: "round",
1390
+ strokeLinejoin: "round",
1391
+ children: [
1392
+ (0, react_jsx_runtime.jsx)("path", { d: "M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" }),
1393
+ (0, react_jsx_runtime.jsx)("polyline", { points: "15 3 21 3 21 9" }),
1394
+ (0, react_jsx_runtime.jsx)("line", {
1395
+ x1: "10",
1396
+ y1: "14",
1397
+ x2: "21",
1398
+ y2: "3"
1399
+ })
1400
+ ]
1401
+ })
1402
+ }),
1403
+ toast ? (0, react_jsx_runtime.jsx)("div", {
1404
+ className: "dsh-ig-toast",
1405
+ children: toast
1406
+ }) : null
1407
+ ]
1408
+ })]
1409
+ }) : null,
1410
+ previewOpen && url !== void 0 ? (0, react_jsx_runtime.jsx)("div", {
1411
+ className: "dsh-ig-lightbox-backdrop",
1412
+ onClick: () => {
1413
+ setPreviewOpen(false);
1414
+ },
1415
+ children: (0, react_jsx_runtime.jsx)("div", {
1416
+ className: "dsh-ig-lightbox-img-wrap",
1417
+ onClick: (e) => {
1418
+ e.stopPropagation();
1419
+ },
1420
+ children: (0, react_jsx_runtime.jsx)("img", {
1421
+ className: "dsh-ig-lightbox-img",
1422
+ src: url,
1423
+ alt: attachment.name ?? "Generated image preview"
1424
+ })
1425
+ })
451
1426
  }) : null
452
1427
  ]
453
1428
  });