@dsh-plus/image-studio 0.1.0 → 0.1.2

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
@@ -4,121 +4,3649 @@ window.__ModuleLoader__.load({
4
4
  var module = { exports: {} };
5
5
  var exports = module.exports;
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let react = require("react");
8
+ let react_jsx_runtime = require("react/jsx-runtime");
9
+ let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
10
+ //#region ../../node_modules/.pnpm/lucide-react@1.38.0_react@18.2.0/node_modules/lucide-react/dist/esm/shared/src/utils/mergeClasses.mjs
11
+ /**
12
+ * @license lucide-react v1.38.0 - ISC
13
+ *
14
+ * This source code is licensed under the ISC license.
15
+ * See the LICENSE file in the root directory of this source tree.
16
+ */
17
+ const mergeClasses = (...classes) => classes.filter((className, index, array) => {
18
+ return Boolean(className) && className.trim() !== "" && array.indexOf(className) === index;
19
+ }).join(" ").trim();
20
+ //#endregion
21
+ //#region ../../node_modules/.pnpm/lucide-react@1.38.0_react@18.2.0/node_modules/lucide-react/dist/esm/shared/src/utils/toKebabCase.mjs
22
+ /**
23
+ * @license lucide-react v1.38.0 - ISC
24
+ *
25
+ * This source code is licensed under the ISC license.
26
+ * See the LICENSE file in the root directory of this source tree.
27
+ */
28
+ const toKebabCase = (string) => string.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
29
+ //#endregion
30
+ //#region ../../node_modules/.pnpm/lucide-react@1.38.0_react@18.2.0/node_modules/lucide-react/dist/esm/shared/src/utils/toCamelCase.mjs
31
+ /**
32
+ * @license lucide-react v1.38.0 - ISC
33
+ *
34
+ * This source code is licensed under the ISC license.
35
+ * See the LICENSE file in the root directory of this source tree.
36
+ */
37
+ const toCamelCase = (string) => string.replace(/^([A-Z])|[\s-_]+(\w)/g, (match, p1, p2) => p2 ? p2.toUpperCase() : p1.toLowerCase());
38
+ //#endregion
39
+ //#region ../../node_modules/.pnpm/lucide-react@1.38.0_react@18.2.0/node_modules/lucide-react/dist/esm/shared/src/utils/toPascalCase.mjs
40
+ /**
41
+ * @license lucide-react v1.38.0 - ISC
42
+ *
43
+ * This source code is licensed under the ISC license.
44
+ * See the LICENSE file in the root directory of this source tree.
45
+ */
46
+ const toPascalCase = (string) => {
47
+ const camelCase = toCamelCase(string);
48
+ return camelCase.charAt(0).toUpperCase() + camelCase.slice(1);
49
+ };
50
+ //#endregion
51
+ //#region ../../node_modules/.pnpm/lucide-react@1.38.0_react@18.2.0/node_modules/lucide-react/dist/esm/defaultAttributes.mjs
52
+ /**
53
+ * @license lucide-react v1.38.0 - ISC
54
+ *
55
+ * This source code is licensed under the ISC license.
56
+ * See the LICENSE file in the root directory of this source tree.
57
+ */
58
+ var defaultAttributes = {
59
+ xmlns: "http://www.w3.org/2000/svg",
60
+ width: 24,
61
+ height: 24,
62
+ viewBox: "0 0 24 24",
63
+ fill: "none",
64
+ stroke: "currentColor",
65
+ strokeWidth: 2,
66
+ strokeLinecap: "round",
67
+ strokeLinejoin: "round"
68
+ };
69
+ //#endregion
70
+ //#region ../../node_modules/.pnpm/lucide-react@1.38.0_react@18.2.0/node_modules/lucide-react/dist/esm/shared/src/utils/hasA11yProp.mjs
71
+ /**
72
+ * @license lucide-react v1.38.0 - ISC
73
+ *
74
+ * This source code is licensed under the ISC license.
75
+ * See the LICENSE file in the root directory of this source tree.
76
+ */
77
+ const hasA11yProp = (props) => {
78
+ for (const prop in props) if (prop.startsWith("aria-") || prop === "role" || prop === "title") return true;
79
+ return false;
80
+ };
81
+ //#endregion
82
+ //#region ../../node_modules/.pnpm/lucide-react@1.38.0_react@18.2.0/node_modules/lucide-react/dist/esm/context.mjs
83
+ /**
84
+ * @license lucide-react v1.38.0 - ISC
85
+ *
86
+ * This source code is licensed under the ISC license.
87
+ * See the LICENSE file in the root directory of this source tree.
88
+ */
89
+ const LucideContext = (0, react.createContext)({});
90
+ const useLucideContext = () => (0, react.useContext)(LucideContext);
91
+ //#endregion
92
+ //#region ../../node_modules/.pnpm/lucide-react@1.38.0_react@18.2.0/node_modules/lucide-react/dist/esm/Icon.mjs
93
+ /**
94
+ * @license lucide-react v1.38.0 - ISC
95
+ *
96
+ * This source code is licensed under the ISC license.
97
+ * See the LICENSE file in the root directory of this source tree.
98
+ */
99
+ const Icon = (0, react.forwardRef)(({ color, size, strokeWidth, absoluteStrokeWidth, className = "", children, iconNode, ...rest }, ref) => {
100
+ const { size: contextSize = 24, strokeWidth: contextStrokeWidth = 2, absoluteStrokeWidth: contextAbsoluteStrokeWidth = false, color: contextColor = "currentColor", className: contextClass = "" } = useLucideContext() ?? {};
101
+ const calculatedStrokeWidth = absoluteStrokeWidth ?? contextAbsoluteStrokeWidth ? Number(strokeWidth ?? contextStrokeWidth) * 24 / Number(size ?? contextSize) : strokeWidth ?? contextStrokeWidth;
102
+ return (0, react.createElement)("svg", {
103
+ ref,
104
+ ...defaultAttributes,
105
+ width: size ?? contextSize ?? defaultAttributes.width,
106
+ height: size ?? contextSize ?? defaultAttributes.height,
107
+ stroke: color ?? contextColor,
108
+ strokeWidth: calculatedStrokeWidth,
109
+ className: mergeClasses("lucide", contextClass, className),
110
+ ...!children && !hasA11yProp(rest) && { "aria-hidden": "true" },
111
+ ...rest
112
+ }, [...iconNode.map(([tag, attrs]) => (0, react.createElement)(tag, attrs)), ...Array.isArray(children) ? children : [children]]);
113
+ });
114
+ //#endregion
115
+ //#region ../../node_modules/.pnpm/lucide-react@1.38.0_react@18.2.0/node_modules/lucide-react/dist/esm/createLucideIcon.mjs
116
+ /**
117
+ * @license lucide-react v1.38.0 - ISC
118
+ *
119
+ * This source code is licensed under the ISC license.
120
+ * See the LICENSE file in the root directory of this source tree.
121
+ */
122
+ const createLucideIcon = (iconName, iconNode) => {
123
+ const Component = (0, react.forwardRef)(({ className, ...props }, ref) => (0, react.createElement)(Icon, {
124
+ ref,
125
+ iconNode,
126
+ className: mergeClasses(`lucide-${toKebabCase(toPascalCase(iconName))}`, `lucide-${iconName}`, className),
127
+ ...props
128
+ }));
129
+ Component.displayName = toPascalCase(iconName);
130
+ return Component;
131
+ };
132
+ /**
133
+ * @license lucide-react v1.38.0 - ISC
134
+ *
135
+ * This source code is licensed under the ISC license.
136
+ * See the LICENSE file in the root directory of this source tree.
137
+ */
138
+ const Brush = createLucideIcon("brush", [
139
+ ["path", {
140
+ d: "m11 10 3 3",
141
+ key: "fzmg1i"
142
+ }],
143
+ ["path", {
144
+ d: "M6.5 21A3.5 3.5 0 1 0 3 17.5a2.62 2.62 0 0 1-.708 1.792A1 1 0 0 0 3 21z",
145
+ key: "p4q2r7"
146
+ }],
147
+ ["path", {
148
+ d: "M9.969 17.031 21.378 5.624a1 1 0 0 0-3.002-3.002L6.967 14.031",
149
+ key: "wy6l02"
150
+ }]
151
+ ]);
152
+ /**
153
+ * @license lucide-react v1.38.0 - ISC
154
+ *
155
+ * This source code is licensed under the ISC license.
156
+ * See the LICENSE file in the root directory of this source tree.
157
+ */
158
+ const Download = createLucideIcon("download", [
159
+ ["path", {
160
+ d: "M12 15V3",
161
+ key: "m9g1x1"
162
+ }],
163
+ ["path", {
164
+ d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",
165
+ key: "ih7n3h"
166
+ }],
167
+ ["path", {
168
+ d: "m7 10 5 5 5-5",
169
+ key: "brsn70"
170
+ }]
171
+ ]);
172
+ /**
173
+ * @license lucide-react v1.38.0 - ISC
174
+ *
175
+ * This source code is licensed under the ISC license.
176
+ * See the LICENSE file in the root directory of this source tree.
177
+ */
178
+ const Image = createLucideIcon("image", [
179
+ ["rect", {
180
+ width: "18",
181
+ height: "18",
182
+ x: "3",
183
+ y: "3",
184
+ rx: "2",
185
+ ry: "2",
186
+ key: "1m3agn"
187
+ }],
188
+ ["circle", {
189
+ cx: "9",
190
+ cy: "9",
191
+ r: "2",
192
+ key: "af1f0g"
193
+ }],
194
+ ["path", {
195
+ d: "m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21",
196
+ key: "1xmnt7"
197
+ }]
198
+ ]);
199
+ /**
200
+ * @license lucide-react v1.38.0 - ISC
201
+ *
202
+ * This source code is licensed under the ISC license.
203
+ * See the LICENSE file in the root directory of this source tree.
204
+ */
205
+ const Layers = createLucideIcon("layers", [
206
+ ["path", {
207
+ d: "M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83z",
208
+ key: "zw3jo"
209
+ }],
210
+ ["path", {
211
+ d: "M2 12a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 12",
212
+ key: "1wduqc"
213
+ }],
214
+ ["path", {
215
+ d: "M2 17a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 17",
216
+ key: "kqbvx6"
217
+ }]
218
+ ]);
219
+ /**
220
+ * @license lucide-react v1.38.0 - ISC
221
+ *
222
+ * This source code is licensed under the ISC license.
223
+ * See the LICENSE file in the root directory of this source tree.
224
+ */
225
+ const Sparkles = createLucideIcon("sparkles", [
226
+ ["path", {
227
+ d: "M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594z",
228
+ key: "1s2grr"
229
+ }],
230
+ ["path", {
231
+ d: "M20 2v4",
232
+ key: "1rf3ol"
233
+ }],
234
+ ["path", {
235
+ d: "M22 4h-4",
236
+ key: "gwowj6"
237
+ }],
238
+ ["circle", {
239
+ cx: "4",
240
+ cy: "20",
241
+ r: "2",
242
+ key: "6kqj1y"
243
+ }]
244
+ ]);
245
+ //#endregion
246
+ //#region ../shared/src/client/icons.tsx
247
+ function lucideProps(props) {
248
+ return {
249
+ size: props.size ?? 14,
250
+ className: props.className,
251
+ "aria-hidden": true
252
+ };
253
+ }
254
+ /** 单图语义(图像工作室入口/画廊标签)。 */
255
+ function IconImage(props) {
256
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Image, { ...lucideProps(props) });
257
+ }
258
+ /** 文生图(灵感/生成)语义。 */
259
+ function IconSparkles(props) {
260
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Sparkles, { ...lucideProps(props) });
261
+ }
262
+ /** 图生图(画笔编辑)语义。 */
263
+ function IconBrush(props) {
264
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Brush, { ...lucideProps(props) });
265
+ }
266
+ /** 下载语义(画廊条目导出)。 */
267
+ function IconDownload(props) {
268
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Download, { ...lucideProps(props) });
269
+ }
270
+ /** 叠加/源图语义(二次编辑、源图选择)。 */
271
+ function IconLayers(props) {
272
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Layers, { ...lucideProps(props) });
273
+ }
274
+ //#endregion
275
+ //#region ../shared/src/client/card.tsx
276
+ const IDLE_STATUS = {
277
+ kind: "idle",
278
+ text: ""
279
+ };
280
+ function CardChrome(props) {
281
+ const p = props.prefix;
282
+ const badge = props.statusBadge;
283
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
284
+ className: `${p}-card${props.open ? ` ${p}-cardOpen` : ""}`,
285
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
286
+ type: "button",
287
+ className: `${p}-header`,
288
+ "aria-expanded": props.open,
289
+ "aria-label": props.title,
290
+ onClick: () => props.onToggle(!props.open),
291
+ children: [
292
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
293
+ className: `${p}-headText`,
294
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
295
+ className: `${p}-name`,
296
+ children: props.title
297
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
298
+ className: `${p}-description`,
299
+ children: props.description
300
+ })]
301
+ }),
302
+ badge !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
303
+ className: `${p}-statusBadge ${badge.on ? `${p}-statusOn` : `${p}-statusOff`}`,
304
+ children: badge.text
305
+ }) : null,
306
+ props.dirty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
307
+ className: `${p}-pending`,
308
+ children: props.dirtyLabel
309
+ }) : null,
310
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, { className: `${p}-chevron${props.open ? ` ${p}-chevronOpen` : ""}` })
311
+ ]
312
+ }), props.open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
313
+ className: `${p}-body`,
314
+ children: [
315
+ props.readOnlyNotice !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
316
+ className: `${p}-readOnly`,
317
+ role: "status",
318
+ children: props.readOnlyNotice
319
+ }) : null,
320
+ props.children,
321
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
322
+ className: `${p}-footer`,
323
+ children: [props.status !== void 0 && props.status.kind !== "idle" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
324
+ className: `${p}-status${props.status.kind === "error" ? ` ${p}-statusError` : ""}`,
325
+ role: "status",
326
+ children: props.status.text
327
+ }) : null, props.actions.map((action) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
328
+ type: "button",
329
+ className: `${p}-btn ${action.variant === "primary" ? `${p}-btnPrimary` : `${p}-btnGhost`}`,
330
+ disabled: action.disabled === true,
331
+ onClick: action.onClick,
332
+ children: action.label
333
+ }, action.key))]
334
+ })
335
+ ]
336
+ }) : null]
337
+ });
338
+ }
339
+ //#endregion
340
+ //#region ../shared/src/client/fetch.ts
341
+ /**
342
+ * 同源自定义端点的最小 fetch 封装(各插件 client/api.ts 的公共收编版)。
343
+ * 仅同源(credentials: same-origin),错误统一抛 Error(body.error)。
344
+ * @module @dsh-plus/shared/client/fetch
345
+ */
346
+ async function parse(res) {
347
+ const body = await res.json();
348
+ if (!res.ok) throw new Error(body.error ?? `HTTP ${res.status}`);
349
+ return body;
350
+ }
351
+ /** GET 同源 JSON 端点。 */
352
+ async function getJson(route) {
353
+ return parse(await fetch(route, { credentials: "same-origin" }));
354
+ }
355
+ /** POST 同源 JSON 端点(body 可省略)。 */
356
+ async function postJson(route, body) {
357
+ return parse(await fetch(route, {
358
+ method: "POST",
359
+ credentials: "same-origin",
360
+ headers: { "content-type": "application/json" },
361
+ body: body === void 0 ? void 0 : JSON.stringify(body)
362
+ }));
363
+ }
364
+ //#endregion
365
+ //#region ../shared/src/client/fields.tsx
366
+ function TextField(props) {
367
+ const p = props.prefix ?? "dshp";
368
+ const invalid = props.invalid === true;
369
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
370
+ className: `${p}-field`,
371
+ children: [
372
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
373
+ className: `${p}-head`,
374
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
375
+ className: `${p}-label`,
376
+ htmlFor: props.id,
377
+ children: props.label
378
+ }), props.badge !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
379
+ className: `${p}-badge ${props.badge.set ? `${p}-badgeSet` : `${p}-badgeUnset`}`,
380
+ children: props.badge.text
381
+ }) : null]
382
+ }),
383
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
384
+ id: props.id,
385
+ className: `${p}-input${invalid ? ` ${p}-inputInvalid` : ""}`,
386
+ type: props.password === true ? "password" : "text",
387
+ inputMode: props.numeric === true ? "numeric" : void 0,
388
+ autoComplete: props.password === true ? "off" : void 0,
389
+ "aria-invalid": invalid || void 0,
390
+ value: props.value,
391
+ disabled: props.disabled === true,
392
+ onChange: (event) => props.onEdit(event.target.value)
393
+ }),
394
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
395
+ className: invalid ? `${p}-invalid` : `${p}-hint`,
396
+ children: invalid ? props.invalidLabel ?? "" : props.hint ?? ""
397
+ })
398
+ ]
399
+ });
400
+ }
401
+ //#endregion
402
+ //#region ../shared/src/client/scope.ts
403
+ /**
404
+ * remote.settings 直连实现的命名空间 scope(官方配置页 tab 同款模式)。
405
+ * 读:describe 取命名空间视图(脱敏 value/revision + 顶层 writable);
406
+ * 刷新:remote `settings/document-updated`(按命名空间过滤)与
407
+ * `connection/reset`;generation 防旧读覆盖新发布。任何页面 origin
408
+ * (loopback 或 tailnet 信任域名)下行为一致。
409
+ * effect 归属调用方 fiber(label 需含插件名以便诊断)。
410
+ */
411
+ function createSettingsScope(c, ns, label) {
412
+ const remote = c.get("remote");
413
+ let state = {
414
+ status: "loading",
415
+ value: void 0,
416
+ revision: void 0,
417
+ writable: false
418
+ };
419
+ const listeners = /* @__PURE__ */ new Set();
420
+ let generation = 0;
421
+ const publish = (next) => {
422
+ state = next;
423
+ for (const listener of [...listeners]) listener();
424
+ };
425
+ const load = async () => {
426
+ const gen = ++generation;
427
+ let view;
428
+ try {
429
+ const response = await remote.settings.describe();
430
+ if (!response.ok) return;
431
+ view = response.value;
432
+ } catch {
433
+ return;
434
+ }
435
+ if (gen !== generation) return;
436
+ const namespace = view.namespaces.find((candidate) => candidate.ns === ns);
437
+ if (namespace === void 0) {
438
+ publish({
439
+ status: "unavailable",
440
+ value: void 0,
441
+ revision: void 0,
442
+ writable: view.writable
443
+ });
444
+ return;
445
+ }
446
+ publish({
447
+ status: "ready",
448
+ value: namespace.value,
449
+ revision: namespace.revision,
450
+ writable: view.writable
451
+ });
452
+ };
453
+ const refresh = (namespace) => {
454
+ if (namespace !== void 0 && namespace !== ns) return;
455
+ load();
456
+ };
457
+ const disposers = [remote.$on("settings/document-updated", refresh), c.on("connection/reset", () => {
458
+ load();
459
+ })];
460
+ c.effect(() => () => {
461
+ for (const dispose of disposers) dispose();
462
+ }, label);
463
+ load();
464
+ return {
465
+ getSnapshot: () => state,
466
+ subscribe: (listener) => {
467
+ listeners.add(listener);
468
+ return () => {
469
+ listeners.delete(listener);
470
+ };
471
+ },
472
+ load
473
+ };
474
+ }
475
+ /** 以 remote.settings 构造命名空间绑定的 api 面。 */
476
+ function createNamespaceApi(remote, ns) {
477
+ return {
478
+ update: async (patch, expectedRevision) => {
479
+ const response = await remote.update(ns, patch, expectedRevision);
480
+ if (!response.ok) throw new Error(response.error.message ?? "settings update failed");
481
+ },
482
+ describe: async () => {
483
+ const response = await remote.describe();
484
+ if (!response.ok) throw new Error(response.error.message ?? "settings describe failed");
485
+ return response.value;
486
+ }
487
+ };
488
+ }
489
+ //#endregion
490
+ //#region ../shared/src/client/styles.ts
491
+ /**
492
+ * 配置卡片样式工厂(各插件 client 的公共收编版)。
493
+ * 视觉规则与迁移前逐字一致(类名前缀换为各插件自身,DOM 类名不变),
494
+ * 另补移动端与可达性增强(767px 断点 + pointer:coarse,对齐 ui-mobile-fit 约定):
495
+ * - 窄屏 input/textarea 16px(防 iOS 聚焦缩放)、可点目标 ≥44px;
496
+ * - footer 按钮等宽换行、状态文字独占一行;
497
+ * - 展开态 body 底部 sticky 保存栏(长表单滚动中始终可达)。
498
+ * 沿用官方 data-plugin / data-plugin-css 约定(HMR 据此卸载),不覆盖上游选择器。
499
+ * @module @dsh-plus/shared/client/styles
500
+ */
501
+ /** 生成全套卡片 CSS;prefix 为插件类名前缀(如 'dne'),extra 为插件特有规则。 */
502
+ function cardCss(prefix, extra = "") {
503
+ return `
504
+ .${prefix}-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}
505
+ .${prefix}-card:hover{border-color:var(--dsw-alias-label-dimmed)}
506
+ .${prefix}-cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}
507
+ .${prefix}-header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}
508
+ .${prefix}-header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}
509
+ .${prefix}-headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}
510
+ .${prefix}-name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}
511
+ .${prefix}-description{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}
512
+ .${prefix}-chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s;display:inline-flex}
513
+ .${prefix}-chevronOpen{transform:rotate(180deg)}
514
+ .${prefix}-pending{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}
515
+ .${prefix}-statusBadge{white-space:nowrap;border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}
516
+ .${prefix}-statusOn{background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary)}
517
+ .${prefix}-statusOff{color:var(--dsw-alias-label-tertiary)}
518
+ .${prefix}-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}
519
+ .${prefix}-field{flex-direction:column;gap:6px;padding:12px 0;display:flex}
520
+ .${prefix}-field+.${prefix}-field{border-top:1px solid var(--dsw-alias-border-l2)}
521
+ .${prefix}-head{align-items:center;gap:8px;display:flex}
522
+ .${prefix}-label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}
523
+ .${prefix}-badge{white-space:nowrap;border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}
524
+ .${prefix}-badgeSet{background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary)}
525
+ .${prefix}-badgeUnset{color:var(--dsw-alias-label-tertiary)}
526
+ .${prefix}-input{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;transition:border-color .18s,background .18s}
527
+ .${prefix}-input:hover:not(:disabled):not(:focus-visible){border-color:var(--dsw-alias-label-dimmed)}
528
+ .${prefix}-input:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}
529
+ .${prefix}-input:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}
530
+ .${prefix}-inputInvalid{border-color:var(--dsw-alias-label-error)}
531
+ .${prefix}-select{appearance:none;background:var(--dsw-alias-bg-module-platform);border:none;cursor:pointer;background-image:linear-gradient(45deg,transparent 50%,var(--dsw-alias-label-secondary) 50%),linear-gradient(135deg,var(--dsw-alias-label-secondary) 50%,transparent 50%);background-position:calc(100% - 16px) 50%,calc(100% - 11px) 50%;background-size:5px 5px;background-repeat:no-repeat;color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;height:34px;box-sizing:border-box;border-radius:8px;padding:0 30px 0 12px;transition:background-color .18s}
532
+ .${prefix}-select:hover:not(:disabled){background-color:var(--dsw-alias-interactive-bg-hover)}
533
+ .${prefix}-hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}
534
+ .${prefix}-invalid{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}
535
+ .${prefix}-checkRow{align-items:center;gap:10px;display:flex;padding:6px 0}
536
+ .${prefix}-checkRow label{color:var(--dsw-alias-label-primary);font-size:13px;line-height:1.5;cursor:pointer;flex:1;min-width:0}
537
+ /* 自绘圆角扁平开关(官方 UI 无可见原生 checkbox,选择类全走胶囊/菜单) */
538
+ .${prefix}-checkRow input{appearance:none;-webkit-appearance:none;position:relative;box-sizing:border-box;background:var(--dsw-alias-bg-module-platform);border:none;border-radius:999px;width:34px;height:20px;flex:none;cursor:pointer;transition:background .18s;margin:0}
539
+ .${prefix}-checkRow input::after{content:"";position:absolute;top:2px;left:2px;background:var(--dsw-alias-label-primary);border-radius:999px;width:16px;height:16px;transition:left .18s cubic-bezier(.4,0,.2,1)}
540
+ .${prefix}-checkRow input:checked{background:var(--dsw-alias-brand-primary)}
541
+ .${prefix}-checkRow input:checked::after{left:16px;background:var(--dsw-alias-bg-base)}
542
+ .${prefix}-checkRow input:disabled{opacity:.4;cursor:default}
543
+ .${prefix}-checkRow input:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}
544
+ .${prefix}-groupLabel{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600;padding:12px 0 2px;margin:0}
545
+ .${prefix}-readOnly{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}
546
+ .${prefix}-warn{color:var(--dsw-alias-label-error);margin:12px 0 0;font-size:12px;line-height:1.5}
547
+ .${prefix}-footer{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex;flex-wrap:wrap}
548
+ .${prefix}-status{min-width:0;color:var(--dsw-alias-label-secondary);flex:1;margin:0;font-size:12px;line-height:1.5}
549
+ .${prefix}-statusError{color:var(--dsw-alias-label-error)}
550
+ .${prefix}-btn{appearance:none;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5}
551
+ .${prefix}-btnGhost{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}
552
+ .${prefix}-btnGhost:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}
553
+ .${prefix}-btnPrimary{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}
554
+ .${prefix}-btn:disabled{opacity:.4;cursor:default}
555
+ .${prefix}-btn:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
556
+ @media (max-width:767px){
557
+ .${prefix}-body{margin:0 10px}
558
+ .${prefix}-header{padding:12px 12px}
559
+ .${prefix}-input,.${prefix}-select{font-size:16px;height:40px}
560
+ .${prefix}-checkRow{min-height:44px;padding:8px 0}
561
+ .${prefix}-footer{position:sticky;bottom:0;background:var(--dsw-alias-bg-layer-2);margin:0 -10px;padding:10px 10px 6px;border-top:1px solid var(--dsw-alias-border-l2);border-bottom-left-radius:12px;border-bottom-right-radius:12px}
562
+ .${prefix}-status{flex-basis:100%;flex:none}
563
+ .${prefix}-btn{min-height:44px;flex:1;min-width:96px}
564
+ .${prefix}-statusBadge{display:none}
565
+ }
566
+ @media (pointer:coarse){
567
+ .${prefix}-btn{min-height:44px}
568
+ .${prefix}-checkRow{min-height:44px}
569
+ .${prefix}-input,.${prefix}-select{height:40px}
570
+ }
571
+ ${extra}
572
+ `;
573
+ }
574
+ /**
575
+ * 幂等注入样式标签(官方 data-plugin-css 约定);返回新标签,
576
+ * 已存在或环境无 document 时返回 null(调用方 effect 卸载用)。
577
+ */
578
+ function injectCardStyle(pluginId, css) {
579
+ if (typeof document === "undefined") return null;
580
+ const tagId = `${pluginId}/card.css`;
581
+ if (document.querySelector(`style[data-plugin-css=${JSON.stringify(tagId)}]`) !== null) return null;
582
+ const tag = document.createElement("style");
583
+ tag.dataset.plugin = pluginId;
584
+ tag.dataset.pluginCss = tagId;
585
+ tag.textContent = css;
586
+ document.head.appendChild(tag);
587
+ return tag;
588
+ }
589
+ //#endregion
590
+ //#region src/ns.ts
591
+ /**
592
+ * settings 命名空间字面量(纯常量,零依赖,浏览器半可安全引入)。
593
+ * 服务端直接以字面量注册(settings 编译期校验命名空间语法,见 config.ts),
594
+ * 浏览器半将其作为 settings.plugin.item keyed 槽位的 key(配置卡片)。
595
+ * @module image-studio/ns
596
+ */
597
+ const SETTINGS_NS = "dsh-plus-image-studio";
598
+ //#endregion
599
+ //#region src/credentials.ts
600
+ /**
601
+ * 凭据引用名派生与校验:提供商预设 id → IMAGE_STUDIO_PRESET_<ID>。
602
+ * 规则对齐《插件存储规范》:大写下划线、全大写、前缀属主。
603
+ * 纯函数,零依赖。
604
+ * @module image-studio/credentials
605
+ */
606
+ /** 引用名前缀(属主段)。 */
607
+ const REF_PREFIX = "IMAGE_STUDIO_PRESET_";
608
+ /** 预设 id 是否合法(kebab-case,用于引用名派生)。 */
609
+ function isValidPresetId(id) {
610
+ return /^[a-z][a-z0-9-]*$/.test(id) && id.length <= 64;
611
+ }
612
+ /** 预设 id → 凭据引用名(kebab → SNAKE 大写)。 */
613
+ function credentialRefNameOf(presetId) {
614
+ if (!isValidPresetId(presetId)) throw new Error(`非法提供商预设 id:${JSON.stringify(presetId)}`);
615
+ return `${REF_PREFIX}${presetId.replaceAll("-", "_").toUpperCase()}`;
616
+ }
617
+ /** 只读目录(冻结防运行期篡改)。 */
618
+ const PARAM_CATALOG = Object.freeze([
619
+ {
620
+ key: "model",
621
+ kind: "string",
622
+ applies: ["generation", "edit"],
623
+ generation: "all",
624
+ advanced: false,
625
+ description: "图像模型 id;请求基础来自提供商预设,此处仅作展示与 inline 覆盖"
626
+ },
627
+ {
628
+ key: "n",
629
+ kind: "integer",
630
+ min: 1,
631
+ max: 10,
632
+ applies: ["generation", "edit"],
633
+ generation: "current",
634
+ advanced: false,
635
+ description: "生成张数(1-10;部分中转仅支持 1)"
636
+ },
637
+ {
638
+ key: "size",
639
+ kind: "string",
640
+ applies: ["generation", "edit"],
641
+ generation: "current",
642
+ advanced: false,
643
+ description: "尺寸,如 1024x1024 / 1536x1024 / 1024x1536 / auto;宽高需为 16 的倍数、最大边 ≤3840、比例 ≤3:1(依模型限制)"
644
+ },
645
+ {
646
+ key: "quality",
647
+ kind: "enum",
648
+ values: [
649
+ "auto",
650
+ "low",
651
+ "medium",
652
+ "high"
653
+ ],
654
+ applies: ["generation", "edit"],
655
+ generation: "current",
656
+ advanced: false,
657
+ description: "质量档位"
658
+ },
659
+ {
660
+ key: "background",
661
+ kind: "enum",
662
+ values: [
663
+ "auto",
664
+ "transparent",
665
+ "opaque"
666
+ ],
667
+ applies: ["generation", "edit"],
668
+ generation: "current",
669
+ advanced: false,
670
+ description: "输出背景透明行为(对提示词里的场景背景无影响)"
671
+ },
672
+ {
673
+ key: "output_format",
674
+ kind: "enum",
675
+ values: [
676
+ "png",
677
+ "jpeg",
678
+ "webp"
679
+ ],
680
+ applies: ["generation", "edit"],
681
+ generation: "current",
682
+ advanced: false,
683
+ description: "输出格式(部分中转不支持 webp)"
684
+ },
685
+ {
686
+ key: "output_compression",
687
+ kind: "integer",
688
+ min: 0,
689
+ max: 100,
690
+ applies: ["generation", "edit"],
691
+ generation: "current",
692
+ advanced: true,
693
+ description: "压缩率 0-100,仅 jpeg/webp 有效"
694
+ },
695
+ {
696
+ key: "moderation",
697
+ kind: "enum",
698
+ values: ["auto", "low"],
699
+ applies: ["generation", "edit"],
700
+ generation: "current",
701
+ advanced: true,
702
+ description: "内容审核严格度"
703
+ },
704
+ {
705
+ key: "response_format",
706
+ kind: "enum",
707
+ values: ["url", "b64_json"],
708
+ applies: ["generation", "edit"],
709
+ generation: "all",
710
+ advanced: false,
711
+ description: "返回形式;插件两种都会落盘本地化,画廊不受外链过期影响"
712
+ },
713
+ {
714
+ key: "stream",
715
+ kind: "boolean",
716
+ applies: ["generation", "edit"],
717
+ generation: "current",
718
+ advanced: true,
719
+ description: "流式返回(多数中转不支持,默认关闭)"
720
+ },
721
+ {
722
+ key: "partial_images",
723
+ kind: "integer",
724
+ min: 0,
725
+ max: 3,
726
+ applies: ["generation", "edit"],
727
+ generation: "current",
728
+ advanced: true,
729
+ description: "流式中间图数量(依赖 stream,默认关闭)"
730
+ },
731
+ {
732
+ key: "user",
733
+ kind: "string",
734
+ applies: ["generation", "edit"],
735
+ generation: "all",
736
+ advanced: true,
737
+ description: "终端用户标识(业务归因用)"
738
+ },
739
+ {
740
+ key: "input_fidelity",
741
+ kind: "enum",
742
+ values: ["low", "high"],
743
+ applies: ["edit"],
744
+ generation: "current",
745
+ advanced: false,
746
+ description: "图生图输入保真度(high 更保留原图细节,token 消耗增大)"
747
+ },
748
+ {
749
+ key: "style",
750
+ kind: "enum",
751
+ values: ["vivid", "natural"],
752
+ applies: ["generation"],
753
+ generation: "dall-e-3",
754
+ advanced: true,
755
+ description: "dall-e-3 代际风格参数(GPT Image 不需要)"
756
+ }
757
+ ].map((entry) => Object.freeze(entry)));
758
+ /** 按字段名查目录条目。 */
759
+ function paramEntryOf(key) {
760
+ return PARAM_CATALOG.find((entry) => entry.key === key) ?? null;
761
+ }
762
+ /** 官方 API 对指定参数是否本就不发送(由请求基础或输入承载)。 */
763
+ function isHostParam(key) {
764
+ return key === "model";
765
+ }
766
+ //#endregion
767
+ //#region src/params/spec.ts
768
+ /** 归一化失败原因(边界校验用,端点转 invalid-request)。 */
769
+ var ParamValidationError = class extends Error {
770
+ key;
771
+ constructor(key, message) {
772
+ super(`参数 ${key} ${message}`);
773
+ this.name = "ParamValidationError";
774
+ this.key = key;
775
+ }
776
+ };
777
+ /** 校验单个值符合目录条目约束;返回规范值。 */
778
+ function validateParamValue(key, value) {
779
+ const entry = paramEntryOf(key);
780
+ if (entry === null) throw new ParamValidationError(key, "不在参数目录中");
781
+ switch (entry.kind) {
782
+ case "string":
783
+ if (typeof value !== "string" || value.length === 0) throw new ParamValidationError(key, "必须是非空字符串");
784
+ return value;
785
+ case "enum":
786
+ if (typeof value !== "string" || !entry.values?.includes(value)) throw new ParamValidationError(key, `必须是 ${entry.values?.join(" | ") ?? "枚举值"} 之一`);
787
+ return value;
788
+ case "integer":
789
+ if (typeof value !== "number" || !Number.isInteger(value)) throw new ParamValidationError(key, "必须是整数");
790
+ if (entry.min !== void 0 && value < entry.min) throw new ParamValidationError(key, `不能小于 ${entry.min}`);
791
+ if (entry.max !== void 0 && value > entry.max) throw new ParamValidationError(key, `不能大于 ${entry.max}`);
792
+ return value;
793
+ case "boolean":
794
+ if (typeof value !== "boolean") throw new ParamValidationError(key, "必须是布尔值");
795
+ return value;
796
+ }
797
+ }
798
+ /** 校验预设里的参数表(结构合法但可跨端点;应用时再按端点归一化)。 */
799
+ function validateParamSpecs(specs) {
800
+ if (specs === null || typeof specs !== "object" || Array.isArray(specs)) throw new ParamValidationError("(整体)", "必须是对象");
801
+ const out = {};
802
+ for (const [key, raw] of Object.entries(specs)) {
803
+ if (raw === null || typeof raw !== "object") throw new ParamValidationError(key, "条目必须是 { enabled, value? }");
804
+ const spec = raw;
805
+ if (typeof spec.enabled !== "boolean") throw new ParamValidationError(key, "enabled 必须是布尔值");
806
+ if (paramEntryOf(key) === null) throw new ParamValidationError(key, "不在参数目录中");
807
+ const item = { enabled: spec.enabled };
808
+ if (spec.value !== void 0) item.value = validateParamValue(key, spec.value);
809
+ out[key] = item;
810
+ }
811
+ return out;
812
+ }
813
+ //#endregion
814
+ //#region src/client/api.ts
815
+ /**
816
+ * 数据端点通道(前端预留):同源 fetch 封装,类型与 dto.ts 对齐。
817
+ * 本次仅交付通道层,不交付调用它的界面。
818
+ * @module image-studio/client/api
819
+ */
820
+ const BASE = "/dsh-plus/image-studio";
821
+ /** 提交生图任务。 */
822
+ function generate(request) {
823
+ return postJson(`${BASE}/generate`, request);
824
+ }
825
+ /** 批量任务轮询。 */
826
+ function fetchTasks() {
827
+ return getJson(`${BASE}/tasks`);
828
+ }
829
+ /** 取消任务。 */
830
+ function cancelTask(taskId) {
831
+ return postJson(`${BASE}/tasks/${taskId}/cancel`);
832
+ }
833
+ /** 画廊列表。 */
834
+ function fetchGallery() {
835
+ return getJson(`${BASE}/gallery`);
836
+ }
837
+ /** 删除画廊条目。 */
838
+ function deleteGalleryItem(itemId) {
839
+ return fetch(`${BASE}/gallery/${itemId}`, {
840
+ method: "DELETE",
841
+ credentials: "same-origin"
842
+ }).then((res) => res.json());
843
+ }
844
+ /** 图片 URL(<img src> 直连,非 fetch)。 */
845
+ function imageUrl(imageId) {
846
+ return `${BASE}/images/${imageId}`;
847
+ }
848
+ /** 凭据状态(describe,永不回传值)。 */
849
+ function fetchCredentialStatus(refName) {
850
+ return getJson(`${BASE}/credentials/${refName}`);
851
+ }
852
+ /** 设置凭据值。 */
853
+ function setCredential(refName, value) {
854
+ return postJson(`${BASE}/credentials/${refName}`, { value });
855
+ }
856
+ /** 删除凭据。 */
857
+ function unsetCredential(refName) {
858
+ return fetch(`${BASE}/credentials/${refName}`, {
859
+ method: "DELETE",
860
+ credentials: "same-origin"
861
+ }).then((res) => res.json());
862
+ }
863
+ /** 协议 registry + 参数目录。 */
864
+ function fetchProviders() {
865
+ return getJson(`${BASE}/providers`);
866
+ }
867
+ /** 预设只读快照(读写走官方 settings RPC)。 */
868
+ function fetchPresets() {
869
+ return getJson(`${BASE}/presets`);
870
+ }
871
+ //#endregion
7
872
  //#region src/client/card.tsx
