@dsh-plus/image-studio 0.1.0 → 0.1.1

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,3630 @@ 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))
22
948
  };
23
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);
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
+ });
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
+ });
59
1719
  };
1720
+ const paramRows = Object.entries(item.params);
1721
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1722
+ className: "ims-detailBackdrop",
1723
+ role: "presentation",
1724
+ onClick: onClose,
1725
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1726
+ className: "ims-detail",
1727
+ role: "dialog",
1728
+ "aria-modal": "true",
1729
+ "aria-label": item.prompt.slice(0, 40),
1730
+ onClick: (event) => event.stopPropagation(),
1731
+ children: [
1732
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1733
+ className: "ims-detailHead",
1734
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1735
+ className: "ims-detailMeta",
1736
+ children: [
1737
+ t("gallery.detail.model"),
1738
+ ":",
1739
+ item.model,
1740
+ " · ",
1741
+ t("gallery.detail.endpoint"),
1742
+ ":",
1743
+ item.endpoint,
1744
+ " · ",
1745
+ t("gallery.detail.time"),
1746
+ ":",
1747
+ fmtTime(item.createdAt)
1748
+ ]
1749
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1750
+ type: "button",
1751
+ className: "ims-btn ims-btnGhost",
1752
+ onClick: onClose,
1753
+ children: t("common.close")
1754
+ })]
1755
+ }),
1756
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1757
+ className: "ims-detailBody",
1758
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1759
+ className: "ims-detailImages",
1760
+ children: item.imageIds.map((imageId, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("figure", {
1761
+ className: "ims-detailFigure",
1762
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
1763
+ src: imageUrl(imageId),
1764
+ alt: item.prompt
1765
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("figcaption", { children: [item.revisedPrompts[index] !== null && item.revisedPrompts[index] !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1766
+ className: "ims-revised",
1767
+ title: item.revisedPrompts[index] ?? "",
1768
+ children: [
1769
+ t("gallery.detail.revised"),
1770
+ ":",
1771
+ item.revisedPrompts[index]
1772
+ ]
1773
+ }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("a", {
1774
+ className: "ims-btn ims-btnGhost ims-btnSmall",
1775
+ href: imageUrl(imageId),
1776
+ download: true,
1777
+ children: [
1778
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconDownload, { size: 14 }),
1779
+ " ",
1780
+ t("common.download")
1781
+ ]
1782
+ })] })]
1783
+ }, imageId))
1784
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1785
+ className: "ims-detailInfo",
1786
+ children: [
1787
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
1788
+ className: "ims-detailLabel",
1789
+ children: t("gallery.detail.prompt")
1790
+ }),
1791
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1792
+ className: "ims-detailPrompt",
1793
+ children: item.prompt
1794
+ }),
1795
+ item.sourceIds.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
1796
+ className: "ims-detailLabel",
1797
+ children: t("gallery.detail.sources")
1798
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1799
+ className: "ims-sources",
1800
+ children: item.sourceIds.map((imageId) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1801
+ className: "ims-thumb",
1802
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
1803
+ src: imageUrl(imageId),
1804
+ alt: ""
1805
+ })
1806
+ }, imageId))
1807
+ })] }) : null,
1808
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
1809
+ className: "ims-detailLabel",
1810
+ children: t("gallery.detail.params")
1811
+ }),
1812
+ paramRows.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1813
+ className: "ims-hint",
1814
+ children: "—"
1815
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("table", {
1816
+ className: "ims-paramTable",
1817
+ 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", {
1818
+ className: "ims-paramTableKey",
1819
+ children: key
1820
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: JSON.stringify(value) })] }, key)) })
1821
+ })
1822
+ ]
1823
+ })]
1824
+ }),
1825
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1826
+ className: "ims-detailFoot",
1827
+ children: [
1828
+ error !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1829
+ className: "ims-status ims-statusError",
1830
+ role: "alert",
1831
+ children: error
1832
+ }) : null,
1833
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1834
+ type: "button",
1835
+ className: "ims-btn ims-btnPrimary",
1836
+ onClick: () => onEdit(item),
1837
+ children: t("gallery.edit")
1838
+ }),
1839
+ confirming ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1840
+ type: "button",
1841
+ className: "ims-btn ims-btnDanger",
1842
+ disabled: deleting,
1843
+ onClick: doDelete,
1844
+ children: t("common.confirmDelete")
1845
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1846
+ type: "button",
1847
+ className: "ims-btn ims-btnGhost",
1848
+ onClick: () => setConfirming(true),
1849
+ children: [
1850
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconTrashOutline16, { size: 14 }),
1851
+ " ",
1852
+ t("common.delete")
1853
+ ]
1854
+ })
1855
+ ]
1856
+ })
1857
+ ]
1858
+ })
1859
+ });
1860
+ }
1861
+ function GalleryView(props) {
1862
+ const { t, items, loading, failed, onRefresh, onEdit, detailId, onDetailChange } = props;
1863
+ const detail = items.find((item) => item.id === detailId) ?? null;
1864
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1865
+ className: "ims-gallery",
1866
+ hidden: props.hidden,
1867
+ children: [
1868
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1869
+ className: "ims-galleryBar",
1870
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1871
+ className: "ims-hint",
1872
+ children: t("gallery.total").replace("{n}", String(items.length))
1873
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1874
+ type: "button",
1875
+ className: "ims-btn ims-btnGhost ims-btnSmall",
1876
+ onClick: onRefresh,
1877
+ children: [
1878
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconRefreshOutline16, { size: 14 }),
1879
+ " ",
1880
+ t("common.refresh")
1881
+ ]
1882
+ })]
1883
+ }),
1884
+ failed ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1885
+ className: "ims-empty",
1886
+ children: [t("common.loadFailed"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1887
+ type: "button",
1888
+ className: "ims-btn ims-btnGhost ims-btnSmall",
1889
+ onClick: onRefresh,
1890
+ children: t("common.retry")
1891
+ })]
1892
+ }) : loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1893
+ className: "ims-empty",
1894
+ children: t("common.loading")
1895
+ }) : items.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1896
+ className: "ims-empty",
1897
+ children: t("gallery.empty")
1898
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1899
+ className: "ims-grid",
1900
+ children: [...items].reverse().map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1901
+ type: "button",
1902
+ className: "ims-cell",
1903
+ onClick: () => onDetailChange(item.id),
1904
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
1905
+ src: imageUrl(item.imageIds[0] ?? ""),
1906
+ alt: item.prompt,
1907
+ loading: "lazy"
1908
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1909
+ className: "ims-cellPrompt",
1910
+ children: item.prompt
1911
+ })]
1912
+ }, item.id))
1913
+ }),
1914
+ detail !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GalleryDetail, {
1915
+ t,
1916
+ item: detail,
1917
+ onClose: () => onDetailChange(null),
1918
+ onEdit: (item) => {
1919
+ onDetailChange(null);
1920
+ onEdit(item);
1921
+ },
1922
+ onDeleted: () => {
1923
+ onDetailChange(null);
1924
+ onRefresh();
1925
+ }
1926
+ }) : null
1927
+ ]
1928
+ });
1929
+ }
1930
+ //#endregion
1931
+ //#region src/client/panel/param-state.ts
1932
+ /**
1933
+ * 参数表单状态纯函数:目录(ParamEntry)驱动的表单状态 ⇄ ParamSpecMap。
1934
+ * - 表单值统一字符串/布尔(输入控件友好),提交时按 kind 解析回数值;
1935
+ * - 画廊条目 params 回放、参数预设应用、预设另存均经此模块换算;
1936
+ * - 纯函数零 DOM 依赖,可 node 单测。
1937
+ * @module image-studio/client/panel/param-state
1938
+ */
1939
+ /** 各参数的顺手默认值(启用控件时的预填,非官方默认)。 */
1940
+ const DEFAULT_VALUES = {
1941
+ n: "1",
1942
+ size: "1024x1024",
1943
+ quality: "auto",
1944
+ background: "auto",
1945
+ output_format: "png",
1946
+ output_compression: "100",
1947
+ moderation: "auto",
1948
+ response_format: "url",
1949
+ stream: false,
1950
+ partial_images: "0",
1951
+ user: "",
1952
+ input_fidelity: "high",
1953
+ style: "vivid"
1954
+ };
1955
+ /** 目录条目 → 表单默认值(enum 取首值兜底)。 */
1956
+ function defaultValueFor(entry) {
1957
+ const known = DEFAULT_VALUES[entry.key];
1958
+ if (known !== void 0) return known;
1959
+ if (entry.kind === "enum") return entry.values?.[0] ?? "";
1960
+ if (entry.kind === "boolean") return false;
1961
+ return "";
1962
+ }
1963
+ /** 端点可用的目录条目(剔除 host 承载参数 model)。 */
1964
+ function paramEntriesFor(catalog, endpoint) {
1965
+ return catalog.filter((entry) => entry.applies.includes(endpoint) && !isHostParam(entry.key));
1966
+ }
1967
+ /** 空表单(全部关闭 + 默认值)。 */
1968
+ function emptyParamForm(catalog, endpoint) {
1969
+ const form = {};
1970
+ for (const entry of paramEntriesFor(catalog, endpoint)) form[entry.key] = {
1971
+ enabled: false,
1972
+ value: defaultValueFor(entry)
1973
+ };
1974
+ return form;
1975
+ }
1976
+ /** 表单值 ← 服务端参数值(按 kind 字符串化;未知键忽略)。 */
1977
+ function fieldValueOf(entry, value) {
1978
+ if (entry.kind === "boolean") return value === true;
1979
+ if (typeof value === "number") return String(value);
1980
+ if (typeof value === "string") return value;
1981
+ return defaultValueFor(entry);
1982
+ }
1983
+ /** 套用参数表(预设应用/画廊回放共用):仅目录内且端点适用的键生效并启用。 */
1984
+ function applyParams(catalog, endpoint, params) {
1985
+ const form = emptyParamForm(catalog, endpoint);
1986
+ for (const entry of paramEntriesFor(catalog, endpoint)) {
1987
+ const value = params[entry.key];
1988
+ if (value === void 0) continue;
1989
+ form[entry.key] = {
1990
+ enabled: true,
1991
+ value: fieldValueOf(entry, value)
1992
+ };
1993
+ }
1994
+ return form;
1995
+ }
1996
+ /** 参数预设(ParamSpecMap 形态)套用到表单:enabled=false 的键显式关闭。 */
1997
+ function applySpecs(catalog, endpoint, specs) {
1998
+ const form = emptyParamForm(catalog, endpoint);
1999
+ for (const entry of paramEntriesFor(catalog, endpoint)) {
2000
+ const spec = specs[entry.key];
2001
+ if (spec === void 0) continue;
2002
+ form[entry.key] = {
2003
+ enabled: spec.enabled,
2004
+ value: spec.enabled && spec.value !== void 0 ? fieldValueOf(entry, spec.value) : defaultValueFor(entry)
2005
+ };
2006
+ }
2007
+ return form;
2008
+ }
2009
+ /**
2010
+ * 表单 → ParamSpecMap(提交/另存预设共用):仅导出启用项,integer 按数值解析;
2011
+ * 空字符串 integer 视为未填(导出 {enabled:true} 无 value,交由边界校验报错)。
2012
+ */
2013
+ function formToSpecs(catalog, endpoint, form) {
2014
+ const specs = {};
2015
+ for (const entry of paramEntriesFor(catalog, endpoint)) {
2016
+ const field = form[entry.key];
2017
+ if (field === void 0 || !field.enabled) continue;
2018
+ if (entry.kind === "boolean") {
2019
+ specs[entry.key] = {
2020
+ enabled: true,
2021
+ value: field.value === true
2022
+ };
2023
+ continue;
2024
+ }
2025
+ const text = String(field.value).trim();
2026
+ if (entry.kind === "integer") {
2027
+ if (text === "") {
2028
+ specs[entry.key] = { enabled: true };
2029
+ continue;
2030
+ }
2031
+ const parsed = Number(text);
2032
+ specs[entry.key] = {
2033
+ enabled: true,
2034
+ value: Number.isNaN(parsed) ? text : parsed
2035
+ };
2036
+ continue;
2037
+ }
2038
+ specs[entry.key] = {
2039
+ enabled: true,
2040
+ value: text
2041
+ };
2042
+ }
2043
+ return specs;
2044
+ }
2045
+ //#endregion
2046
+ //#region src/client/panel/params-form.tsx
2047
+ /** 单参数行:开关 + 名称/说明 + 值控件。 */
2048
+ function ParamRow(props) {
2049
+ const { t, entry, form, onChange } = props;
2050
+ const field = form[entry.key] ?? {
2051
+ enabled: false,
2052
+ value: ""
2053
+ };
2054
+ const patch = (next) => onChange({
2055
+ ...form,
2056
+ [entry.key]: {
2057
+ ...field,
2058
+ ...next
2059
+ }
2060
+ });
2061
+ const control = (() => {
2062
+ if (entry.kind === "boolean") return null;
2063
+ if (entry.kind === "enum") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
2064
+ className: "ims-input ims-paramControl",
2065
+ value: String(field.value),
2066
+ disabled: !field.enabled,
2067
+ onChange: (event) => patch({ value: event.target.value }),
2068
+ children: (entry.values ?? []).map((value) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2069
+ value,
2070
+ children: value
2071
+ }, value))
2072
+ });
2073
+ if (entry.kind === "integer") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2074
+ className: "ims-input ims-paramControl",
2075
+ type: "number",
2076
+ inputMode: "numeric",
2077
+ min: entry.min,
2078
+ max: entry.max,
2079
+ value: String(field.value),
2080
+ disabled: !field.enabled,
2081
+ onChange: (event) => patch({ value: event.target.value })
2082
+ });
2083
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2084
+ className: "ims-input ims-paramControl",
2085
+ type: "text",
2086
+ value: String(field.value),
2087
+ disabled: !field.enabled,
2088
+ onChange: (event) => patch({ value: event.target.value })
2089
+ });
2090
+ })();
2091
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2092
+ className: `ims-paramRow${field.enabled ? " ims-paramRowOn" : ""}`,
2093
+ children: [
2094
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2095
+ className: "ims-switch",
2096
+ title: t("params.enabled"),
2097
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2098
+ type: "checkbox",
2099
+ role: "switch",
2100
+ "aria-label": `${entry.key} ${t("params.enabled")}`,
2101
+ "aria-checked": field.enabled,
2102
+ checked: field.enabled,
2103
+ onChange: (event) => entry.kind === "boolean" ? patch({
2104
+ enabled: event.target.checked,
2105
+ value: event.target.checked
2106
+ }) : patch({ enabled: event.target.checked })
2107
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2108
+ className: "ims-switchTrack",
2109
+ "aria-hidden": "true"
2110
+ })]
2111
+ }),
2112
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2113
+ className: "ims-paramText",
2114
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2115
+ className: "ims-paramKey",
2116
+ children: [entry.key, entry.generation !== "current" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2117
+ className: "ims-paramGen",
2118
+ children: entry.generation
2119
+ }) : null]
2120
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2121
+ className: "ims-paramDesc",
2122
+ children: entry.description
2123
+ })]
2124
+ }),
2125
+ control
2126
+ ]
2127
+ });
2128
+ }
2129
+ function ParamsForm(props) {
2130
+ const { t, catalog, endpoint, form, onChange } = props;
2131
+ const entries = paramEntriesFor(catalog, endpoint);
2132
+ const basics = entries.filter((entry) => !entry.advanced);
2133
+ const advanced = entries.filter((entry) => entry.advanced);
2134
+ const advancedOn = advanced.filter((entry) => form[entry.key]?.enabled === true).length;
2135
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2136
+ className: "ims-params",
2137
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2138
+ className: "ims-paramGroup",
2139
+ children: basics.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ParamRow, {
2140
+ t,
2141
+ entry,
2142
+ form,
2143
+ onChange
2144
+ }, entry.key))
2145
+ }), advanced.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
2146
+ className: "ims-adv",
2147
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("summary", {
2148
+ className: "ims-advSummary",
2149
+ children: [t("params.advanced"), advancedOn > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2150
+ className: "ims-advCount",
2151
+ children: advancedOn
2152
+ }) : null]
2153
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2154
+ className: "ims-paramGroup",
2155
+ children: advanced.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ParamRow, {
2156
+ t,
2157
+ entry,
2158
+ form,
2159
+ onChange
2160
+ }, entry.key))
2161
+ })]
2162
+ }) : null]
2163
+ });
2164
+ }
2165
+ //#endregion
2166
+ //#region src/client/panel/picker.tsx
2167
+ /**
2168
+ * 画廊图片选择器(模态):源图多选(≤16)与遮罩单选共用。
2169
+ * 扁平化全部画廊条目的图片,倒序排列;底部确认条显示已选数量。
2170
+ * @module image-studio/client/panel/picker
2171
+ */
2172
+ function flattenImages(items) {
2173
+ const out = [];
2174
+ for (const item of [...items].reverse()) for (const imageId of item.imageIds) out.push({
2175
+ imageId,
2176
+ prompt: item.prompt,
2177
+ createdAt: item.createdAt
2178
+ });
2179
+ return out;
2180
+ }
2181
+ function ImagePicker(props) {
2182
+ const { t, title, items, multiple, initial, onConfirm, onClose } = props;
2183
+ const [selected, setSelected] = (0, react.useState)(initial);
2184
+ const images = flattenImages(items);
2185
+ const toggle = (imageId) => {
2186
+ if (multiple) {
2187
+ setSelected((current) => current.includes(imageId) ? current.filter((id) => id !== imageId) : current.length >= 16 ? current : [...current, imageId]);
2188
+ return;
2189
+ }
2190
+ setSelected((current) => current.includes(imageId) ? [] : [imageId]);
2191
+ };
2192
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2193
+ className: "ims-pickerBackdrop",
2194
+ role: "presentation",
2195
+ onClick: onClose,
2196
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2197
+ className: "ims-picker",
2198
+ role: "dialog",
2199
+ "aria-modal": "true",
2200
+ "aria-label": title,
2201
+ onClick: (event) => event.stopPropagation(),
2202
+ children: [
2203
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2204
+ className: "ims-pickerHead",
2205
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
2206
+ className: "ims-pickerTitle",
2207
+ children: title
2208
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2209
+ type: "button",
2210
+ className: "ims-btn ims-btnGhost",
2211
+ onClick: onClose,
2212
+ children: t("common.close")
2213
+ })]
2214
+ }),
2215
+ images.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2216
+ className: "ims-empty",
2217
+ children: t("picker.empty")
2218
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2219
+ className: "ims-pickerGrid",
2220
+ role: "listbox",
2221
+ "aria-multiselectable": multiple,
2222
+ children: images.map((image) => {
2223
+ const on = selected.includes(image.imageId);
2224
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2225
+ type: "button",
2226
+ role: "option",
2227
+ "aria-selected": on,
2228
+ className: `ims-pickerCell${on ? " ims-pickerCellOn" : ""}`,
2229
+ title: image.prompt,
2230
+ onClick: () => toggle(image.imageId),
2231
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
2232
+ src: imageUrl(image.imageId),
2233
+ alt: image.prompt,
2234
+ loading: "lazy"
2235
+ }), on ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2236
+ className: "ims-pickerMark",
2237
+ children: selected.indexOf(image.imageId) + 1
2238
+ }) : null]
2239
+ }, image.imageId);
2240
+ })
2241
+ }),
2242
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2243
+ className: "ims-pickerFoot",
2244
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2245
+ type: "button",
2246
+ className: "ims-btn ims-btnPrimary",
2247
+ disabled: selected.length === 0,
2248
+ onClick: () => onConfirm(selected),
2249
+ children: t("picker.confirm").replace("{n}", String(selected.length))
2250
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2251
+ type: "button",
2252
+ className: "ims-btn ims-btnGhost",
2253
+ onClick: onClose,
2254
+ children: t("common.cancel")
2255
+ })]
2256
+ })
2257
+ ]
2258
+ })
2259
+ });
2260
+ }
2261
+ //#endregion
2262
+ //#region src/client/panel/generator.tsx
2263
+ /**
2264
+ * 生图工作台表单(文生图/图生图共用,endpoint 区分):
2265
+ * - 提供商预设选择 + 凭据状态徽标 + inline 覆盖(凭据沿用预设);
2266
+ * - 提示词编辑 + 预设插入/另存;
2267
+ * - 目录驱动的参数表单 + 参数预设应用/另存;
2268
+ * - 图生图追加源图(≤16)与可选遮罩选择(画廊取图);
2269
+ * - 提交 generate → 202 taskId 交由面板任务条轮询。
2270
+ * 预设另存走 settings RPC(scope/api 注入),成功后经 onPresetSaved 刷新快照。
2271
+ * @module image-studio/client/panel/generator
2272
+ */
2273
+ const IDLE = {
2274
+ kind: "idle",
2275
+ text: ""
2276
+ };
2277
+ /** 预设 id 生成(稳定唯一即可;provider 预设的 kebab 约束不适用此处)。 */
2278
+ function presetId(prefix) {
2279
+ return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
2280
+ }
2281
+ function GeneratorForm(props) {
2282
+ const { t, endpoint, catalog, protocols, presets, gallery, scope, api } = props;
2283
+ const [providerId, setProviderId] = (0, react.useState)("");
2284
+ const [inline, setInline] = (0, react.useState)({
2285
+ open: false,
2286
+ protocol: protocols[0]?.id ?? "openai-images",
2287
+ baseUrl: "",
2288
+ model: ""
2289
+ });
2290
+ const [credentialOk, setCredentialOk] = (0, react.useState)(null);
2291
+ const [prompt, setPrompt] = (0, react.useState)("");
2292
+ const [form, setForm] = (0, react.useState)({});
2293
+ const [sourceIds, setSourceIds] = (0, react.useState)([]);
2294
+ const [maskId, setMaskId] = (0, react.useState)(null);
2295
+ const [picker, setPicker] = (0, react.useState)(null);
2296
+ const [naming, setNaming] = (0, react.useState)(null);
2297
+ const [status, setStatus] = (0, react.useState)(IDLE);
2298
+ const [submitting, setSubmitting] = (0, react.useState)(false);
2299
+ /** 目录就绪后初始化表单一次(catalog 会话内静态,重载不覆盖用户编辑)。 */
2300
+ const formReadyRef = (0, react.useRef)(false);
2301
+ /** 已消费的种子 nonce(防重复回填)。 */
2302
+ const seedRef = (0, react.useRef)(0);
2303
+ (0, react.useEffect)(() => {
2304
+ if (formReadyRef.current || catalog.length === 0) return;
2305
+ formReadyRef.current = true;
2306
+ setForm(emptyParamForm(catalog, endpoint));
2307
+ }, [catalog, endpoint]);
2308
+ const seed = endpoint === "edit" ? props.editSeed : null;
2309
+ (0, react.useEffect)(() => {
2310
+ if (seed === null || seed.nonce === seedRef.current || !formReadyRef.current) return;
2311
+ seedRef.current = seed.nonce;
2312
+ setPrompt(seed.prompt);
2313
+ setSourceIds(seed.sourceIds);
2314
+ setForm(applyParams(catalog, endpoint, seed.params));
2315
+ if (seed.providerPresetId !== null) setProviderId(seed.providerPresetId);
2316
+ setStatus(IDLE);
2317
+ }, [
2318
+ seed,
2319
+ catalog,
2320
+ endpoint
2321
+ ]);
2322
+ (0, react.useEffect)(() => {
2323
+ if (providerId === "") {
2324
+ setCredentialOk(null);
2325
+ return;
2326
+ }
2327
+ let alive = true;
2328
+ fetchCredentialStatus(providerId).then((res) => {
2329
+ if (alive) setCredentialOk(res.configured);
2330
+ }).catch(() => {
2331
+ if (alive) setCredentialOk(null);
2332
+ });
2333
+ return () => {
2334
+ alive = false;
2335
+ };
2336
+ }, [providerId]);
2337
+ const paramPresets = (0, react.useMemo)(() => presets.paramPresets.filter((preset) => preset.endpoint === endpoint), [presets, endpoint]);
2338
+ const selectedProvider = presets.providerPresets.find((preset) => preset.id === providerId);
2339
+ const toggleInline = (open) => {
2340
+ setInline((current) => ({
2341
+ ...current,
2342
+ open,
2343
+ baseUrl: open && current.baseUrl === "" ? selectedProvider?.baseUrl ?? "" : current.baseUrl,
2344
+ model: open && current.model === "" ? selectedProvider?.model ?? "" : current.model,
2345
+ protocol: open && current.protocol === "" ? selectedProvider?.protocol ?? current.protocol : current.protocol
2346
+ }));
2347
+ };
2348
+ const applyParamPreset = (id) => {
2349
+ if (id === "") return;
2350
+ const preset = paramPresets.find((item) => item.id === id);
2351
+ if (preset === void 0) return;
2352
+ try {
2353
+ const specs = validateParamSpecs(preset.paramSpecs);
2354
+ setForm(applySpecs(catalog, endpoint, specs));
2355
+ setStatus(IDLE);
2356
+ } catch (error) {
2357
+ setStatus({
2358
+ kind: "error",
2359
+ text: error instanceof Error ? error.message : String(error)
2360
+ });
2361
+ }
2362
+ };
2363
+ /** 预设另存(settings RPC + revision fencing,成功后刷新快照)。 */
2364
+ const savePreset = () => {
2365
+ if (naming === null || naming.name.trim() === "") return;
2366
+ const snapshot = scope.getSnapshot();
2367
+ const value = snapshot.value ?? {};
2368
+ const patch = naming.kind === "prompt" ? { promptPresets: [...value.promptPresets ?? [], {
2369
+ id: presetId("prompt"),
2370
+ name: naming.name.trim(),
2371
+ text: prompt
2372
+ }] } : { paramPresets: [...value.paramPresets ?? [], {
2373
+ id: presetId("param"),
2374
+ name: naming.name.trim(),
2375
+ endpoint,
2376
+ paramSpecs: formToSpecs(catalog, endpoint, form)
2377
+ }] };
2378
+ api.update(patch, snapshot.revision).then(() => scope.load()).then(() => {
2379
+ setNaming(null);
2380
+ setStatus({
2381
+ kind: "ok",
2382
+ text: t(naming.kind === "prompt" ? "prompt.preset.saved" : "params.preset.saved")
2383
+ });
2384
+ props.onPresetSaved();
2385
+ }).catch((error) => {
2386
+ setStatus({
2387
+ kind: "error",
2388
+ text: `${t("card.saveFailed")}${error instanceof Error ? error.message : String(error)}`
2389
+ });
2390
+ });
2391
+ };
2392
+ const submit = () => {
2393
+ if (providerId === "") {
2394
+ setStatus({
2395
+ kind: "error",
2396
+ text: t("generate.needProvider")
2397
+ });
2398
+ return;
2399
+ }
2400
+ if (prompt.trim() === "") {
2401
+ setStatus({
2402
+ kind: "error",
2403
+ text: t("generate.needPrompt")
2404
+ });
2405
+ return;
2406
+ }
2407
+ if (endpoint === "edit" && sourceIds.length === 0) {
2408
+ setStatus({
2409
+ kind: "error",
2410
+ text: t("generate.needSources")
2411
+ });
2412
+ return;
2413
+ }
2414
+ let paramSpecs;
2415
+ try {
2416
+ paramSpecs = validateParamSpecs(formToSpecs(catalog, endpoint, form));
2417
+ } catch (error) {
2418
+ setStatus({
2419
+ kind: "error",
2420
+ text: error instanceof Error ? error.message : String(error)
2421
+ });
2422
+ return;
2423
+ }
2424
+ const request = {
2425
+ providerPresetId: providerId,
2426
+ endpoint,
2427
+ prompt: prompt.trim(),
2428
+ paramSpecs
2429
+ };
2430
+ if (inline.open) request.inlineProvider = {
2431
+ protocol: inline.protocol,
2432
+ baseUrl: inline.baseUrl.trim(),
2433
+ model: inline.model.trim()
2434
+ };
2435
+ if (endpoint === "edit") {
2436
+ request.sourceIds = sourceIds;
2437
+ if (maskId !== null) request.maskId = maskId;
2438
+ }
2439
+ setSubmitting(true);
2440
+ setStatus(IDLE);
2441
+ generate(request).then(({ taskId }) => {
2442
+ setStatus({
2443
+ kind: "ok",
2444
+ text: t("generate.submitted").replace("{id}", taskId)
2445
+ });
2446
+ props.onSubmitted(taskId);
2447
+ }).catch((error) => {
2448
+ setStatus({
2449
+ kind: "error",
2450
+ text: `${t("generate.failed")}${error instanceof Error ? error.message : String(error)}`
2451
+ });
2452
+ }).finally(() => setSubmitting(false));
2453
+ };
2454
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2455
+ className: "ims-gen",
2456
+ hidden: props.hidden,
2457
+ children: [
2458
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2459
+ className: "ims-genMain",
2460
+ children: [
2461
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2462
+ className: "ims-block",
2463
+ children: [
2464
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
2465
+ className: "ims-blockTitle",
2466
+ children: t("provider.label")
2467
+ }),
2468
+ presets.providerPresets.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2469
+ className: "ims-hint",
2470
+ children: t("provider.empty")
2471
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2472
+ className: "ims-providerRow",
2473
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
2474
+ className: "ims-input",
2475
+ value: providerId,
2476
+ "aria-label": t("provider.label"),
2477
+ onChange: (event) => setProviderId(event.target.value),
2478
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2479
+ value: "",
2480
+ children: t("provider.placeholder")
2481
+ }), presets.providerPresets.map((preset) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
2482
+ value: preset.id,
2483
+ children: [
2484
+ preset.name,
2485
+ "(",
2486
+ preset.model,
2487
+ ")"
2488
+ ]
2489
+ }, preset.id))]
2490
+ }), credentialOk !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2491
+ className: `ims-badge ${credentialOk ? "ims-badgeOn" : "ims-badgeOff"}`,
2492
+ children: t(credentialOk ? "provider.credential.configured" : "provider.credential.missing")
2493
+ }) : null]
2494
+ }),
2495
+ providerId !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2496
+ className: "ims-checkLine",
2497
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2498
+ type: "checkbox",
2499
+ checked: inline.open,
2500
+ onChange: (event) => toggleInline(event.target.checked)
2501
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("provider.inline.toggle") })]
2502
+ }), inline.open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2503
+ className: "ims-inlineGrid",
2504
+ children: [
2505
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2506
+ className: "ims-mini",
2507
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("provider.inline.protocol") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
2508
+ className: "ims-input",
2509
+ value: inline.protocol,
2510
+ onChange: (event) => setInline({
2511
+ ...inline,
2512
+ protocol: event.target.value
2513
+ }),
2514
+ children: protocols.map((protocol) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2515
+ value: protocol.id,
2516
+ children: protocol.label
2517
+ }, protocol.id))
2518
+ })]
2519
+ }),
2520
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2521
+ className: "ims-mini",
2522
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("provider.inline.baseUrl") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2523
+ className: "ims-input",
2524
+ value: inline.baseUrl,
2525
+ placeholder: "https://api.openai.com/v1",
2526
+ onChange: (event) => setInline({
2527
+ ...inline,
2528
+ baseUrl: event.target.value
2529
+ })
2530
+ })]
2531
+ }),
2532
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2533
+ className: "ims-mini",
2534
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("provider.inline.model") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2535
+ className: "ims-input",
2536
+ value: inline.model,
2537
+ placeholder: "gpt-image-1",
2538
+ onChange: (event) => setInline({
2539
+ ...inline,
2540
+ model: event.target.value
2541
+ })
2542
+ })]
2543
+ }),
2544
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2545
+ className: "ims-hint",
2546
+ children: t("provider.inline.hint")
2547
+ })
2548
+ ]
2549
+ }) : null] }) : null
2550
+ ]
2551
+ }),
2552
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2553
+ className: "ims-block",
2554
+ children: [
2555
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
2556
+ className: "ims-blockTitle",
2557
+ children: t("prompt.label")
2558
+ }),
2559
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
2560
+ className: "ims-input ims-prompt",
2561
+ rows: 5,
2562
+ value: prompt,
2563
+ placeholder: t("prompt.placeholder"),
2564
+ onChange: (event) => setPrompt(event.target.value)
2565
+ }),
2566
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2567
+ className: "ims-rowActions",
2568
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
2569
+ className: "ims-input ims-rowGrow",
2570
+ value: "",
2571
+ "aria-label": t("prompt.preset.insert"),
2572
+ onChange: (event) => {
2573
+ const preset = presets.promptPresets.find((item) => item.id === event.target.value);
2574
+ if (preset !== void 0) setPrompt((current) => current.trim() === "" ? preset.text : `${current}\n${preset.text}`);
2575
+ },
2576
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2577
+ value: "",
2578
+ children: t("prompt.preset.insert")
2579
+ }), presets.promptPresets.map((preset) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2580
+ value: preset.id,
2581
+ children: preset.name
2582
+ }, preset.id))]
2583
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2584
+ type: "button",
2585
+ className: "ims-btn ims-btnGhost",
2586
+ disabled: prompt.trim() === "",
2587
+ onClick: () => setNaming({
2588
+ kind: "prompt",
2589
+ name: ""
2590
+ }),
2591
+ children: t("prompt.preset.save")
2592
+ })]
2593
+ })
2594
+ ]
2595
+ }),
2596
+ endpoint === "edit" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2597
+ className: "ims-block",
2598
+ children: [
2599
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
2600
+ className: "ims-blockTitle",
2601
+ children: t("sources.label")
2602
+ }),
2603
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2604
+ className: "ims-sources",
2605
+ children: [sourceIds.map((imageId) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2606
+ className: "ims-thumb",
2607
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
2608
+ src: imageUrl(imageId),
2609
+ alt: ""
2610
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2611
+ type: "button",
2612
+ className: "ims-thumbRemove",
2613
+ "aria-label": t("common.delete"),
2614
+ onClick: () => setSourceIds((current) => current.filter((id) => id !== imageId)),
2615
+ children: "×"
2616
+ })]
2617
+ }, imageId)), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2618
+ type: "button",
2619
+ className: "ims-thumbAdd",
2620
+ onClick: () => setPicker("sources"),
2621
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconLayers, { size: 18 }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("sources.pick") })]
2622
+ })]
2623
+ }),
2624
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2625
+ className: "ims-hint",
2626
+ children: t("sources.count").replace("{n}", String(sourceIds.length))
2627
+ }),
2628
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
2629
+ className: "ims-blockTitle",
2630
+ children: t("sources.mask")
2631
+ }),
2632
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2633
+ className: "ims-sources",
2634
+ children: [maskId !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2635
+ className: "ims-thumb",
2636
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
2637
+ src: imageUrl(maskId),
2638
+ alt: ""
2639
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2640
+ type: "button",
2641
+ className: "ims-thumbRemove",
2642
+ "aria-label": t("sources.maskClear"),
2643
+ onClick: () => setMaskId(null),
2644
+ children: "×"
2645
+ })]
2646
+ }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2647
+ type: "button",
2648
+ className: "ims-thumbAdd",
2649
+ onClick: () => setPicker("mask"),
2650
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconLayers, { size: 18 }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("sources.maskPick") })]
2651
+ })]
2652
+ })
2653
+ ]
2654
+ }) : null
2655
+ ]
2656
+ }),
2657
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2658
+ className: "ims-genSide",
2659
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2660
+ className: "ims-block",
2661
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2662
+ className: "ims-rowActions ims-blockHead",
2663
+ children: [
2664
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
2665
+ className: "ims-blockTitle",
2666
+ children: t("params.basic")
2667
+ }),
2668
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
2669
+ className: "ims-input ims-rowGrow",
2670
+ value: "",
2671
+ "aria-label": t("params.preset.placeholder"),
2672
+ onChange: (event) => applyParamPreset(event.target.value),
2673
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2674
+ value: "",
2675
+ children: t("params.preset.placeholder")
2676
+ }), paramPresets.map((preset) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2677
+ value: preset.id,
2678
+ children: preset.name
2679
+ }, preset.id))]
2680
+ }),
2681
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2682
+ type: "button",
2683
+ className: "ims-btn ims-btnGhost",
2684
+ onClick: () => setNaming({
2685
+ kind: "params",
2686
+ name: ""
2687
+ }),
2688
+ children: t("params.preset.save")
2689
+ })
2690
+ ]
2691
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ParamsForm, {
2692
+ t,
2693
+ catalog,
2694
+ endpoint,
2695
+ form,
2696
+ onChange: setForm
2697
+ })]
2698
+ })
2699
+ }),
2700
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2701
+ className: "ims-genFoot",
2702
+ children: [
2703
+ naming !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2704
+ className: "ims-naming",
2705
+ children: [
2706
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2707
+ className: "ims-input ims-rowGrow",
2708
+ value: naming.name,
2709
+ placeholder: t("prompt.preset.name"),
2710
+ autoFocus: true,
2711
+ onChange: (event) => setNaming({
2712
+ ...naming,
2713
+ name: event.target.value
2714
+ }),
2715
+ onKeyDown: (event) => {
2716
+ if (event.key === "Enter") savePreset();
2717
+ if (event.key === "Escape") setNaming(null);
2718
+ }
2719
+ }),
2720
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2721
+ type: "button",
2722
+ className: "ims-btn ims-btnPrimary",
2723
+ disabled: naming.name.trim() === "",
2724
+ onClick: savePreset,
2725
+ children: t("common.save")
2726
+ }),
2727
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2728
+ type: "button",
2729
+ className: "ims-btn ims-btnGhost",
2730
+ onClick: () => setNaming(null),
2731
+ children: t("common.cancel")
2732
+ })
2733
+ ]
2734
+ }) : null,
2735
+ status.kind !== "idle" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2736
+ className: `ims-status${status.kind === "error" ? " ims-statusError" : ""}`,
2737
+ role: "status",
2738
+ children: status.text
2739
+ }) : null,
2740
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2741
+ type: "button",
2742
+ className: "ims-btn ims-btnPrimary ims-submit",
2743
+ disabled: submitting,
2744
+ onClick: submit,
2745
+ children: t("generate.submit")
2746
+ })
2747
+ ]
2748
+ }),
2749
+ picker !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImagePicker, {
2750
+ t,
2751
+ title: t(picker === "sources" ? "sources.label" : "sources.mask"),
2752
+ items: gallery,
2753
+ multiple: picker === "sources",
2754
+ initial: picker === "sources" ? sourceIds : [],
2755
+ onConfirm: (ids) => {
2756
+ if (picker === "sources") setSourceIds(ids);
2757
+ else setMaskId(ids[0] ?? null);
2758
+ setPicker(null);
2759
+ },
2760
+ onClose: () => setPicker(null)
2761
+ }) : null
2762
+ ]
2763
+ });
2764
+ }
2765
+ //#endregion
2766
+ //#region src/client/panel/tasks.tsx
2767
+ /**
2768
+ * 任务条:面板底部的横向任务流(最新在左)。
2769
+ * 活跃任务(排队/生成中)带旋转标与取消按钮;已完成可一键跳画廊详情。
2770
+ * 轮询由面板根组件负责,本组件纯渲染。
2771
+ * @module image-studio/client/panel/tasks
2772
+ */
2773
+ const STATE_KEYS = {
2774
+ queued: "tasks.state.queued",
2775
+ running: "tasks.state.running",
2776
+ succeeded: "tasks.state.succeeded",
2777
+ failed: "tasks.state.failed",
2778
+ cancelled: "tasks.state.cancelled"
2779
+ };
2780
+ function TasksBar(props) {
2781
+ const { t, tasks, onCancel, onView } = props;
2782
+ if (tasks.length === 0) return null;
2783
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
2784
+ className: "ims-tasks",
2785
+ "aria-label": t("tasks.title"),
2786
+ children: tasks.slice(0, 20).map((task) => {
2787
+ const active = task.state === "queued" || task.state === "running";
2788
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2789
+ className: `ims-task ims-task-${task.state}`,
2790
+ children: [
2791
+ active ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconLoadingOutline16, {
2792
+ className: "ims-spin",
2793
+ size: 14
2794
+ }) : null,
2795
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2796
+ className: `ims-taskState ims-taskState-${task.state}`,
2797
+ children: t(STATE_KEYS[task.state])
2798
+ }),
2799
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2800
+ className: "ims-taskPrompt",
2801
+ title: task.promptPreview,
2802
+ children: task.promptPreview
2803
+ }),
2804
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2805
+ className: "ims-taskModel",
2806
+ children: task.model
2807
+ }),
2808
+ active ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2809
+ type: "button",
2810
+ className: "ims-btn ims-btnGhost ims-btnSmall",
2811
+ onClick: () => onCancel(task.id),
2812
+ children: t("common.cancel")
2813
+ }) : null,
2814
+ task.state === "succeeded" && task.galleryItemId !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2815
+ type: "button",
2816
+ className: "ims-btn ims-btnGhost ims-btnSmall",
2817
+ onClick: () => onView(task.galleryItemId),
2818
+ children: t("tasks.view")
2819
+ }) : null,
2820
+ task.state === "failed" && task.error !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2821
+ className: "ims-taskError",
2822
+ title: task.error,
2823
+ children: task.error
2824
+ }) : null
2825
+ ]
2826
+ }, task.id);
2827
+ })
2828
+ });
2829
+ }
2830
+ //#endregion
2831
+ //#region src/client/panel/panel.tsx
2832
+ const EMPTY_PRESETS = {
2833
+ promptPresets: [],
2834
+ paramPresets: [],
2835
+ providerPresets: []
2836
+ };
2837
+ function StudioPanel(props) {
2838
+ const { studio, t, scope, api } = props;
2839
+ const { open, tab, editSeed } = (0, react.useSyncExternalStore)(studio.subscribe, studio.getSnapshot);
2840
+ const [providers, setProviders] = (0, react.useState)(null);
2841
+ const [presets, setPresets] = (0, react.useState)(EMPTY_PRESETS);
2842
+ const [gallery, setGallery] = (0, react.useState)([]);
2843
+ const [galleryLoading, setGalleryLoading] = (0, react.useState)(false);
2844
+ const [galleryFailed, setGalleryFailed] = (0, react.useState)(false);
2845
+ const [tasks, setTasks] = (0, react.useState)([]);
2846
+ const [detailId, setDetailId] = (0, react.useState)(null);
2847
+ /** 上一轮任务快照(成功转移侦测)。 */
2848
+ const prevTasksRef = (0, react.useRef)(/* @__PURE__ */ new Map());
2849
+ const reloadGallery = (0, react.useCallback)(() => {
2850
+ setGalleryLoading(true);
2851
+ fetchGallery().then((res) => {
2852
+ setGallery(res.items);
2853
+ setGalleryFailed(false);
2854
+ }).catch(() => setGalleryFailed(true)).finally(() => setGalleryLoading(false));
2855
+ }, []);
2856
+ const reloadPresets = (0, react.useCallback)(() => {
2857
+ fetchPresets().then(setPresets).catch(() => {});
2858
+ }, []);
2859
+ const reloadTasks = (0, react.useCallback)(() => {
2860
+ fetchTasks().then((res) => setTasks(res.tasks)).catch(() => {});
2861
+ }, []);
2862
+ (0, react.useEffect)(() => {
2863
+ if (!open) return;
2864
+ if (providers === null) fetchProviders().then(setProviders).catch(() => {});
2865
+ reloadPresets();
2866
+ reloadGallery();
2867
+ reloadTasks();
2868
+ const timer = setInterval(() => {
2869
+ fetchTasks().then((res) => {
2870
+ setTasks(res.tasks);
2871
+ const prev = prevTasksRef.current;
2872
+ const next = /* @__PURE__ */ new Map();
2873
+ let succeeded = false;
2874
+ for (const task of res.tasks) {
2875
+ next.set(task.id, task.state);
2876
+ if (task.state === "succeeded" && prev.get(task.id) !== "succeeded") succeeded = true;
2877
+ }
2878
+ prevTasksRef.current = next;
2879
+ if (succeeded) reloadGallery();
2880
+ }).catch(() => {});
2881
+ }, 2e3);
2882
+ return () => clearInterval(timer);
2883
+ }, [
2884
+ open,
2885
+ providers,
2886
+ reloadPresets,
2887
+ reloadGallery,
2888
+ reloadTasks
2889
+ ]);
2890
+ if (!open) return null;
2891
+ const catalog = providers?.params ?? [];
2892
+ const protocols = providers?.protocols ?? [];
2893
+ const tabs = [
2894
+ {
2895
+ key: "generation",
2896
+ label: t("tab.generation"),
2897
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconSparkles, { size: 15 })
2898
+ },
2899
+ {
2900
+ key: "edit",
2901
+ label: t("tab.edit"),
2902
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconBrush, { size: 15 })
2903
+ },
2904
+ {
2905
+ key: "gallery",
2906
+ label: t("tab.gallery"),
2907
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconImage, { size: 15 })
2908
+ }
2909
+ ];
2910
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2911
+ className: "ims-backdrop",
2912
+ role: "presentation",
2913
+ onClick: () => studio.setOpen(false),
2914
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2915
+ className: "ims-panel",
2916
+ role: "dialog",
2917
+ "aria-modal": "true",
2918
+ "aria-label": t("panel.title"),
2919
+ onClick: (event) => event.stopPropagation(),
2920
+ children: [
2921
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
2922
+ className: "ims-head",
2923
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2924
+ className: "ims-tabs",
2925
+ role: "tablist",
2926
+ "aria-label": t("panel.title"),
2927
+ children: tabs.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2928
+ type: "button",
2929
+ role: "tab",
2930
+ "aria-selected": tab === item.key,
2931
+ className: `ims-tab${tab === item.key ? " ims-tabActive" : ""}`,
2932
+ onClick: () => studio.setTab(item.key),
2933
+ children: [item.icon, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: item.label })]
2934
+ }, item.key))
2935
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2936
+ type: "button",
2937
+ className: "ims-iconBtn",
2938
+ "aria-label": t("common.close"),
2939
+ onClick: () => studio.setOpen(false),
2940
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCloseOutline16, { size: 16 })
2941
+ })]
2942
+ }),
2943
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2944
+ className: "ims-body",
2945
+ children: [
2946
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GeneratorForm, {
2947
+ t,
2948
+ endpoint: "generation",
2949
+ catalog,
2950
+ protocols,
2951
+ presets,
2952
+ gallery,
2953
+ scope,
2954
+ api,
2955
+ editSeed: null,
2956
+ onPresetSaved: reloadPresets,
2957
+ onSubmitted: reloadTasks,
2958
+ hidden: tab !== "generation"
2959
+ }),
2960
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GeneratorForm, {
2961
+ t,
2962
+ endpoint: "edit",
2963
+ catalog,
2964
+ protocols,
2965
+ presets,
2966
+ gallery,
2967
+ scope,
2968
+ api,
2969
+ editSeed,
2970
+ onPresetSaved: reloadPresets,
2971
+ onSubmitted: reloadTasks,
2972
+ hidden: tab !== "edit"
2973
+ }),
2974
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GalleryView, {
2975
+ t,
2976
+ items: gallery,
2977
+ loading: galleryLoading,
2978
+ failed: galleryFailed,
2979
+ onRefresh: reloadGallery,
2980
+ onEdit: (item) => studio.seedEdit({
2981
+ sourceIds: [...item.imageIds],
2982
+ prompt: item.prompt,
2983
+ params: item.params,
2984
+ providerPresetId: item.providerPresetId
2985
+ }),
2986
+ detailId,
2987
+ onDetailChange: setDetailId,
2988
+ hidden: tab !== "gallery"
2989
+ })
2990
+ ]
2991
+ }),
2992
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TasksBar, {
2993
+ t,
2994
+ tasks,
2995
+ onCancel: (taskId) => {
2996
+ cancelTask(taskId).then(reloadTasks).catch(() => {});
2997
+ },
2998
+ onView: (galleryItemId) => {
2999
+ studio.setTab("gallery");
3000
+ setDetailId(galleryItemId);
3001
+ }
3002
+ })
3003
+ ]
3004
+ })
3005
+ });
60
3006
  }
