dsh-image-gen 0.1.3 → 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,6 +843,7 @@ 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
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}
@@ -124,11 +853,63 @@ window.__ModuleLoader__.load({
124
853
  .dsh-ig-tool-btn:hover{background:rgba(255,255,255,0.25)}
125
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}
126
855
  .dsh-ig-image{display:block;max-width:100%;max-height:520px;border-radius:12px;background:#f2f3f5;cursor:pointer}
127
- .dsh-ig-modal{position:fixed;inset:0;z-index:99999;background:rgba(0,0,0,0.88);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);display:flex;align-items:center;justify-content:center;padding:20px;box-sizing:border-box;cursor:zoom-out;animation:dsh-ig-fade .15s ease-out}
128
856
  @keyframes dsh-ig-fade{from{opacity:0}to{opacity:1}}
129
- .dsh-ig-modal-img{max-width:92vw;max-height:92vh;object-fit:contain;border-radius:8px;box-shadow:0 24px 60px rgba(0,0,0,0.6);user-select:none;cursor:default}
130
857
  .dsh-ig-error{color:var(--dsw-alias-label-error,#d33);font-size:13px}
131
- .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
+ `;
132
913
  /** Required browser services. */
133
914
  const inject = [
134
915
  "slots",
@@ -137,7 +918,7 @@ window.__ModuleLoader__.load({
137
918
  "settingsScope",
138
919
  "locale"
139
920
  ];
140
- /** Mount the settings card and generated-image card. */
921
+ /** Mount the settings card, generated-image card, and native conversation gallery view. */
141
922
  function apply(ctx) {
142
923
  const { api } = ctx.get("connection");
143
924
  const scope = ctx.settingsScope.bind({ namespace: IMAGE_GENERATION_NAMESPACE });
@@ -163,16 +944,26 @@ window.__ModuleLoader__.load({
163
944
  locale
164
945
  })
165
946
  }, ImageGenerationSettingsCard));
166
- ctx.slots.inject("tool.call.toolview", () => ctx.slots.register({
947
+ ctx.slots.inject("tool.call.toolview", () => register({
167
948
  name: "tool.call.toolview",
168
- key: "generate_image"
949
+ key: "generate_image",
950
+ inject: () => ({ locale })
169
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));
170
961
  }
171
962
  /** Edit provider settings and its write-only API credential. */
172
963
  function ImageGenerationSettingsCard(props) {
173
964
  const [open, setOpen] = (0, react.useState)(false);
174
965
  const [snapshot, setSnapshot] = (0, react.useState)(() => props.scope.getSnapshot());
175
- 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");
176
967
  const [provider, setProvider] = (0, react.useState)("google");
177
968
  const [model, setModel] = (0, react.useState)("");
178
969
  const [baseURL, setBaseURL] = (0, react.useState)("");
@@ -185,7 +976,7 @@ window.__ModuleLoader__.load({
185
976
  }), [props.scope]);
186
977
  (0, react.useEffect)(() => {
187
978
  return props.locale?.subscribe?.(() => {
188
- setLang(props.locale?.getSnapshot?.()?.active ?? "zh");
979
+ setLang(props.locale?.getSnapshot?.()?.active?.startsWith("en") ? "en" : "zh");
189
980
  });
190
981
  }, [props.locale]);
191
982
  const t = (keyName, params) => {
@@ -413,6 +1204,34 @@ window.__ModuleLoader__.load({
413
1204
  const [error, setError] = (0, react.useState)();
414
1205
  const [previewOpen, setPreviewOpen] = (0, react.useState)(false);
415
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]);
416
1235
  (0, react.useEffect)(() => {
417
1236
  if (!previewOpen) return;
418
1237
  const onKeyDown = (e) => {
@@ -433,7 +1252,7 @@ window.__ModuleLoader__.load({
433
1252
  headers: { "content-type": "application/json" },
434
1253
  body: JSON.stringify({ attachment })
435
1254
  }).then(async (response) => {
436
- if (!response.ok) throw new Error(`图片读取失败 (${String(response.status)})`);
1255
+ if (!response.ok) throw new Error(t("loadFailed", { status: String(response.status) }));
437
1256
  const resBlob = await response.blob();
438
1257
  if (controller.signal.aborted) return;
439
1258
  setBlob(resBlob);
@@ -446,12 +1265,12 @@ window.__ModuleLoader__.load({
446
1265
  controller.abort();
447
1266
  if (objectUrl !== void 0) URL.revokeObjectURL(objectUrl);
448
1267
  };
449
- }, [attachment?.attachmentId]);
1268
+ }, [attachment?.attachmentId, lang]);
450
1269
  const copy = async (e) => {
451
1270
  e.stopPropagation();
452
1271
  if (!blob) return;
453
1272
  const ok = await copyImageBlob(blob);
454
- setToast(ok ? "已复制图片" : "复制失败");
1273
+ setToast(ok ? t("copiedImage") : t("copyFailed"));
455
1274
  setTimeout(() => {
456
1275
  setToast(void 0);
457
1276
  }, 2e3);
@@ -473,15 +1292,15 @@ window.__ModuleLoader__.load({
473
1292
  };
474
1293
  if (attachment === void 0) return (0, react_jsx_runtime.jsx)("div", {
475
1294
  className: "dsh-ig-loading",
476
- children: "正在生成图片…"
1295
+ children: t("generating")
477
1296
  });
478
1297
  return (0, react_jsx_runtime.jsxs)("section", {
479
1298
  className: "dsh-ig-result",
480
- "aria-label": "Generated image",
1299
+ "aria-label": t("generatedTitle"),
481
1300
  children: [
482
1301
  (0, react_jsx_runtime.jsx)("div", {
483
1302
  className: "dsh-ig-result-title",
484
- children: "Generated image"
1303
+ children: t("generatedTitle")
485
1304
  }),
486
1305
  error !== void 0 ? (0, react_jsx_runtime.jsx)("div", {
487
1306
  className: "dsh-ig-error",
@@ -489,7 +1308,7 @@ window.__ModuleLoader__.load({
489
1308
  }) : null,
490
1309
  url === void 0 && error === void 0 ? (0, react_jsx_runtime.jsx)("div", {
491
1310
  className: "dsh-ig-loading",
492
- children: "正在加载图片…"
1311
+ children: t("loading")
493
1312
  }) : null,
494
1313
  url !== void 0 ? (0, react_jsx_runtime.jsxs)("div", {
495
1314
  className: "dsh-ig-container",
@@ -506,7 +1325,7 @@ window.__ModuleLoader__.load({
506
1325
  (0, react_jsx_runtime.jsx)("button", {
507
1326
  type: "button",
508
1327
  className: "dsh-ig-tool-btn",
509
- title: "复制图片",
1328
+ title: t("copyImg"),
510
1329
  onClick: (e) => {
511
1330
  copy(e);
512
1331
  },
@@ -532,7 +1351,7 @@ window.__ModuleLoader__.load({
532
1351
  (0, react_jsx_runtime.jsx)("button", {
533
1352
  type: "button",
534
1353
  className: "dsh-ig-tool-btn",
535
- title: "下载图片",
1354
+ title: t("download"),
536
1355
  onClick: download,
537
1356
  children: (0, react_jsx_runtime.jsxs)("svg", {
538
1357
  width: "15",
@@ -558,7 +1377,7 @@ window.__ModuleLoader__.load({
558
1377
  (0, react_jsx_runtime.jsx)("button", {
559
1378
  type: "button",
560
1379
  className: "dsh-ig-tool-btn",
561
- title: "新标签页打开",
1380
+ title: t("openNewTab"),
562
1381
  onClick: openNewTab,
563
1382
  children: (0, react_jsx_runtime.jsxs)("svg", {
564
1383
  width: "15",
@@ -589,52 +1408,25 @@ window.__ModuleLoader__.load({
589
1408
  })]
590
1409
  }) : null,
591
1410
  previewOpen && url !== void 0 ? (0, react_jsx_runtime.jsx)("div", {
592
- className: "dsh-ig-modal",
1411
+ className: "dsh-ig-lightbox-backdrop",
593
1412
  onClick: () => {
594
1413
  setPreviewOpen(false);
595
1414
  },
596
- children: (0, react_jsx_runtime.jsx)("img", {
597
- className: "dsh-ig-modal-img",
598
- src: url,
599
- alt: attachment.name ?? "Generated image preview",
1415
+ children: (0, react_jsx_runtime.jsx)("div", {
1416
+ className: "dsh-ig-lightbox-img-wrap",
600
1417
  onClick: (e) => {
601
1418
  e.stopPropagation();
602
- }
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
+ })
603
1425
  })
604
1426
  }) : null
605
1427
  ]
606
1428
  });
607
1429
  }
608
- async function copyImageBlob(blob) {
609
- try {
610
- if (blob.type === "image/png") {
611
- await navigator.clipboard.write([new ClipboardItem({ "image/png": blob })]);
612
- return true;
613
- }
614
- const img = new Image();
615
- const url = URL.createObjectURL(blob);
616
- await new Promise((resolve, reject) => {
617
- img.onload = resolve;
618
- img.onerror = reject;
619
- img.src = url;
620
- });
621
- const canvas = document.createElement("canvas");
622
- canvas.width = img.naturalWidth;
623
- canvas.height = img.naturalHeight;
624
- const ctx = canvas.getContext("2d");
625
- if (!ctx) throw new Error("Canvas unavailable");
626
- ctx.drawImage(img, 0, 0);
627
- URL.revokeObjectURL(url);
628
- const pngBlob = await new Promise((res) => {
629
- canvas.toBlob(res, "image/png");
630
- });
631
- if (!pngBlob) throw new Error("Blob conversion failed");
632
- await navigator.clipboard.write([new ClipboardItem({ "image/png": pngBlob })]);
633
- return true;
634
- } catch (_err) {
635
- return false;
636
- }
637
- }
638
1430
  function modelOf(provider, value) {
639
1431
  const stored = provider === "google" ? value?.googleModel : provider === "openai" ? value?.openaiModel : value?.seedreamModel;
640
1432
  return typeof stored === "string" && stored.length > 0 ? stored : DEFAULT_MODELS[provider];