8
873
  /**
9
- * 配置卡片占位组件:本次交付为空壳(不做前端设计与编码)。
10
- * 后续版本在此渲染三组预设编辑与凭据管理;keyed 槽位契约已声明。
874
+ * 配置卡片(settings.plugin.item,key = settings 命名空间):
875
+ * 三组预设(提供商/参数/提示词)+ 高级项(并发/超时/代理/画廊上限)的
876
+ * staged draft 编辑;提供商预设逐条附 API Key 管理(凭据端点,值不回显)。
877
+ * JSON 字段(extraHeaders/paramSpecs)以文本镜像编辑,保存时解析 +
878
+ * validateParamSpecs 本地校验,非法即拦在保存前。
11
879
  * @module image-studio/client/card
12
880
  */
13
- /** 占位组件。 */
14
- function StudioConfigCard(props) {
15
- const t = props.t ?? ((key) => key);
16
- const el = document.createElement("div");
17
- el.className = "dsh-plus-image-studio-card-placeholder";
18
- el.textContent = t("cardPlaceholder");
881
+ /** 配置值 → 编辑草稿。 */
882
+ function draftOf(value) {
883
+ return {
884
+ providers: value.providerPresets.map((p) => ({
885
+ id: p.id,
886
+ name: p.name,
887
+ protocol: p.protocol,
888
+ baseUrl: p.baseUrl,
889
+ model: p.model,
890
+ extraHeadersText: JSON.stringify(p.extraHeaders ?? {}, null, 2)
891
+ })),
892
+ params: value.paramPresets.map((p) => ({
893
+ id: p.id,
894
+ name: p.name,
895
+ endpoint: p.endpoint,
896
+ specsText: JSON.stringify(p.paramSpecs ?? {}, null, 2)
897
+ })),
898
+ prompts: value.promptPresets.map((p) => ({
899
+ id: p.id,
900
+ name: p.name,
901
+ text: p.text
902
+ })),
903
+ maxConcurrent: String(value.maxConcurrent),
904
+ requestTimeoutMs: String(value.requestTimeoutMs),
905
+ proxy: value.proxy,
906
+ galleryMaxItems: String(value.galleryMaxItems)
907
+ };
908
+ }
909
+ function parseJson(text, label) {
910
+ try {
911
+ return JSON.parse(text);
912
+ } catch (error) {
913
+ throw new Error(`${label} ${error instanceof Error ? error.message : String(error)}`);
914
+ }
915
+ }
916
+ /** 草稿 → settings 载荷(边界校验:JSON 解析、参数表校验、预设 id 形态)。 */
917
+ function payloadOf(draft) {
918
+ const providers = draft.providers.map((p) => {
919
+ if (!isValidPresetId(p.id)) throw new Error(p.id);
920
+ return {
921
+ id: p.id,
922
+ name: p.name,
923
+ protocol: p.protocol,
924
+ baseUrl: p.baseUrl,
925
+ model: p.model,
926
+ credentialRef: credentialRefNameOf(p.id),
927
+ extraHeaders: parseJson(p.extraHeadersText, p.id)
928
+ };
929
+ });
930
+ const params = draft.params.map((p) => ({
931
+ id: p.id,
932
+ name: p.name,
933
+ endpoint: p.endpoint,
934
+ paramSpecs: validateParamSpecs(parseJson(p.specsText, p.id))
935
+ }));
19
936
  return {
20
- type: el,
21
- props: {}
937
+ promptPresets: draft.prompts.map((p) => ({
938
+ id: p.id,
939
+ name: p.name,
940
+ text: p.text
941
+ })),
942
+ paramPresets: params,
943
+ providerPresets: providers,
944
+ maxConcurrent: Math.max(0, Math.floor(Number(draft.maxConcurrent) || 0)),
945
+ requestTimeoutMs: Math.max(1e4, Math.floor(Number(draft.requestTimeoutMs) || 3e5)),
946
+ proxy: draft.proxy,
947
+ galleryMaxItems: Math.max(0, Math.floor(Number(draft.galleryMaxItems) || 0))
948
+ };
949
+ }
950
+ /** 单个提供商预设的 API Key 行(describe 徽标 + 设置/删除,值不回显)。 */
951
+ function CredentialRow(props) {
952
+ const { t, presetId, disabled } = props;
953
+ const [configured, setConfigured] = (0, react.useState)(null);
954
+ const [value, setValue] = (0, react.useState)("");
955
+ const [busy, setBusy] = (0, react.useState)(false);
956
+ (0, react.useEffect)(() => {
957
+ if (!isValidPresetId(presetId)) return;
958
+ let alive = true;
959
+ fetchCredentialStatus(presetId).then((res) => {
960
+ if (alive) setConfigured(res.configured);
961
+ }).catch(() => {});
962
+ return () => {
963
+ alive = false;
964
+ };
965
+ }, [presetId]);
966
+ const act = (fn) => {
967
+ setBusy(true);
968
+ fn().then(() => fetchCredentialStatus(presetId)).then((res) => {
969
+ setConfigured(res.configured);
970
+ setValue("");
971
+ }).catch(() => {}).finally(() => setBusy(false));
972
+ };
973
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
974
+ className: "imsc-credRow",
975
+ children: [
976
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
977
+ className: "imsc-input",
978
+ type: "password",
979
+ autoComplete: "off",
980
+ placeholder: t("card.credential.placeholder"),
981
+ "aria-label": t("card.credential.label"),
982
+ value,
983
+ disabled: disabled || busy,
984
+ onChange: (event) => setValue(event.target.value)
985
+ }),
986
+ configured !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
987
+ className: `imsc-badge ${configured ? "imsc-badgeSet" : "imsc-badgeUnset"}`,
988
+ children: t(configured ? "card.credential.configured" : "card.credential.unconfigured")
989
+ }) : null,
990
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
991
+ type: "button",
992
+ className: "imsc-btn imsc-btnGhost",
993
+ disabled: disabled || busy || value === "" || !isValidPresetId(presetId),
994
+ onClick: () => act(() => setCredential(presetId, value)),
995
+ children: t("card.credential.set")
996
+ }),
997
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
998
+ type: "button",
999
+ className: "imsc-btn imsc-btnGhost",
1000
+ disabled: disabled || busy || configured !== true || !isValidPresetId(presetId),
1001
+ onClick: () => act(() => unsetCredential(presetId)),
1002
+ children: t("card.credential.unset")
1003
+ })
1004
+ ]
1005
+ });
1006
+ }
1007
+ function StudioConfigCard(props) {
1008
+ const { t, scope, api } = props;
1009
+ const snapshot = (0, react.useSyncExternalStore)((listener) => scope.subscribe(listener), () => scope.getSnapshot());
1010
+ const value = snapshot.value;
1011
+ const [open, setOpen] = (0, react.useState)(false);
1012
+ const [draft, setDraft] = (0, react.useState)(null);
1013
+ const [baseline, setBaseline] = (0, react.useState)("");
1014
+ const [saving, setSaving] = (0, react.useState)(false);
1015
+ const [status, setStatus] = (0, react.useState)(IDLE_STATUS);
1016
+ const [protocols, setProtocols] = (0, react.useState)([]);
1017
+ (0, react.useEffect)(() => {
1018
+ fetchProviders().then((res) => setProtocols(res.protocols.map((p) => ({
1019
+ id: p.id,
1020
+ label: p.label
1021
+ })))).catch(() => {});
1022
+ }, []);
1023
+ (0, react.useEffect)(() => {
1024
+ if (value === void 0 || draft !== null) return;
1025
+ const initial = draftOf(value);
1026
+ setDraft(initial);
1027
+ setBaseline(JSON.stringify(initial));
1028
+ }, [value, draft]);
1029
+ const dirty = (0, react.useMemo)(() => draft !== null && JSON.stringify(draft) !== baseline, [draft, baseline]);
1030
+ if (value === void 0 || draft === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
1031
+ className: "imsc-card",
1032
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1033
+ className: "imsc-loading",
1034
+ children: t("common.loading")
1035
+ })
1036
+ });
1037
+ const disabled = !snapshot.writable;
1038
+ const onSave = () => {
1039
+ let payload;
1040
+ try {
1041
+ payload = payloadOf(draft);
1042
+ } catch (error) {
1043
+ setStatus({
1044
+ kind: "error",
1045
+ text: `${t("card.specsInvalid")}${error instanceof Error ? error.message : String(error)}`
1046
+ });
1047
+ return;
1048
+ }
1049
+ setSaving(true);
1050
+ api.update(payload, snapshot.revision).then(() => scope.load()).then(() => {
1051
+ setBaseline(JSON.stringify(draft));
1052
+ setStatus({
1053
+ kind: "ok",
1054
+ text: t("card.saved")
1055
+ });
1056
+ }).catch((error) => {
1057
+ setStatus({
1058
+ kind: "error",
1059
+ text: `${t("card.saveFailed")}${error instanceof Error ? error.message : String(error)}`
1060
+ });
1061
+ }).finally(() => setSaving(false));
1062
+ };
1063
+ const patch = (next) => {
1064
+ setDraft({
1065
+ ...draft,
1066
+ ...next
1067
+ });
1068
+ setStatus(IDLE_STATUS);
22
1069
  };
