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/README.en.md +163 -0
- package/README.md +159 -160
- package/README.zh-CN.md +159 -160
- package/lib/client.js +848 -56
- package/lib/client.js.map +1 -1
- package/lib/index.js +6 -2
- package/lib/types/client/gallery-store.d.ts +41 -0
- package/lib/types/client/gallery-view.d.ts +63 -0
- package/lib/types/client/index.d.ts +6 -8
- package/package.json +125 -124
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: "
|
|
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
|
|
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", () =>
|
|
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
|
|
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
|
|
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(
|
|
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": "
|
|
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: "
|
|
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-
|
|
1411
|
+
className: "dsh-ig-lightbox-backdrop",
|
|
593
1412
|
onClick: () => {
|
|
594
1413
|
setPreviewOpen(false);
|
|
595
1414
|
},
|
|
596
|
-
children: (0, react_jsx_runtime.jsx)("
|
|
597
|
-
className: "dsh-ig-
|
|
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];
|