61
3007
  //#endregion
62
3008
  //#region src/client/styles.ts
63
3009
  /**
64
- * 画廊/生图占位样式:本次交付仅注入极简占位类名(无视觉设计)。
65
- * 沿用官方约定 <style data-plugin data-plugin-css>,HMR 按此卸载。
3010
+ * 图像工作室前端样式:侧栏入口 + overlay 工作台 + 配置卡片附加件。
3011
+ * 全部颜色引用 --dsw-* 设计令牌(跟随原生深浅色主题);响应式断点 767px
3012
+ * 与 @dsh-plus/ui-mobile-fit 对齐(移动端面板全屏、生图表单单列、画廊两列)。
3013
+ * 注入沿用 <style data-plugin data-plugin-css> 约定(HMR 可卸载)。
66
3014
  * @module image-studio/client/styles
67
3015
  */
68
3016
  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) {
3017
+ const STYLE_TAG_ID = `${PKG}/styles.css`;
3018
+ const imageStudioCss = `
3019
+ /* ── 侧边栏入口(与文件/终端入口同款;flex:1 1 0 与邻居等分 footer 行宽)── */
3020
+ .ims-entry {
3021
+ box-sizing: border-box;
3022
+ display: flex;
3023
+ flex: 1 1 0;
3024
+ min-width: 0;
3025
+ align-items: center;
3026
+ gap: 8px;
3027
+ width: 100%;
3028
+ height: 42px;
3029
+ margin: 4px 0;
3030
+ padding: 0 10px 0 8px;
3031
+ border: none;
3032
+ border-radius: 12px;
3033
+ background: transparent;
3034
+ color: var(--dsw-alias-label-primary, #1f2328);
3035
+ font-family: inherit;
3036
+ font-size: 14px;
3037
+ line-height: 22px;
3038
+ cursor: pointer;
3039
+ overflow: hidden;
3040
+ }
3041
+ .ims-entry:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(0,0,0,.06)); }
3042
+ .ims-entry-rail {
3043
+ flex: none;
3044
+ width: 36px;
3045
+ height: 36px;
3046
+ margin: 8px 0 10px;
3047
+ padding: 0;
3048
+ border-radius: 50%;
3049
+ justify-content: center;
3050
+ gap: 0;
3051
+ }
3052
+ .ims-entry-label { white-space: nowrap; overflow: hidden; }
3053
+
3054
+ /* ── 面板骨架 ── */
3055
+ .ims-backdrop {
3056
+ position: fixed;
3057
+ inset: 0;
3058
+ z-index: 60;
3059
+ display: flex;
3060
+ align-items: center;
3061
+ justify-content: center;
3062
+ background: var(--dsw-alias-overlay-bg, rgba(0,0,0,.4));
3063
+ }
3064
+ .ims-panel {
3065
+ display: flex;
3066
+ flex-direction: column;
3067
+ width: min(1100px, 94vw);
3068
+ height: min(780px, 88vh);
3069
+ border-radius: 16px;
3070
+ background: var(--dsw-alias-bg-primary, #fff);
3071
+ color: var(--dsw-alias-label-primary, #1f2328);
3072
+ box-shadow: 0 16px 48px rgba(0,0,0,.24);
3073
+ overflow: hidden;
3074
+ }
3075
+ .ims-head {
3076
+ display: flex;
3077
+ flex: none;
3078
+ align-items: center;
3079
+ gap: 8px;
3080
+ padding: 10px 12px;
3081
+ border-bottom: .5px solid var(--dsw-alias-border-l3, rgba(0,0,0,.08));
3082
+ }
3083
+ .ims-tabs { display: flex; flex: 1; gap: 4px; min-width: 0; }
3084
+ .ims-tab {
3085
+ display: inline-flex;
3086
+ align-items: center;
3087
+ gap: 6px;
3088
+ height: 34px;
3089
+ padding: 0 14px;
3090
+ border: none;
3091
+ border-radius: 10px;
3092
+ background: transparent;
3093
+ color: var(--dsw-alias-label-secondary, #57606a);
3094
+ font-family: inherit;
3095
+ font-size: 14px;
3096
+ cursor: pointer;
3097
+ white-space: nowrap;
3098
+ }
3099
+ .ims-tab:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(0,0,0,.06)); }
3100
+ .ims-tabActive {
3101
+ background: var(--dsw-alias-button-elevated-fill, rgba(0,0,0,.06));
3102
+ color: var(--dsw-alias-label-primary, #1f2328);
3103
+ font-weight: 500;
3104
+ }
3105
+ .ims-iconBtn {
3106
+ display: inline-flex;
3107
+ flex: none;
3108
+ align-items: center;
3109
+ justify-content: center;
3110
+ width: 32px;
3111
+ height: 32px;
3112
+ border: none;
3113
+ border-radius: 8px;
3114
+ background: transparent;
3115
+ color: var(--dsw-alias-label-primary, #1f2328);
3116
+ cursor: pointer;
3117
+ }
3118
+ .ims-iconBtn:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(0,0,0,.06)); }
3119
+ .ims-body { flex: 1; min-height: 0; overflow-y: auto; }
3120
+
3121
+ /* ── 通用件 ── */
3122
+ .ims-input {
3123
+ box-sizing: border-box;
3124
+ width: 100%;
3125
+ min-height: 34px;
3126
+ padding: 6px 10px;
3127
+ border: .5px solid var(--dsw-alias-border-l2, rgba(0,0,0,.16));
3128
+ border-radius: 8px;
3129
+ background: var(--dsw-alias-bg-primary, #fff);
3130
+ color: var(--dsw-alias-label-primary, #1f2328);
3131
+ font-family: inherit;
3132
+ font-size: 13px;
3133
+ line-height: 20px;
3134
+ }
3135
+ .ims-input:focus { outline: 2px solid var(--dsw-alias-focus-ring, #4d9fff); outline-offset: 0; }
3136
+ .ims-btn {
3137
+ display: inline-flex;
3138
+ align-items: center;
3139
+ justify-content: center;
3140
+ gap: 6px;
3141
+ min-height: 34px;
3142
+ padding: 6px 14px;
3143
+ border: none;
3144
+ border-radius: 8px;
3145
+ font-family: inherit;
3146
+ font-size: 13px;
3147
+ font-weight: 500;
3148
+ cursor: pointer;
3149
+ white-space: nowrap;
3150
+ }
3151
+ .ims-btn:disabled { opacity: .5; cursor: not-allowed; }
3152
+ .ims-btnPrimary {
3153
+ background: var(--dsw-alias-button-primary-fill, #1f2328);
3154
+ color: var(--dsw-alias-button-primary-label, #fff);
3155
+ }
3156
+ .ims-btnGhost {
3157
+ background: transparent;
3158
+ color: var(--dsw-alias-label-primary, #1f2328);
3159
+ border: .5px solid var(--dsw-alias-border-l2, rgba(0,0,0,.16));
3160
+ }
3161
+ .ims-btnGhost:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover, rgba(0,0,0,.06)); }
3162
+ .ims-btnDanger { background: var(--dsw-alias-status-error, #d1242f); color: #fff; }
3163
+ .ims-btnSmall { min-height: 28px; padding: 3px 10px; font-size: 12px; }
3164
+ .ims-hint { margin: 4px 0 0; font-size: 12px; color: var(--dsw-alias-label-secondary, #57606a); }
3165
+ .ims-empty {
3166
+ padding: 32px 16px;
3167
+ text-align: center;
3168
+ font-size: 13px;
3169
+ color: var(--dsw-alias-label-secondary, #57606a);
3170
+ }
3171
+ .ims-badge {
3172
+ flex: none;
3173
+ padding: 2px 8px;
3174
+ border-radius: 999px;
3175
+ font-size: 11px;
3176
+ line-height: 16px;
3177
+ }
3178
+ .ims-badgeOn { background: var(--dsw-alias-status-success-subtle, rgba(35,134,54,.15)); color: var(--dsw-alias-status-success, #1a7f37); }
3179
+ .ims-badgeOff { background: var(--dsw-alias-status-error-subtle, rgba(209,36,47,.12)); color: var(--dsw-alias-status-error, #d1242f); }
3180
+ .ims-status { margin: 8px 0 0; font-size: 12px; color: var(--dsw-alias-status-success, #1a7f37); }
3181
+ .ims-statusError { color: var(--dsw-alias-status-error, #d1242f); }
3182
+ .ims-spin { animation: ims-rotate 1s linear infinite; }
3183
+ @keyframes ims-rotate { to { transform: rotate(360deg); } }
3184
+
3185
+ /* ── 生图表单 ── */
3186
+ .ims-gen {
3187
+ display: grid;
3188
+ grid-template-columns: minmax(0, 5fr) minmax(0, 4fr);
3189
+ gap: 0 20px;
3190
+ padding: 16px;
3191
+ }
3192
+ .ims-gen[hidden] { display: none; }
3193
+ .ims-genMain, .ims-genSide { min-width: 0; }
3194
+ .ims-block { margin-bottom: 16px; }
3195
+ .ims-blockTitle { margin: 0 0 8px; font-size: 13px; font-weight: 600; }
3196
+ .ims-blockHead { align-items: center; }
3197
+ .ims-blockHead .ims-blockTitle { margin: 0; flex: none; }
3198
+ .ims-providerRow { display: flex; align-items: center; gap: 8px; }
3199
+ .ims-checkLine {
3200
+ display: flex;
3201
+ align-items: center;
3202
+ gap: 8px;
3203
+ margin-top: 10px;
3204
+ font-size: 13px;
3205
+ cursor: pointer;
3206
+ }
3207
+ .ims-inlineGrid { display: grid; gap: 8px; margin-top: 8px; }
3208
+ .ims-mini { display: grid; gap: 4px; font-size: 12px; color: var(--dsw-alias-label-secondary, #57606a); }
3209
+ .ims-prompt { resize: vertical; min-height: 96px; }
3210
+ .ims-rowActions { display: flex; gap: 8px; margin-top: 8px; align-items: center; }
3211
+ .ims-rowGrow { flex: 1; min-width: 0; }
3212
+
3213
+ /* 参数表单 */
3214
+ .ims-paramGroup { display: grid; gap: 6px; }
3215
+ .ims-paramRow {
3216
+ display: flex;
3217
+ align-items: center;
3218
+ gap: 10px;
3219
+ padding: 6px 8px;
3220
+ border-radius: 8px;
3221
+ }
3222
+ .ims-paramRowOn { background: var(--dsw-alias-bg-secondary, rgba(0,0,0,.04)); }
3223
+ .ims-paramText { flex: 1; min-width: 0; display: grid; }
3224
+ .ims-paramKey { font-size: 12px; font-weight: 600; font-family: var(--ds-font-family-code, monospace); }
3225
+ .ims-paramGen {
3226
+ margin-left: 6px;
3227
+ padding: 0 5px;
3228
+ border-radius: 4px;
3229
+ background: var(--dsw-alias-bg-tertiary, rgba(0,0,0,.08));
3230
+ font-size: 10px;
3231
+ font-weight: 400;
3232
+ }
3233
+ .ims-paramDesc {
3234
+ font-size: 11px;
3235
+ color: var(--dsw-alias-label-secondary, #57606a);
3236
+ overflow: hidden;
3237
+ text-overflow: ellipsis;
3238
+ white-space: nowrap;
3239
+ }
3240
+ .ims-paramControl { width: 132px; flex: none; min-height: 30px; padding: 4px 8px; }
3241
+ .ims-adv { margin-top: 8px; }
3242
+ .ims-advSummary {
3243
+ cursor: pointer;
3244
+ font-size: 12px;
3245
+ font-weight: 600;
3246
+ padding: 6px 0;
3247
+ color: var(--dsw-alias-label-secondary, #57606a);
3248
+ user-select: none;
3249
+ }
3250
+ .ims-advCount {
3251
+ margin-left: 6px;
3252
+ padding: 0 6px;
3253
+ border-radius: 999px;
3254
+ background: var(--dsw-alias-button-primary-fill, #1f2328);
3255
+ color: var(--dsw-alias-button-primary-label, #fff);
3256
+ font-size: 10px;
3257
+ }
3258
+
3259
+ /* 开关(switch) */
3260
+ .ims-switch { position: relative; display: inline-flex; flex: none; width: 30px; height: 18px; }
3261
+ .ims-switch input { position: absolute; inset: 0; opacity: 0; margin: 0; cursor: pointer; }
3262
+ .ims-switchTrack {
3263
+ position: absolute;
3264
+ inset: 0;
3265
+ border-radius: 999px;
3266
+ background: var(--dsw-alias-border-l1, rgba(0,0,0,.24));
3267
+ transition: background .15s;
3268
+ pointer-events: none;
3269
+ }
3270
+ .ims-switchTrack::after {
3271
+ content: '';
3272
+ position: absolute;
3273
+ top: 2px;
3274
+ left: 2px;
3275
+ width: 14px;
3276
+ height: 14px;
3277
+ border-radius: 50%;
3278
+ background: #fff;
3279
+ transition: transform .15s;
3280
+ }
3281
+ .ims-switch input:checked + .ims-switchTrack { background: var(--dsw-alias-status-success, #1a7f37); }
3282
+ .ims-switch input:checked + .ims-switchTrack::after { transform: translateX(12px); }
3283
+
3284
+ /* 源图/遮罩缩略 */
3285
+ .ims-sources { display: flex; flex-wrap: wrap; gap: 8px; }
3286
+ .ims-thumb { position: relative; width: 64px; height: 64px; border-radius: 8px; overflow: hidden; }
3287
+ .ims-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
3288
+ .ims-thumbRemove {
3289
+ position: absolute;
3290
+ top: 2px;
3291
+ right: 2px;
3292
+ width: 18px;
3293
+ height: 18px;
3294
+ border: none;
3295
+ border-radius: 50%;
3296
+ background: rgba(0,0,0,.55);
3297
+ color: #fff;
3298
+ font-size: 12px;
3299
+ line-height: 1;
3300
+ cursor: pointer;
3301
+ }
3302
+ .ims-thumbAdd {
3303
+ display: inline-flex;
3304
+ flex-direction: column;
3305
+ align-items: center;
3306
+ justify-content: center;
3307
+ gap: 4px;
3308
+ width: 64px;
3309
+ height: 64px;
3310
+ border: 1px dashed var(--dsw-alias-border-l1, rgba(0,0,0,.24));
3311
+ border-radius: 8px;
3312
+ background: transparent;
3313
+ color: var(--dsw-alias-label-secondary, #57606a);
3314
+ font-size: 11px;
3315
+ cursor: pointer;
3316
+ }
3317
+ .ims-thumbAdd:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(0,0,0,.06)); }
3318
+
3319
+ /* 提交区 */
3320
+ .ims-genFoot { grid-column: 1 / -1; border-top: .5px solid var(--dsw-alias-border-l3, rgba(0,0,0,.08)); padding-top: 12px; }
3321
+ .ims-naming { display: flex; gap: 8px; margin-bottom: 8px; }
3322
+ .ims-submit { width: 100%; min-height: 40px; font-size: 14px; margin-top: 8px; }
3323
+
3324
+ /* ── 图片选择器 ── */
3325
+ .ims-pickerBackdrop, .ims-detailBackdrop {
3326
+ position: absolute;
3327
+ inset: 0;
3328
+ z-index: 5;
3329
+ display: flex;
3330
+ align-items: center;
3331
+ justify-content: center;
3332
+ background: var(--dsw-alias-overlay-bg, rgba(0,0,0,.4));
3333
+ }
3334
+ .ims-picker, .ims-detail {
3335
+ display: flex;
3336
+ flex-direction: column;
3337
+ width: min(760px, 92%);
3338
+ max-height: 86%;
3339
+ border-radius: 14px;
3340
+ background: var(--dsw-alias-bg-primary, #fff);
3341
+ overflow: hidden;
3342
+ }
3343
+ .ims-pickerHead, .ims-detailHead {
3344
+ display: flex;
3345
+ flex: none;
3346
+ align-items: center;
3347
+ justify-content: space-between;
3348
+ gap: 8px;
3349
+ padding: 10px 14px;
3350
+ border-bottom: .5px solid var(--dsw-alias-border-l3, rgba(0,0,0,.08));
3351
+ }
3352
+ .ims-pickerTitle { margin: 0; font-size: 14px; }
3353
+ .ims-pickerGrid {
3354
+ flex: 1;
3355
+ min-height: 0;
3356
+ overflow-y: auto;
3357
+ display: grid;
3358
+ grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
3359
+ gap: 8px;
3360
+ padding: 12px 14px;
3361
+ }
3362
+ .ims-pickerCell {
3363
+ position: relative;
3364
+ aspect-ratio: 1;
3365
+ border: 2px solid transparent;
3366
+ border-radius: 8px;
3367
+ padding: 0;
3368
+ background: none;
3369
+ cursor: pointer;
3370
+ overflow: hidden;
3371
+ }
3372
+ .ims-pickerCell img { width: 100%; height: 100%; object-fit: cover; display: block; }
3373
+ .ims-pickerCellOn { border-color: var(--dsw-alias-focus-ring, #4d9fff); }
3374
+ .ims-pickerMark {
3375
+ position: absolute;
3376
+ top: 4px;
3377
+ right: 4px;
3378
+ min-width: 18px;
3379
+ height: 18px;
3380
+ border-radius: 999px;
3381
+ background: var(--dsw-alias-button-primary-fill, #1f2328);
3382
+ color: var(--dsw-alias-button-primary-label, #fff);
3383
+ font-size: 11px;
3384
+ line-height: 18px;
3385
+ text-align: center;
3386
+ }
3387
+ .ims-pickerFoot {
3388
+ display: flex;
3389
+ flex: none;
3390
+ justify-content: flex-end;
3391
+ gap: 8px;
3392
+ padding: 10px 14px;
3393
+ border-top: .5px solid var(--dsw-alias-border-l3, rgba(0,0,0,.08));
3394
+ }
3395
+
3396
+ /* ── 画廊 ── */
3397
+ .ims-gallery { padding: 12px 16px; }
3398
+ .ims-gallery[hidden] { display: none; }
3399
+ .ims-galleryBar { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
3400
+ .ims-grid {
3401
+ display: grid;
3402
+ grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
3403
+ gap: 10px;
3404
+ }
3405
+ .ims-cell {
3406
+ position: relative;
3407
+ aspect-ratio: 1;
3408
+ border: none;
3409
+ border-radius: 10px;
3410
+ padding: 0;
3411
+ background: var(--dsw-alias-bg-secondary, rgba(0,0,0,.04));
3412
+ cursor: pointer;
3413
+ overflow: hidden;
3414
+ }
3415
+ .ims-cell img { width: 100%; height: 100%; object-fit: cover; display: block; }
3416
+ .ims-cellPrompt {
3417
+ position: absolute;
3418
+ left: 0;
3419
+ right: 0;
3420
+ bottom: 0;
3421
+ padding: 14px 8px 6px;
3422
+ background: linear-gradient(transparent, rgba(0,0,0,.72));
3423
+ color: #fff;
3424
+ font-size: 11px;
3425
+ text-align: left;
3426
+ overflow: hidden;
3427
+ text-overflow: ellipsis;
3428
+ white-space: nowrap;
3429
+ opacity: 0;
3430
+ transition: opacity .15s;
3431
+ }
3432
+ .ims-cell:hover .ims-cellPrompt, .ims-cell:focus-visible .ims-cellPrompt { opacity: 1; }
3433
+
3434
+ /* 详情模态 */
3435
+ .ims-detail { width: min(920px, 94%); }
3436
+ .ims-detailMeta { font-size: 12px; color: var(--dsw-alias-label-secondary, #57606a); }
3437
+ .ims-detailBody {
3438
+ flex: 1;
3439
+ min-height: 0;
3440
+ overflow-y: auto;
3441
+ display: grid;
3442
+ grid-template-columns: minmax(0, 3fr) minmax(0, 2fr);
3443
+ gap: 16px;
3444
+ padding: 14px;
3445
+ }
3446
+ .ims-detailImages { display: grid; gap: 10px; align-content: start; }
3447
+ .ims-detailFigure { margin: 0; display: grid; gap: 6px; }
3448
+ .ims-detailFigure img {
3449
+ width: 100%;
3450
+ border-radius: 10px;
3451
+ background: var(--dsw-alias-bg-secondary, rgba(0,0,0,.04));
3452
+ display: block;
3453
+ }
3454
+ .ims-detailFigure figcaption { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
3455
+ .ims-revised {
3456
+ font-size: 11px;
3457
+ color: var(--dsw-alias-label-secondary, #57606a);
3458
+ overflow: hidden;
3459
+ text-overflow: ellipsis;
3460
+ white-space: nowrap;
3461
+ }
3462
+ .ims-detailInfo { min-width: 0; }
3463
+ .ims-detailLabel { margin: 12px 0 6px; font-size: 12px; font-weight: 600; }
3464
+ .ims-detailLabel:first-child { margin-top: 0; }
3465
+ .ims-detailPrompt { margin: 0; font-size: 13px; white-space: pre-wrap; word-break: break-word; }
3466
+ .ims-paramTable { width: 100%; border-collapse: collapse; font-size: 12px; }
3467
+ .ims-paramTable td { padding: 4px 6px; border-top: .5px solid var(--dsw-alias-border-l3, rgba(0,0,0,.08)); word-break: break-all; }
3468
+ .ims-paramTableKey { font-family: var(--ds-font-family-code, monospace); font-weight: 600; white-space: nowrap; }
3469
+ .ims-detailFoot {
3470
+ display: flex;
3471
+ flex: none;
3472
+ align-items: center;
3473
+ justify-content: flex-end;
3474
+ gap: 8px;
3475
+ padding: 10px 14px;
3476
+ border-top: .5px solid var(--dsw-alias-border-l3, rgba(0,0,0,.08));
3477
+ }
3478
+ .ims-detailFoot .ims-status { margin: 0 auto 0 0; }
3479
+
3480
+ /* ── 任务条 ── */
3481
+ .ims-tasks {
3482
+ display: flex;
3483
+ flex: none;
3484
+ gap: 8px;
3485
+ padding: 8px 12px;
3486
+ overflow-x: auto;
3487
+ border-top: .5px solid var(--dsw-alias-border-l3, rgba(0,0,0,.08));
3488
+ }
3489
+ .ims-task {
3490
+ display: inline-flex;
3491
+ flex: none;
3492
+ align-items: center;
3493
+ gap: 6px;
3494
+ max-width: 340px;
3495
+ padding: 4px 10px;
3496
+ border-radius: 999px;
3497
+ background: var(--dsw-alias-bg-secondary, rgba(0,0,0,.04));
3498
+ font-size: 12px;
3499
+ }
3500
+ .ims-taskState { font-weight: 600; flex: none; }
3501
+ .ims-taskState-running { color: var(--dsw-alias-status-info, #0969da); }
3502
+ .ims-taskState-queued { color: var(--dsw-alias-label-secondary, #57606a); }
3503
+ .ims-taskState-succeeded { color: var(--dsw-alias-status-success, #1a7f37); }
3504
+ .ims-taskState-failed { color: var(--dsw-alias-status-error, #d1242f); }
3505
+ .ims-taskState-cancelled { color: var(--dsw-alias-label-secondary, #57606a); }
3506
+ .ims-taskPrompt { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
3507
+ .ims-taskModel { flex: none; color: var(--dsw-alias-label-secondary, #57606a); font-size: 11px; }
3508
+ .ims-taskError {
3509
+ max-width: 140px;
3510
+ overflow: hidden;
3511
+ text-overflow: ellipsis;
3512
+ white-space: nowrap;
3513
+ color: var(--dsw-alias-status-error, #d1242f);
3514
+ }
3515
+
3516
+ /* ── 配置卡片附加件(imsc- 基础类由 cardCss('imsc') 生成)── */
3517
+ .imsc-loading{padding:14px 16px;margin:0;font-size:13px;color:var(--dsw-alias-label-tertiary)}
3518
+ .imsc-groupTitle { margin: 14px 0 6px; font-size: 13px; font-weight: 600; }
3519
+ .imsc-presetBox {
3520
+ border: .5px solid var(--dsw-alias-border-l3, rgba(0,0,0,.08));
3521
+ border-radius: 10px;
3522
+ padding: 10px;
3523
+ margin-bottom: 10px;
3524
+ display: grid;
3525
+ gap: 8px;
3526
+ }
3527
+ .imsc-presetHead { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
3528
+ .imsc-presetName { font-size: 13px; font-weight: 600; }
3529
+ .imsc-grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
3530
+ .imsc-textarea {
3531
+ box-sizing: border-box;
3532
+ width: 100%;
3533
+ min-height: 64px;
3534
+ padding: 6px 10px;
3535
+ border: .5px solid var(--dsw-alias-border-l2, rgba(0,0,0,.16));
3536
+ border-radius: 8px;
3537
+ background: var(--dsw-alias-bg-primary, #fff);
3538
+ color: var(--dsw-alias-label-primary, #1f2328);
3539
+ font-family: var(--ds-font-family-code, monospace);
3540
+ font-size: 12px;
3541
+ resize: vertical;
3542
+ }
3543
+ .imsc-credRow { display: flex; align-items: center; gap: 8px; }
3544
+ .imsc-credRow .imsc-input { flex: 1; min-width: 0; }
3545
+
3546
+ /* ── 移动端(≤767px,与 ui-mobile-fit 同断点)── */
3547
+ @media (max-width: 767px) {
3548
+ .ims-backdrop { align-items: stretch; justify-content: stretch; }
3549
+ .ims-panel { width: 100%; height: 100%; border-radius: 0; }
3550
+ .ims-gen { grid-template-columns: minmax(0, 1fr); }
3551
+ .ims-genSide { margin-top: 4px; }
3552
+ .ims-tab { padding: 0 10px; }
3553
+ .ims-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
3554
+ .ims-detailBody { grid-template-columns: minmax(0, 1fr); }
3555
+ .ims-picker, .ims-detail { width: 100%; max-height: 100%; height: 100%; border-radius: 0; }
3556
+ .ims-pickerBackdrop, .ims-detailBackdrop { align-items: stretch; justify-content: stretch; }
3557
+ .ims-paramControl { width: 108px; }
3558
+ .ims-btn { min-height: 40px; }
3559
+ .ims-btnSmall { min-height: 32px; }
3560
+ .imsc-grid2 { grid-template-columns: 1fr; }
3561
+ }
3562
+ `;
3563
+ /** 注入面板 + 入口 + 卡片附加样式(重复调用幂等:已存在则跳过)。 */
3564
+ function injectStudioStyles() {
78
3565
  if (typeof document === "undefined") return null;
3566
+ if (document.querySelector(`style[data-plugin-css=${JSON.stringify(STYLE_TAG_ID)}]`) !== null) return null;
79
3567
  const tag = document.createElement("style");
80
3568
  tag.dataset.plugin = PKG;
81
- tag.dataset.pluginCss = `${PKG}/${id}`;
82
- tag.textContent = css;
3569
+ tag.dataset.pluginCss = STYLE_TAG_ID;
3570
+ tag.textContent = imageStudioCss;
83
3571
  document.head.appendChild(tag);
84
3572
  return tag;
85
3573
  }
86
3574
  //#endregion
87
3575
  //#region src/client/client.ts
88
3576
  const name = "dsh-plus-image-studio";
89
- /** 浏览器半需要的 cordis 服务 key(loader 据此注入)。 */
3577
+ /** 浏览器半需要的 cordis 服务 key(loader 据此注入;package.json 的 dsh.client.inject 管包加载顺序)。 */
90
3578
  const inject = [
91
3579
  "slots",
92
3580
  "locale",
93
3581
  "remote",
94
3582
  "remote.settings"
95
3583
  ];
3584
+ const PLUGIN_ID = "@dsh-plus/image-studio";
96
3585
  function apply(ctx) {
97
3586
  const c = ctx;
98
- const sectionTag = injectSectionStyle();
99
- const cardTag = injectCardStyles();
100
- c.effect(() => () => {
101
- sectionTag?.remove();
102
- cardTag?.remove();
103
- }, "image-studio: style");
3587
+ const panelTag = injectStudioStyles();
3588
+ const cardTag = injectCardStyle(PLUGIN_ID, cardCss("imsc"));
3589
+ const controller = createPanelController();
104
3590
  c.effect(() => c.locale.register(NS, {
105
3591
  zh,
106
3592
  en
107
3593
  }), "image-studio: locale");
108
3594
  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));