1070
+ const patchProvider = (index, next) => patch({ providers: draft.providers.map((p, i) => i === index ? {
1071
+ ...p,
1072
+ ...next
1073
+ } : p) });
1074
+ const patchParam = (index, next) => patch({ params: draft.params.map((p, i) => i === index ? {
1075
+ ...p,
1076
+ ...next
1077
+ } : p) });
1078
+ const patchPrompt = (index, next) => patch({ prompts: draft.prompts.map((p, i) => i === index ? {
1079
+ ...p,
1080
+ ...next
1081
+ } : p) });
1082
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(CardChrome, {
1083
+ prefix: "imsc",
1084
+ title: t("card.title"),
1085
+ description: t("card.description"),
1086
+ open,
1087
+ onToggle: setOpen,
1088
+ statusBadge: {
1089
+ text: draft.providers.length > 0 ? t("card.badgeOn").replace("{n}", String(draft.providers.length)) : t("card.badgeOff"),
1090
+ on: draft.providers.length > 0
1091
+ },
1092
+ dirty,
1093
+ dirtyLabel: t("common.unsaved"),
1094
+ readOnlyNotice: disabled ? t("card.readOnly") : void 0,
1095
+ status,
1096
+ actions: [{
1097
+ key: "discard",
1098
+ label: t("card.discard"),
1099
+ disabled: !dirty || saving,
1100
+ onClick: () => {
1101
+ const restored = draftOf(value);
1102
+ setDraft(restored);
1103
+ setBaseline(JSON.stringify(restored));
1104
+ setStatus(IDLE_STATUS);
1105
+ }
1106
+ }, {
1107
+ key: "save",
1108
+ label: t(saving ? "common.saving" : "card.save"),
1109
+ variant: "primary",
1110
+ disabled: !dirty || saving || disabled,
1111
+ onClick: onSave
1112
+ }],
1113
+ children: [
1114
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
1115
+ className: "imsc-groupTitle",
1116
+ children: t("card.providers")
1117
+ }),
1118
+ draft.providers.map((provider, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1119
+ className: "imsc-presetBox",
1120
+ children: [
1121
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1122
+ className: "imsc-presetHead",
1123
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1124
+ className: "imsc-presetName",
1125
+ children: provider.name || provider.id || "—"
1126
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1127
+ type: "button",
1128
+ className: "imsc-btn imsc-btnGhost",
1129
+ disabled,
1130
+ onClick: () => patch({ providers: draft.providers.filter((_, i) => i !== index) }),
1131
+ children: t("common.delete")
1132
+ })]
1133
+ }),
1134
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1135
+ className: "imsc-grid2",
1136
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
1137
+ prefix: "imsc",
1138
+ id: `imsc-pid-${index}`,
1139
+ label: t("card.field.id"),
1140
+ hint: t("card.idHint"),
1141
+ value: provider.id,
1142
+ invalid: provider.id !== "" && !isValidPresetId(provider.id),
1143
+ invalidLabel: t("card.idInvalid"),
1144
+ disabled,
1145
+ onEdit: (v) => patchProvider(index, { id: v })
1146
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
1147
+ prefix: "imsc",
1148
+ id: `imsc-pname-${index}`,
1149
+ label: t("card.field.name"),
1150
+ value: provider.name,
1151
+ disabled,
1152
+ onEdit: (v) => patchProvider(index, { name: v })
1153
+ })]
1154
+ }),
1155
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1156
+ className: "imsc-grid2",
1157
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1158
+ className: "imsc-field",
1159
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1160
+ className: "imsc-head",
1161
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
1162
+ className: "imsc-label",
1163
+ htmlFor: `imsc-pproto-${index}`,
1164
+ children: t("card.field.protocol")
1165
+ })
1166
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
1167
+ id: `imsc-pproto-${index}`,
1168
+ className: "imsc-select",
1169
+ value: provider.protocol,
1170
+ disabled,
1171
+ onChange: (event) => patchProvider(index, { protocol: event.target.value }),
1172
+ children: (protocols.length > 0 ? protocols : [{
1173
+ id: provider.protocol,
1174
+ label: provider.protocol
1175
+ }]).map((protocol) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1176
+ value: protocol.id,
1177
+ children: protocol.label
1178
+ }, protocol.id))
1179
+ })]
1180
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
1181
+ prefix: "imsc",
1182
+ id: `imsc-pmodel-${index}`,
1183
+ label: t("card.field.model"),
1184
+ value: provider.model,
1185
+ disabled,
1186
+ onEdit: (v) => patchProvider(index, { model: v })
1187
+ })]
1188
+ }),
1189
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
1190
+ prefix: "imsc",
1191
+ id: `imsc-purl-${index}`,
1192
+ label: t("card.field.baseUrl"),
1193
+ value: provider.baseUrl,
1194
+ disabled,
1195
+ onEdit: (v) => patchProvider(index, { baseUrl: v })
1196
+ }),
1197
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1198
+ className: "imsc-field",
1199
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1200
+ className: "imsc-head",
1201
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
1202
+ className: "imsc-label",
1203
+ htmlFor: `imsc-pheaders-${index}`,
1204
+ children: t("card.field.extraHeaders")
1205
+ })
1206
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
1207
+ id: `imsc-pheaders-${index}`,
1208
+ className: "imsc-textarea",
1209
+ value: provider.extraHeadersText,
1210
+ disabled,
1211
+ onChange: (event) => patchProvider(index, { extraHeadersText: event.target.value })
1212
+ })]
1213
+ }),
1214
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CredentialRow, {
1215
+ t,
1216
+ presetId: provider.id,
1217
+ disabled
1218
+ })
1219
+ ]
1220
+ }, `provider-${index}`)),
1221
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1222
+ type: "button",
1223
+ className: "imsc-btn imsc-btnGhost",
1224
+ disabled,
1225
+ onClick: () => patch({ providers: [...draft.providers, {
1226
+ id: "",
1227
+ name: "",
1228
+ protocol: protocols[0]?.id ?? "openai-images",
1229
+ baseUrl: "",
1230
+ model: "",
1231
+ extraHeadersText: "{}"
1232
+ }] }),
1233
+ children: [
1234
+ t("card.add"),
1235
+ " · ",
1236
+ t("card.providers")
1237
+ ]
1238
+ }),
1239
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
1240
+ className: "imsc-groupTitle",
1241
+ children: t("card.prompts")
1242
+ }),
1243
+ draft.prompts.map((prompt, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1244
+ className: "imsc-presetBox",
1245
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1246
+ className: "imsc-presetHead",
1247
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1248
+ className: "imsc-input",
1249
+ "aria-label": t("card.field.name"),
1250
+ value: prompt.name,
1251
+ disabled,
1252
+ onChange: (event) => patchPrompt(index, { name: event.target.value })
1253
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1254
+ type: "button",
1255
+ className: "imsc-btn imsc-btnGhost",
1256
+ disabled,
1257
+ onClick: () => patch({ prompts: draft.prompts.filter((_, i) => i !== index) }),
1258
+ children: t("common.delete")
1259
+ })]
1260
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
1261
+ className: "imsc-textarea",
1262
+ "aria-label": t("card.field.text"),
1263
+ value: prompt.text,
1264
+ disabled,
1265
+ onChange: (event) => patchPrompt(index, { text: event.target.value })
1266
+ })]
1267
+ }, `prompt-${index}`)),
1268
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1269
+ type: "button",
1270
+ className: "imsc-btn imsc-btnGhost",
1271
+ disabled,
1272
+ onClick: () => patch({ prompts: [...draft.prompts, {
1273
+ id: `prompt-${Date.now().toString(36)}`,
1274
+ name: "",
1275
+ text: ""
1276
+ }] }),
1277
+ children: [
1278
+ t("card.add"),
1279
+ " · ",
1280
+ t("card.prompts")
1281
+ ]
1282
+ }),
1283
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
1284
+ className: "imsc-groupTitle",
1285
+ children: t("card.params")
1286
+ }),
1287
+ draft.params.map((param, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1288
+ className: "imsc-presetBox",
1289
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1290
+ className: "imsc-presetHead",
1291
+ children: [
1292
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1293
+ className: "imsc-input",
1294
+ "aria-label": t("card.field.name"),
1295
+ value: param.name,
1296
+ disabled,
1297
+ onChange: (event) => patchParam(index, { name: event.target.value })
1298
+ }),
1299
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1300
+ className: "imsc-select",
1301
+ "aria-label": t("card.field.endpoint"),
1302
+ value: param.endpoint,
1303
+ disabled,
1304
+ onChange: (event) => patchParam(index, { endpoint: event.target.value }),
1305
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1306
+ value: "generation",
1307
+ children: t("tab.generation")
1308
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1309
+ value: "edit",
1310
+ children: t("tab.edit")
1311
+ })]
1312
+ }),
1313
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1314
+ type: "button",
1315
+ className: "imsc-btn imsc-btnGhost",
1316
+ disabled,
1317
+ onClick: () => patch({ params: draft.params.filter((_, i) => i !== index) }),
1318
+ children: t("common.delete")
1319
+ })
1320
+ ]
1321
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
1322
+ className: "imsc-textarea",
1323
+ "aria-label": t("card.field.paramSpecs"),
1324
+ placeholder: "{ \"size\": { \"enabled\": true, \"value\": \"1024x1024\" } }",
1325
+ value: param.specsText,
1326
+ disabled,
1327
+ onChange: (event) => patchParam(index, { specsText: event.target.value })
1328
+ })]
1329
+ }, `param-${index}`)),
1330
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1331
+ type: "button",
1332
+ className: "imsc-btn imsc-btnGhost",
1333
+ disabled,
1334
+ onClick: () => patch({ params: [...draft.params, {
1335
+ id: `param-${Date.now().toString(36)}`,
1336
+ name: "",
1337
+ endpoint: "generation",
1338
+ specsText: "{}"
1339
+ }] }),
1340
+ children: [
1341
+ t("card.add"),
1342
+ " · ",
1343
+ t("card.params")
1344
+ ]
1345
+ }),
1346
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
1347
+ className: "imsc-groupTitle",
1348
+ children: t("card.advanced")
1349
+ }),
1350
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1351
+ className: "imsc-grid2",
1352
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
1353
+ prefix: "imsc",
1354
+ id: "imsc-conc",
1355
+ label: t("card.maxConcurrent"),
1356
+ numeric: true,
1357
+ value: draft.maxConcurrent,
1358
+ disabled,
1359
+ onEdit: (v) => patch({ maxConcurrent: v })
1360
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
1361
+ prefix: "imsc",
1362
+ id: "imsc-timeout",
1363
+ label: t("card.requestTimeoutMs"),
1364
+ numeric: true,
1365
+ value: draft.requestTimeoutMs,
1366
+ disabled,
1367
+ onEdit: (v) => patch({ requestTimeoutMs: v })
1368
+ })]
1369
+ }),
1370
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1371
+ className: "imsc-grid2",
1372
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
1373
+ prefix: "imsc",
1374
+ id: "imsc-proxy",
1375
+ label: t("card.proxy"),
1376
+ value: draft.proxy,
1377
+ disabled,
1378
+ onEdit: (v) => patch({ proxy: v })
1379
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
1380
+ prefix: "imsc",
1381
+ id: "imsc-gmax",
1382
+ label: t("card.galleryMaxItems"),
1383
+ numeric: true,
1384
+ value: draft.galleryMaxItems,
1385
+ disabled,
1386
+ onEdit: (v) => patch({ galleryMaxItems: v })
1387
+ })]
1388
+ })
1389
+ ]
1390
+ });
23
1391
  }
