dsh-image-gen 0.1.1 → 0.1.3

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
@@ -34,24 +34,114 @@ window.__ModuleLoader__.load({
34
34
  openai: "OPENAI_API_KEY",
35
35
  seedream: "ARK_API_KEY"
36
36
  };
37
- const PROVIDER_LABEL = {
38
- google: "Google Gemini",
39
- openai: "OpenAI / 中转站",
40
- seedream: "字节 Seedream"
37
+ const DICT = {
38
+ zh: {
39
+ title: "图像生成",
40
+ description: "选择厂商并配置生图模型。",
41
+ provider: "Provider",
42
+ providerGoogle: "Google Gemini",
43
+ providerOpenAI: "OpenAI / 中转站",
44
+ providerSeedream: "字节 Seedream",
45
+ apiKeyLabel: "{provider} API Key",
46
+ apiKeyPlaceholder: "留空即可保留已配置的 Key",
47
+ apiKeyHint: "安全保存为 {key};页面不会读回明文。",
48
+ endpoint: "接口地址",
49
+ reset: "重置",
50
+ resetTitle: "重置为默认官方地址",
51
+ endpointHintGoogle: "Google 官方地址或反代端点(全路径)。",
52
+ endpointHintOpenAI: "中转站请填其 OpenAI 兼容的 /v1 地址。",
53
+ endpointHintSeedream: "火山方舟兼容的 /api/v3 地址。",
54
+ model: "模型",
55
+ saving: "保存中…",
56
+ save: "保存",
57
+ saved: "已保存",
58
+ checkingKey: "正在检查 API Key…",
59
+ keyConfigured: "已配置 API Key",
60
+ keyNotConfigured: "尚未配置 API Key",
61
+ generating: "正在生成图片…",
62
+ loading: "正在加载图片…",
63
+ loadFailed: "图片读取失败 ({status})",
64
+ generatedTitle: "Generated image"
65
+ },
66
+ en: {
67
+ title: "Image Generation",
68
+ description: "Select provider and configure image generation models.",
69
+ provider: "Provider",
70
+ providerGoogle: "Google Gemini",
71
+ providerOpenAI: "OpenAI / Relay",
72
+ providerSeedream: "ByteDance Seedream",
73
+ apiKeyLabel: "{provider} API Key",
74
+ apiKeyPlaceholder: "Leave empty to keep configured key",
75
+ apiKeyHint: "Securely saved as {key}; never read back in plaintext.",
76
+ endpoint: "Endpoint / Base URL",
77
+ reset: "Reset",
78
+ resetTitle: "Reset to official default URL",
79
+ endpointHintGoogle: "Official Google endpoint or reverse proxy (full path).",
80
+ endpointHintOpenAI: "OpenAI-compatible /v1 base URL for relays.",
81
+ endpointHintSeedream: "Volcengine Ark compatible /api/v3 base URL.",
82
+ model: "Model",
83
+ saving: "Saving…",
84
+ save: "Save",
85
+ saved: "Saved",
86
+ checkingKey: "Checking API Key…",
87
+ keyConfigured: "API Key configured",
88
+ keyNotConfigured: "API Key not configured",
89
+ generating: "Generating image…",
90
+ loading: "Loading image…",
91
+ loadFailed: "Failed to load image ({status})",
92
+ generatedTitle: "Generated image"
93
+ }
41
94
  };
42
95
  const STYLE = `
43
- .dsh-ig-card{list-style:none;border:1px solid var(--border-color,#e5e7eb);border-radius:14px;background:var(--surface-color,#fff);overflow:hidden}.dsh-ig-head{width:100%;padding:18px 20px;border:0;background:transparent;text-align:left;cursor:pointer;display:flex;justify-content:space-between;gap:16px;color:inherit}.dsh-ig-title{display:block;font-size:16px;font-weight:650}.dsh-ig-desc{display:block;margin-top:5px;color:#7b818b;font-size:14px}.dsh-ig-body{padding:0 20px 20px;border-top:1px solid var(--border-color,#eee)}.dsh-ig-field{display:grid;gap:7px;margin-top:18px}.dsh-ig-label{font-size:14px;font-weight:600}.dsh-ig-input{box-sizing:border-box;width:100%;padding:10px 12px;border:1px solid #d7dbe0;border-radius:9px;background:transparent;color:inherit}.dsh-ig-input-group{display:flex;gap:8px;align-items:center}.dsh-ig-btn-reset{padding:9px 14px;border:1px solid #d7dbe0;border-radius:9px;background:var(--surface-color,#f9fafb);color:inherit;font-size:13px;cursor:pointer;white-space:nowrap;transition:background .15s}.dsh-ig-btn-reset:hover{background:#edf0f3}.dsh-ig-hint,.dsh-ig-status{margin:0;color:#7b818b;font-size:12px}.dsh-ig-actions{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-top:16px}.dsh-ig-save{border:0;border-radius:9px;padding:9px 16px;background:#4c78ff;color:#fff;cursor:pointer}.dsh-ig-save:disabled{opacity:.55;cursor:default}.dsh-ig-result{display:grid;gap:10px;max-width:520px}.dsh-ig-result-title{font-size:14px;font-weight:600}.dsh-ig-image{display:block;max-width:100%;max-height:520px;border-radius:14px;background:#f2f3f5}.dsh-ig-error{color:#d33;font-size:13px}.dsh-ig-loading{color:#7b818b;font-size:13px}`;
96
+ .dsh-ig-card{list-style:none;border:1px solid var(--dsw-alias-border-l2,#e5e7eb);border-radius:12px;background:var(--dsw-alias-bg-layer-3,#fff);transition:border-color .16s,background .16s;overflow:hidden}
97
+ .dsh-ig-card:hover{border-color:var(--dsw-alias-label-dimmed,#9ca3af)}
98
+ .dsh-ig-card-open{background:var(--dsw-alias-bg-layer-2,#fff);border-color:var(--dsw-alias-label-dimmed,#9ca3af)}
99
+ .dsh-ig-head{width:100%;appearance:none;border:0;background:none;font:inherit;color:inherit;text-align:left;cursor:pointer;display:flex;align-items:center;justify-content:space-between;gap:12px;padding:14px 16px;border-radius:12px}
100
+ .dsh-ig-head:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#4c78ff);outline-offset:-2px}
101
+ .dsh-ig-head-text{flex:1;min-width:0;display:flex;flex-direction:column;gap:4px}
102
+ .dsh-ig-title{display:block;font-size:15px;font-weight:600;line-height:1.4;color:var(--dsw-alias-label-primary,inherit)}
103
+ .dsh-ig-desc{display:block;font-size:13px;line-height:1.5;color:var(--dsw-alias-label-tertiary,#7b818b)}
104
+ .dsh-ig-chevron{flex:none;color:var(--dsw-alias-label-tertiary,#7b818b);transition:transform .16s;display:inline-flex;align-items:center}
105
+ .dsh-ig-chevron-open{transform:rotate(180deg)}
106
+ .dsh-ig-body{border-top:1px solid var(--dsw-alias-border-l2,#eee);padding:0 16px 16px}
107
+ .dsh-ig-field{display:grid;gap:6px;margin-top:14px}
108
+ .dsh-ig-label{font-size:13px;font-weight:500;color:var(--dsw-alias-label-primary,inherit)}
109
+ .dsh-ig-input{box-sizing:border-box;width:100%;padding:8px 12px;font-size:13px;border:1px solid var(--dsw-alias-border-l2,#d7dbe0);border-radius:8px;background:var(--dsw-alias-bg-layer-3,transparent);color:inherit;outline:none;transition:border-color .15s}
110
+ .dsh-ig-input:focus{border-color:var(--dsw-alias-brand-primary,#4c78ff)}
111
+ .dsh-ig-input-group{display:flex;gap:8px;align-items:center}
112
+ .dsh-ig-btn-reset{appearance:none;border:1px solid var(--dsw-alias-border-l2,#d7dbe0);border-radius:8px;padding:7px 12px;background:var(--dsw-alias-bg-layer-3,#f9fafb);color:var(--dsw-alias-label-secondary,inherit);font:inherit;font-size:13px;cursor:pointer;white-space:nowrap;transition:background .15s,border-color .15s}
113
+ .dsh-ig-btn-reset:hover{background:var(--dsw-alias-bg-layer-2,#edf0f3);border-color:var(--dsw-alias-label-dimmed,#9ca3af)}
114
+ .dsh-ig-hint,.dsh-ig-status{margin:0;color:var(--dsw-alias-label-tertiary,#7b818b);font-size:12px;line-height:1.4}
115
+ .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
+ .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
+ .dsh-ig-save:disabled{opacity:.4;cursor:default}
118
+ .dsh-ig-result{display:grid;gap:10px;max-width:520px}
119
+ .dsh-ig-result-title{font-size:14px;font-weight:600}
120
+ .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}
121
+ .dsh-ig-container:hover .dsh-ig-toolbar{opacity:1;pointer-events:auto}
122
+ .dsh-ig-toolbar{position:absolute;top:8px;left:8px;display:flex;align-items:center;gap:5px;padding:3px 5px;border-radius:8px;background:rgba(0,0,0,0.65);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);opacity:0;pointer-events:none;transition:opacity .18s ease;z-index:10;line-height:1}
123
+ .dsh-ig-tool-btn{appearance:none;border:0;background:transparent;color:#fff;padding:5px;border-radius:6px;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;transition:background .15s}
124
+ .dsh-ig-tool-btn:hover{background:rgba(255,255,255,0.25)}
125
+ .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
+ .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
+ @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
+ .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}`;
44
132
  /** Required browser services. */
45
133
  const inject = [
46
134
  "slots",
47
135
  "connection",
48
136
  "remote",
49
- "settingsScope"
137
+ "settingsScope",
138
+ "locale"
50
139
  ];
51
140
  /** Mount the settings card and generated-image card. */
52
141
  function apply(ctx) {
53
142
  const { api } = ctx.get("connection");
54
143
  const scope = ctx.settingsScope.bind({ namespace: IMAGE_GENERATION_NAMESPACE });
144
+ const locale = ctx.get("locale");
55
145
  ctx.effect(() => {
56
146
  const style = document.createElement("style");
57
147
  style.dataset.plugin = "dsh-image-gen";
@@ -66,9 +156,11 @@ window.__ModuleLoader__.load({
66
156
  name: "settings.plugin.item",
67
157
  key: IMAGE_GENERATION_NAMESPACE,
68
158
  id: IMAGE_GENERATION_NAMESPACE,
159
+ order: 100,
69
160
  inject: () => ({
70
161
  scope,
71
- credentials: api.credentials
162
+ credentials: api.credentials,
163
+ locale
72
164
  })
73
165
  }, ImageGenerationSettingsCard));
74
166
  ctx.slots.inject("tool.call.toolview", () => ctx.slots.register({
@@ -80,6 +172,7 @@ window.__ModuleLoader__.load({
80
172
  function ImageGenerationSettingsCard(props) {
81
173
  const [open, setOpen] = (0, react.useState)(false);
82
174
  const [snapshot, setSnapshot] = (0, react.useState)(() => props.scope.getSnapshot());
175
+ const [lang, setLang] = (0, react.useState)(() => props.locale?.getSnapshot?.()?.active ?? "zh");
83
176
  const [provider, setProvider] = (0, react.useState)("google");
84
177
  const [model, setModel] = (0, react.useState)("");
85
178
  const [baseURL, setBaseURL] = (0, react.useState)("");
@@ -90,6 +183,21 @@ window.__ModuleLoader__.load({
90
183
  (0, react.useEffect)(() => props.scope.subscribe(() => {
91
184
  setSnapshot(props.scope.getSnapshot());
92
185
  }), [props.scope]);
186
+ (0, react.useEffect)(() => {
187
+ return props.locale?.subscribe?.(() => {
188
+ setLang(props.locale?.getSnapshot?.()?.active ?? "zh");
189
+ });
190
+ }, [props.locale]);
191
+ const t = (keyName, params) => {
192
+ let text = (lang === "en" ? DICT.en : DICT.zh)[keyName] || DICT.zh[keyName] || keyName;
193
+ if (params) for (const [k, v] of Object.entries(params)) text = text.replace(`{${k}}`, v);
194
+ return text;
195
+ };
196
+ const providerLabels = {
197
+ google: t("providerGoogle"),
198
+ openai: t("providerOpenAI"),
199
+ seedream: t("providerSeedream")
200
+ };
93
201
  (0, react.useEffect)(() => {
94
202
  const value = snapshot.value;
95
203
  const next = value?.provider ?? "google";
@@ -125,16 +233,16 @@ window.__ModuleLoader__.load({
125
233
  setKey("");
126
234
  setConfigured(true);
127
235
  }
128
- setMessage("已保存");
236
+ setMessage(t("saved"));
129
237
  } catch (cause) {
130
238
  setMessage(cause instanceof Error ? cause.message : String(cause));
131
239
  } finally {
132
240
  setSaving(false);
133
241
  }
134
242
  };
135
- const keyStatus = configured === void 0 ? "正在检查 API Key…" : configured ? "已配置 API Key" : "尚未配置 API Key";
243
+ const keyStatus = configured === void 0 ? t("checkingKey") : configured ? t("keyConfigured") : t("keyNotConfigured");
136
244
  return (0, react_jsx_runtime.jsxs)("li", {
137
- className: "dsh-ig-card",
245
+ className: `dsh-ig-card ${open ? "dsh-ig-card-open" : ""}`,
138
246
  children: [(0, react_jsx_runtime.jsxs)("button", {
139
247
  type: "button",
140
248
  className: "dsh-ig-head",
@@ -142,15 +250,29 @@ window.__ModuleLoader__.load({
142
250
  onClick: () => {
143
251
  setOpen((value) => !value);
144
252
  },
145
- children: [(0, react_jsx_runtime.jsxs)("span", { children: [(0, react_jsx_runtime.jsx)("span", {
146
- className: "dsh-ig-title",
147
- children: "图像生成"
253
+ children: [(0, react_jsx_runtime.jsxs)("span", {
254
+ className: "dsh-ig-head-text",
255
+ children: [(0, react_jsx_runtime.jsx)("span", {
256
+ className: "dsh-ig-title",
257
+ children: t("title")
258
+ }), (0, react_jsx_runtime.jsx)("span", {
259
+ className: "dsh-ig-desc",
260
+ children: t("description")
261
+ })]
148
262
  }), (0, react_jsx_runtime.jsx)("span", {
149
- className: "dsh-ig-desc",
150
- children: "选择厂商并配置生图模型。"
151
- })] }), (0, react_jsx_runtime.jsx)("span", {
263
+ className: `dsh-ig-chevron ${open ? "dsh-ig-chevron-open" : ""}`,
152
264
  "aria-hidden": "true",
153
- children: open ? "⌃" : "⌄"
265
+ children: (0, react_jsx_runtime.jsx)("svg", {
266
+ width: "14",
267
+ height: "14",
268
+ viewBox: "0 0 16 16",
269
+ fill: "none",
270
+ stroke: "currentColor",
271
+ strokeWidth: "2",
272
+ strokeLinecap: "round",
273
+ strokeLinejoin: "round",
274
+ children: (0, react_jsx_runtime.jsx)("path", { d: "M4 6l4 4 4-4" })
275
+ })
154
276
  })]
155
277
  }), open ? (0, react_jsx_runtime.jsxs)("form", {
156
278
  className: "dsh-ig-body",
@@ -163,7 +285,7 @@ window.__ModuleLoader__.load({
163
285
  children: [
164
286
  (0, react_jsx_runtime.jsx)("span", {
165
287
  className: "dsh-ig-label",
166
- children: "Provider"
288
+ children: t("provider")
167
289
  }),
168
290
  (0, react_jsx_runtime.jsxs)("select", {
169
291
  className: "dsh-ig-input",
@@ -178,30 +300,30 @@ window.__ModuleLoader__.load({
178
300
  children: [
179
301
  (0, react_jsx_runtime.jsx)("option", {
180
302
  value: "google",
181
- children: "Google Gemini"
303
+ children: t("providerGoogle")
182
304
  }),
183
305
  (0, react_jsx_runtime.jsx)("option", {
184
306
  value: "openai",
185
- children: "OpenAI / 中转站"
307
+ children: t("providerOpenAI")
186
308
  }),
187
309
  (0, react_jsx_runtime.jsx)("option", {
188
310
  value: "seedream",
189
- children: "字节 Seedream"
311
+ children: t("providerSeedream")
190
312
  })
191
313
  ]
192
314
  }),
193
315
  (0, react_jsx_runtime.jsx)("span", {
194
316
  className: "dsh-ig-hint",
195
- children: PROVIDER_LABEL[provider]
317
+ children: providerLabels[provider]
196
318
  })
197
319
  ]
198
320
  }),
199
321
  (0, react_jsx_runtime.jsxs)("label", {
200
322
  className: "dsh-ig-field",
201
323
  children: [
202
- (0, react_jsx_runtime.jsxs)("span", {
324
+ (0, react_jsx_runtime.jsx)("span", {
203
325
  className: "dsh-ig-label",
204
- children: [PROVIDER_LABEL[provider], " API Key"]
326
+ children: t("apiKeyLabel", { provider: providerLabels[provider] })
205
327
  }),
206
328
  (0, react_jsx_runtime.jsx)("input", {
207
329
  className: "dsh-ig-input",
@@ -211,15 +333,11 @@ window.__ModuleLoader__.load({
211
333
  onChange: (event) => {
212
334
  setKey(event.target.value);
213
335
  },
214
- placeholder: configured ? "留空即可保留已配置的 Key" : ""
336
+ placeholder: configured ? t("apiKeyPlaceholder") : ""
215
337
  }),
216
- (0, react_jsx_runtime.jsxs)("span", {
338
+ (0, react_jsx_runtime.jsx)("span", {
217
339
  className: "dsh-ig-hint",
218
- children: [
219
- "安全保存为 ",
220
- KEY_REF[provider],
221
- ";页面不会读回明文。"
222
- ]
340
+ children: t("apiKeyHint", { key: KEY_REF[provider] })
223
341
  })
224
342
  ]
225
343
  }),
@@ -228,7 +346,7 @@ window.__ModuleLoader__.load({
228
346
  children: [
229
347
  (0, react_jsx_runtime.jsx)("span", {
230
348
  className: "dsh-ig-label",
231
- children: "接口地址"
349
+ children: t("endpoint")
232
350
  }),
233
351
  (0, react_jsx_runtime.jsxs)("div", {
234
352
  className: "dsh-ig-input-group",
@@ -243,16 +361,16 @@ window.__ModuleLoader__.load({
243
361
  }), (0, react_jsx_runtime.jsx)("button", {
244
362
  type: "button",
245
363
  className: "dsh-ig-btn-reset",
246
- title: "重置为默认官方地址",
364
+ title: t("resetTitle"),
247
365
  onClick: () => {
248
366
  setBaseURL(DEFAULT_BASE_URLS[provider]);
249
367
  },
250
- children: "重置"
368
+ children: t("reset")
251
369
  })]
252
370
  }),
253
371
  (0, react_jsx_runtime.jsx)("span", {
254
372
  className: "dsh-ig-hint",
255
- children: provider === "google" ? "Google 官方地址或反代端点(全路径)。" : provider === "openai" ? "中转站请填其 OpenAI 兼容的 /v1 地址。" : "火山方舟兼容的 /api/v3 地址。"
373
+ children: provider === "google" ? t("endpointHintGoogle") : provider === "openai" ? t("endpointHintOpenAI") : t("endpointHintSeedream")
256
374
  })
257
375
  ]
258
376
  }),
@@ -260,7 +378,7 @@ window.__ModuleLoader__.load({
260
378
  className: "dsh-ig-field",
261
379
  children: [(0, react_jsx_runtime.jsx)("span", {
262
380
  className: "dsh-ig-label",
263
- children: "模型"
381
+ children: t("model")
264
382
  }), (0, react_jsx_runtime.jsx)("input", {
265
383
  className: "dsh-ig-input",
266
384
  value: model,
@@ -280,7 +398,7 @@ window.__ModuleLoader__.load({
280
398
  className: "dsh-ig-save",
281
399
  type: "submit",
282
400
  disabled: saving || !snapshot.writable,
283
- children: saving ? "保存中…" : "保存"
401
+ children: saving ? t("saving") : t("save")
284
402
  })]
285
403
  })
286
404
  ]
@@ -291,7 +409,20 @@ window.__ModuleLoader__.load({
291
409
  function GeneratedImageCard(props) {
292
410
  const attachment = imageRef(props.block);
293
411
  const [url, setUrl] = (0, react.useState)();
412
+ const [blob, setBlob] = (0, react.useState)();
294
413
  const [error, setError] = (0, react.useState)();
414
+ const [previewOpen, setPreviewOpen] = (0, react.useState)(false);
415
+ const [toast, setToast] = (0, react.useState)();
416
+ (0, react.useEffect)(() => {
417
+ if (!previewOpen) return;
418
+ const onKeyDown = (e) => {
419
+ if (e.key === "Escape") setPreviewOpen(false);
420
+ };
421
+ window.addEventListener("keydown", onKeyDown);
422
+ return () => {
423
+ window.removeEventListener("keydown", onKeyDown);
424
+ };
425
+ }, [previewOpen]);
295
426
  (0, react.useEffect)(() => {
296
427
  if (attachment === void 0) return;
297
428
  const controller = new AbortController();
@@ -303,9 +434,10 @@ window.__ModuleLoader__.load({
303
434
  body: JSON.stringify({ attachment })
304
435
  }).then(async (response) => {
305
436
  if (!response.ok) throw new Error(`图片读取失败 (${String(response.status)})`);
306
- const blob = await response.blob();
437
+ const resBlob = await response.blob();
307
438
  if (controller.signal.aborted) return;
308
- objectUrl = URL.createObjectURL(blob);
439
+ setBlob(resBlob);
440
+ objectUrl = URL.createObjectURL(resBlob);
309
441
  setUrl(objectUrl);
310
442
  }).catch((cause) => {
311
443
  if (!controller.signal.aborted) setError(cause instanceof Error ? cause.message : String(cause));
@@ -315,6 +447,30 @@ window.__ModuleLoader__.load({
315
447
  if (objectUrl !== void 0) URL.revokeObjectURL(objectUrl);
316
448
  };
317
449
  }, [attachment?.attachmentId]);
450
+ const copy = async (e) => {
451
+ e.stopPropagation();
452
+ if (!blob) return;
453
+ const ok = await copyImageBlob(blob);
454
+ setToast(ok ? "已复制图片" : "复制失败");
455
+ setTimeout(() => {
456
+ setToast(void 0);
457
+ }, 2e3);
458
+ };
459
+ const download = (e) => {
460
+ e.stopPropagation();
461
+ if (!url) return;
462
+ const a = document.createElement("a");
463
+ a.href = url;
464
+ a.download = attachment?.name || `dsh-image-${Date.now()}.png`;
465
+ document.body.appendChild(a);
466
+ a.click();
467
+ document.body.removeChild(a);
468
+ };
469
+ const openNewTab = (e) => {
470
+ e.stopPropagation();
471
+ if (!url) return;
472
+ window.open(url, "_blank", "noopener,noreferrer");
473
+ };
318
474
  if (attachment === void 0) return (0, react_jsx_runtime.jsx)("div", {
319
475
  className: "dsh-ig-loading",
320
476
  children: "正在生成图片…"
@@ -335,14 +491,150 @@ window.__ModuleLoader__.load({
335
491
  className: "dsh-ig-loading",
336
492
  children: "正在加载图片…"
337
493
  }) : null,
338
- url !== void 0 ? (0, react_jsx_runtime.jsx)("img", {
339
- className: "dsh-ig-image",
340
- src: url,
341
- alt: attachment.name ?? "Generated image"
494
+ url !== void 0 ? (0, react_jsx_runtime.jsxs)("div", {
495
+ className: "dsh-ig-container",
496
+ children: [(0, react_jsx_runtime.jsx)("img", {
497
+ className: "dsh-ig-image",
498
+ src: url,
499
+ alt: attachment.name ?? "Generated image",
500
+ onClick: () => {
501
+ setPreviewOpen(true);
502
+ }
503
+ }), (0, react_jsx_runtime.jsxs)("div", {
504
+ className: "dsh-ig-toolbar",
505
+ children: [
506
+ (0, react_jsx_runtime.jsx)("button", {
507
+ type: "button",
508
+ className: "dsh-ig-tool-btn",
509
+ title: "复制图片",
510
+ onClick: (e) => {
511
+ copy(e);
512
+ },
513
+ children: (0, react_jsx_runtime.jsxs)("svg", {
514
+ width: "15",
515
+ height: "15",
516
+ viewBox: "0 0 24 24",
517
+ fill: "none",
518
+ stroke: "currentColor",
519
+ strokeWidth: "2",
520
+ strokeLinecap: "round",
521
+ strokeLinejoin: "round",
522
+ children: [(0, react_jsx_runtime.jsx)("rect", {
523
+ x: "9",
524
+ y: "9",
525
+ width: "13",
526
+ height: "13",
527
+ rx: "2",
528
+ ry: "2"
529
+ }), (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" })]
530
+ })
531
+ }),
532
+ (0, react_jsx_runtime.jsx)("button", {
533
+ type: "button",
534
+ className: "dsh-ig-tool-btn",
535
+ title: "下载图片",
536
+ onClick: download,
537
+ children: (0, react_jsx_runtime.jsxs)("svg", {
538
+ width: "15",
539
+ height: "15",
540
+ viewBox: "0 0 24 24",
541
+ fill: "none",
542
+ stroke: "currentColor",
543
+ strokeWidth: "2",
544
+ strokeLinecap: "round",
545
+ strokeLinejoin: "round",
546
+ children: [
547
+ (0, react_jsx_runtime.jsx)("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
548
+ (0, react_jsx_runtime.jsx)("polyline", { points: "7 10 12 15 17 10" }),
549
+ (0, react_jsx_runtime.jsx)("line", {
550
+ x1: "12",
551
+ y1: "15",
552
+ x2: "12",
553
+ y2: "3"
554
+ })
555
+ ]
556
+ })
557
+ }),
558
+ (0, react_jsx_runtime.jsx)("button", {
559
+ type: "button",
560
+ className: "dsh-ig-tool-btn",
561
+ title: "新标签页打开",
562
+ onClick: openNewTab,
563
+ children: (0, react_jsx_runtime.jsxs)("svg", {
564
+ width: "15",
565
+ height: "15",
566
+ viewBox: "0 0 24 24",
567
+ fill: "none",
568
+ stroke: "currentColor",
569
+ strokeWidth: "2",
570
+ strokeLinecap: "round",
571
+ strokeLinejoin: "round",
572
+ children: [
573
+ (0, react_jsx_runtime.jsx)("path", { d: "M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" }),
574
+ (0, react_jsx_runtime.jsx)("polyline", { points: "15 3 21 3 21 9" }),
575
+ (0, react_jsx_runtime.jsx)("line", {
576
+ x1: "10",
577
+ y1: "14",
578
+ x2: "21",
579
+ y2: "3"
580
+ })
581
+ ]
582
+ })
583
+ }),
584
+ toast ? (0, react_jsx_runtime.jsx)("div", {
585
+ className: "dsh-ig-toast",
586
+ children: toast
587
+ }) : null
588
+ ]
589
+ })]
590
+ }) : null,
591
+ previewOpen && url !== void 0 ? (0, react_jsx_runtime.jsx)("div", {
592
+ className: "dsh-ig-modal",
593
+ onClick: () => {
594
+ setPreviewOpen(false);
595
+ },
596
+ children: (0, react_jsx_runtime.jsx)("img", {
597
+ className: "dsh-ig-modal-img",
598
+ src: url,
599
+ alt: attachment.name ?? "Generated image preview",
600
+ onClick: (e) => {
601
+ e.stopPropagation();
602
+ }
603
+ })
342
604
  }) : null
343
605
  ]
344
606
  });
345
607
  }
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
+ }
346
638
  function modelOf(provider, value) {
347
639
  const stored = provider === "google" ? value?.googleModel : provider === "openai" ? value?.openaiModel : value?.seedreamModel;
348
640
  return typeof stored === "string" && stored.length > 0 ? stored : DEFAULT_MODELS[provider];