3595
+ c.effect(() => c.slots.inject("sidebar.footer.action", () => c.slots.register({
3596
+ name: "sidebar.footer.action",
3597
+ id: "image-studio-entry",
3598
+ locale: NS,
3599
+ inject: () => ({
3600
+ studio: controller,
3601
+ t
3602
+ })
3603
+ }, StudioEntryButton)), "image-studio: entry slot");
3604
+ const scope = createSettingsScope(c, SETTINGS_NS, "image-studio: settings scope");
3605
+ const api = createNamespaceApi(c.get("remote").settings, SETTINGS_NS);
3606
+ c.effect(() => c.slots.inject("shell.overlay", () => c.slots.register({
3607
+ name: "shell.overlay",
3608
+ id: "image-studio-panel",
3609
+ locale: NS,
3610
+ inject: () => ({
3611
+ studio: controller,
3612
+ t,
3613
+ scope,
3614
+ api
3615
+ })
3616
+ }, StudioPanel)), "image-studio: overlay slot");
116
3617
  c.slots.inject("settings.plugin.item", () => c.slots.register({
117
3618
  name: "settings.plugin.item",
118
- key: "dsh-plus-image-studio",
3619
+ key: SETTINGS_NS,
119
3620
  locale: NS,
120
- inject: () => ({ t })
3621
+ inject: () => ({
3622
+ t,
3623
+ scope,
3624
+ api
3625
+ })
121
3626
  }, StudioConfigCard));
3627
+ c.effect(() => () => {
3628
+ panelTag?.remove();
3629
+ cardTag?.remove();
3630
+ }, "image-studio: cleanup");
122
3631
  }
123
3632
  //#endregion
124
3633
  exports.apply = apply;