24
1392
  //#endregion
25
1393
  //#region src/client/i18n.ts
26
1394
  /**
27
- * i18n 字典(zh 为主,en 兜底);NS 与 settings 命名空间分离,
28
- * 仅承载前端文案 key。本次只含占位文案。
1395
+ * i18n 字典(zh 为键域事实源,en 全量对译);NS 与 settings 命名空间分离,
1396
+ * 仅承载前端文案 key。面板 / 入口 / 配置卡片共用同一命名空间。
29
1397
  * @module image-studio/client/i18n
30
1398
  */
31
1399
  /** locale 命名空间。 */
32
1400
  const NS = "dsh-plus-image-studio";
33
1401
  const zh = {
34
- nav: "图像工作室",
35
- placeholder: "生图工作台与画廊将在后续版本提供",
36
- cardPlaceholder: "图像工作室配置(预设/凭据管理将在后续版本提供)"
1402
+ "entry.label": "图像",
1403
+ "panel.title": "图像工作室",
1404
+ "tab.generation": "文生图",
1405
+ "tab.edit": "图生图",
1406
+ "tab.gallery": "画廊",
1407
+ "common.close": "关闭",
1408
+ "common.cancel": "取消",
1409
+ "common.save": "保存",
1410
+ "common.saving": "保存中…",
1411
+ "common.retry": "重试",
1412
+ "common.loading": "加载中…",
1413
+ "common.loadFailed": "加载失败",
1414
+ "common.delete": "删除",
1415
+ "common.confirmDelete": "确认删除",
1416
+ "common.refresh": "刷新",
1417
+ "common.download": "下载",
1418
+ "common.unsaved": "未保存",
1419
+ "provider.label": "提供商",
1420
+ "provider.placeholder": "选择提供商预设…",
1421
+ "provider.empty": "暂无提供商预设:请到 设置 → 插件 → 图像工作室 添加",
1422
+ "provider.credential.configured": "Key 已配置",
1423
+ "provider.credential.missing": "Key 未配置",
1424
+ "provider.inline.toggle": "临时覆盖协议/地址/模型",
1425
+ "provider.inline.protocol": "协议",
1426
+ "provider.inline.baseUrl": "API 地址",
1427
+ "provider.inline.model": "模型",
1428
+ "provider.inline.hint": "覆盖时凭据沿用所选预设的 API Key",
1429
+ "prompt.label": "提示词",
1430
+ "prompt.placeholder": "描述想要生成的画面…",
1431
+ "prompt.preset.insert": "插入提示词预设…",
1432
+ "prompt.preset.save": "存为预设",
1433
+ "prompt.preset.name": "预设名称",
1434
+ "prompt.preset.saved": "已保存提示词预设",
1435
+ "params.basic": "常用参数",
1436
+ "params.advanced": "高级参数",
1437
+ "params.preset.placeholder": "应用参数预设…",
1438
+ "params.preset.save": "存为预设",
1439
+ "params.preset.saved": "已保存参数预设",
1440
+ "params.enabled": "启用",
1441
+ "sources.label": "源图",
1442
+ "sources.pick": "选择源图",
1443
+ "sources.count": "已选 {n}/16 张",
1444
+ "sources.mask": "遮罩(可选)",
1445
+ "sources.maskPick": "选择遮罩",
1446
+ "sources.maskClear": "清除",
1447
+ "picker.empty": "画廊暂无图片",
1448
+ "picker.confirm": "使用所选({n})",
1449
+ "generate.submit": "生成",
1450
+ "generate.submitted": "已提交任务 {id},进度见下方任务条",
1451
+ "generate.failed": "提交失败:",
1452
+ "generate.needProvider": "请先选择提供商预设",
1453
+ "generate.needPrompt": "请填写提示词",
1454
+ "generate.needSources": "请先选择至少一张源图",
1455
+ "tasks.title": "任务",
1456
+ "tasks.state.queued": "排队中",
1457
+ "tasks.state.running": "生成中",
1458
+ "tasks.state.succeeded": "已完成",
1459
+ "tasks.state.failed": "失败",
1460
+ "tasks.state.cancelled": "已取消",
1461
+ "tasks.view": "查看",
1462
+ "gallery.empty": "画廊为空,去文生图生成第一张吧",
1463
+ "gallery.total": "共 {n} 条",
1464
+ "gallery.detail.prompt": "提示词",
1465
+ "gallery.detail.revised": "改写提示词",
1466
+ "gallery.detail.params": "请求参数",
1467
+ "gallery.detail.model": "模型",
1468
+ "gallery.detail.endpoint": "端点",
1469
+ "gallery.detail.time": "时间",
1470
+ "gallery.detail.sources": "源图",
1471
+ "gallery.edit": "二次编辑",
1472
+ "card.title": "图像工作室",
1473
+ "card.description": "文生图/图生图:提供商与 API Key、参数/提示词预设",
1474
+ "card.providers": "提供商预设",
1475
+ "card.prompts": "提示词预设",
1476
+ "card.params": "参数预设",
1477
+ "card.advanced": "高级设置",
1478
+ "card.add": "添加",
1479
+ "card.field.id": "ID",
1480
+ "card.field.name": "名称",
1481
+ "card.field.protocol": "协议",
1482
+ "card.field.baseUrl": "API 地址",
1483
+ "card.field.model": "模型",
1484
+ "card.field.extraHeaders": "附加请求头(JSON)",
1485
+ "card.field.text": "提示词模板",
1486
+ "card.field.endpoint": "适用端点",
1487
+ "card.field.paramSpecs": "参数表(JSON:{ \"size\": { \"enabled\": true, \"value\": \"1024x1024\" } })",
1488
+ "card.idHint": "小写字母开头,仅小写字母/数字/中划线(凭据引用名派生用)",
1489
+ "card.credential.label": "API Key",
1490
+ "card.credential.placeholder": "输入 Key(保存后不回显)",
1491
+ "card.credential.set": "保存 Key",
1492
+ "card.credential.unset": "删除 Key",
1493
+ "card.credential.configured": "已配置",
1494
+ "card.credential.unconfigured": "未配置",
1495
+ "card.maxConcurrent": "生图并发上限(0 = 不限)",
1496
+ "card.requestTimeoutMs": "单请求超时(毫秒)",
1497
+ "card.proxy": "上游代理(空 = 直连)",
1498
+ "card.galleryMaxItems": "画廊保留上限(0 = 不清理)",
1499
+ "card.specsInvalid": "参数表非法:",
1500
+ "card.idInvalid": "ID 非法:",
1501
+ "card.save": "保存",
1502
+ "card.discard": "放弃修改",
1503
+ "card.saved": "已保存",
1504
+ "card.saveFailed": "保存失败:",
1505
+ "card.readOnly": "当前部署的设置文档为只读",
1506
+ "card.badgeOn": "{n} 个提供商",
1507
+ "card.badgeOff": "未配置"
37
1508
  };
38
1509
  const en = {
39
- nav: "Image Studio",
40
- placeholder: "Image workspace and gallery arrive in a later release",
41
- cardPlaceholder: "Image Studio settings (preset/credential management arrives later)"
1510
+ "entry.label": "Image",
1511
+ "panel.title": "Image Studio",
1512
+ "tab.generation": "Text to Image",
1513
+ "tab.edit": "Image to Image",
1514
+ "tab.gallery": "Gallery",
1515
+ "common.close": "Close",
1516
+ "common.cancel": "Cancel",
1517
+ "common.save": "Save",
1518
+ "common.saving": "Saving…",
1519
+ "common.retry": "Retry",
1520
+ "common.loading": "Loading…",
1521
+ "common.loadFailed": "Load failed",
1522
+ "common.delete": "Delete",
1523
+ "common.confirmDelete": "Confirm delete",
1524
+ "common.refresh": "Refresh",
1525
+ "common.download": "Download",
1526
+ "common.unsaved": "Unsaved",
1527
+ "provider.label": "Provider",
1528
+ "provider.placeholder": "Select a provider preset…",
1529
+ "provider.empty": "No provider presets: add one in Settings → Plugins → Image Studio",
1530
+ "provider.credential.configured": "Key configured",
1531
+ "provider.credential.missing": "Key missing",
1532
+ "provider.inline.toggle": "Override protocol/URL/model",
1533
+ "provider.inline.protocol": "Protocol",
1534
+ "provider.inline.baseUrl": "API base URL",
1535
+ "provider.inline.model": "Model",
1536
+ "provider.inline.hint": "Overrides reuse the API key of the selected preset",
1537
+ "prompt.label": "Prompt",
1538
+ "prompt.placeholder": "Describe the image you want…",
1539
+ "prompt.preset.insert": "Insert prompt preset…",
1540
+ "prompt.preset.save": "Save as preset",
1541
+ "prompt.preset.name": "Preset name",
1542
+ "prompt.preset.saved": "Prompt preset saved",
1543
+ "params.basic": "Common parameters",
1544
+ "params.advanced": "Advanced parameters",
1545
+ "params.preset.placeholder": "Apply a parameter preset…",
1546
+ "params.preset.save": "Save as preset",
1547
+ "params.preset.saved": "Parameter preset saved",
1548
+ "params.enabled": "Enabled",
1549
+ "sources.label": "Source images",
1550
+ "sources.pick": "Pick sources",
1551
+ "sources.count": "{n}/16 selected",
1552
+ "sources.mask": "Mask (optional)",
1553
+ "sources.maskPick": "Pick mask",
1554
+ "sources.maskClear": "Clear",
1555
+ "picker.empty": "No images in the gallery yet",
1556
+ "picker.confirm": "Use selected ({n})",
1557
+ "generate.submit": "Generate",
1558
+ "generate.submitted": "Task {id} submitted; progress in the task bar below",
1559
+ "generate.failed": "Submit failed: ",
1560
+ "generate.needProvider": "Select a provider preset first",
1561
+ "generate.needPrompt": "Enter a prompt first",
1562
+ "generate.needSources": "Pick at least one source image",
1563
+ "tasks.title": "Tasks",
1564
+ "tasks.state.queued": "Queued",
1565
+ "tasks.state.running": "Running",
1566
+ "tasks.state.succeeded": "Done",
1567
+ "tasks.state.failed": "Failed",
1568
+ "tasks.state.cancelled": "Cancelled",
1569
+ "tasks.view": "View",
1570
+ "gallery.empty": "Gallery is empty — generate your first image",
1571
+ "gallery.total": "{n} items",
1572
+ "gallery.detail.prompt": "Prompt",
1573
+ "gallery.detail.revised": "Revised prompt",
1574
+ "gallery.detail.params": "Request parameters",
1575
+ "gallery.detail.model": "Model",
1576
+ "gallery.detail.endpoint": "Endpoint",
1577
+ "gallery.detail.time": "Time",
1578
+ "gallery.detail.sources": "Sources",
1579
+ "gallery.edit": "Edit again",
1580
+ "card.title": "Image Studio",
1581
+ "card.description": "Text/Image-to-image: providers & API keys, param/prompt presets",
1582
+ "card.providers": "Provider presets",
1583
+ "card.prompts": "Prompt presets",
1584
+ "card.params": "Parameter presets",
1585
+ "card.advanced": "Advanced",
1586
+ "card.add": "Add",
1587
+ "card.field.id": "ID",
1588
+ "card.field.name": "Name",
1589
+ "card.field.protocol": "Protocol",
1590
+ "card.field.baseUrl": "API base URL",
1591
+ "card.field.model": "Model",
1592
+ "card.field.extraHeaders": "Extra headers (JSON)",
1593
+ "card.field.text": "Prompt template",
1594
+ "card.field.endpoint": "Endpoint",
1595
+ "card.field.paramSpecs": "Param specs (JSON: { \"size\": { \"enabled\": true, \"value\": \"1024x1024\" } })",
1596
+ "card.idHint": "Lowercase letter first; lowercase/digits/dashes only (derives credential ref)",
1597
+ "card.credential.label": "API Key",
1598
+ "card.credential.placeholder": "Enter key (never echoed back)",
1599
+ "card.credential.set": "Save key",
1600
+ "card.credential.unset": "Remove key",
1601
+ "card.credential.configured": "Configured",
1602
+ "card.credential.unconfigured": "Not configured",
1603
+ "card.maxConcurrent": "Max concurrent tasks (0 = unlimited)",
1604
+ "card.requestTimeoutMs": "Request timeout (ms)",
1605
+ "card.proxy": "Upstream proxy (empty = direct)",
1606
+ "card.galleryMaxItems": "Gallery retention (0 = keep all)",
1607
+ "card.specsInvalid": "Invalid param specs: ",
1608
+ "card.idInvalid": "Invalid ID: ",
1609
+ "card.save": "Save",
1610
+ "card.discard": "Discard",
1611
+ "card.saved": "Saved",
1612
+ "card.saveFailed": "Save failed: ",
1613
+ "card.readOnly": "Settings document is read-only in this deployment",
1614
+ "card.badgeOn": "{n} providers",
1615
+ "card.badgeOff": "Not configured"
42
1616
  };
43
1617
  //#endregion
44
- //#region src/client/section.tsx
1618
+ //#region src/client/panel/controller.ts
1619
+ /** 创建一个模块级控制器(apply 内调用一次,inject 给两个 slot 入口)。 */
1620
+ function createPanelController() {
1621
+ let state = {
1622
+ open: false,
1623
+ tab: "generation",
1624
+ editSeed: null
1625
+ };
1626
+ let nonce = 0;
1627
+ const listeners = /* @__PURE__ */ new Set();
1628
+ const publish = () => {
1629
+ for (const listener of [...listeners]) listener();
1630
+ };
1631
+ const set = (next) => {
1632
+ state = next;
1633
+ publish();
1634
+ };
1635
+ return {
1636
+ subscribe: (listener) => {
1637
+ listeners.add(listener);
1638
+ return () => listeners.delete(listener);
1639
+ },
1640
+ getSnapshot: () => state,
1641
+ setOpen: (open) => {
1642
+ if (state.open === open) return;
1643
+ set({
1644
+ ...state,
1645
+ open
1646
+ });
1647
+ },
1648
+ toggle: () => set({
1649
+ ...state,
1650
+ open: !state.open
1651
+ }),
1652
+ setTab: (tab) => {
1653
+ if (state.tab === tab) return;
1654
+ set({
1655
+ ...state,
1656
+ tab
1657
+ });
1658
+ },
1659
+ seedEdit: (seed) => {
1660
+ nonce += 1;
1661
+ set({
1662
+ ...state,
1663
+ tab: "edit",
1664
+ editSeed: {
1665
+ ...seed,
1666
+ nonce
1667
+ }
1668
+ });
1669
+ }
1670
+ };
1671
+ }
1672
+ //#endregion
1673
+ //#region src/client/panel/entry.tsx
45
1674
  /**
46
- * 独立设置页占位组件:本次交付为空壳(不做前端设计与编码)。
47
- * 后续版本在此渲染生图工作台与画廊;插槽契约已按官方 settings.section 形态声明。
48
- * @module image-studio/client/section
1675
+ * 侧边栏 footer 入口按钮:与文件/终端入口同款(整行图标 + 文案;
1676
+ * rail 折叠态退化为居中圆形图标)。flex:1 由样式层与邻居等分行宽。
1677
+ * @module image-studio/client/panel/entry
49
1678
  */
50
- /** 占位组件(props 形态对齐 settings.section 四 share 注入)。 */
51
- function StudioSection(props) {
52
- const t = props.t ?? ((key) => key);
53
- const el = document.createElement("div");
54
- el.className = "dsh-plus-image-studio-section-placeholder";
55
- el.textContent = t("placeholder");
56
- return {
57
- type: el,
58
- props: {}
1679
+ function StudioEntryButton({ studio, t, wide }) {
1680
+ const { open } = (0, react.useSyncExternalStore)(studio.subscribe, studio.getSnapshot);
1681
+ const rail = wide === false;
1682
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1683
+ type: "button",
1684
+ className: rail ? "ims-entry ims-entry-rail" : "ims-entry",
1685
+ "aria-haspopup": "dialog",
1686
+ "aria-expanded": open,
1687
+ "aria-label": t("entry.label"),
1688
+ onClick: () => studio.toggle(),
1689
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconImage, { size: rail ? 18 : 16 }), !rail && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1690
+ className: "ims-entry-label",
1691
+ children: t("entry.label")
1692
+ })]
1693
+ });
1694
+ }
1695
+ //#endregion
1696
+ //#region src/client/panel/gallery.tsx
1697
+ /**
1698
+ * 画廊视图:条目网格(首图缩略)+ 详情模态(全部图片、提示词/改写、
1699
+ * 请求参数回放、衍生链源图、下载/删除/二次编辑)。
1700
+ * @module image-studio/client/panel/gallery
1701
+ */
1702
+ function fmtTime(iso) {
1703
+ const date = new Date(iso);
1704
+ return Number.isNaN(date.getTime()) ? iso : date.toLocaleString();
1705
+ }
1706
+ /** 详情模态。 */
1707
+ function GalleryDetail(props) {
1708
+ const { t, item, onClose, onEdit, onDeleted } = props;
1709
+ const [confirming, setConfirming] = (0, react.useState)(false);
1710
+ const [deleting, setDeleting] = (0, react.useState)(false);
1711
+ const [error, setError] = (0, react.useState)(null);
1712
+ const doDelete = () => {
1713
+ setDeleting(true);
1714
+ deleteGalleryItem(item.id).then(() => onDeleted(item.id)).catch((err) => {
1715
+ setError(err instanceof Error ? err.message : String(err));
1716
+ setDeleting(false);
1717
+ setConfirming(false);
1718
+ });
1719
+ };
1720
+ const paramRows = Object.entries(item.params);
1721
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
1722
+ open: true,
1723
+ onClose,
1724
+ title: item.prompt.slice(0, 40),
1725
+ headless: true,
1726
+ className: "ims-detailModal",
1727
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1728
+ className: "ims-sub",
1729
+ children: [
1730
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1731
+ className: "ims-subHead",
1732
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1733
+ className: "ims-detailMeta",
1734
+ children: [
1735
+ t("gallery.detail.model"),
1736
+ ":",
1737
+ item.model,
1738
+ " · ",
1739
+ t("gallery.detail.endpoint"),
1740
+ ":",
1741
+ item.endpoint,
1742
+ " · ",
1743
+ t("gallery.detail.time"),
1744
+ ":",
1745
+ fmtTime(item.createdAt)
1746
+ ]
1747
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1748
+ type: "button",
1749
+ className: "ims-btn ims-btnGhost",
1750
+ onClick: onClose,
1751
+ children: t("common.close")
1752
+ })]
1753
+ }),
1754
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1755
+ className: "ims-detailBody",
1756
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1757
+ className: "ims-detailImages",
1758
+ children: item.imageIds.map((imageId, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("figure", {
1759
+ className: "ims-detailFigure",
1760
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
1761
+ src: imageUrl(imageId),
1762
+ alt: item.prompt
1763
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("figcaption", { children: [item.revisedPrompts[index] !== null && item.revisedPrompts[index] !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1764
+ className: "ims-revised",
1765
+ title: item.revisedPrompts[index] ?? "",
1766
+ children: [
1767
+ t("gallery.detail.revised"),
1768
+ ":",
1769
+ item.revisedPrompts[index]
1770
+ ]
1771
+ }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("a", {
1772
+ className: "ims-btn ims-btnGhost ims-btnSmall",
1773
+ href: imageUrl(imageId),
1774
+ download: true,
1775
+ children: [
1776
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconDownload, { size: 14 }),
1777
+ " ",
1778
+ t("common.download")
1779
+ ]
1780
+ })] })]
1781
+ }, imageId))
1782
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1783
+ className: "ims-detailInfo",
1784
+ children: [
1785
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
1786
+ className: "ims-detailLabel",
1787
+ children: t("gallery.detail.prompt")
1788
+ }),
1789
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1790
+ className: "ims-detailPrompt",
1791
+ children: item.prompt
1792
+ }),
1793
+ item.sourceIds.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
1794
+ className: "ims-detailLabel",
1795
+ children: t("gallery.detail.sources")
1796
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1797
+ className: "ims-sources",
1798
+ children: item.sourceIds.map((imageId) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1799
+ className: "ims-thumb",
1800
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
1801
+ src: imageUrl(imageId),
1802
+ alt: ""
1803
+ })
1804
+ }, imageId))
1805
+ })] }) : null,
1806
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
1807
+ className: "ims-detailLabel",
1808
+ children: t("gallery.detail.params")
1809
+ }),
1810
+ paramRows.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1811
+ className: "ims-hint",
1812
+ children: "—"
1813
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("table", {
1814
+ className: "ims-paramTable",
1815
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tbody", { children: paramRows.map(([key, value]) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
1816
+ className: "ims-paramTableKey",
1817
+ children: key
1818
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: JSON.stringify(value) })] }, key)) })
1819
+ })
1820
+ ]
1821
+ })]
1822
+ }),
1823
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1824
+ className: "ims-subFoot",
1825
+ children: [
1826
+ error !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1827
+ className: "ims-status ims-statusError",
1828
+ role: "alert",
1829
+ children: error
1830
+ }) : null,
1831
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1832
+ type: "button",
1833
+ className: "ims-btn ims-btnPrimary",
1834
+ onClick: () => onEdit(item),
1835
+ children: t("gallery.edit")
1836
+ }),
1837
+ confirming ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1838
+ type: "button",
1839
+ className: "ims-btn ims-btnDanger",
1840
+ disabled: deleting,
1841
+ onClick: doDelete,
1842
+ children: t("common.confirmDelete")
1843
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1844
+ type: "button",
1845
+ className: "ims-btn ims-btnGhost",
1846
+ onClick: () => setConfirming(true),
1847
+ children: [
1848
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconTrashOutline16, { size: 14 }),
1849
+ " ",
1850
+ t("common.delete")
1851
+ ]
1852
+ })
1853
+ ]
1854
+ })
1855
+ ]
1856
+ })
1857
+ });
1858
+ }
1859
+ function GalleryView(props) {
1860
+ const { t, items, loading, failed, onRefresh, onEdit, detailId, onDetailChange } = props;
1861
+ const detail = items.find((item) => item.id === detailId) ?? null;
1862
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1863
+ className: "ims-gallery",
1864
+ hidden: props.hidden,
1865
+ children: [
1866
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1867
+ className: "ims-galleryBar",
1868
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1869
+ className: "ims-hint",
1870
+ children: t("gallery.total").replace("{n}", String(items.length))
1871
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1872
+ type: "button",
1873
+ className: "ims-btn ims-btnGhost ims-btnSmall",
1874
+ onClick: onRefresh,
1875
+ children: [
1876
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconRefreshOutline16, { size: 14 }),
1877
+ " ",
1878
+ t("common.refresh")
1879
+ ]
1880
+ })]
1881
+ }),
1882
+ failed ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1883
+ className: "ims-empty",
1884
+ children: [t("common.loadFailed"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1885
+ type: "button",
1886
+ className: "ims-btn ims-btnGhost ims-btnSmall",
1887
+ onClick: onRefresh,
1888
+ children: t("common.retry")
1889
+ })]
1890
+ }) : loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1891
+ className: "ims-empty",
1892
+ children: t("common.loading")
1893
+ }) : items.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1894
+ className: "ims-empty",
1895
+ children: t("gallery.empty")
1896
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1897
+ className: "ims-grid",
1898
+ children: [...items].reverse().map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1899
+ type: "button",
1900
+ className: "ims-cell",
1901
+ onClick: () => onDetailChange(item.id),
1902
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
1903
+ src: imageUrl(item.imageIds[0] ?? ""),
1904
+ alt: item.prompt,
1905
+ loading: "lazy"
1906
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1907
+ className: "ims-cellPrompt",
1908
+ children: item.prompt
1909
+ })]
1910
+ }, item.id))
1911
+ }),
1912
+ detail !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GalleryDetail, {
1913
+ t,
1914
+ item: detail,
1915
+ onClose: () => onDetailChange(null),
1916
+ onEdit: (item) => {
1917
+ onDetailChange(null);
1918
+ onEdit(item);
1919
+ },
1920
+ onDeleted: () => {
1921
+ onDetailChange(null);
1922
+ onRefresh();
1923
+ }
1924
+ }) : null
1925
+ ]
1926
+ });
1927
+ }
1928
+ //#endregion
1929
+ //#region src/client/panel/param-state.ts
1930
+ /**
1931
+ * 参数表单状态纯函数:目录(ParamEntry)驱动的表单状态 ⇄ ParamSpecMap。
1932
+ * - 表单值统一字符串/布尔(输入控件友好),提交时按 kind 解析回数值;
1933
+ * - 画廊条目 params 回放、参数预设应用、预设另存均经此模块换算;
1934
+ * - 纯函数零 DOM 依赖,可 node 单测。
1935
+ * @module image-studio/client/panel/param-state
1936
+ */
1937
+ /** 各参数的顺手默认值(启用控件时的预填,非官方默认)。 */
1938
+ const DEFAULT_VALUES = {
1939
+ n: "1",
1940
+ size: "1024x1024",
1941
+ quality: "auto",
1942
+ background: "auto",
1943
+ output_format: "png",
1944
+ output_compression: "100",
1945
+ moderation: "auto",
1946
+ response_format: "url",
1947
+ stream: false,
1948
+ partial_images: "0",
1949
+ user: "",
1950
+ input_fidelity: "high",
1951
+ style: "vivid"
1952
+ };
1953
+ /** 目录条目 → 表单默认值(enum 取首值兜底)。 */
1954
+ function defaultValueFor(entry) {
1955
+ const known = DEFAULT_VALUES[entry.key];
1956
+ if (known !== void 0) return known;
1957
+ if (entry.kind === "enum") return entry.values?.[0] ?? "";
1958
+ if (entry.kind === "boolean") return false;
1959
+ return "";
1960
+ }
1961
+ /** 端点可用的目录条目(剔除 host 承载参数 model)。 */
1962
+ function paramEntriesFor(catalog, endpoint) {
1963
+ return catalog.filter((entry) => entry.applies.includes(endpoint) && !isHostParam(entry.key));
1964
+ }
1965
+ /** 空表单(全部关闭 + 默认值)。 */
1966
+ function emptyParamForm(catalog, endpoint) {
1967
+ const form = {};
1968
+ for (const entry of paramEntriesFor(catalog, endpoint)) form[entry.key] = {
1969
+ enabled: false,
1970
+ value: defaultValueFor(entry)
1971
+ };
1972
+ return form;
1973
+ }
1974
+ /** 表单值 ← 服务端参数值(按 kind 字符串化;未知键忽略)。 */
1975
+ function fieldValueOf(entry, value) {
1976
+ if (entry.kind === "boolean") return value === true;
1977
+ if (typeof value === "number") return String(value);
1978
+ if (typeof value === "string") return value;
1979
+ return defaultValueFor(entry);
1980
+ }
1981
+ /** 套用参数表(预设应用/画廊回放共用):仅目录内且端点适用的键生效并启用。 */
1982
+ function applyParams(catalog, endpoint, params) {
1983
+ const form = emptyParamForm(catalog, endpoint);
1984
+ for (const entry of paramEntriesFor(catalog, endpoint)) {
1985
+ const value = params[entry.key];
1986
+ if (value === void 0) continue;
1987
+ form[entry.key] = {
1988
+ enabled: true,
1989
+ value: fieldValueOf(entry, value)
1990
+ };
1991
+ }
1992
+ return form;
1993
+ }
1994
+ /** 参数预设(ParamSpecMap 形态)套用到表单:enabled=false 的键显式关闭。 */
1995
+ function applySpecs(catalog, endpoint, specs) {
1996
+ const form = emptyParamForm(catalog, endpoint);
1997
+ for (const entry of paramEntriesFor(catalog, endpoint)) {
1998
+ const spec = specs[entry.key];
1999
+ if (spec === void 0) continue;
2000
+ form[entry.key] = {
2001
+ enabled: spec.enabled,
2002
+ value: spec.enabled && spec.value !== void 0 ? fieldValueOf(entry, spec.value) : defaultValueFor(entry)
2003
+ };
2004
+ }
2005
+ return form;
2006
+ }
2007
+ /**
2008
+ * 表单 → ParamSpecMap(提交/另存预设共用):仅导出启用项,integer 按数值解析;
2009
+ * 空字符串 integer 视为未填(导出 {enabled:true} 无 value,交由边界校验报错)。
2010
+ */
2011
+ function formToSpecs(catalog, endpoint, form) {
2012
+ const specs = {};
2013
+ for (const entry of paramEntriesFor(catalog, endpoint)) {
2014
+ const field = form[entry.key];
2015
+ if (field === void 0 || !field.enabled) continue;
2016
+ if (entry.kind === "boolean") {
2017
+ specs[entry.key] = {
2018
+ enabled: true,
2019
+ value: field.value === true
2020
+ };
2021
+ continue;
2022
+ }
2023
+ const text = String(field.value).trim();
2024
+ if (entry.kind === "integer") {
2025
+ if (text === "") {
2026
+ specs[entry.key] = { enabled: true };
2027
+ continue;
2028
+ }
2029
+ const parsed = Number(text);
2030
+ specs[entry.key] = {
2031
+ enabled: true,
2032
+ value: Number.isNaN(parsed) ? text : parsed
2033
+ };
2034
+ continue;
2035
+ }
2036
+ specs[entry.key] = {
2037
+ enabled: true,
2038
+ value: text
2039
+ };
2040
+ }
2041
+ return specs;
2042
+ }
2043
+ //#endregion
2044
+ //#region src/client/panel/params-form.tsx
2045
+ /** 单参数行:开关 + 名称/说明 + 值控件。 */
2046
+ function ParamRow(props) {
2047
+ const { t, entry, form, onChange } = props;
2048
+ const field = form[entry.key] ?? {
2049
+ enabled: false,
2050
+ value: ""
2051
+ };
2052
+ const patch = (next) => onChange({
2053
+ ...form,
2054
+ [entry.key]: {
2055
+ ...field,
2056
+ ...next
2057
+ }
2058
+ });
2059
+ const control = (() => {
2060
+ if (entry.kind === "boolean") return null;
2061
+ if (entry.kind === "enum") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
2062
+ className: "ims-input ims-paramControl",
2063
+ value: String(field.value),
2064
+ disabled: !field.enabled,
2065
+ onChange: (event) => patch({ value: event.target.value }),
2066
+ children: (entry.values ?? []).map((value) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2067
+ value,
2068
+ children: value
2069
+ }, value))
2070
+ });
2071
+ if (entry.kind === "integer") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2072
+ className: "ims-input ims-paramControl",
2073
+ type: "number",
2074
+ inputMode: "numeric",
2075
+ min: entry.min,
2076
+ max: entry.max,
2077
+ value: String(field.value),
2078
+ disabled: !field.enabled,
2079
+ onChange: (event) => patch({ value: event.target.value })
2080
+ });
2081
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2082
+ className: "ims-input ims-paramControl",
2083
+ type: "text",
2084
+ value: String(field.value),
2085
+ disabled: !field.enabled,
2086
+ onChange: (event) => patch({ value: event.target.value })
2087
+ });
2088
+ })();
2089
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2090
+ className: `ims-paramRow${field.enabled ? " ims-paramRowOn" : ""}`,
2091
+ children: [
2092
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2093
+ className: "ims-switch",
2094
+ title: t("params.enabled"),
2095
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2096
+ type: "checkbox",
2097
+ role: "switch",
2098
+ "aria-label": `${entry.key} ${t("params.enabled")}`,
2099
+ "aria-checked": field.enabled,
2100
+ checked: field.enabled,
2101
+ onChange: (event) => entry.kind === "boolean" ? patch({
2102
+ enabled: event.target.checked,
2103
+ value: event.target.checked
2104
+ }) : patch({ enabled: event.target.checked })
2105
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2106
+ className: "ims-switchTrack",
2107
+ "aria-hidden": "true"
2108
+ })]
2109
+ }),
2110
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2111
+ className: "ims-paramText",
2112
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2113
+ className: "ims-paramKey",
2114
+ children: [entry.key, entry.generation !== "current" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2115
+ className: "ims-paramGen",
2116
+ children: entry.generation
2117
+ }) : null]
2118
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2119
+ className: "ims-paramDesc",
2120
+ children: entry.description
2121
+ })]
2122
+ }),
2123
+ control
2124
+ ]
2125
+ });
2126
+ }
2127
+ function ParamsForm(props) {
2128
+ const { t, catalog, endpoint, form, onChange } = props;
2129
+ const entries = paramEntriesFor(catalog, endpoint);
2130
+ const basics = entries.filter((entry) => !entry.advanced);
2131
+ const advanced = entries.filter((entry) => entry.advanced);
2132
+ const advancedOn = advanced.filter((entry) => form[entry.key]?.enabled === true).length;
2133
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2134
+ className: "ims-params",
2135
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2136
+ className: "ims-paramGroup",
2137
+ children: basics.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ParamRow, {
2138
+ t,
2139
+ entry,
2140
+ form,
2141
+ onChange
2142
+ }, entry.key))
2143
+ }), advanced.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
2144
+ className: "ims-adv",
2145
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("summary", {
2146
+ className: "ims-advSummary",
2147
+ children: [t("params.advanced"), advancedOn > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2148
+ className: "ims-advCount",
2149
+ children: advancedOn
2150
+ }) : null]
2151
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2152
+ className: "ims-paramGroup",
2153
+ children: advanced.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ParamRow, {
2154
+ t,
2155
+ entry,
2156
+ form,
2157
+ onChange
2158
+ }, entry.key))
2159
+ })]
2160
+ }) : null]
2161
+ });
2162
+ }
2163
+ //#endregion
2164
+ //#region src/client/panel/picker.tsx
2165
+ /**
2166
+ * 画廊图片选择器(模态):源图多选(≤16)与遮罩单选共用。
2167
+ * 扁平化全部画廊条目的图片,倒序排列;底部确认条显示已选数量。
2168
+ * @module image-studio/client/panel/picker
2169
+ */
2170
+ function flattenImages(items) {
2171
+ const out = [];
2172
+ for (const item of [...items].reverse()) for (const imageId of item.imageIds) out.push({
2173
+ imageId,
2174
+ prompt: item.prompt,
2175
+ createdAt: item.createdAt
2176
+ });
2177
+ return out;
2178
+ }
2179
+ function ImagePicker(props) {
2180
+ const { t, title, items, multiple, initial, onConfirm, onClose } = props;
2181
+ const [selected, setSelected] = (0, react.useState)(initial);
2182
+ const images = flattenImages(items);
2183
+ const toggle = (imageId) => {
2184
+ if (multiple) {
2185
+ setSelected((current) => current.includes(imageId) ? current.filter((id) => id !== imageId) : current.length >= 16 ? current : [...current, imageId]);
2186
+ return;
2187
+ }
2188
+ setSelected((current) => current.includes(imageId) ? [] : [imageId]);
2189
+ };
2190
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
2191
+ open: true,
2192
+ onClose,
2193
+ title,
2194
+ headless: true,
2195
+ className: "ims-pickerModal",
2196
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2197
+ className: "ims-sub",
2198
+ children: [
2199
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2200
+ className: "ims-subHead",
2201
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
2202
+ className: "ims-subTitle",
2203
+ children: title
2204
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2205
+ type: "button",
2206
+ className: "ims-btn ims-btnGhost",
2207
+ onClick: onClose,
2208
+ children: t("common.close")
2209
+ })]
2210
+ }),
2211
+ images.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2212
+ className: "ims-empty",
2213
+ children: t("picker.empty")
2214
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2215
+ className: "ims-pickerGrid",
2216
+ role: "listbox",
2217
+ "aria-multiselectable": multiple,
2218
+ children: images.map((image) => {
2219
+ const on = selected.includes(image.imageId);
2220
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2221
+ type: "button",
2222
+ role: "option",
2223
+ "aria-selected": on,
2224
+ className: `ims-pickerCell${on ? " ims-pickerCellOn" : ""}`,
2225
+ title: image.prompt,
2226
+ onClick: () => toggle(image.imageId),
2227
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
2228
+ src: imageUrl(image.imageId),
2229
+ alt: image.prompt,
2230
+ loading: "lazy"
2231
+ }), on ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2232
+ className: "ims-pickerMark",
2233
+ children: selected.indexOf(image.imageId) + 1
2234
+ }) : null]
2235
+ }, image.imageId);
2236
+ })
2237
+ }),
2238
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2239
+ className: "ims-subFoot",
2240
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2241
+ type: "button",
2242
+ className: "ims-btn ims-btnPrimary",
2243
+ disabled: selected.length === 0,
2244
+ onClick: () => onConfirm(selected),
2245
+ children: t("picker.confirm").replace("{n}", String(selected.length))
2246
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2247
+ type: "button",
2248
+ className: "ims-btn ims-btnGhost",
2249
+ onClick: onClose,
2250
+ children: t("common.cancel")
2251
+ })]
2252
+ })
2253
+ ]
2254
+ })
2255
+ });
2256
+ }
2257
+ //#endregion
2258
+ //#region src/client/panel/generator.tsx
2259
+ /**
2260
+ * 生图工作台表单(文生图/图生图共用,endpoint 区分):
2261
+ * - 提供商预设选择 + 凭据状态徽标 + inline 覆盖(凭据沿用预设);
2262
+ * - 提示词编辑 + 预设插入/另存;
2263
+ * - 目录驱动的参数表单 + 参数预设应用/另存;
2264
+ * - 图生图追加源图(≤16)与可选遮罩选择(画廊取图);
2265
+ * - 提交 generate → 202 taskId 交由面板任务条轮询。
2266
+ * 预设另存走 settings RPC(scope/api 注入),成功后经 onPresetSaved 刷新快照。
2267
+ * @module image-studio/client/panel/generator
2268
+ */
2269
+ const IDLE = {
2270
+ kind: "idle",
2271
+ text: ""
2272
+ };
2273
+ /** 预设 id 生成(稳定唯一即可;provider 预设的 kebab 约束不适用此处)。 */
2274
+ function presetId(prefix) {
2275
+ return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
2276
+ }
2277
+ function GeneratorForm(props) {
2278
+ const { t, endpoint, catalog, protocols, presets, gallery, scope, api } = props;
2279
+ const [providerId, setProviderId] = (0, react.useState)("");
2280
+ const [inline, setInline] = (0, react.useState)({
2281
+ open: false,
2282
+ protocol: protocols[0]?.id ?? "openai-images",
2283
+ baseUrl: "",
2284
+ model: ""
2285
+ });
2286
+ const [credentialOk, setCredentialOk] = (0, react.useState)(null);
2287
+ const [prompt, setPrompt] = (0, react.useState)("");
2288
+ const [form, setForm] = (0, react.useState)({});
2289
+ const [sourceIds, setSourceIds] = (0, react.useState)([]);
2290
+ const [maskId, setMaskId] = (0, react.useState)(null);
2291
+ const [picker, setPicker] = (0, react.useState)(null);
2292
+ const [naming, setNaming] = (0, react.useState)(null);
2293
+ const [status, setStatus] = (0, react.useState)(IDLE);
2294
+ const [submitting, setSubmitting] = (0, react.useState)(false);
2295
+ /** 目录就绪后初始化表单一次(catalog 会话内静态,重载不覆盖用户编辑)。 */
2296
+ const formReadyRef = (0, react.useRef)(false);
2297
+ /** 已消费的种子 nonce(防重复回填)。 */
2298
+ const seedRef = (0, react.useRef)(0);
2299
+ (0, react.useEffect)(() => {
2300
+ if (formReadyRef.current || catalog.length === 0) return;
2301
+ formReadyRef.current = true;
2302
+ setForm(emptyParamForm(catalog, endpoint));
2303
+ }, [catalog, endpoint]);
2304
+ const seed = endpoint === "edit" ? props.editSeed : null;
2305
+ (0, react.useEffect)(() => {
2306
+ if (seed === null || seed.nonce === seedRef.current || !formReadyRef.current) return;
2307
+ seedRef.current = seed.nonce;
2308
+ setPrompt(seed.prompt);
2309
+ setSourceIds(seed.sourceIds);
2310
+ setForm(applyParams(catalog, endpoint, seed.params));
2311
+ if (seed.providerPresetId !== null) setProviderId(seed.providerPresetId);
2312
+ setStatus(IDLE);
2313
+ }, [
2314
+ seed,
2315
+ catalog,
2316
+ endpoint
2317
+ ]);
2318
+ (0, react.useEffect)(() => {
2319
+ if (providerId === "") {
2320
+ setCredentialOk(null);
2321
+ return;
2322
+ }
2323
+ let alive = true;
2324
+ fetchCredentialStatus(providerId).then((res) => {
2325
+ if (alive) setCredentialOk(res.configured);
2326
+ }).catch(() => {
2327
+ if (alive) setCredentialOk(null);
2328
+ });
2329
+ return () => {
2330
+ alive = false;
2331
+ };
2332
+ }, [providerId]);
2333
+ const paramPresets = (0, react.useMemo)(() => presets.paramPresets.filter((preset) => preset.endpoint === endpoint), [presets, endpoint]);
2334
+ const selectedProvider = presets.providerPresets.find((preset) => preset.id === providerId);
2335
+ const toggleInline = (open) => {
2336
+ setInline((current) => ({
2337
+ ...current,
2338
+ open,
2339
+ baseUrl: open && current.baseUrl === "" ? selectedProvider?.baseUrl ?? "" : current.baseUrl,
2340
+ model: open && current.model === "" ? selectedProvider?.model ?? "" : current.model,
2341
+ protocol: open && current.protocol === "" ? selectedProvider?.protocol ?? current.protocol : current.protocol
2342
+ }));
2343
+ };
2344
+ const applyParamPreset = (id) => {
2345
+ if (id === "") return;
2346
+ const preset = paramPresets.find((item) => item.id === id);
2347
+ if (preset === void 0) return;
2348
+ try {
2349
+ const specs = validateParamSpecs(preset.paramSpecs);
2350
+ setForm(applySpecs(catalog, endpoint, specs));
2351
+ setStatus(IDLE);
2352
+ } catch (error) {
2353
+ setStatus({
2354
+ kind: "error",
2355
+ text: error instanceof Error ? error.message : String(error)
2356
+ });
2357
+ }
59
2358
  };
2359
+ /** 预设另存(settings RPC + revision fencing,成功后刷新快照)。 */
2360
+ const savePreset = () => {
2361
+ if (naming === null || naming.name.trim() === "") return;
2362
+ const snapshot = scope.getSnapshot();
2363
+ const value = snapshot.value ?? {};
2364
+ const patch = naming.kind === "prompt" ? { promptPresets: [...value.promptPresets ?? [], {
2365
+ id: presetId("prompt"),
2366
+ name: naming.name.trim(),
2367
+ text: prompt
2368
+ }] } : { paramPresets: [...value.paramPresets ?? [], {
2369
+ id: presetId("param"),
2370
+ name: naming.name.trim(),
2371
+ endpoint,
2372
+ paramSpecs: formToSpecs(catalog, endpoint, form)
2373
+ }] };
2374
+ api.update(patch, snapshot.revision).then(() => scope.load()).then(() => {
2375
+ setNaming(null);
2376
+ setStatus({
2377
+ kind: "ok",
2378
+ text: t(naming.kind === "prompt" ? "prompt.preset.saved" : "params.preset.saved")
2379
+ });
2380
+ props.onPresetSaved();
2381
+ }).catch((error) => {
2382
+ setStatus({
2383
+ kind: "error",
2384
+ text: `${t("card.saveFailed")}${error instanceof Error ? error.message : String(error)}`
2385
+ });
2386
+ });
2387
+ };
2388
+ const submit = () => {
2389
+ if (providerId === "") {
2390
+ setStatus({
2391
+ kind: "error",
2392
+ text: t("generate.needProvider")
2393
+ });
2394
+ return;
2395
+ }
2396
+ if (prompt.trim() === "") {
2397
+ setStatus({
2398
+ kind: "error",
2399
+ text: t("generate.needPrompt")
2400
+ });
2401
+ return;
2402
+ }
2403
+ if (endpoint === "edit" && sourceIds.length === 0) {
2404
+ setStatus({
2405
+ kind: "error",
2406
+ text: t("generate.needSources")
2407
+ });
2408
+ return;
2409
+ }
2410
+ let paramSpecs;
2411
+ try {
2412
+ paramSpecs = validateParamSpecs(formToSpecs(catalog, endpoint, form));
2413
+ } catch (error) {
2414
+ setStatus({
2415
+ kind: "error",
2416
+ text: error instanceof Error ? error.message : String(error)
2417
+ });
2418
+ return;
2419
+ }
2420
+ const request = {
2421
+ providerPresetId: providerId,
2422
+ endpoint,
2423
+ prompt: prompt.trim(),
2424
+ paramSpecs
2425
+ };
2426
+ if (inline.open) request.inlineProvider = {
2427
+ protocol: inline.protocol,
2428
+ baseUrl: inline.baseUrl.trim(),
2429
+ model: inline.model.trim()
2430
+ };
2431
+ if (endpoint === "edit") {
2432
+ request.sourceIds = sourceIds;
2433
+ if (maskId !== null) request.maskId = maskId;
2434
+ }
2435
+ setSubmitting(true);
2436
+ setStatus(IDLE);
2437
+ generate(request).then(({ taskId }) => {
2438
+ setStatus({
2439
+ kind: "ok",
2440
+ text: t("generate.submitted").replace("{id}", taskId)
2441
+ });
2442
+ props.onSubmitted(taskId);
2443
+ }).catch((error) => {
2444
+ setStatus({
2445
+ kind: "error",
2446
+ text: `${t("generate.failed")}${error instanceof Error ? error.message : String(error)}`
2447
+ });
2448
+ }).finally(() => setSubmitting(false));
2449
+ };
2450
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2451
+ className: "ims-gen",
2452
+ hidden: props.hidden,
2453
+ children: [
2454
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2455
+ className: "ims-genMain",
2456
+ children: [
2457
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2458
+ className: "ims-block",
2459
+ children: [
2460
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
2461
+ className: "ims-blockTitle",
2462
+ children: t("provider.label")
2463
+ }),
2464
+ presets.providerPresets.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2465
+ className: "ims-hint",
2466
+ children: t("provider.empty")
2467
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2468
+ className: "ims-providerRow",
2469
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
2470
+ className: "ims-input",
2471
+ value: providerId,
2472
+ "aria-label": t("provider.label"),
2473
+ onChange: (event) => setProviderId(event.target.value),
2474
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2475
+ value: "",
2476
+ children: t("provider.placeholder")
2477
+ }), presets.providerPresets.map((preset) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
2478
+ value: preset.id,
2479
+ children: [
2480
+ preset.name,
2481
+ "(",
2482
+ preset.model,
2483
+ ")"
2484
+ ]
2485
+ }, preset.id))]
2486
+ }), credentialOk !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2487
+ className: `ims-badge ${credentialOk ? "ims-badgeOn" : "ims-badgeOff"}`,
2488
+ children: t(credentialOk ? "provider.credential.configured" : "provider.credential.missing")
2489
+ }) : null]
2490
+ }),
2491
+ providerId !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2492
+ className: "ims-checkLine",
2493
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2494
+ type: "checkbox",
2495
+ checked: inline.open,
2496
+ onChange: (event) => toggleInline(event.target.checked)
2497
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("provider.inline.toggle") })]
2498
+ }), inline.open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2499
+ className: "ims-inlineGrid",
2500
+ children: [
2501
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2502
+ className: "ims-mini",
2503
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("provider.inline.protocol") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
2504
+ className: "ims-input",
2505
+ value: inline.protocol,
2506
+ onChange: (event) => setInline({
2507
+ ...inline,
2508
+ protocol: event.target.value
2509
+ }),
2510
+ children: protocols.map((protocol) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2511
+ value: protocol.id,
2512
+ children: protocol.label
2513
+ }, protocol.id))
2514
+ })]
2515
+ }),
2516
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2517
+ className: "ims-mini",
2518
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("provider.inline.baseUrl") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2519
+ className: "ims-input",
2520
+ value: inline.baseUrl,
2521
+ placeholder: "https://api.openai.com/v1",
2522
+ onChange: (event) => setInline({
2523
+ ...inline,
2524
+ baseUrl: event.target.value
2525
+ })
2526
+ })]
2527
+ }),
2528
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2529
+ className: "ims-mini",
2530
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("provider.inline.model") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2531
+ className: "ims-input",
2532
+ value: inline.model,
2533
+ placeholder: "gpt-image-1",
2534
+ onChange: (event) => setInline({
2535
+ ...inline,
2536
+ model: event.target.value
2537
+ })
2538
+ })]
2539
+ }),
2540
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2541
+ className: "ims-hint",
2542
+ children: t("provider.inline.hint")
2543
+ })
2544
+ ]
2545
+ }) : null] }) : null
2546
+ ]
2547
+ }),
2548
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2549
+ className: "ims-block",
2550
+ children: [
2551
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
2552
+ className: "ims-blockTitle",
2553
+ children: t("prompt.label")
2554
+ }),
2555
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
2556
+ className: "ims-input ims-prompt",
2557
+ rows: 5,
2558
+ value: prompt,
2559
+ placeholder: t("prompt.placeholder"),
2560
+ onChange: (event) => setPrompt(event.target.value)
2561
+ }),
2562
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2563
+ className: "ims-rowActions",
2564
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
2565
+ className: "ims-input ims-rowGrow",
2566
+ value: "",
2567
+ "aria-label": t("prompt.preset.insert"),
2568
+ onChange: (event) => {
2569
+ const preset = presets.promptPresets.find((item) => item.id === event.target.value);
2570
+ if (preset !== void 0) setPrompt((current) => current.trim() === "" ? preset.text : `${current}\n${preset.text}`);
2571
+ },
2572
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2573
+ value: "",
2574
+ children: t("prompt.preset.insert")
2575
+ }), presets.promptPresets.map((preset) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2576
+ value: preset.id,
2577
+ children: preset.name
2578
+ }, preset.id))]
2579
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2580
+ type: "button",
2581
+ className: "ims-btn ims-btnGhost",
2582
+ disabled: prompt.trim() === "",
2583
+ onClick: () => setNaming({
2584
+ kind: "prompt",
2585
+ name: ""
2586
+ }),
2587
+ children: t("prompt.preset.save")
2588
+ })]
2589
+ })
2590
+ ]
2591
+ }),
2592
+ endpoint === "edit" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2593
+ className: "ims-block",
2594
+ children: [
2595
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
2596
+ className: "ims-blockTitle",
2597
+ children: t("sources.label")
2598
+ }),
2599
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2600
+ className: "ims-sources",
2601
+ children: [sourceIds.map((imageId) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2602
+ className: "ims-thumb",
2603
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
2604
+ src: imageUrl(imageId),
2605
+ alt: ""
2606
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2607
+ type: "button",
2608
+ className: "ims-thumbRemove",
2609
+ "aria-label": t("common.delete"),
2610
+ onClick: () => setSourceIds((current) => current.filter((id) => id !== imageId)),
2611
+ children: "×"
2612
+ })]
2613
+ }, imageId)), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2614
+ type: "button",
2615
+ className: "ims-thumbAdd",
2616
+ onClick: () => setPicker("sources"),
2617
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconLayers, { size: 18 }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("sources.pick") })]
2618
+ })]
2619
+ }),
2620
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2621
+ className: "ims-hint",
2622
+ children: t("sources.count").replace("{n}", String(sourceIds.length))
2623
+ }),
2624
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
2625
+ className: "ims-blockTitle",
2626
+ children: t("sources.mask")
2627
+ }),
2628
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2629
+ className: "ims-sources",
2630
+ children: [maskId !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2631
+ className: "ims-thumb",
2632
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
2633
+ src: imageUrl(maskId),
2634
+ alt: ""
2635
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2636
+ type: "button",
2637
+ className: "ims-thumbRemove",
2638
+ "aria-label": t("sources.maskClear"),
2639
+ onClick: () => setMaskId(null),
2640
+ children: "×"
2641
+ })]
2642
+ }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2643
+ type: "button",
2644
+ className: "ims-thumbAdd",
2645
+ onClick: () => setPicker("mask"),
2646
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconLayers, { size: 18 }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("sources.maskPick") })]
2647
+ })]
2648
+ })
2649
+ ]
2650
+ }) : null
2651
+ ]
2652
+ }),
2653
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2654
+ className: "ims-genSide",
2655
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2656
+ className: "ims-block",
2657
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2658
+ className: "ims-rowActions ims-blockHead",
2659
+ children: [
2660
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
2661
+ className: "ims-blockTitle",
2662
+ children: t("params.basic")
2663
+ }),
2664
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
2665
+ className: "ims-input ims-rowGrow",
2666
+ value: "",
2667
+ "aria-label": t("params.preset.placeholder"),
2668
+ onChange: (event) => applyParamPreset(event.target.value),
2669
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2670
+ value: "",
2671
+ children: t("params.preset.placeholder")
2672
+ }), paramPresets.map((preset) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2673
+ value: preset.id,
2674
+ children: preset.name
2675
+ }, preset.id))]
2676
+ }),
2677
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2678
+ type: "button",
2679
+ className: "ims-btn ims-btnGhost",
2680
+ onClick: () => setNaming({
2681
+ kind: "params",
2682
+ name: ""
2683
+ }),
2684
+ children: t("params.preset.save")
2685
+ })
2686
+ ]
2687
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ParamsForm, {
2688
+ t,
2689
+ catalog,
2690
+ endpoint,
2691
+ form,
2692
+ onChange: setForm
2693
+ })]
2694
+ })
2695
+ }),
2696
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2697
+ className: "ims-genFoot",
2698
+ children: [
2699
+ naming !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2700
+ className: "ims-naming",
2701
+ children: [
2702
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2703
+ className: "ims-input ims-rowGrow",
2704
+ value: naming.name,
2705
+ placeholder: t("prompt.preset.name"),
2706
+ autoFocus: true,
2707
+ onChange: (event) => setNaming({
2708
+ ...naming,
2709
+ name: event.target.value
2710
+ }),
2711
+ onKeyDown: (event) => {
2712
+ if (event.key === "Enter") savePreset();
2713
+ if (event.key === "Escape") setNaming(null);
2714
+ }
2715
+ }),
2716
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2717
+ type: "button",
2718
+ className: "ims-btn ims-btnPrimary",
2719
+ disabled: naming.name.trim() === "",
2720
+ onClick: savePreset,
2721
+ children: t("common.save")
2722
+ }),
2723
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2724
+ type: "button",
2725
+ className: "ims-btn ims-btnGhost",
2726
+ onClick: () => setNaming(null),
2727
+ children: t("common.cancel")
2728
+ })
2729
+ ]
2730
+ }) : null,
2731
+ status.kind !== "idle" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2732
+ className: `ims-status${status.kind === "error" ? " ims-statusError" : ""}`,
2733
+ role: "status",
2734
+ children: status.text
2735
+ }) : null,
2736
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2737
+ type: "button",
2738
+ className: "ims-btn ims-btnPrimary ims-submit",
2739
+ disabled: submitting,
2740
+ onClick: submit,
2741
+ children: t("generate.submit")
2742
+ })
2743
+ ]
2744
+ }),
2745
+ picker !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImagePicker, {
2746
+ t,
2747
+ title: t(picker === "sources" ? "sources.label" : "sources.mask"),
2748
+ items: gallery,
2749
+ multiple: picker === "sources",
2750
+ initial: picker === "sources" ? sourceIds : [],
2751
+ onConfirm: (ids) => {
2752
+ if (picker === "sources") setSourceIds(ids);
2753
+ else setMaskId(ids[0] ?? null);
2754
+ setPicker(null);
2755
+ },
2756
+ onClose: () => setPicker(null)
2757
+ }) : null
2758
+ ]
2759
+ });
2760
+ }
2761
+ //#endregion
2762
+ //#region src/client/panel/tasks.tsx
2763
+ /**
2764
+ * 任务条:面板底部的横向任务流(最新在左)。
2765
+ * 活跃任务(排队/生成中)带旋转标与取消按钮;已完成可一键跳画廊详情。
2766
+ * 轮询由面板根组件负责,本组件纯渲染。
2767
+ * @module image-studio/client/panel/tasks
2768
+ */
2769
+ const STATE_KEYS = {
2770
+ queued: "tasks.state.queued",
2771
+ running: "tasks.state.running",
2772
+ succeeded: "tasks.state.succeeded",
2773
+ failed: "tasks.state.failed",
2774
+ cancelled: "tasks.state.cancelled"
2775
+ };
2776
+ function TasksBar(props) {
2777
+ const { t, tasks, onCancel, onView } = props;
2778
+ if (tasks.length === 0) return null;
2779
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
2780
+ className: "ims-tasks",
2781
+ "aria-label": t("tasks.title"),
2782
+ children: tasks.slice(0, 20).map((task) => {
2783
+ const active = task.state === "queued" || task.state === "running";
2784
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2785
+ className: `ims-task ims-task-${task.state}`,
2786
+ children: [
2787
+ active ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconLoadingOutline16, {
2788
+ className: "ims-spin",
2789
+ size: 14
2790
+ }) : null,
2791
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2792
+ className: `ims-taskState ims-taskState-${task.state}`,
2793
+ children: t(STATE_KEYS[task.state])
2794
+ }),
2795
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2796
+ className: "ims-taskPrompt",
2797
+ title: task.promptPreview,
2798
+ children: task.promptPreview
2799
+ }),
2800
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2801
+ className: "ims-taskModel",
2802
+ children: task.model
2803
+ }),
2804
+ active ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2805
+ type: "button",
2806
+ className: "ims-btn ims-btnGhost ims-btnSmall",
2807
+ onClick: () => onCancel(task.id),
2808
+ children: t("common.cancel")
2809
+ }) : null,
2810
+ task.state === "succeeded" && task.galleryItemId !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2811
+ type: "button",
2812
+ className: "ims-btn ims-btnGhost ims-btnSmall",
2813
+ onClick: () => onView(task.galleryItemId),
2814
+ children: t("tasks.view")
2815
+ }) : null,
2816
+ task.state === "failed" && task.error !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2817
+ className: "ims-taskError",
2818
+ title: task.error,
2819
+ children: task.error
2820
+ }) : null
2821
+ ]
2822
+ }, task.id);
2823
+ })
2824
+ });
2825
+ }
2826
+ //#endregion
2827
+ //#region src/client/panel/panel.tsx
2828
+ /**
2829
+ * 图像工作室面板根组件:侧栏 footer 入口打开的 overlay 工作台。
2830
+ * 三个标签(文生图/图生图/画廊)+ 底部任务条;两个生图表单常驻挂载
2831
+ * (hidden 切换,保留各自表单态)。数据归口:providers 一次性加载,
2832
+ * presets/gallery/tasks 面板打开时加载,任务 2s 轮询并侦测成功转移
2833
+ * 自动刷画廊。
2834
+ * 响应式:桌面居中模态(≤1100px),≤767px 全屏抽屉(见 styles.ts)。
2835
+ * @module image-studio/client/panel/panel
2836
+ */
2837
+ const EMPTY_PRESETS = {
2838
+ promptPresets: [],
2839
+ paramPresets: [],
2840
+ providerPresets: []
2841
+ };
2842
+ function StudioPanel(props) {
2843
+ const { studio, t, scope, api } = props;
2844
+ const { open, tab, editSeed } = (0, react.useSyncExternalStore)(studio.subscribe, studio.getSnapshot);
2845
+ const [providers, setProviders] = (0, react.useState)(null);
2846
+ const [presets, setPresets] = (0, react.useState)(EMPTY_PRESETS);
2847
+ const [gallery, setGallery] = (0, react.useState)([]);
2848
+ const [galleryLoading, setGalleryLoading] = (0, react.useState)(false);
2849
+ const [galleryFailed, setGalleryFailed] = (0, react.useState)(false);
2850
+ const [tasks, setTasks] = (0, react.useState)([]);
2851
+ const [detailId, setDetailId] = (0, react.useState)(null);
2852
+ /** 上一轮任务快照(成功转移侦测)。 */
2853
+ const prevTasksRef = (0, react.useRef)(/* @__PURE__ */ new Map());
2854
+ const reloadGallery = (0, react.useCallback)(() => {
2855
+ setGalleryLoading(true);
2856
+ fetchGallery().then((res) => {
2857
+ setGallery(res.items);
2858
+ setGalleryFailed(false);
2859
+ }).catch(() => setGalleryFailed(true)).finally(() => setGalleryLoading(false));
2860
+ }, []);
2861
+ const reloadPresets = (0, react.useCallback)(() => {
2862
+ fetchPresets().then(setPresets).catch(() => {});
2863
+ }, []);
2864
+ const reloadTasks = (0, react.useCallback)(() => {
2865
+ fetchTasks().then((res) => setTasks(res.tasks)).catch(() => {});
2866
+ }, []);
2867
+ (0, react.useEffect)(() => {
2868
+ if (!open) return;
2869
+ if (providers === null) fetchProviders().then(setProviders).catch(() => {});
2870
+ reloadPresets();
2871
+ reloadGallery();
2872
+ reloadTasks();
2873
+ const timer = setInterval(() => {
2874
+ fetchTasks().then((res) => {
2875
+ setTasks(res.tasks);
2876
+ const prev = prevTasksRef.current;
2877
+ const next = /* @__PURE__ */ new Map();
2878
+ let succeeded = false;
2879
+ for (const task of res.tasks) {
2880
+ next.set(task.id, task.state);
2881
+ if (task.state === "succeeded" && prev.get(task.id) !== "succeeded") succeeded = true;
2882
+ }
2883
+ prevTasksRef.current = next;
2884
+ if (succeeded) reloadGallery();
2885
+ }).catch(() => {});
2886
+ }, 2e3);
2887
+ return () => clearInterval(timer);
2888
+ }, [
2889
+ open,
2890
+ providers,
2891
+ reloadPresets,
2892
+ reloadGallery,
2893
+ reloadTasks
2894
+ ]);
2895
+ const catalog = providers?.params ?? [];
2896
+ const protocols = providers?.protocols ?? [];
2897
+ const tabs = [
2898
+ {
2899
+ key: "generation",
2900
+ label: t("tab.generation"),
2901
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconSparkles, { size: 15 })
2902
+ },
2903
+ {
2904
+ key: "edit",
2905
+ label: t("tab.edit"),
2906
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconBrush, { size: 15 })
2907
+ },
2908
+ {
2909
+ key: "gallery",
2910
+ label: t("tab.gallery"),
2911
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconImage, { size: 15 })
2912
+ }
2913
+ ];
2914
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
2915
+ open,
2916
+ onClose: () => studio.setOpen(false),
2917
+ title: t("panel.title"),
2918
+ headless: true,
2919
+ className: "ims-modal",
2920
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2921
+ className: "ims-panel",
2922
+ children: [
2923
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
2924
+ className: "ims-head",
2925
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2926
+ className: "ims-tabs",
2927
+ role: "tablist",
2928
+ "aria-label": t("panel.title"),
2929
+ children: tabs.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2930
+ type: "button",
2931
+ role: "tab",
2932
+ "aria-selected": tab === item.key,
2933
+ className: `ims-tab${tab === item.key ? " ims-tabActive" : ""}`,
2934
+ onClick: () => studio.setTab(item.key),
2935
+ children: [item.icon, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: item.label })]
2936
+ }, item.key))
2937
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2938
+ type: "button",
2939
+ className: "ims-iconBtn",
2940
+ "aria-label": t("common.close"),
2941
+ onClick: () => studio.setOpen(false),
2942
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCloseOutline16, { size: 16 })
2943
+ })]
2944
+ }),
2945
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2946
+ className: "ims-body",
2947
+ children: [
2948
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GeneratorForm, {
2949
+ t,
2950
+ endpoint: "generation",
2951
+ catalog,
2952
+ protocols,
2953
+ presets,
2954
+ gallery,
2955
+ scope,
2956
+ api,
2957
+ editSeed: null,
2958
+ onPresetSaved: reloadPresets,
2959
+ onSubmitted: reloadTasks,
2960
+ hidden: tab !== "generation"
2961
+ }),
2962
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GeneratorForm, {
2963
+ t,
2964
+ endpoint: "edit",
2965
+ catalog,
2966
+ protocols,
2967
+ presets,
2968
+ gallery,
2969
+ scope,
2970
+ api,
2971
+ editSeed,
2972
+ onPresetSaved: reloadPresets,
2973
+ onSubmitted: reloadTasks,
2974
+ hidden: tab !== "edit"
2975
+ }),
2976
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GalleryView, {
2977
+ t,
2978
+ items: gallery,
2979
+ loading: galleryLoading,
2980
+ failed: galleryFailed,
2981
+ onRefresh: reloadGallery,
2982
+ onEdit: (item) => studio.seedEdit({
2983
+ sourceIds: [...item.imageIds],
2984
+ prompt: item.prompt,
2985
+ params: item.params,
2986
+ providerPresetId: item.providerPresetId
2987
+ }),
2988
+ detailId,
2989
+ onDetailChange: setDetailId,
2990
+ hidden: tab !== "gallery"
2991
+ })
2992
+ ]
2993
+ }),
2994
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TasksBar, {
2995
+ t,
2996
+ tasks,
2997
+ onCancel: (taskId) => {
2998
+ cancelTask(taskId).then(reloadTasks).catch(() => {});
2999
+ },
3000
+ onView: (galleryItemId) => {
3001
+ studio.setTab("gallery");
3002
+ setDetailId(galleryItemId);
3003
+ }
3004
+ })
3005
+ ]
3006
+ })
3007
+ });
60
3008
  }
61
3009
  //#endregion
62
3010
  //#region src/client/styles.ts
63
3011
  /**
64
- * 画廊/生图占位样式:本次交付仅注入极简占位类名(无视觉设计)。
65
- * 沿用官方约定 <style data-plugin data-plugin-css>,HMR 按此卸载。
3012
+ * 图像工作室前端样式:侧栏入口 + 工作台(官方 Modal 承载)+ 配置卡片附加件。
3013
+ * 设计令牌仅用 dsh-client-ui-theme 实测存在的 --dsw-alias-*(深浅色自适应);
3014
+ * 模态骨架(遮罩/Escape/portal 层叠)由官方 Modal 原语承载,本表只覆写尺寸
3015
+ * 与内部布局(.ims-modal 等,模式同 web-files/web-terminal 的 wf-/wt-modal)。
3016
+ * 响应式断点 767px 与 @dsh-plus/ui-mobile-fit 对齐(移动端全屏、单列、44px 热区)。
66
3017
  * @module image-studio/client/styles
67
3018
  */
68
3019
  const PKG = "@dsh-plus/image-studio";
69
- /** 注入设置页占位样式。 */
70
- function injectSectionStyle() {
71
- return inject$1("dsh-plus-image-studio-section", ".dsh-plus-image-studio-section-placeholder{padding:16px;opacity:.6}");
72
- }
73
- /** 注入配置卡片占位样式。 */
74
- function injectCardStyles() {
75
- return inject$1("dsh-plus-image-studio-card", ".dsh-plus-image-studio-card-placeholder{padding:8px;opacity:.6}");
76
- }
77
- function inject$1(id, css) {
3020
+ const STYLE_TAG_ID = `${PKG}/styles.css`;
3021
+ const imageStudioCss = `
3022
+ /* ── 侧边栏入口(与文件/终端入口同款;flex:1 1 0 与邻居等分 footer 行宽)── */
3023
+ .ims-entry {
3024
+ box-sizing: border-box;
3025
+ display: flex;
3026
+ flex: 1 1 0;
3027
+ min-width: 0;
3028
+ align-items: center;
3029
+ gap: 8px;
3030
+ width: 100%;
3031
+ height: 42px;
3032
+ margin: 4px 0;
3033
+ padding: 0 10px 0 8px;
3034
+ border: none;
3035
+ border-radius: 12px;
3036
+ background: transparent;
3037
+ color: var(--dsw-alias-label-primary);
3038
+ font-family: inherit;
3039
+ font-size: 14px;
3040
+ line-height: 22px;
3041
+ cursor: pointer;
3042
+ overflow: hidden;
3043
+ }
3044
+ .ims-entry:hover { background: var(--dsw-alias-interactive-bg-hover); }
3045
+ .ims-entry-rail {
3046
+ flex: none;
3047
+ width: 36px;
3048
+ height: 36px;
3049
+ margin: 8px 0 10px;
3050
+ padding: 0;
3051
+ border-radius: 50%;
3052
+ justify-content: center;
3053
+ gap: 0;
3054
+ }
3055
+ .ims-entry-label { white-space: nowrap; overflow: hidden; }
3056
+
3057
+ /* ── 工作台模态尺寸覆写(官方 Modal headless 卡片)── */
3058
+ .ims-modal {
3059
+ width: min(1100px, 94vw) !important;
3060
+ max-width: none !important;
3061
+ height: min(780px, 88vh);
3062
+ padding: 0 !important;
3063
+ gap: 0 !important;
3064
+ display: flex;
3065
+ flex-direction: column;
3066
+ overflow: hidden;
3067
+ }
3068
+
3069
+ /* ── 面板骨架 ── */
3070
+ .ims-panel {
3071
+ display: flex;
3072
+ flex-direction: column;
3073
+ height: 100%;
3074
+ min-height: 0;
3075
+ position: relative;
3076
+ color: var(--dsw-alias-label-primary);
3077
+ }
3078
+ .ims-head {
3079
+ display: flex;
3080
+ flex: none;
3081
+ align-items: center;
3082
+ gap: 8px;
3083
+ padding: 10px 12px;
3084
+ border-bottom: 1px solid var(--dsw-alias-border-l3);
3085
+ }
3086
+ .ims-tabs { display: flex; flex: 1; gap: 4px; min-width: 0; }
3087
+ .ims-tab {
3088
+ display: inline-flex;
3089
+ align-items: center;
3090
+ gap: 6px;
3091
+ height: 34px;
3092
+ padding: 0 14px;
3093
+ border: none;
3094
+ border-radius: 10px;
3095
+ background: transparent;
3096
+ color: var(--dsw-alias-label-secondary);
3097
+ font-family: inherit;
3098
+ font-size: 14px;
3099
+ cursor: pointer;
3100
+ white-space: nowrap;
3101
+ }
3102
+ .ims-tab:hover { background: var(--dsw-alias-interactive-bg-hover); }
3103
+ .ims-tabActive {
3104
+ background: var(--dsw-alias-button-elevated-fill);
3105
+ color: var(--dsw-alias-label-primary);
3106
+ font-weight: 500;
3107
+ }
3108
+ .ims-iconBtn {
3109
+ display: inline-flex;
3110
+ flex: none;
3111
+ align-items: center;
3112
+ justify-content: center;
3113
+ width: 32px;
3114
+ height: 32px;
3115
+ border: none;
3116
+ border-radius: 8px;
3117
+ background: transparent;
3118
+ color: var(--dsw-alias-label-primary);
3119
+ cursor: pointer;
3120
+ }
3121
+ .ims-iconBtn:hover { background: var(--dsw-alias-interactive-bg-hover); }
3122
+ .ims-body { flex: 1; min-height: 0; overflow-y: auto; }
3123
+
3124
+ /* ── 通用件 ── */
3125
+ .ims-input {
3126
+ box-sizing: border-box;
3127
+ width: 100%;
3128
+ min-height: 34px;
3129
+ padding: 6px 10px;
3130
+ border: 1px solid var(--dsw-alias-border-l2);
3131
+ border-radius: 8px;
3132
+ background: var(--dsw-alias-bg-layer-3);
3133
+ color: var(--dsw-alias-label-primary);
3134
+ font-family: inherit;
3135
+ font-size: 13px;
3136
+ line-height: 20px;
3137
+ }
3138
+ .ims-input:focus { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 0; }
3139
+ .ims-btn {
3140
+ display: inline-flex;
3141
+ align-items: center;
3142
+ justify-content: center;
3143
+ gap: 6px;
3144
+ min-height: 34px;
3145
+ padding: 6px 14px;
3146
+ border: none;
3147
+ border-radius: 8px;
3148
+ font-family: inherit;
3149
+ font-size: 13px;
3150
+ font-weight: 500;
3151
+ cursor: pointer;
3152
+ white-space: nowrap;
3153
+ }
3154
+ .ims-btn:disabled { opacity: .5; cursor: not-allowed; }
3155
+ .ims-btnPrimary {
3156
+ background: var(--dsw-alias-button-primary-fill);
3157
+ color: var(--dsw-alias-label-primary-inverted);
3158
+ }
3159
+ .ims-btnPrimary:hover:not(:disabled) { background: var(--dsw-alias-button-primary-hover); }
3160
+ .ims-btnGhost {
3161
+ background: transparent;
3162
+ color: var(--dsw-alias-label-primary);
3163
+ border: 1px solid var(--dsw-alias-border-l2);
3164
+ }
3165
+ .ims-btnGhost:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
3166
+ .ims-btnDanger { background: var(--dsw-alias-state-error-primary); color: var(--dsw-alias-label-primary-inverted); }
3167
+ .ims-btnSmall { min-height: 28px; padding: 3px 10px; font-size: 12px; }
3168
+ .ims-hint { margin: 4px 0 0; font-size: 12px; color: var(--dsw-alias-label-tertiary); }
3169
+ .ims-empty {
3170
+ padding: 32px 16px;
3171
+ text-align: center;
3172
+ font-size: 13px;
3173
+ color: var(--dsw-alias-label-tertiary);
3174
+ }
3175
+ .ims-badge {
3176
+ flex: none;
3177
+ padding: 2px 8px;
3178
+ border-radius: 999px;
3179
+ font-size: 11px;
3180
+ line-height: 16px;
3181
+ }
3182
+ .ims-badgeOn { background: var(--dsw-alias-state-success-secondary); color: var(--dsw-alias-state-success-primary); }
3183
+ .ims-badgeOff { background: var(--dsw-alias-state-error-secondary); color: var(--dsw-alias-state-error-primary); }
3184
+ .ims-status { margin: 8px 0 0; font-size: 12px; color: var(--dsw-alias-state-success-primary); }
3185
+ .ims-statusError { color: var(--dsw-alias-state-error-primary); }
3186
+ .ims-spin { animation: ims-rotate 1s linear infinite; }
3187
+ @keyframes ims-rotate { to { transform: rotate(360deg); } }
3188
+
3189
+ /* ── 生图表单 ── */
3190
+ .ims-gen {
3191
+ display: grid;
3192
+ grid-template-columns: minmax(0, 5fr) minmax(0, 4fr);
3193
+ gap: 0 20px;
3194
+ padding: 16px;
3195
+ }
3196
+ .ims-gen[hidden] { display: none; }
3197
+ .ims-genMain, .ims-genSide { min-width: 0; }
3198
+ .ims-block { margin-bottom: 16px; }
3199
+ .ims-blockTitle { margin: 0 0 8px; font-size: 13px; font-weight: 600; }
3200
+ .ims-blockHead { align-items: center; }
3201
+ .ims-blockHead .ims-blockTitle { margin: 0; flex: none; }
3202
+ .ims-providerRow { display: flex; align-items: center; gap: 8px; }
3203
+ .ims-checkLine {
3204
+ display: flex;
3205
+ align-items: center;
3206
+ gap: 8px;
3207
+ margin-top: 10px;
3208
+ font-size: 13px;
3209
+ cursor: pointer;
3210
+ }
3211
+ .ims-inlineGrid { display: grid; gap: 8px; margin-top: 8px; }
3212
+ .ims-mini { display: grid; gap: 4px; font-size: 12px; color: var(--dsw-alias-label-secondary); }
3213
+ .ims-prompt { resize: vertical; min-height: 96px; }
3214
+ .ims-rowActions { display: flex; gap: 8px; margin-top: 8px; align-items: center; }
3215
+ .ims-rowGrow { flex: 1; min-width: 0; }
3216
+
3217
+ /* 参数表单 */
3218
+ .ims-paramGroup { display: grid; gap: 6px; }
3219
+ .ims-paramRow {
3220
+ display: flex;
3221
+ align-items: center;
3222
+ gap: 10px;
3223
+ padding: 6px 8px;
3224
+ border-radius: 8px;
3225
+ }
3226
+ .ims-paramRowOn { background: var(--dsw-alias-bg-layer-1); }
3227
+ .ims-paramText { flex: 1; min-width: 0; display: grid; }
3228
+ .ims-paramKey { font-size: 12px; font-weight: 600; font-family: var(--ds-font-family-code); }
3229
+ .ims-paramGen {
3230
+ margin-left: 6px;
3231
+ padding: 0 5px;
3232
+ border-radius: 4px;
3233
+ background: var(--dsw-alias-bg-module-platform);
3234
+ color: var(--dsw-alias-label-secondary);
3235
+ font-size: 10px;
3236
+ font-weight: 400;
3237
+ }
3238
+ .ims-paramDesc {
3239
+ font-size: 11px;
3240
+ color: var(--dsw-alias-label-tertiary);
3241
+ overflow: hidden;
3242
+ text-overflow: ellipsis;
3243
+ white-space: nowrap;
3244
+ }
3245
+ .ims-paramControl { width: 132px; flex: none; min-height: 30px; padding: 4px 8px; }
3246
+ .ims-adv { margin-top: 8px; }
3247
+ .ims-advSummary {
3248
+ cursor: pointer;
3249
+ font-size: 12px;
3250
+ font-weight: 600;
3251
+ padding: 6px 0;
3252
+ color: var(--dsw-alias-label-secondary);
3253
+ user-select: none;
3254
+ }
3255
+ .ims-advCount {
3256
+ margin-left: 6px;
3257
+ padding: 0 6px;
3258
+ border-radius: 999px;
3259
+ background: var(--dsw-alias-button-primary-fill);
3260
+ color: var(--dsw-alias-label-primary-inverted);
3261
+ font-size: 10px;
3262
+ }
3263
+
3264
+ /* 开关(switch) */
3265
+ .ims-switch { position: relative; display: inline-flex; flex: none; width: 30px; height: 18px; }
3266
+ .ims-switch input { position: absolute; inset: 0; opacity: 0; margin: 0; cursor: pointer; }
3267
+ .ims-switchTrack {
3268
+ position: absolute;
3269
+ inset: 0;
3270
+ border-radius: 999px;
3271
+ background: var(--dsw-alias-bg-module-platform);
3272
+ transition: background .15s;
3273
+ pointer-events: none;
3274
+ }
3275
+ .ims-switchTrack::after {
3276
+ content: '';
3277
+ position: absolute;
3278
+ top: 2px;
3279
+ left: 2px;
3280
+ width: 14px;
3281
+ height: 14px;
3282
+ border-radius: 50%;
3283
+ background: var(--dsw-alias-label-primary);
3284
+ transition: transform .15s;
3285
+ }
3286
+ .ims-switch input:checked + .ims-switchTrack { background: var(--dsw-alias-brand-primary); }
3287
+ .ims-switch input:checked + .ims-switchTrack::after { transform: translateX(12px); }
3288
+
3289
+ /* 源图/遮罩缩略 */
3290
+ .ims-sources { display: flex; flex-wrap: wrap; gap: 8px; }
3291
+ .ims-thumb { position: relative; width: 64px; height: 64px; border-radius: 8px; overflow: hidden; }
3292
+ .ims-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
3293
+ .ims-thumbRemove {
3294
+ position: absolute;
3295
+ top: 2px;
3296
+ right: 2px;
3297
+ width: 18px;
3298
+ height: 18px;
3299
+ border: none;
3300
+ border-radius: 50%;
3301
+ background: rgba(0,0,0,.55);
3302
+ color: #fff;
3303
+ font-size: 12px;
3304
+ line-height: 1;
3305
+ cursor: pointer;
3306
+ }
3307
+ .ims-thumbAdd {
3308
+ display: inline-flex;
3309
+ flex-direction: column;
3310
+ align-items: center;
3311
+ justify-content: center;
3312
+ gap: 4px;
3313
+ width: 64px;
3314
+ height: 64px;
3315
+ border: 1px dashed var(--dsw-alias-border-l1);
3316
+ border-radius: 8px;
3317
+ background: transparent;
3318
+ color: var(--dsw-alias-label-secondary);
3319
+ font-size: 11px;
3320
+ cursor: pointer;
3321
+ }
3322
+ .ims-thumbAdd:hover { background: var(--dsw-alias-interactive-bg-hover); }
3323
+
3324
+ /* 提交区 */
3325
+ .ims-genFoot { grid-column: 1 / -1; border-top: 1px solid var(--dsw-alias-border-l3); padding-top: 12px; }
3326
+ .ims-naming { display: flex; gap: 8px; margin-bottom: 8px; }
3327
+ .ims-submit { width: 100%; min-height: 40px; font-size: 14px; margin-top: 8px; }
3328
+
3329
+ /* ── 子模态(图片选择器 / 画廊详情,官方 Modal 承载)── */
3330
+ .ims-pickerModal {
3331
+ width: min(760px, 92vw) !important;
3332
+ max-width: none !important;
3333
+ max-height: 86vh;
3334
+ padding: 0 !important;
3335
+ gap: 0 !important;
3336
+ }
3337
+ .ims-detailModal {
3338
+ width: min(920px, 94vw) !important;
3339
+ max-width: none !important;
3340
+ max-height: 88vh;
3341
+ padding: 0 !important;
3342
+ gap: 0 !important;
3343
+ }
3344
+ .ims-sub {
3345
+ display: flex;
3346
+ flex-direction: column;
3347
+ min-height: 0;
3348
+ max-height: inherit;
3349
+ color: var(--dsw-alias-label-primary);
3350
+ }
3351
+ .ims-subHead {
3352
+ display: flex;
3353
+ flex: none;
3354
+ align-items: center;
3355
+ justify-content: space-between;
3356
+ gap: 8px;
3357
+ padding: 12px 14px;
3358
+ border-bottom: 1px solid var(--dsw-alias-border-l3);
3359
+ }
3360
+ .ims-subTitle { margin: 0; font-size: 14px; }
3361
+ .ims-subFoot {
3362
+ display: flex;
3363
+ flex: none;
3364
+ align-items: center;
3365
+ justify-content: flex-end;
3366
+ gap: 8px;
3367
+ padding: 10px 14px;
3368
+ border-top: 1px solid var(--dsw-alias-border-l3);
3369
+ }
3370
+ .ims-subFoot .ims-status { margin: 0 auto 0 0; }
3371
+
3372
+ /* 选择器网格 */
3373
+ .ims-pickerGrid {
3374
+ flex: 1;
3375
+ min-height: 0;
3376
+ overflow-y: auto;
3377
+ display: grid;
3378
+ grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
3379
+ gap: 8px;
3380
+ padding: 12px 14px;
3381
+ }
3382
+ .ims-pickerCell {
3383
+ position: relative;
3384
+ aspect-ratio: 1;
3385
+ border: 2px solid transparent;
3386
+ border-radius: 8px;
3387
+ padding: 0;
3388
+ background: none;
3389
+ cursor: pointer;
3390
+ overflow: hidden;
3391
+ }
3392
+ .ims-pickerCell img { width: 100%; height: 100%; object-fit: cover; display: block; }
3393
+ .ims-pickerCellOn { border-color: var(--dsw-alias-brand-primary); }
3394
+ .ims-pickerMark {
3395
+ position: absolute;
3396
+ top: 4px;
3397
+ right: 4px;
3398
+ min-width: 18px;
3399
+ height: 18px;
3400
+ border-radius: 999px;
3401
+ background: var(--dsw-alias-button-primary-fill);
3402
+ color: var(--dsw-alias-label-primary-inverted);
3403
+ font-size: 11px;
3404
+ line-height: 18px;
3405
+ text-align: center;
3406
+ }
3407
+
3408
+ /* ── 画廊 ── */
3409
+ .ims-gallery { padding: 12px 16px; }
3410
+ .ims-gallery[hidden] { display: none; }
3411
+ .ims-galleryBar { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
3412
+ .ims-grid {
3413
+ display: grid;
3414
+ grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
3415
+ gap: 10px;
3416
+ }
3417
+ .ims-cell {
3418
+ position: relative;
3419
+ aspect-ratio: 1;
3420
+ border: none;
3421
+ border-radius: 10px;
3422
+ padding: 0;
3423
+ background: var(--dsw-alias-bg-layer-1);
3424
+ cursor: pointer;
3425
+ overflow: hidden;
3426
+ }
3427
+ .ims-cell img { width: 100%; height: 100%; object-fit: cover; display: block; }
3428
+ .ims-cellPrompt {
3429
+ position: absolute;
3430
+ left: 0;
3431
+ right: 0;
3432
+ bottom: 0;
3433
+ padding: 14px 8px 6px;
3434
+ background: linear-gradient(transparent, rgba(0,0,0,.72));
3435
+ color: #fff;
3436
+ font-size: 11px;
3437
+ text-align: left;
3438
+ overflow: hidden;
3439
+ text-overflow: ellipsis;
3440
+ white-space: nowrap;
3441
+ opacity: 0;
3442
+ transition: opacity .15s;
3443
+ }
3444
+ .ims-cell:hover .ims-cellPrompt, .ims-cell:focus-visible .ims-cellPrompt { opacity: 1; }
3445
+
3446
+ /* 详情 */
3447
+ .ims-detailMeta { font-size: 12px; color: var(--dsw-alias-label-tertiary); }
3448
+ .ims-detailBody {
3449
+ flex: 1;
3450
+ min-height: 0;
3451
+ overflow-y: auto;
3452
+ display: grid;
3453
+ grid-template-columns: minmax(0, 3fr) minmax(0, 2fr);
3454
+ gap: 16px;
3455
+ padding: 14px;
3456
+ }
3457
+ .ims-detailImages { display: grid; gap: 10px; align-content: start; }
3458
+ .ims-detailFigure { margin: 0; display: grid; gap: 6px; }
3459
+ .ims-detailFigure img {
3460
+ width: 100%;
3461
+ border-radius: 10px;
3462
+ background: var(--dsw-alias-bg-layer-1);
3463
+ display: block;
3464
+ }
3465
+ .ims-detailFigure figcaption { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
3466
+ .ims-revised {
3467
+ font-size: 11px;
3468
+ color: var(--dsw-alias-label-tertiary);
3469
+ overflow: hidden;
3470
+ text-overflow: ellipsis;
3471
+ white-space: nowrap;
3472
+ }
3473
+ .ims-detailInfo { min-width: 0; }
3474
+ .ims-detailLabel { margin: 12px 0 6px; font-size: 12px; font-weight: 600; }
3475
+ .ims-detailLabel:first-child { margin-top: 0; }
3476
+ .ims-detailPrompt { margin: 0; font-size: 13px; white-space: pre-wrap; word-break: break-word; }
3477
+ .ims-paramTable { width: 100%; border-collapse: collapse; font-size: 12px; }
3478
+ .ims-paramTable td { padding: 4px 6px; border-top: 1px solid var(--dsw-alias-border-l3); word-break: break-all; }
3479
+ .ims-paramTableKey { font-family: var(--ds-font-family-code); font-weight: 600; white-space: nowrap; }
3480
+
3481
+ /* ── 任务条 ── */
3482
+ .ims-tasks {
3483
+ display: flex;
3484
+ flex: none;
3485
+ gap: 8px;
3486
+ padding: 8px 12px;
3487
+ margin: 0;
3488
+ overflow-x: auto;
3489
+ border-top: 1px solid var(--dsw-alias-border-l3);
3490
+ }
3491
+ .ims-task {
3492
+ display: inline-flex;
3493
+ flex: none;
3494
+ align-items: center;
3495
+ gap: 6px;
3496
+ max-width: 340px;
3497
+ padding: 4px 10px;
3498
+ border-radius: 999px;
3499
+ background: var(--dsw-alias-bg-layer-1);
3500
+ font-size: 12px;
3501
+ }
3502
+ .ims-taskState { font-weight: 600; flex: none; }
3503
+ .ims-taskState-running { color: var(--dsw-alias-brand-primary); }
3504
+ .ims-taskState-queued { color: var(--dsw-alias-label-tertiary); }
3505
+ .ims-taskState-succeeded { color: var(--dsw-alias-state-success-primary); }
3506
+ .ims-taskState-failed { color: var(--dsw-alias-state-error-primary); }
3507
+ .ims-taskState-cancelled { color: var(--dsw-alias-label-tertiary); }
3508
+ .ims-taskPrompt { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
3509
+ .ims-taskModel { flex: none; color: var(--dsw-alias-label-tertiary); font-size: 11px; }
3510
+ .ims-taskError {
3511
+ max-width: 140px;
3512
+ overflow: hidden;
3513
+ text-overflow: ellipsis;
3514
+ white-space: nowrap;
3515
+ color: var(--dsw-alias-state-error-primary);
3516
+ }
3517
+
3518
+ /* ── 配置卡片附加件(imsc- 基础类由 cardCss('imsc') 生成)── */
3519
+ .imsc-loading{padding:14px 16px;margin:0;font-size:13px;color:var(--dsw-alias-label-tertiary)}
3520
+ .imsc-groupTitle { margin: 14px 0 6px; font-size: 13px; font-weight: 600; }
3521
+ .imsc-presetBox {
3522
+ border: 1px solid var(--dsw-alias-border-l3);
3523
+ border-radius: 10px;
3524
+ padding: 10px;
3525
+ margin-bottom: 10px;
3526
+ display: grid;
3527
+ gap: 8px;
3528
+ }
3529
+ .imsc-presetHead { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
3530
+ .imsc-presetName { font-size: 13px; font-weight: 600; }
3531
+ .imsc-grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
3532
+ .imsc-textarea {
3533
+ box-sizing: border-box;
3534
+ width: 100%;
3535
+ min-height: 64px;
3536
+ padding: 6px 10px;
3537
+ border: 1px solid var(--dsw-alias-border-l2);
3538
+ border-radius: 8px;
3539
+ background: var(--dsw-alias-bg-layer-3);
3540
+ color: var(--dsw-alias-label-primary);
3541
+ font-family: var(--ds-font-family-code);
3542
+ font-size: 12px;
3543
+ resize: vertical;
3544
+ }
3545
+ .imsc-credRow { display: flex; align-items: center; gap: 8px; }
3546
+ .imsc-credRow .imsc-input { flex: 1; min-width: 0; }
3547
+
3548
+ /* ── 移动端(≤767px,与 ui-mobile-fit 同断点)── */
3549
+ @media (max-width: 767px) {
3550
+ .ims-modal {
3551
+ position: fixed;
3552
+ inset: 0;
3553
+ margin: 0;
3554
+ width: 100% !important;
3555
+ max-width: none !important;
3556
+ height: 100%;
3557
+ max-height: none;
3558
+ border: none;
3559
+ border-radius: 0 !important;
3560
+ }
3561
+ .ims-gen { grid-template-columns: minmax(0, 1fr); }
3562
+ .ims-genSide { margin-top: 4px; }
3563
+ .ims-tab { padding: 0 10px; }
3564
+ .ims-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
3565
+ .ims-detailBody { grid-template-columns: minmax(0, 1fr); }
3566
+ .ims-pickerModal, .ims-detailModal {
3567
+ position: fixed;
3568
+ inset: 0;
3569
+ width: 100% !important;
3570
+ max-width: none !important;
3571
+ max-height: none;
3572
+ height: 100%;
3573
+ border: none;
3574
+ border-radius: 0 !important;
3575
+ }
3576
+ .ims-paramControl { width: 108px; }
3577
+ .ims-btn { min-height: 40px; }
3578
+ .ims-btnSmall { min-height: 32px; }
3579
+ .imsc-grid2 { grid-template-columns: 1fr; }
3580
+ }
3581
+ `;
3582
+ /** 注入面板 + 入口 + 卡片附加样式(重复调用幂等:已存在则跳过)。 */
3583
+ function injectStudioStyles() {
78
3584
  if (typeof document === "undefined") return null;
3585
+ if (document.querySelector(`style[data-plugin-css=${JSON.stringify(STYLE_TAG_ID)}]`) !== null) return null;
79
3586
  const tag = document.createElement("style");
80
3587
  tag.dataset.plugin = PKG;
81
- tag.dataset.pluginCss = `${PKG}/${id}`;
82
- tag.textContent = css;
3588
+ tag.dataset.pluginCss = STYLE_TAG_ID;
3589
+ tag.textContent = imageStudioCss;
83
3590
  document.head.appendChild(tag);
84
3591
  return tag;
85
3592
  }
86
3593
  //#endregion
87
3594
  //#region src/client/client.ts
88
3595
  const name = "dsh-plus-image-studio";
89
- /** 浏览器半需要的 cordis 服务 key(loader 据此注入)。 */
3596
+ /** 浏览器半需要的 cordis 服务 key(loader 据此注入;package.json 的 dsh.client.inject 管包加载顺序)。 */
90
3597
  const inject = [
91
3598
  "slots",
92
3599
  "locale",
93
3600
  "remote",
94
3601
  "remote.settings"
95
3602
  ];
3603
+ const PLUGIN_ID = "@dsh-plus/image-studio";
96
3604
  function apply(ctx) {
97
3605
  const c = ctx;
98
- const sectionTag = injectSectionStyle();
99
- const cardTag = injectCardStyles();
100
- c.effect(() => () => {
101
- sectionTag?.remove();
102
- cardTag?.remove();
103
- }, "image-studio: style");
3606
+ const panelTag = injectStudioStyles();
3607
+ const cardTag = injectCardStyle(PLUGIN_ID, cardCss("imsc"));
3608
+ const controller = createPanelController();
104
3609
  c.effect(() => c.locale.register(NS, {
105
3610
  zh,
106
3611
  en
107
3612
  }), "image-studio: locale");
108
3613
  const t = c.locale.bind(NS);
109
- c.slots.inject("settings.section", () => c.slots.register({
110
- name: "settings.section",
111
- id: "dsh-plus-image-studio",
112
- order: 16,
113
- label: () => t("nav"),
114
- inject: () => ({ t })
115
- }, StudioSection));
3614
+ c.effect(() => c.slots.inject("sidebar.footer.action", () => c.slots.register({
3615
+ name: "sidebar.footer.action",
3616
+ id: "image-studio-entry",
3617
+ locale: NS,
3618
+ inject: () => ({
3619
+ studio: controller,
3620
+ t
3621
+ })
3622
+ }, StudioEntryButton)), "image-studio: entry slot");
3623
+ const scope = createSettingsScope(c, SETTINGS_NS, "image-studio: settings scope");
3624
+ const api = createNamespaceApi(c.get("remote").settings, SETTINGS_NS);
3625
+ c.effect(() => c.slots.inject("shell.overlay", () => c.slots.register({
3626
+ name: "shell.overlay",
3627
+ id: "image-studio-panel",
3628
+ locale: NS,
3629
+ inject: () => ({
3630
+ studio: controller,
3631
+ t,
3632
+ scope,
3633
+ api
3634
+ })
3635
+ }, StudioPanel)), "image-studio: overlay slot");
116
3636
  c.slots.inject("settings.plugin.item", () => c.slots.register({
117
3637
  name: "settings.plugin.item",
118
- key: "dsh-plus-image-studio",
3638
+ key: SETTINGS_NS,
119
3639
  locale: NS,
120
- inject: () => ({ t })
3640
+ inject: () => ({
3641
+ t,
3642
+ scope,
3643
+ api
3644
+ })
121
3645
  }, StudioConfigCard));
3646
+ c.effect(() => () => {
3647
+ panelTag?.remove();
3648
+ cardTag?.remove();
3649
+ }, "image-studio: cleanup");
122
3650
  }
123
3651
  //#endregion
124
3652
  exports.apply = apply;