@huanlin/dsh-plugin-aigc-canvas 0.1.0
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/LICENSE +664 -0
- package/README.md +243 -0
- package/assets/stub-audio.mp3 +0 -0
- package/assets/stub-image.png +0 -0
- package/assets/stub-video.mp4 +0 -0
- package/cordis.patch.yml +19 -0
- package/lib/client.js +3419 -0
- package/lib/client.js.map +1 -0
- package/lib/index.js +8191 -0
- package/lib/invariant.js +22 -0
- package/package.json +124 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,3419 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@huanlin/dsh-plugin-aigc-canvas",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let react = require("react");
|
|
8
|
+
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
9
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
10
|
+
//#region src/client/api.ts
|
|
11
|
+
/**
|
|
12
|
+
* Typed fetch wrapper over the /aigc-canvas JSON API.
|
|
13
|
+
*/
|
|
14
|
+
/** One wire failure. */
|
|
15
|
+
var AigcApiError = class extends Error {
|
|
16
|
+
code;
|
|
17
|
+
constructor(code, message) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.code = code;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
/** All QualityHint values as a readonly array (for select dropdowns). */
|
|
23
|
+
const RUNTIME_QUALITY_HINTS = [
|
|
24
|
+
"fast",
|
|
25
|
+
"balanced",
|
|
26
|
+
"quality"
|
|
27
|
+
];
|
|
28
|
+
/** All Capability values (for select dropdowns). */
|
|
29
|
+
const RUNTIME_CAPABILITIES = [
|
|
30
|
+
"t2i",
|
|
31
|
+
"i2i",
|
|
32
|
+
"t2v",
|
|
33
|
+
"i2v",
|
|
34
|
+
"fl2v",
|
|
35
|
+
"ref2v",
|
|
36
|
+
"tts",
|
|
37
|
+
"music",
|
|
38
|
+
"transcribe",
|
|
39
|
+
"edit",
|
|
40
|
+
"chat"
|
|
41
|
+
];
|
|
42
|
+
/** All ResponseKind values (for select dropdowns). */
|
|
43
|
+
const RUNTIME_RESPONSE_KINDS = [
|
|
44
|
+
"b64_json_array",
|
|
45
|
+
"b64_json_field",
|
|
46
|
+
"binary",
|
|
47
|
+
"url_field",
|
|
48
|
+
"json_text"
|
|
49
|
+
];
|
|
50
|
+
/** All HTTP methods (for select dropdowns). */
|
|
51
|
+
const RUNTIME_HTTP_METHODS = [
|
|
52
|
+
"GET",
|
|
53
|
+
"POST",
|
|
54
|
+
"PUT",
|
|
55
|
+
"PATCH"
|
|
56
|
+
];
|
|
57
|
+
/** All parameter types (for select dropdowns). */
|
|
58
|
+
const RUNTIME_PARAM_TYPES = [
|
|
59
|
+
"string",
|
|
60
|
+
"number",
|
|
61
|
+
"integer",
|
|
62
|
+
"boolean",
|
|
63
|
+
"array",
|
|
64
|
+
"object",
|
|
65
|
+
"image_ref",
|
|
66
|
+
"video_ref",
|
|
67
|
+
"audio_ref"
|
|
68
|
+
];
|
|
69
|
+
async function call(method, payload, signal) {
|
|
70
|
+
let response;
|
|
71
|
+
try {
|
|
72
|
+
response = await fetch(`/aigc-canvas/api/${method}`, {
|
|
73
|
+
method: "POST",
|
|
74
|
+
headers: { "content-type": "application/json" },
|
|
75
|
+
body: JSON.stringify(payload),
|
|
76
|
+
signal
|
|
77
|
+
});
|
|
78
|
+
} catch (error) {
|
|
79
|
+
throw new AigcApiError("network", error instanceof Error ? error.message : String(error));
|
|
80
|
+
}
|
|
81
|
+
const parsed = await response.json().catch(() => null);
|
|
82
|
+
if (!response.ok || parsed === null || parsed.ok !== true || parsed.value === void 0) throw new AigcApiError(parsed?.error?.code ?? "http", parsed?.error?.message ?? `HTTP ${response.status}`);
|
|
83
|
+
return parsed.value;
|
|
84
|
+
}
|
|
85
|
+
/** Fetch the canvas state (elements + edges) for one session. */
|
|
86
|
+
function fetchCanvas(sessionId, signal) {
|
|
87
|
+
return call("canvas.list", { sessionId }, signal);
|
|
88
|
+
}
|
|
89
|
+
/** Persist one element's new canvas position (after a client drag). */
|
|
90
|
+
function moveCanvasElement(sessionId, uuid, x, y, signal) {
|
|
91
|
+
return call("canvas.move", {
|
|
92
|
+
sessionId,
|
|
93
|
+
uuid,
|
|
94
|
+
x,
|
|
95
|
+
y
|
|
96
|
+
}, signal);
|
|
97
|
+
}
|
|
98
|
+
/** Delete one element from the canvas (also removes its edges). */
|
|
99
|
+
function deleteCanvasElement(sessionId, uuid, signal) {
|
|
100
|
+
return call("canvas.delete", {
|
|
101
|
+
sessionId,
|
|
102
|
+
uuid
|
|
103
|
+
}, signal);
|
|
104
|
+
}
|
|
105
|
+
/** Upload a file (drag-dropped onto the canvas) and place it as a new element. */
|
|
106
|
+
function uploadCanvasFile(sessionId, fileName, mediaBase64, opts, signal) {
|
|
107
|
+
return call("canvas.upload", {
|
|
108
|
+
sessionId,
|
|
109
|
+
fileName,
|
|
110
|
+
mediaBase64,
|
|
111
|
+
...opts
|
|
112
|
+
}, signal);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Inject a user-role notice into the agent's next-step context
|
|
116
|
+
* (non-waking). Used by the canvas UI's right-click menu and quick
|
|
117
|
+
* action toolbar to ask the agent to regenerate / edit / run a
|
|
118
|
+
* workflow. Per docs/product/04-ux-reliability.md §1 + §7.
|
|
119
|
+
*
|
|
120
|
+
* The optional `summary` is the short label shown in the agent's
|
|
121
|
+
* inbox (truncated to 120 chars by the host).
|
|
122
|
+
*/
|
|
123
|
+
function notifyAgent(sessionId, message, summary, signal) {
|
|
124
|
+
const payload = {
|
|
125
|
+
sessionId,
|
|
126
|
+
message
|
|
127
|
+
};
|
|
128
|
+
if (summary !== void 0) payload.summary = summary;
|
|
129
|
+
return call("canvas.notify", payload, signal);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Update one element's lifecycle status (draft/ready/rejected/archived)
|
|
133
|
+
* and optional winner flag. Per docs/product/01-agent-autonomy.md §5
|
|
134
|
+
* + docs/product/04-ux-reliability.md §1 (right-click → mark as
|
|
135
|
+
* winner / rejected / archive).
|
|
136
|
+
*/
|
|
137
|
+
function setElementStatus(sessionId, uuid, status, winner, signal) {
|
|
138
|
+
const payload = {
|
|
139
|
+
sessionId,
|
|
140
|
+
uuid,
|
|
141
|
+
status
|
|
142
|
+
};
|
|
143
|
+
if (winner !== void 0) payload.winner = winner;
|
|
144
|
+
return call("canvas.set_status", payload, signal);
|
|
145
|
+
}
|
|
146
|
+
/** Fetch the full runtime config (providers + global settings). */
|
|
147
|
+
function fetchConfig(signal) {
|
|
148
|
+
return call("config.get", {}, signal);
|
|
149
|
+
}
|
|
150
|
+
/** Add a new provider. */
|
|
151
|
+
function addProvider(provider, signal) {
|
|
152
|
+
return call("providers.add", { provider }, signal);
|
|
153
|
+
}
|
|
154
|
+
/** Update an existing provider. */
|
|
155
|
+
function updateProvider(provider, signal) {
|
|
156
|
+
return call("providers.update", { provider }, signal);
|
|
157
|
+
}
|
|
158
|
+
/** Remove a provider by id. */
|
|
159
|
+
function removeProvider(id, signal) {
|
|
160
|
+
return call("providers.remove", { id }, signal);
|
|
161
|
+
}
|
|
162
|
+
/** Build the media URL for one element's media file (by uuid; the host resolves to the file). */
|
|
163
|
+
function mediaUrlOf(sessionId, uuid, download = false) {
|
|
164
|
+
const params = new URLSearchParams({
|
|
165
|
+
sessionId,
|
|
166
|
+
uuid
|
|
167
|
+
});
|
|
168
|
+
if (download) params.set("download", "1");
|
|
169
|
+
return `/aigc-canvas/file?${params.toString()}`;
|
|
170
|
+
}
|
|
171
|
+
/** Build the WebSocket URL for the canvas push endpoint. */
|
|
172
|
+
function canvasWsUrl(sessionId) {
|
|
173
|
+
return `${window.location.protocol === "https:" ? "wss:" : "ws:"}//${window.location.host}/aigc-canvas/ws/canvas?sessionId=${encodeURIComponent(sessionId)}`;
|
|
174
|
+
}
|
|
175
|
+
/** Fetch the request log for one session (newest last). */
|
|
176
|
+
function fetchRequestLog(sessionId, signal) {
|
|
177
|
+
return call("logs.list", { sessionId }, signal);
|
|
178
|
+
}
|
|
179
|
+
/** Clear the request log for one session. */
|
|
180
|
+
function clearRequestLog(sessionId, signal) {
|
|
181
|
+
return call("logs.clear", { sessionId }, signal);
|
|
182
|
+
}
|
|
183
|
+
/** Fetch the per-session cost summary (for the canvas header). */
|
|
184
|
+
function fetchSessionCost(sessionId, signal) {
|
|
185
|
+
return call("cost.get", { sessionId }, signal);
|
|
186
|
+
}
|
|
187
|
+
/** Promote one canvas element (by uuid) to the asset library. */
|
|
188
|
+
function promoteAsset(sessionId, uuid, opts, signal) {
|
|
189
|
+
return call("library.promote", {
|
|
190
|
+
sessionId,
|
|
191
|
+
uuid,
|
|
192
|
+
...opts
|
|
193
|
+
}, signal);
|
|
194
|
+
}
|
|
195
|
+
//#endregion
|
|
196
|
+
//#region src/client/store.ts
|
|
197
|
+
/**
|
|
198
|
+
* Canvas view store: subscribes to the host push WebSocket for one session
|
|
199
|
+
* and exposes a synchronous snapshot through useSyncExternalStore. Replays
|
|
200
|
+
* the latest snapshot on reconnect; falls back to a one-shot HTTP fetch
|
|
201
|
+
* when the WS is unavailable so a deployment without the upgrade route
|
|
202
|
+
* still renders the canvas (with manual refresh).
|
|
203
|
+
*
|
|
204
|
+
* The store is per-session: the better-sidebar tab instantiates one per
|
|
205
|
+
* scope.sessionId, and disposes it on tab close (the WS closes with it).
|
|
206
|
+
*/
|
|
207
|
+
/** Empty canvas state used as the pre-load placeholder. */
|
|
208
|
+
function emptyState(sessionId) {
|
|
209
|
+
return {
|
|
210
|
+
sessionId,
|
|
211
|
+
elements: [],
|
|
212
|
+
edges: []
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
/** One store instance per tab activation. */
|
|
216
|
+
var CanvasStore = class {
|
|
217
|
+
opts;
|
|
218
|
+
/** The session id this store is bound to. */
|
|
219
|
+
sessionId;
|
|
220
|
+
state;
|
|
221
|
+
listeners = /* @__PURE__ */ new Set();
|
|
222
|
+
ws;
|
|
223
|
+
reconnectTimer;
|
|
224
|
+
disposed = false;
|
|
225
|
+
fetchAbort;
|
|
226
|
+
constructor(opts) {
|
|
227
|
+
this.opts = opts;
|
|
228
|
+
this.sessionId = opts.sessionId;
|
|
229
|
+
this.state = emptyState(opts.sessionId);
|
|
230
|
+
this.refresh();
|
|
231
|
+
this.openWs();
|
|
232
|
+
}
|
|
233
|
+
/** Snapshot reader for useSyncExternalStore. */
|
|
234
|
+
getSnapshot = () => this.state;
|
|
235
|
+
/** Subscribe listener; returns disposer. */
|
|
236
|
+
subscribe = (listener) => {
|
|
237
|
+
this.listeners.add(listener);
|
|
238
|
+
return () => {
|
|
239
|
+
this.listeners.delete(listener);
|
|
240
|
+
};
|
|
241
|
+
};
|
|
242
|
+
/** Force a refresh (e.g. user clicked a refresh button). */
|
|
243
|
+
async refresh() {
|
|
244
|
+
if (this.disposed) return;
|
|
245
|
+
this.fetchAbort?.abort();
|
|
246
|
+
const ac = new AbortController();
|
|
247
|
+
this.fetchAbort = ac;
|
|
248
|
+
try {
|
|
249
|
+
const next = await fetchCanvas(this.opts.sessionId, ac.signal);
|
|
250
|
+
if (!this.disposed) this.setState(next);
|
|
251
|
+
} catch {}
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Persist a dragged element's new position. The authoritative snapshot
|
|
255
|
+
* arrives over the WS push (the host notifies after persisting), so no
|
|
256
|
+
* local state update is applied here.
|
|
257
|
+
*/
|
|
258
|
+
async move(uuid, x, y) {
|
|
259
|
+
if (this.disposed) return;
|
|
260
|
+
try {
|
|
261
|
+
await moveCanvasElement(this.opts.sessionId, uuid, x, y);
|
|
262
|
+
} catch {}
|
|
263
|
+
}
|
|
264
|
+
/** Delete an element (right-click → Delete). Best-effort; WS push catches up. */
|
|
265
|
+
async deleteElement(uuid) {
|
|
266
|
+
if (this.disposed) return;
|
|
267
|
+
try {
|
|
268
|
+
await deleteCanvasElement(this.opts.sessionId, uuid);
|
|
269
|
+
} catch {}
|
|
270
|
+
}
|
|
271
|
+
/** Upload a drag-dropped file and place it on the canvas. */
|
|
272
|
+
async uploadFile(fileName, mediaBase64, opts) {
|
|
273
|
+
if (this.disposed) return;
|
|
274
|
+
try {
|
|
275
|
+
await uploadCanvasFile(this.opts.sessionId, fileName, mediaBase64, opts);
|
|
276
|
+
} catch {}
|
|
277
|
+
}
|
|
278
|
+
/** Tear down: close WS, abort any in-flight fetch, drop listeners. */
|
|
279
|
+
dispose() {
|
|
280
|
+
this.disposed = true;
|
|
281
|
+
this.fetchAbort?.abort();
|
|
282
|
+
if (this.reconnectTimer !== void 0) {
|
|
283
|
+
window.clearTimeout(this.reconnectTimer);
|
|
284
|
+
this.reconnectTimer = void 0;
|
|
285
|
+
}
|
|
286
|
+
if (this.ws !== void 0) {
|
|
287
|
+
try {
|
|
288
|
+
this.ws.close();
|
|
289
|
+
} catch {}
|
|
290
|
+
this.ws = void 0;
|
|
291
|
+
}
|
|
292
|
+
this.listeners.clear();
|
|
293
|
+
}
|
|
294
|
+
setState(next) {
|
|
295
|
+
this.state = next;
|
|
296
|
+
for (const fn of [...this.listeners]) fn();
|
|
297
|
+
}
|
|
298
|
+
openWs() {
|
|
299
|
+
if (this.disposed) return;
|
|
300
|
+
let ws;
|
|
301
|
+
try {
|
|
302
|
+
ws = new WebSocket(canvasWsUrl(this.opts.sessionId));
|
|
303
|
+
} catch {
|
|
304
|
+
this.scheduleReconnect();
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
this.ws = ws;
|
|
308
|
+
ws.onmessage = (event) => {
|
|
309
|
+
try {
|
|
310
|
+
const parsed = JSON.parse(event.data);
|
|
311
|
+
if (parsed && typeof parsed.sessionId === "string") this.setState(parsed);
|
|
312
|
+
} catch {}
|
|
313
|
+
};
|
|
314
|
+
ws.onclose = () => {
|
|
315
|
+
if (this.disposed) return;
|
|
316
|
+
this.ws = void 0;
|
|
317
|
+
this.scheduleReconnect();
|
|
318
|
+
};
|
|
319
|
+
ws.onerror = () => {
|
|
320
|
+
try {
|
|
321
|
+
ws.close();
|
|
322
|
+
} catch {}
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
scheduleReconnect() {
|
|
326
|
+
if (this.disposed) return;
|
|
327
|
+
if (this.reconnectTimer !== void 0) return;
|
|
328
|
+
this.reconnectTimer = window.setTimeout(() => {
|
|
329
|
+
this.reconnectTimer = void 0;
|
|
330
|
+
this.openWs();
|
|
331
|
+
}, 2e3);
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
//#endregion
|
|
335
|
+
//#region \0dsh-css:D:\Projects\deepseek-harness\dsh-aigc-canvas\src\client\canvas.module.css.mjs
|
|
336
|
+
const css$1 = "/* dsh-aigc-canvas client styles.\r\n *\r\n * Renders inside a better-sidebar tab panel. Follows the DSH core design\r\n * language: `--dsw-alias-*` semantic tokens, `--dsw-font-*` typography,\r\n * 36px header bar + 24x24 r6 icon buttons (mirror of SubagentView), 8px\r\n * radius cards on `--dsw-alias-bg-layer-2`, no aggressive borders —\r\n * hover/active fills carry the interaction state.\r\n */\r\n\r\n.i0op5y_canvas {\r\n position: relative;\r\n display: flex;\r\n flex-direction: column;\r\n height: 100%;\r\n font: var(--dsw-font-s-14);\r\n color: var(--dsw-alias-label-primary);\r\n background: transparent;\r\n}\r\n\r\n/* ── Header bar (mirror SubagentView: 36px h, 0 8px 0 12px) ────────────── */\r\n\r\n.i0op5y_header {\r\n flex: none;\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n height: 36px;\r\n padding: 0 8px 0 12px;\r\n}\r\n\r\n.i0op5y_title {\r\n flex: 1;\r\n min-width: 0;\r\n font: var(--dsw-font-s-14);\r\n color: var(--dsw-alias-label-secondary);\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n white-space: nowrap;\r\n}\r\n\r\n.i0op5y_count {\r\n flex: none;\r\n font: var(--dsw-font-xxxs-11);\r\n color: var(--dsw-alias-label-tertiary);\r\n}\r\n\r\n/* Cost display in the header (per docs/product/04-ux-reliability.i0op5y_md §5). */\r\n.i0op5y_costDisplay {\r\n flex: none;\r\n font: var(--dsw-font-xxxs-11);\r\n color: #4caf50;\r\n font-variant-numeric: tabular-nums;\r\n}\r\n\r\n.i0op5y_zoom {\r\n flex: none;\r\n min-width: 36px;\r\n text-align: center;\r\n font: var(--dsw-font-xxxs-11);\r\n color: var(--dsw-alias-label-tertiary);\r\n}\r\n\r\n.i0op5y_iconButton {\r\n flex: none;\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n width: 24px;\r\n height: 24px;\r\n border: none;\r\n border-radius: 6px;\r\n background: transparent;\r\n color: var(--dsw-alias-label-secondary);\r\n cursor: pointer;\r\n font: var(--dsw-font-s-14);\r\n line-height: 1;\r\n}\r\n\r\n.i0op5y_iconButton:hover {\r\n background: var(--dsw-alias-interactive-bg-hover);\r\n color: var(--dsw-alias-label-primary);\r\n}\r\n\r\n.i0op5y_iconButtonActive {\r\n background: var(--dsw-alias-interactive-bg-hover);\r\n color: var(--dsw-alias-label-primary);\r\n}\r\n\r\n/* Zoom slider in the header. */\r\n.i0op5y_zoomSlider {\r\n flex: none;\r\n width: 80px;\r\n height: 4px;\r\n -webkit-appearance: none;\r\n appearance: none;\r\n background: var(--dsw-alias-interactive-bg-hover);\r\n border-radius: 2px;\r\n outline: none;\r\n cursor: pointer;\r\n}\r\n\r\n.i0op5y_zoomSlider::-webkit-slider-thumb {\r\n -webkit-appearance: none;\r\n appearance: none;\r\n width: 12px;\r\n height: 12px;\r\n border-radius: 50%;\r\n background: var(--dsw-alias-label-secondary);\r\n cursor: pointer;\r\n}\r\n\r\n.i0op5y_zoomSlider::-moz-range-thumb {\r\n width: 12px;\r\n height: 12px;\r\n border: none;\r\n border-radius: 50%;\r\n background: var(--dsw-alias-label-secondary);\r\n cursor: pointer;\r\n}\r\n\r\n/* ── Infinite canvas surface ───────────────────────────────────────────── */\r\n\r\n.i0op5y_surface {\r\n flex: 1 1 auto;\r\n position: relative;\r\n overflow: hidden;\r\n touch-action: none;\r\n cursor: grab;\r\n background-color: transparent;\r\n background-image:\r\n radial-gradient(var(--dsw-alias-border-l2) 1px, transparent 1px);\r\n background-size: 24px 24px;\r\n}\r\n\r\n.i0op5y_surface:active {\r\n cursor: grabbing;\r\n}\r\n\r\n.i0op5y_world {\r\n position: absolute;\r\n left: 0;\r\n top: 0;\r\n transform-origin: 0 0;\r\n}\r\n\r\n.i0op5y_edgeLayer {\r\n position: absolute;\r\n left: 0;\r\n top: 0;\r\n width: 1px;\r\n height: 1px;\r\n overflow: visible;\r\n pointer-events: none;\r\n}\r\n\r\n.i0op5y_edgeLine {\r\n stroke: var(--dsw-alias-label-tertiary);\r\n stroke-width: 2.5;\r\n stroke-opacity: 0.55;\r\n fill: none;\r\n stroke-linecap: round;\r\n stroke-linejoin: round;\r\n}\r\n\r\n/* Line-style variants per EdgeRelation group (see CanvasView.i0op5y_lineStyleOf).\r\n * Applied as an additional class alongside .i0op5y_edgeLine / .i0op5y_edgeArrow. */\r\n\r\n/* Direct inputs: input / first_frame / last_frame / audio_track (default solid). */\r\n.i0op5y_edgeLineSolid {\r\n stroke: var(--dsw-alias-label-tertiary);\r\n stroke-dasharray: none;\r\n stroke-width: 2.5;\r\n}\r\n\r\n/* References: reference / style / mask (dashed). */\r\n.i0op5y_edgeLineDashed {\r\n stroke: #5b8def;\r\n stroke-dasharray: 8 4;\r\n stroke-width: 2;\r\n stroke-opacity: 0.7;\r\n}\r\n\r\n/* Variations / candidates: variation_of / remix_of / alternative_of (dotted). */\r\n.i0op5y_edgeLineDotted {\r\n stroke: #c77dff;\r\n stroke-dasharray: 2 4;\r\n stroke-width: 2;\r\n stroke-opacity: 0.7;\r\n}\r\n\r\n/* Edit chain: edited_from (bold solid). */\r\n.i0op5y_edgeLineBold {\r\n stroke: var(--dsw-alias-label-secondary);\r\n stroke-dasharray: none;\r\n stroke-width: 4;\r\n stroke-opacity: 0.8;\r\n}\r\n\r\n.i0op5y_edgeArrow {\r\n fill: var(--dsw-alias-label-tertiary);\r\n fill-opacity: 0.6;\r\n stroke: var(--dsw-alias-label-tertiary);\r\n stroke-width: 1;\r\n stroke-opacity: 0.6;\r\n}\r\n\r\n/* The line-style class also applies to the arrowhead so the arrow matches\r\n * the line color + opacity of its relation group. */\r\n.i0op5y_edgeArrow.i0op5y_edgeLineDashed {\r\n fill: #5b8def;\r\n stroke: #5b8def;\r\n fill-opacity: 0.7;\r\n stroke-opacity: 0.7;\r\n}\r\n.i0op5y_edgeArrow.i0op5y_edgeLineDotted {\r\n fill: #c77dff;\r\n stroke: #c77dff;\r\n fill-opacity: 0.7;\r\n stroke-opacity: 0.7;\r\n}\r\n.i0op5y_edgeArrow.i0op5y_edgeLineBold {\r\n fill: var(--dsw-alias-label-secondary);\r\n stroke: var(--dsw-alias-label-secondary);\r\n fill-opacity: 0.8;\r\n stroke-opacity: 0.8;\r\n}\r\n\r\n/* Label chip rendered at the curve midpoint (the relation short name). */\r\n.i0op5y_edgeLabelBg {\r\n fill: var(--dsw-alias-bg-layer-2);\r\n stroke: var(--dsw-alias-border-l2);\r\n stroke-width: 1;\r\n}\r\n\r\n.i0op5y_edgeLabel {\r\n font: var(--dsw-font-xxxs-11);\r\n fill: var(--dsw-alias-label-secondary);\r\n text-anchor: middle;\r\n dominant-baseline: middle;\r\n user-select: none;\r\n pointer-events: none;\r\n}\r\n\r\n.i0op5y_edgePort {\r\n fill: var(--dsw-alias-bg-layer-2);\r\n stroke: var(--dsw-alias-label-tertiary);\r\n stroke-width: 2;\r\n stroke-opacity: 0.7;\r\n}\r\n\r\n.i0op5y_nodeBox {\r\n position: absolute;\r\n left: 0;\r\n top: 0;\r\n width: 240px;\r\n}\r\n\r\n.i0op5y_nodeBoxDraggable {\r\n cursor: move;\r\n touch-action: none;\r\n}\r\n\r\n.i0op5y_empty {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 2px;\r\n padding: 24px 16px;\r\n font: var(--dsw-font-xxs-12);\r\n color: var(--dsw-alias-label-tertiary);\r\n text-align: center;\r\n}\r\n\r\n.i0op5y_emptyHint {\r\n font: var(--dsw-font-xxxs-11);\r\n color: var(--dsw-alias-label-dimmed);\r\n}\r\n\r\n/* ── Node card (8px radius, bg-layer-2, no aggressive borders) ─────────── */\r\n\r\n.i0op5y_node {\r\n border-radius: 8px;\r\n background: var(--dsw-alias-bg-layer-2);\r\n overflow: hidden;\r\n}\r\n\r\n/* Lifecycle status visual differences (per docs/product/01-agent-autonomy.i0op5y_md §5).\r\n * draft: semi-transparent + loading animation\r\n * rejected: greyed 50% + strikethrough title\r\n * archived: greyed 30% (default hidden, shown only when filter is on)\r\n */\r\n.i0op5y_nodeStatus_draft {\r\n opacity: 0.6;\r\n}\r\n\r\n.i0op5y_nodeStatus_rejected {\r\n opacity: 0.5;\r\n filter: grayscale(0.7);\r\n}\r\n\r\n.i0op5y_nodeStatus_rejected .i0op5y_nodeTitle {\r\n text-decoration: line-through;\r\n}\r\n\r\n.i0op5y_nodeStatus_archived {\r\n opacity: 0.3;\r\n filter: grayscale(0.8);\r\n}\r\n\r\n/* Winner badge (gold star, shown when element.i0op5y_winner === true). */\r\n.i0op5y_winnerBadge {\r\n margin-left: 4px;\r\n color: #ffc107;\r\n font-size: 14px;\r\n line-height: 1;\r\n}\r\n\r\n.i0op5y_nodeHeader {\r\n display: flex;\r\n align-items: center;\r\n gap: 6px;\r\n padding: 6px 10px;\r\n}\r\n\r\n.i0op5y_kindDot {\r\n flex: none;\r\n width: 6px;\r\n height: 6px;\r\n border-radius: 50%;\r\n background: var(--dsw-alias-label-tertiary);\r\n}\r\n\r\n.i0op5y_kindDot_image { background: #4caf50; }\r\n.i0op5y_kindDot_video { background: #ff9800; }\r\n.i0op5y_kindDot_audio { background: #ab47bc; }\r\n.i0op5y_kindDot_prompt { background: #6b8cff; }\r\n\r\n.i0op5y_kindLabel {\r\n font: var(--dsw-font-xxxs-11);\r\n color: var(--dsw-alias-label-tertiary);\r\n text-transform: lowercase;\r\n}\r\n\r\n.i0op5y_nodeTime {\r\n margin-left: auto;\r\n font: var(--dsw-font-xxxs-11);\r\n color: var(--dsw-alias-label-dimmed);\r\n}\r\n\r\n.i0op5y_nodeTitle {\r\n padding: 0 10px 4px;\r\n font: var(--dsw-font-s-14);\r\n color: var(--dsw-alias-label-primary);\r\n word-break: break-word;\r\n}\r\n\r\n.i0op5y_nodeDescription {\r\n padding: 0 10px 4px;\r\n font: var(--dsw-font-xxs-12);\r\n color: var(--dsw-alias-label-tertiary);\r\n font-style: italic;\r\n word-break: break-word;\r\n}\r\n\r\n.i0op5y_nodeMedia {\r\n padding: 0 10px 8px;\r\n display: flex;\r\n flex-direction: column;\r\n gap: 4px;\r\n max-width: 100%;\r\n}\r\n\r\n.i0op5y_promptText {\r\n margin: 0;\r\n padding: 6px 8px;\r\n background: var(--dsw-alias-interactive-bg-hover);\r\n border-radius: 6px;\r\n font: var(--dsw-font-xxs-12);\r\n font-family: var(--ds-font-family-code);\r\n white-space: pre-wrap;\r\n word-break: break-word;\r\n max-height: 200px;\r\n overflow-y: auto;\r\n color: var(--dsw-alias-label-secondary);\r\n}\r\n\r\n.i0op5y_mediaImage {\r\n /* Counter-scale trick: decode at the on-screen pixel size (layout width\r\n * × zoom) so zooming in stays crisp, then visually shrink back to the\r\n * layout box. `--canvas-scale` is set on the world layer by CanvasView;\r\n * `--media-ratio` (h/w) is set on each img by CanvasNode after onLoad.\r\n *\r\n * width = 100% × scale → layout box (e.i0op5y_g. 880px at 4× zoom)\r\n * transform: scale(1/scale) → visual 220px (back to layout box size)\r\n * margin-right: 100% × (1-s) → cancel horizontal layout inflation\r\n * margin-bottom: 100% × ratio × (1-s) → cancel vertical layout inflation\r\n *\r\n * Percentages on margin resolve against the containing block's WIDTH\r\n * (the 220px card content area), so `100%` = 220px here — which is\r\n * exactly the visual width, making the math work out. */\r\n width: calc(100% * var(--canvas-scale, 1));\r\n max-width: none;\r\n height: auto;\r\n transform: scale(calc(1 / var(--canvas-scale, 1)));\r\n transform-origin: top left;\r\n margin-right: calc(100% * (1 - var(--canvas-scale, 1)));\r\n margin-bottom: calc(100% * var(--media-ratio, 0.75) * (1 - var(--canvas-scale, 1)));\r\n border-radius: 4px;\r\n display: block;\r\n}\r\n\r\n.i0op5y_mediaVideo {\r\n width: calc(100% * var(--canvas-scale, 1));\r\n max-width: none;\r\n height: auto;\r\n transform: scale(calc(1 / var(--canvas-scale, 1)));\r\n transform-origin: top left;\r\n margin-right: calc(100% * (1 - var(--canvas-scale, 1)));\r\n margin-bottom: calc(100% * var(--media-ratio, 0.75) * (1 - var(--canvas-scale, 1)));\r\n border-radius: 4px;\r\n display: block;\r\n}\r\n\r\n.i0op5y_mediaAudio {\r\n width: 100%;\r\n display: block;\r\n}\r\n\r\n.i0op5y_boundaryError {\r\n margin: 8px;\r\n padding: 8px 12px;\r\n font: var(--dsw-font-xxs-12);\r\n color: var(--dsw-alias-state-error-primary);\r\n background: var(--dsw-alias-interactive-bg-hover);\r\n border-radius: 6px;\r\n}\r\n\r\n/* ── Detail panel (elevated surface, mirror Modal/Panel aesthetic) ─────── */\r\n\r\n.i0op5y_detailPanel {\r\n position: absolute;\r\n right: 8px;\r\n top: 44px;\r\n bottom: 8px;\r\n width: 280px;\r\n display: flex;\r\n flex-direction: column;\r\n background: var(--dsw-alias-bg-layer-2);\r\n border-radius: 12px;\r\n box-shadow: var(--dsw-shadow-lv3);\r\n z-index: 10;\r\n overflow: hidden;\r\n --dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2);\r\n --dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);\r\n}\r\n\r\n.i0op5y_detailHeader {\r\n flex: none;\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n padding: 8px 10px;\r\n}\r\n\r\n.i0op5y_detailTitle {\r\n flex: 1 1 auto;\r\n min-width: 0;\r\n font: var(--dsw-font-s-14);\r\n color: var(--dsw-alias-label-primary);\r\n word-break: break-word;\r\n}\r\n\r\n.i0op5y_detailClose {\r\n flex: none;\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n width: 24px;\r\n height: 24px;\r\n border: none;\r\n border-radius: 6px;\r\n background: transparent;\r\n color: var(--dsw-alias-label-secondary);\r\n cursor: pointer;\r\n font: var(--dsw-font-s-14);\r\n line-height: 1;\r\n}\r\n\r\n.i0op5y_detailClose:hover {\r\n background: var(--dsw-alias-interactive-bg-hover);\r\n color: var(--dsw-alias-label-primary);\r\n}\r\n\r\n.i0op5y_detailBody {\r\n flex: 1 1 auto;\r\n min-height: 0;\r\n overflow-y: auto;\r\n padding: 0 10px 10px;\r\n display: flex;\r\n flex-direction: column;\r\n gap: 10px;\r\n}\r\n\r\n.i0op5y_detailBlock {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 4px;\r\n min-width: 0;\r\n}\r\n\r\n.i0op5y_detailLabel {\r\n font: var(--dsw-font-xxxs-11);\r\n color: var(--dsw-alias-label-tertiary);\r\n text-transform: lowercase;\r\n}\r\n\r\n.i0op5y_detailPrompt {\r\n margin: 0;\r\n padding: 6px 8px;\r\n background: var(--dsw-alias-interactive-bg-hover);\r\n border-radius: 6px;\r\n font: var(--dsw-font-xxs-12);\r\n font-family: var(--ds-font-family-code);\r\n white-space: pre-wrap;\r\n word-break: break-word;\r\n max-height: 160px;\r\n overflow-y: auto;\r\n color: var(--dsw-alias-label-secondary);\r\n}\r\n\r\n.i0op5y_detailValue {\r\n font: var(--dsw-font-xxs-12);\r\n color: var(--dsw-alias-label-secondary);\r\n word-break: break-word;\r\n}\r\n\r\n.i0op5y_metaList {\r\n margin: 0;\r\n padding: 0;\r\n display: grid;\r\n grid-template-columns: auto 1fr;\r\n gap: 2px 8px;\r\n font: var(--dsw-font-xxxs-11);\r\n color: var(--dsw-alias-label-tertiary);\r\n}\r\n\r\n.i0op5y_metaKey {\r\n font: var(--dsw-font-xxxs-strong-11);\r\n color: var(--dsw-alias-label-secondary);\r\n text-transform: lowercase;\r\n}\r\n\r\n.i0op5y_metaValue {\r\n margin: 0;\r\n color: var(--dsw-alias-label-secondary);\r\n word-break: break-all;\r\n font-family: var(--ds-font-family-code);\r\n}\r\n\r\n.i0op5y_filePath {\r\n display: block;\r\n font: var(--dsw-font-xxxs-11);\r\n font-family: var(--ds-font-family-code);\r\n color: var(--dsw-alias-label-tertiary);\r\n word-break: break-all;\r\n background: var(--dsw-alias-interactive-bg-hover);\r\n padding: 4px 6px;\r\n border-radius: 4px;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n}\r\n\r\n/* ── Minimap (bottom-right overview) ──────────────────────────────────── */\r\n\r\n.i0op5y_minimap {\r\n position: absolute;\r\n right: 8px;\r\n bottom: 8px;\r\n z-index: 5;\r\n background: var(--dsw-alias-bg-layer-2);\r\n border-radius: 8px;\r\n box-shadow: 0 2px 12px rgba(0, 0, 0, 0.3);\r\n padding: 4px;\r\n cursor: pointer;\r\n overflow: hidden;\r\n}\r\n\r\n.i0op5y_minimapSvg {\r\n display: block;\r\n pointer-events: none;\r\n}\r\n\r\n/* ── Right-click context menu ─────────────────────────────────────────── */\r\n\r\n.i0op5y_contextMenu {\r\n position: fixed;\r\n z-index: 20;\r\n min-width: 160px;\r\n background: var(--dsw-alias-bg-layer-2);\r\n border-radius: 8px;\r\n box-shadow: var(--dsw-shadow-lv3);\r\n padding: 4px;\r\n display: flex;\r\n flex-direction: column;\r\n gap: 2px;\r\n}\r\n\r\n.i0op5y_contextMenuItem {\r\n display: block;\r\n width: 100%;\r\n padding: 6px 10px;\r\n border: none;\r\n border-radius: 6px;\r\n background: transparent;\r\n color: var(--dsw-alias-label-primary);\r\n font: var(--dsw-font-xxs-12);\r\n text-align: left;\r\n cursor: pointer;\r\n}\r\n\r\n.i0op5y_contextMenuItem:hover {\r\n background: var(--dsw-alias-interactive-bg-hover);\r\n color: var(--dsw-alias-label-primary);\r\n}\r\n\r\n/* The Delete item keeps the destructive (red) styling from the old\r\n * single-item menu so it's visually distinct from the status / agent\r\n * actions above the separator. */\r\n.i0op5y_contextMenuItemDanger:hover {\r\n background: var(--dsw-alias-state-error-bg);\r\n color: var(--dsw-alias-state-error-primary);\r\n}\r\n\r\n/* Disabled state (e.i0op5y_g. \"Mark as winner\" on a prompt element with no media,\r\n * or \"Edit selected\" with no selection). */\r\n.i0op5y_contextMenuItemDisabled {\r\n opacity: 0.4;\r\n cursor: default;\r\n}\r\n\r\n.i0op5y_contextMenuItemDisabled:hover {\r\n background: transparent;\r\n color: var(--dsw-alias-label-primary);\r\n}\r\n\r\n/* Horizontal rule between action groups (regenerate → download,\r\n * promote → status, status → delete). */\r\n.i0op5y_contextMenuSeparator {\r\n height: 1px;\r\n margin: 4px 6px;\r\n background: var(--dsw-alias-border-l2);\r\n border: none;\r\n}\r\n\r\n/* ── Quick action toolbar (left side of the canvas, per doc 04 §7) ────── */\r\n\r\n.i0op5y_toolbar {\r\n position: absolute;\r\n left: 8px;\r\n top: 8px;\r\n z-index: 8;\r\n display: flex;\r\n gap: 4px;\r\n padding: 4px;\r\n background: var(--dsw-alias-bg-layer-2);\r\n border-radius: 8px;\r\n box-shadow: 0 2px 12px rgba(0, 0, 0, 0.25);\r\n}\r\n\r\n.i0op5y_toolbarButton {\r\n display: inline-flex;\r\n align-items: center;\r\n gap: 4px;\r\n padding: 5px 10px;\r\n border: none;\r\n border-radius: 6px;\r\n background: transparent;\r\n color: var(--dsw-alias-label-primary);\r\n font: var(--dsw-font-xxs-12);\r\n cursor: pointer;\r\n white-space: nowrap;\r\n}\r\n\r\n.i0op5y_toolbarButton:hover:not(:disabled) {\r\n background: var(--dsw-alias-interactive-bg-hover);\r\n color: var(--dsw-alias-label-primary);\r\n}\r\n\r\n.i0op5y_toolbarButton:disabled {\r\n opacity: 0.4;\r\n cursor: default;\r\n}\r\n\r\n/* ── Drag-drop indicator + upload overlay ─────────────────────────────── */\r\n\r\n.i0op5y_dropIndicator {\r\n position: absolute;\r\n left: 0;\r\n top: 0;\r\n width: 240px;\r\n height: 110px;\r\n border: 2px dashed var(--dsw-alias-label-secondary);\r\n border-radius: 8px;\r\n background: var(--dsw-alias-interactive-bg-hover);\r\n opacity: 0.5;\r\n pointer-events: none;\r\n transform-origin: 0 0;\r\n}\r\n\r\n.i0op5y_uploadOverlay {\r\n position: absolute;\r\n inset: 0;\r\n z-index: 15;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n background: var(--dsw-alias-bg-layer-1);\r\n opacity: 0.6;\r\n font: var(--dsw-font-s-14);\r\n color: var(--dsw-alias-label-secondary);\r\n pointer-events: none;\r\n}\r\n\r\n/* ── Request log panel (per docs/product/04-ux-reliability.i0op5y_md §3) ──────── */\r\n\r\n.i0op5y_logPanel {\r\n position: absolute;\r\n right: 8px;\r\n top: 44px;\r\n bottom: 8px;\r\n width: 480px;\r\n max-width: calc(100% - 16px);\r\n display: flex;\r\n flex-direction: column;\r\n background: var(--dsw-alias-bg-layer-2);\r\n border-radius: 12px;\r\n box-shadow: var(--dsw-shadow-lv3);\r\n z-index: 12;\r\n overflow: hidden;\r\n --dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2);\r\n --dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);\r\n}\r\n\r\n.i0op5y_logPanelHeader {\r\n flex: none;\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n padding: 8px 10px;\r\n border-bottom: 1px solid var(--dsw-alias-border-l2);\r\n}\r\n\r\n.i0op5y_logPanelTitle {\r\n flex: 1;\r\n font: var(--dsw-font-s-14);\r\n color: var(--dsw-alias-label-primary);\r\n}\r\n\r\n.i0op5y_logPanelClear {\r\n flex: none;\r\n padding: 4px 10px;\r\n border: none;\r\n border-radius: 6px;\r\n background: transparent;\r\n color: var(--dsw-alias-label-secondary);\r\n font: var(--dsw-font-xxs-12);\r\n cursor: pointer;\r\n}\r\n\r\n.i0op5y_logPanelClear:hover:not(:disabled) {\r\n background: var(--dsw-alias-interactive-bg-hover);\r\n color: var(--dsw-alias-label-primary);\r\n}\r\n\r\n.i0op5y_logPanelClear:disabled {\r\n opacity: 0.4;\r\n cursor: default;\r\n}\r\n\r\n.i0op5y_logList {\r\n flex: 1 1 auto;\r\n min-height: 0;\r\n overflow-y: auto;\r\n padding: 4px;\r\n display: flex;\r\n flex-direction: column;\r\n gap: 2px;\r\n}\r\n\r\n.i0op5y_logRow {\r\n display: flex;\r\n flex-direction: column;\r\n border-radius: 6px;\r\n overflow: hidden;\r\n}\r\n\r\n.i0op5y_logRowHeader {\r\n display: grid;\r\n grid-template-columns: 80px 1fr 40px 50px 60px 16px;\r\n gap: 6px;\r\n align-items: center;\r\n padding: 5px 8px;\r\n border: none;\r\n background: transparent;\r\n color: var(--dsw-alias-label-secondary);\r\n font: var(--dsw-font-xxxs-11);\r\n font-family: var(--ds-font-family-code);\r\n cursor: pointer;\r\n text-align: left;\r\n border-radius: 6px;\r\n}\r\n\r\n.i0op5y_logRowHeader:hover {\r\n background: var(--dsw-alias-interactive-bg-hover);\r\n}\r\n\r\n.i0op5y_logRowFailed {\r\n color: var(--dsw-alias-state-error-primary);\r\n background: var(--dsw-alias-state-error-bg);\r\n}\r\n\r\n.i0op5y_logRowFailed:hover {\r\n background: var(--dsw-alias-state-error-bg);\r\n filter: brightness(0.95);\r\n}\r\n\r\n.i0op5y_logTime {\r\n color: var(--dsw-alias-label-dimmed);\r\n white-space: nowrap;\r\n}\r\n\r\n.i0op5y_logLabel {\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n white-space: nowrap;\r\n color: var(--dsw-alias-label-primary);\r\n}\r\n\r\n.i0op5y_logStatus {\r\n text-align: right;\r\n font-variant-numeric: tabular-nums;\r\n}\r\n\r\n.i0op5y_logDuration {\r\n text-align: right;\r\n font-variant-numeric: tabular-nums;\r\n color: var(--dsw-alias-label-tertiary);\r\n}\r\n\r\n.i0op5y_logSize {\r\n text-align: right;\r\n font-variant-numeric: tabular-nums;\r\n color: var(--dsw-alias-label-tertiary);\r\n}\r\n\r\n.i0op5y_logExpand {\r\n text-align: center;\r\n color: var(--dsw-alias-label-dimmed);\r\n}\r\n\r\n.i0op5y_logDetail {\r\n padding: 6px 10px 8px;\r\n background: var(--dsw-alias-interactive-bg-hover);\r\n display: flex;\r\n flex-direction: column;\r\n gap: 6px;\r\n border-top: 1px solid var(--dsw-alias-border-l2);\r\n}\r\n\r\n.i0op5y_logError {\r\n font: var(--dsw-font-xxs-12);\r\n color: var(--dsw-alias-state-error-primary);\r\n word-break: break-word;\r\n}\r\n\r\n.i0op5y_logDetailBlock {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 2px;\r\n min-width: 0;\r\n}\r\n\r\n.i0op5y_logDetailLabel {\r\n font: var(--dsw-font-xxxs-11);\r\n color: var(--dsw-alias-label-tertiary);\r\n text-transform: lowercase;\r\n}\r\n\r\n.i0op5y_logDetailPre {\r\n margin: 0;\r\n padding: 4px 6px;\r\n background: var(--dsw-alias-bg-layer-1);\r\n border-radius: 4px;\r\n font: var(--dsw-font-xxxs-11);\r\n font-family: var(--ds-font-family-code);\r\n white-space: pre-wrap;\r\n word-break: break-all;\r\n max-height: 160px;\r\n overflow-y: auto;\r\n color: var(--dsw-alias-label-secondary);\r\n}\r\n\r\n.i0op5y_logFilePath {\r\n display: block;\r\n font: var(--dsw-font-xxxs-11);\r\n font-family: var(--ds-font-family-code);\r\n color: var(--dsw-alias-label-tertiary);\r\n word-break: break-all;\r\n background: var(--dsw-alias-bg-layer-1);\r\n padding: 4px 6px;\r\n border-radius: 4px;\r\n}\r\n\r\n.i0op5y_logLocateButton {\r\n align-self: flex-start;\r\n margin-top: 4px;\r\n padding: 3px 8px;\r\n border: none;\r\n border-radius: 4px;\r\n background: var(--dsw-alias-interactive-bg-hover);\r\n color: var(--dsw-alias-label-secondary);\r\n font: var(--dsw-font-xxxs-11);\r\n cursor: pointer;\r\n}\r\n\r\n.i0op5y_logLocateButton:hover {\r\n background: var(--dsw-alias-interactive-bg-active);\r\n color: var(--dsw-alias-label-primary);\r\n}\r\n\r\n/* ── Multi-select + Compare view (per docs/product/04-ux-reliability.i0op5y_md §2) */\r\n\r\n/* Outline drawn around nodes that are in the multi-select set. The\r\n * outline uses the brand color so it's clearly distinguishable from the\r\n * regular hover/focus ring. Drawn on the nodeBox wrapper (not the inner\r\n * .i0op5y_node card) so it doesn't fight with the card's own border-radius. */\r\n.i0op5y_nodeBoxMultiSelected {\r\n outline: 2px solid var(--dsw-alias-brand-primary);\r\n outline-offset: 2px;\r\n border-radius: 10px;\r\n}\r\n\r\n/* Floating bar above the canvas (top-center) that appears when 2-4\r\n * elements are in the multi-select set. Shows the count + a \"Compare\"\r\n * button + a \"Clear\" button. */\r\n.i0op5y_multiSelectBar {\r\n position: absolute;\r\n top: 8px;\r\n left: 50%;\r\n transform: translateX(-50%);\r\n z-index: 9;\r\n display: inline-flex;\r\n align-items: center;\r\n gap: 6px;\r\n padding: 4px 6px;\r\n background: var(--dsw-alias-bg-layer-2);\r\n border-radius: 8px;\r\n box-shadow: 0 2px 12px rgba(0, 0, 0, 0.25);\r\n}\r\n\r\n.i0op5y_multiSelectCount {\r\n font: var(--dsw-font-xxs-12);\r\n color: var(--dsw-alias-label-secondary);\r\n padding: 0 4px;\r\n white-space: nowrap;\r\n}\r\n\r\n.i0op5y_multiSelectCompareButton {\r\n display: inline-flex;\r\n align-items: center;\r\n padding: 5px 12px;\r\n border: none;\r\n border-radius: 6px;\r\n background: var(--dsw-alias-brand-primary);\r\n color: var(--dsw-alias-label-primary-foreground);\r\n font: var(--dsw-font-xxs-12);\r\n cursor: pointer;\r\n white-space: nowrap;\r\n}\r\n\r\n.i0op5y_multiSelectCompareButton:hover:not(:disabled) {\r\n filter: brightness(1.05);\r\n}\r\n\r\n.i0op5y_multiSelectCompareButton:disabled {\r\n opacity: 0.4;\r\n cursor: default;\r\n}\r\n\r\n.i0op5y_multiSelectClearButton {\r\n display: inline-flex;\r\n align-items: center;\r\n padding: 5px 10px;\r\n border: none;\r\n border-radius: 6px;\r\n background: transparent;\r\n color: var(--dsw-alias-label-secondary);\r\n font: var(--dsw-font-xxs-12);\r\n cursor: pointer;\r\n white-space: nowrap;\r\n}\r\n\r\n.i0op5y_multiSelectClearButton:hover {\r\n background: var(--dsw-alias-interactive-bg-hover);\r\n color: var(--dsw-alias-label-primary);\r\n}\r\n\r\n/* The compare overlay: covers the entire canvas surface with a semi-opaque\r\n * backdrop so the user can focus on comparing the selected elements.\r\n * Stops wheel + pointer events from reaching the canvas surface below. */\r\n.i0op5y_compareOverlay {\r\n position: absolute;\r\n inset: 36px 0 0 0;\r\n z-index: 18;\r\n display: flex;\r\n flex-direction: column;\r\n background: var(--dsw-alias-bg-layer-1);\r\n /* Slight opacity so the canvas is still hinted behind, but the focus\r\n * is clearly on the compare cards. */\r\n backdrop-filter: blur(2px);\r\n}\r\n\r\n.i0op5y_compareHeader {\r\n flex: none;\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n padding: 8px 12px;\r\n border-bottom: 1px solid var(--dsw-alias-border-l2);\r\n}\r\n\r\n.i0op5y_compareTitle {\r\n flex: 1;\r\n font: var(--dsw-font-s-14);\r\n color: var(--dsw-alias-label-primary);\r\n}\r\n\r\n.i0op5y_compareCloseButton {\r\n flex: none;\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n width: 24px;\r\n height: 24px;\r\n border: none;\r\n border-radius: 6px;\r\n background: transparent;\r\n color: var(--dsw-alias-label-secondary);\r\n cursor: pointer;\r\n font: var(--dsw-font-s-14);\r\n line-height: 1;\r\n}\r\n\r\n.i0op5y_compareCloseButton:hover {\r\n background: var(--dsw-alias-interactive-bg-hover);\r\n color: var(--dsw-alias-label-primary);\r\n}\r\n\r\n/* Grid of compare cards. Up to 4 columns; each card takes an equal\r\n * fraction of the available width. On narrow viewports the cards wrap\r\n * to the next row (rare in practice — the overlay is wide). */\r\n.i0op5y_compareGrid {\r\n flex: 1 1 auto;\r\n min-height: 0;\r\n overflow-y: auto;\r\n padding: 12px;\r\n display: grid;\r\n grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));\r\n gap: 12px;\r\n align-content: start;\r\n}\r\n\r\n/* One compare card: media box on top, prompt + meta + winner button below. */\r\n.i0op5y_compareCard {\r\n display: flex;\r\n flex-direction: column;\r\n background: var(--dsw-alias-bg-layer-2);\r\n border-radius: 12px;\r\n box-shadow: var(--dsw-shadow-lv3);\r\n overflow: hidden;\r\n /* Equal height per row (so the winner buttons line up at the bottom). */\r\n min-height: 320px;\r\n}\r\n\r\n/* Media box: fixed target height so all cards in one row line up.\r\n * Media is `object-fit: contain` so different aspect ratios still\r\n * render fully without distortion. */\r\n.i0op5y_compareCardMedia {\r\n flex: 0 0 200px;\r\n width: 100%;\r\n background: var(--dsw-alias-interactive-bg-hover);\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n overflow: hidden;\r\n}\r\n\r\n.i0op5y_compareMediaImage,\r\n.i0op5y_compareMediaVideo {\r\n max-width: 100%;\r\n max-height: 100%;\r\n object-fit: contain;\r\n display: block;\r\n}\r\n\r\n.i0op5y_compareMediaAudioWrap {\r\n width: 100%;\r\n padding: 0 12px;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n}\r\n\r\n.i0op5y_compareMediaAudio {\r\n width: 100%;\r\n}\r\n\r\n.i0op5y_compareMediaEmpty {\r\n font: var(--dsw-font-xxs-12);\r\n color: var(--dsw-alias-label-tertiary);\r\n text-align: center;\r\n padding: 12px;\r\n word-break: break-word;\r\n}\r\n\r\n.i0op5y_compareCardBody {\r\n flex: 1 1 auto;\r\n min-height: 0;\r\n overflow-y: auto;\r\n padding: 10px;\r\n display: flex;\r\n flex-direction: column;\r\n gap: 6px;\r\n}\r\n\r\n.i0op5y_comparePrompt {\r\n margin: 0;\r\n padding: 6px 8px;\r\n background: var(--dsw-alias-interactive-bg-hover);\r\n border-radius: 6px;\r\n font: var(--dsw-font-xxs-12);\r\n font-family: var(--ds-font-family-code);\r\n white-space: pre-wrap;\r\n word-break: break-word;\r\n max-height: 120px;\r\n overflow-y: auto;\r\n color: var(--dsw-alias-label-secondary);\r\n}\r\n\r\n.i0op5y_comparePromptEmpty {\r\n font: var(--dsw-font-xxs-12);\r\n color: var(--dsw-alias-label-dimmed);\r\n font-style: italic;\r\n padding: 6px 8px;\r\n}\r\n\r\n.i0op5y_compareMetaRow {\r\n display: flex;\r\n gap: 6px;\r\n align-items: baseline;\r\n font: var(--dsw-font-xxxs-11);\r\n color: var(--dsw-alias-label-tertiary);\r\n}\r\n\r\n.i0op5y_compareMetaLabel {\r\n flex: none;\r\n color: var(--dsw-alias-label-secondary);\r\n text-transform: lowercase;\r\n}\r\n\r\n.i0op5y_compareMetaValue {\r\n flex: 1 1 auto;\r\n color: var(--dsw-alias-label-primary);\r\n font-variant-numeric: tabular-nums;\r\n word-break: break-all;\r\n}\r\n\r\n.i0op5y_compareCardFooter {\r\n flex: none;\r\n padding: 8px 10px;\r\n border-top: 1px solid var(--dsw-alias-border-l2);\r\n display: flex;\r\n justify-content: center;\r\n}\r\n\r\n/* Compare footer (overlay-level): the \"Reject all\" + \"Close\" buttons. */\r\n.i0op5y_compareFooter {\r\n flex: none;\r\n display: flex;\r\n justify-content: flex-end;\r\n gap: 8px;\r\n padding: 10px 12px;\r\n border-top: 1px solid var(--dsw-alias-border-l2);\r\n background: var(--dsw-alias-bg-layer-2);\r\n}\r\n\r\n/* Button styles for the compare view (primary / secondary / danger).\r\n * These mirror the toolbar button aesthetic but are slightly taller\r\n * (h32 r8 instead of h24 r6) since the compare footer has more room\r\n * and the actions are consequential. */\r\n.i0op5y_compareButtonPrimary {\r\n display: inline-flex;\r\n align-items: center;\r\n padding: 6px 14px;\r\n border: none;\r\n border-radius: 6px;\r\n background: var(--dsw-alias-brand-primary);\r\n color: var(--dsw-alias-label-primary-foreground);\r\n font: var(--dsw-font-xxs-12);\r\n cursor: pointer;\r\n white-space: nowrap;\r\n}\r\n\r\n.i0op5y_compareButtonPrimary:hover:not(:disabled) {\r\n filter: brightness(1.05);\r\n}\r\n\r\n.i0op5y_compareButtonPrimary:disabled {\r\n opacity: 0.4;\r\n cursor: default;\r\n}\r\n\r\n.i0op5y_compareButtonSecondary {\r\n display: inline-flex;\r\n align-items: center;\r\n padding: 6px 14px;\r\n border: 1px solid var(--dsw-alias-border-l2);\r\n border-radius: 6px;\r\n background: transparent;\r\n color: var(--dsw-alias-label-primary);\r\n font: var(--dsw-font-xxs-12);\r\n cursor: pointer;\r\n white-space: nowrap;\r\n}\r\n\r\n.i0op5y_compareButtonSecondary:hover:not(:disabled) {\r\n background: var(--dsw-alias-interactive-bg-hover-solid);\r\n}\r\n\r\n.i0op5y_compareButtonDanger {\r\n display: inline-flex;\r\n align-items: center;\r\n padding: 6px 14px;\r\n border: none;\r\n border-radius: 6px;\r\n background: transparent;\r\n color: var(--dsw-alias-state-error-primary);\r\n font: var(--dsw-font-xxs-12);\r\n cursor: pointer;\r\n white-space: nowrap;\r\n}\r\n\r\n.i0op5y_compareButtonDanger:hover:not(:disabled) {\r\n background: var(--dsw-alias-interactive-bg-hover-danger);\r\n}\r\n\r\n.i0op5y_compareButtonDanger:disabled {\r\n opacity: 0.4;\r\n cursor: default;\r\n}\r\n";
|
|
337
|
+
const tagId$1 = "@huanlin/dsh-plugin-aigc-canvas/canvas.module.css";
|
|
338
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
|
|
339
|
+
const tag = document.createElement("style");
|
|
340
|
+
tag.dataset.plugin = "@huanlin/dsh-plugin-aigc-canvas";
|
|
341
|
+
tag.dataset.pluginCss = tagId$1;
|
|
342
|
+
tag.textContent = css$1;
|
|
343
|
+
document.head.appendChild(tag);
|
|
344
|
+
}
|
|
345
|
+
var canvas_module_css_default = {
|
|
346
|
+
"canvas": "i0op5y_canvas",
|
|
347
|
+
"header": "i0op5y_header",
|
|
348
|
+
"title": "i0op5y_title",
|
|
349
|
+
"count": "i0op5y_count",
|
|
350
|
+
"md": "i0op5y_md",
|
|
351
|
+
"costDisplay": "i0op5y_costDisplay",
|
|
352
|
+
"zoom": "i0op5y_zoom",
|
|
353
|
+
"iconButton": "i0op5y_iconButton",
|
|
354
|
+
"iconButtonActive": "i0op5y_iconButtonActive",
|
|
355
|
+
"zoomSlider": "i0op5y_zoomSlider",
|
|
356
|
+
"surface": "i0op5y_surface",
|
|
357
|
+
"world": "i0op5y_world",
|
|
358
|
+
"edgeLayer": "i0op5y_edgeLayer",
|
|
359
|
+
"edgeLine": "i0op5y_edgeLine",
|
|
360
|
+
"lineStyleOf": "i0op5y_lineStyleOf",
|
|
361
|
+
"edgeArrow": "i0op5y_edgeArrow",
|
|
362
|
+
"edgeLineSolid": "i0op5y_edgeLineSolid",
|
|
363
|
+
"edgeLineDashed": "i0op5y_edgeLineDashed",
|
|
364
|
+
"edgeLineDotted": "i0op5y_edgeLineDotted",
|
|
365
|
+
"edgeLineBold": "i0op5y_edgeLineBold",
|
|
366
|
+
"edgeLabelBg": "i0op5y_edgeLabelBg",
|
|
367
|
+
"edgeLabel": "i0op5y_edgeLabel",
|
|
368
|
+
"edgePort": "i0op5y_edgePort",
|
|
369
|
+
"nodeBox": "i0op5y_nodeBox",
|
|
370
|
+
"nodeBoxDraggable": "i0op5y_nodeBoxDraggable",
|
|
371
|
+
"empty": "i0op5y_empty",
|
|
372
|
+
"emptyHint": "i0op5y_emptyHint",
|
|
373
|
+
"node": "i0op5y_node",
|
|
374
|
+
"nodeStatus_draft": "i0op5y_nodeStatus_draft",
|
|
375
|
+
"nodeStatus_rejected": "i0op5y_nodeStatus_rejected",
|
|
376
|
+
"nodeTitle": "i0op5y_nodeTitle",
|
|
377
|
+
"nodeStatus_archived": "i0op5y_nodeStatus_archived",
|
|
378
|
+
"winner": "i0op5y_winner",
|
|
379
|
+
"winnerBadge": "i0op5y_winnerBadge",
|
|
380
|
+
"nodeHeader": "i0op5y_nodeHeader",
|
|
381
|
+
"kindDot": "i0op5y_kindDot",
|
|
382
|
+
"kindDot_image": "i0op5y_kindDot_image",
|
|
383
|
+
"kindDot_video": "i0op5y_kindDot_video",
|
|
384
|
+
"kindDot_audio": "i0op5y_kindDot_audio",
|
|
385
|
+
"kindDot_prompt": "i0op5y_kindDot_prompt",
|
|
386
|
+
"kindLabel": "i0op5y_kindLabel",
|
|
387
|
+
"nodeTime": "i0op5y_nodeTime",
|
|
388
|
+
"nodeDescription": "i0op5y_nodeDescription",
|
|
389
|
+
"nodeMedia": "i0op5y_nodeMedia",
|
|
390
|
+
"promptText": "i0op5y_promptText",
|
|
391
|
+
"mediaImage": "i0op5y_mediaImage",
|
|
392
|
+
"g": "i0op5y_g",
|
|
393
|
+
"mediaVideo": "i0op5y_mediaVideo",
|
|
394
|
+
"mediaAudio": "i0op5y_mediaAudio",
|
|
395
|
+
"boundaryError": "i0op5y_boundaryError",
|
|
396
|
+
"detailPanel": "i0op5y_detailPanel",
|
|
397
|
+
"detailHeader": "i0op5y_detailHeader",
|
|
398
|
+
"detailTitle": "i0op5y_detailTitle",
|
|
399
|
+
"detailClose": "i0op5y_detailClose",
|
|
400
|
+
"detailBody": "i0op5y_detailBody",
|
|
401
|
+
"detailBlock": "i0op5y_detailBlock",
|
|
402
|
+
"detailLabel": "i0op5y_detailLabel",
|
|
403
|
+
"detailPrompt": "i0op5y_detailPrompt",
|
|
404
|
+
"detailValue": "i0op5y_detailValue",
|
|
405
|
+
"metaList": "i0op5y_metaList",
|
|
406
|
+
"metaKey": "i0op5y_metaKey",
|
|
407
|
+
"metaValue": "i0op5y_metaValue",
|
|
408
|
+
"filePath": "i0op5y_filePath",
|
|
409
|
+
"minimap": "i0op5y_minimap",
|
|
410
|
+
"minimapSvg": "i0op5y_minimapSvg",
|
|
411
|
+
"contextMenu": "i0op5y_contextMenu",
|
|
412
|
+
"contextMenuItem": "i0op5y_contextMenuItem",
|
|
413
|
+
"contextMenuItemDanger": "i0op5y_contextMenuItemDanger",
|
|
414
|
+
"contextMenuItemDisabled": "i0op5y_contextMenuItemDisabled",
|
|
415
|
+
"contextMenuSeparator": "i0op5y_contextMenuSeparator",
|
|
416
|
+
"toolbar": "i0op5y_toolbar",
|
|
417
|
+
"toolbarButton": "i0op5y_toolbarButton",
|
|
418
|
+
"dropIndicator": "i0op5y_dropIndicator",
|
|
419
|
+
"uploadOverlay": "i0op5y_uploadOverlay",
|
|
420
|
+
"logPanel": "i0op5y_logPanel",
|
|
421
|
+
"logPanelHeader": "i0op5y_logPanelHeader",
|
|
422
|
+
"logPanelTitle": "i0op5y_logPanelTitle",
|
|
423
|
+
"logPanelClear": "i0op5y_logPanelClear",
|
|
424
|
+
"logList": "i0op5y_logList",
|
|
425
|
+
"logRow": "i0op5y_logRow",
|
|
426
|
+
"logRowHeader": "i0op5y_logRowHeader",
|
|
427
|
+
"logRowFailed": "i0op5y_logRowFailed",
|
|
428
|
+
"logTime": "i0op5y_logTime",
|
|
429
|
+
"logLabel": "i0op5y_logLabel",
|
|
430
|
+
"logStatus": "i0op5y_logStatus",
|
|
431
|
+
"logDuration": "i0op5y_logDuration",
|
|
432
|
+
"logSize": "i0op5y_logSize",
|
|
433
|
+
"logExpand": "i0op5y_logExpand",
|
|
434
|
+
"logDetail": "i0op5y_logDetail",
|
|
435
|
+
"logError": "i0op5y_logError",
|
|
436
|
+
"logDetailBlock": "i0op5y_logDetailBlock",
|
|
437
|
+
"logDetailLabel": "i0op5y_logDetailLabel",
|
|
438
|
+
"logDetailPre": "i0op5y_logDetailPre",
|
|
439
|
+
"logFilePath": "i0op5y_logFilePath",
|
|
440
|
+
"logLocateButton": "i0op5y_logLocateButton",
|
|
441
|
+
"nodeBoxMultiSelected": "i0op5y_nodeBoxMultiSelected",
|
|
442
|
+
"multiSelectBar": "i0op5y_multiSelectBar",
|
|
443
|
+
"multiSelectCount": "i0op5y_multiSelectCount",
|
|
444
|
+
"multiSelectCompareButton": "i0op5y_multiSelectCompareButton",
|
|
445
|
+
"multiSelectClearButton": "i0op5y_multiSelectClearButton",
|
|
446
|
+
"compareOverlay": "i0op5y_compareOverlay",
|
|
447
|
+
"compareHeader": "i0op5y_compareHeader",
|
|
448
|
+
"compareTitle": "i0op5y_compareTitle",
|
|
449
|
+
"compareCloseButton": "i0op5y_compareCloseButton",
|
|
450
|
+
"compareGrid": "i0op5y_compareGrid",
|
|
451
|
+
"compareCard": "i0op5y_compareCard",
|
|
452
|
+
"compareCardMedia": "i0op5y_compareCardMedia",
|
|
453
|
+
"compareMediaImage": "i0op5y_compareMediaImage",
|
|
454
|
+
"compareMediaVideo": "i0op5y_compareMediaVideo",
|
|
455
|
+
"compareMediaAudioWrap": "i0op5y_compareMediaAudioWrap",
|
|
456
|
+
"compareMediaAudio": "i0op5y_compareMediaAudio",
|
|
457
|
+
"compareMediaEmpty": "i0op5y_compareMediaEmpty",
|
|
458
|
+
"compareCardBody": "i0op5y_compareCardBody",
|
|
459
|
+
"comparePrompt": "i0op5y_comparePrompt",
|
|
460
|
+
"comparePromptEmpty": "i0op5y_comparePromptEmpty",
|
|
461
|
+
"compareMetaRow": "i0op5y_compareMetaRow",
|
|
462
|
+
"compareMetaLabel": "i0op5y_compareMetaLabel",
|
|
463
|
+
"compareMetaValue": "i0op5y_compareMetaValue",
|
|
464
|
+
"compareCardFooter": "i0op5y_compareCardFooter",
|
|
465
|
+
"compareFooter": "i0op5y_compareFooter",
|
|
466
|
+
"compareButtonPrimary": "i0op5y_compareButtonPrimary",
|
|
467
|
+
"compareButtonSecondary": "i0op5y_compareButtonSecondary",
|
|
468
|
+
"compareButtonDanger": "i0op5y_compareButtonDanger"
|
|
469
|
+
};
|
|
470
|
+
//#endregion
|
|
471
|
+
//#region src/client/CanvasNode.tsx
|
|
472
|
+
/**
|
|
473
|
+
* One canvas node: renders the element's media (image / video / audio) or
|
|
474
|
+
* its prompt text, plus a small header row with kind dot + label and the
|
|
475
|
+
* creation time. Rendered inside the infinite-canvas world layer at its
|
|
476
|
+
* (x, y) position; dragging is handled by the parent view (the node div
|
|
477
|
+
* carries the drag pointer handlers). Double-click opens the detail panel.
|
|
478
|
+
*
|
|
479
|
+
* ZOOM / BLUR FIX
|
|
480
|
+
* ---------------
|
|
481
|
+
* The world layer is scaled via `transform: scale(s)`. Browsers decode
|
|
482
|
+
* `<img>`/`<video>` at their CSS layout size (the card's 220px content
|
|
483
|
+
* width), NOT the post-transform screen size — so zooming in upscales a
|
|
484
|
+
* small decoded bitmap and the media looks blurry.
|
|
485
|
+
*
|
|
486
|
+
* To get crisp media at every zoom level, each img/video sets its CSS
|
|
487
|
+
* width to `100% * scale` (the on-screen pixel width) and then applies
|
|
488
|
+
* `transform: scale(1/scale)` to visually shrink back to the 220px layout
|
|
489
|
+
* box. The browser then decodes at the larger size and the world
|
|
490
|
+
* transform produces a 1:1 (or downscaled) screen image — sharp.
|
|
491
|
+
*
|
|
492
|
+
* The layout box still grows to `220*scale` wide, which would push
|
|
493
|
+
* siblings and inflate the card. Negative `margin-right` / `margin-bottom`
|
|
494
|
+
* (expressed as `%` of the container width = 220px) cancel the excess so
|
|
495
|
+
* the effective layout footprint is unchanged. The bottom margin needs
|
|
496
|
+
* the media's aspect ratio (h/w), captured from `onLoad`/`onLoadedMetadata`.
|
|
497
|
+
*/
|
|
498
|
+
/** Short label for one element kind. */
|
|
499
|
+
function kindLabel(kind, t) {
|
|
500
|
+
switch (kind) {
|
|
501
|
+
case "prompt": return t("prompt");
|
|
502
|
+
case "image": return t("image");
|
|
503
|
+
case "video": return t("video");
|
|
504
|
+
case "audio": return t("audio");
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
/** Format the createdAt timestamp as a short HH:MM:SS. */
|
|
508
|
+
function formatTime$1(ms) {
|
|
509
|
+
const d = new Date(ms);
|
|
510
|
+
const pad = (n) => n < 10 ? `0${n}` : String(n);
|
|
511
|
+
return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
512
|
+
}
|
|
513
|
+
/** Default aspect ratio (h/w) used before media metadata loads. 4:3 → 0.75. */
|
|
514
|
+
const DEFAULT_RATIO = .75;
|
|
515
|
+
/**
|
|
516
|
+
* One image element. Captures its natural aspect ratio on load so the
|
|
517
|
+
* negative bottom margin (which cancels the layout-box inflation from the
|
|
518
|
+
* counter-scale trick) can be computed from CSS variables alone.
|
|
519
|
+
*/
|
|
520
|
+
function MediaImage({ url, alt }) {
|
|
521
|
+
const [ratio, setRatio] = (0, react.useState)(DEFAULT_RATIO);
|
|
522
|
+
const onLoad = (e) => {
|
|
523
|
+
const img = e.currentTarget;
|
|
524
|
+
if (img.naturalWidth > 0) setRatio(img.naturalHeight / img.naturalWidth);
|
|
525
|
+
};
|
|
526
|
+
return (0, react.createElement)("img", {
|
|
527
|
+
className: canvas_module_css_default.mediaImage,
|
|
528
|
+
src: url,
|
|
529
|
+
alt,
|
|
530
|
+
loading: "lazy",
|
|
531
|
+
draggable: false,
|
|
532
|
+
onLoad,
|
|
533
|
+
style: { ["--media-ratio"]: ratio }
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* One video element. Same counter-scale trick as MediaImage; the aspect
|
|
538
|
+
* ratio comes from `loadedmetadata` (videoWidth / videoHeight).
|
|
539
|
+
*/
|
|
540
|
+
function MediaVideo({ url }) {
|
|
541
|
+
const [ratio, setRatio] = (0, react.useState)(DEFAULT_RATIO);
|
|
542
|
+
const onLoadedMetadata = (e) => {
|
|
543
|
+
const v = e.currentTarget;
|
|
544
|
+
if (v.videoWidth > 0) setRatio(v.videoHeight / v.videoWidth);
|
|
545
|
+
};
|
|
546
|
+
return (0, react.createElement)("video", {
|
|
547
|
+
className: canvas_module_css_default.mediaVideo,
|
|
548
|
+
src: url,
|
|
549
|
+
controls: true,
|
|
550
|
+
preload: "metadata",
|
|
551
|
+
onLoadedMetadata,
|
|
552
|
+
style: { ["--media-ratio"]: ratio }
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
/** Render one element's media (or prompt text) based on its kind. */
|
|
556
|
+
function renderMedia(el) {
|
|
557
|
+
if (el.kind === "prompt") return (0, react.createElement)("pre", { className: canvas_module_css_default.promptText }, el.promptText ?? "");
|
|
558
|
+
if (el.uuid === void 0) return null;
|
|
559
|
+
const url = mediaUrlOf(el.sessionId ?? "", el.uuid);
|
|
560
|
+
if (el.kind === "image") return (0, react.createElement)(MediaImage, {
|
|
561
|
+
url,
|
|
562
|
+
alt: el.title
|
|
563
|
+
});
|
|
564
|
+
if (el.kind === "video") return (0, react.createElement)(MediaVideo, { url });
|
|
565
|
+
return (0, react.createElement)("audio", {
|
|
566
|
+
className: canvas_module_css_default.mediaAudio,
|
|
567
|
+
src: url,
|
|
568
|
+
controls: true,
|
|
569
|
+
preload: "metadata"
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
/** One canvas node (fixed-width card; height follows content). */
|
|
573
|
+
function CanvasNode({ element, t }) {
|
|
574
|
+
const kindDotClass = `${canvas_module_css_default.kindDot} ${canvas_module_css_default[`kindDot_${element.kind}`] ?? ""}`;
|
|
575
|
+
const statusClass = element.status !== void 0 && element.status !== "ready" ? ` ${canvas_module_css_default[`nodeStatus_${element.status}`] ?? ""}` : "";
|
|
576
|
+
const winnerBadge = element.winner === true;
|
|
577
|
+
return (0, react.createElement)("div", {
|
|
578
|
+
className: `${canvas_module_css_default.node}${statusClass}`,
|
|
579
|
+
"data-uuid": element.uuid ?? "",
|
|
580
|
+
"data-filepath": element.filePath
|
|
581
|
+
}, (0, react.createElement)("div", { className: canvas_module_css_default.nodeHeader }, (0, react.createElement)("span", {
|
|
582
|
+
className: kindDotClass,
|
|
583
|
+
"aria-hidden": true
|
|
584
|
+
}), (0, react.createElement)("span", { className: canvas_module_css_default.kindLabel }, kindLabel(element.kind, t)), winnerBadge ? (0, react.createElement)("span", {
|
|
585
|
+
className: canvas_module_css_default.winnerBadge,
|
|
586
|
+
title: t("winner")
|
|
587
|
+
}, "★") : null, (0, react.createElement)("span", { className: canvas_module_css_default.nodeTime }, formatTime$1(element.createdAt))), (0, react.createElement)("div", { className: canvas_module_css_default.nodeTitle }, element.title), element.description !== void 0 && element.description !== "" ? (0, react.createElement)("div", { className: canvas_module_css_default.nodeDescription }, element.description) : null, (0, react.createElement)("div", { className: canvas_module_css_default.nodeMedia }, renderMedia(element)));
|
|
588
|
+
}
|
|
589
|
+
//#endregion
|
|
590
|
+
//#region src/client/RequestLogPanel.tsx
|
|
591
|
+
/**
|
|
592
|
+
* Floating request log panel: shows every aigc_http_request + aigc_media_edit
|
|
593
|
+
* call (success or failure) so the user can debug failed generations from the
|
|
594
|
+
* canvas UI. Per docs/product/04-ux-reliability.md §3.
|
|
595
|
+
*
|
|
596
|
+
* Features:
|
|
597
|
+
* - Toggle button in the canvas header (shows the entry count badge)
|
|
598
|
+
* - Floating panel (right side, like the detail panel) with the entry list
|
|
599
|
+
* - Each entry: timestamp + type icon + provider/path + status + duration + size
|
|
600
|
+
* - Click an entry to expand details (request headers/body + response preview;
|
|
601
|
+
* apiKey is already redacted on the host side)
|
|
602
|
+
* - "Clear" button wipes the session log
|
|
603
|
+
* - Failed entries (status >= 400) are highlighted in red
|
|
604
|
+
* - "Locate on canvas" button pans to the element produced by the request
|
|
605
|
+
* (when elementPath is set) — wired through a callback prop
|
|
606
|
+
*
|
|
607
|
+
* The panel polls /aigc-canvas/api/logs.list every 2s when open (lightweight;
|
|
608
|
+
* avoids WS protocol changes). The host caps at 200 entries per session.
|
|
609
|
+
*/
|
|
610
|
+
/** Format a timestamp as HH:MM:SS.mmm. */
|
|
611
|
+
function formatTime(ms) {
|
|
612
|
+
const d = new Date(ms);
|
|
613
|
+
return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}:${String(d.getSeconds()).padStart(2, "0")}.${String(d.getMilliseconds()).padStart(3, "0")}`;
|
|
614
|
+
}
|
|
615
|
+
/** Format a byte size human-readably. */
|
|
616
|
+
function formatSize(bytes) {
|
|
617
|
+
if (bytes === void 0) return "-";
|
|
618
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
619
|
+
if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
620
|
+
return `${(bytes / 1048576).toFixed(1)}MB`;
|
|
621
|
+
}
|
|
622
|
+
/** Format a duration in ms. */
|
|
623
|
+
function formatDuration(ms) {
|
|
624
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
625
|
+
return `${(ms / 1e3).toFixed(1)}s`;
|
|
626
|
+
}
|
|
627
|
+
/** One collapsible log entry row. */
|
|
628
|
+
var LogEntryRow = class extends react.Component {
|
|
629
|
+
state = { expanded: false };
|
|
630
|
+
render() {
|
|
631
|
+
const { entry, t, locateElement } = this.props;
|
|
632
|
+
const failed = entry.status >= 400 || entry.error !== void 0;
|
|
633
|
+
const label = entry.type === "http" ? `${entry.method ?? "?"} ${entry.path ?? "?"}` : `ffmpeg: ${entry.operation ?? "?"}`;
|
|
634
|
+
const provider = entry.providerId !== void 0 ? ` ${entry.providerId}` : "";
|
|
635
|
+
return (0, react.createElement)("div", { className: canvas_module_css_default.logRow }, (0, react.createElement)("button", {
|
|
636
|
+
type: "button",
|
|
637
|
+
className: `${canvas_module_css_default.logRowHeader} ${failed ? canvas_module_css_default.logRowFailed : ""}`,
|
|
638
|
+
onClick: () => this.setState({ expanded: !this.state.expanded })
|
|
639
|
+
}, (0, react.createElement)("span", { className: canvas_module_css_default.logTime }, formatTime(entry.timestamp)), (0, react.createElement)("span", { className: canvas_module_css_default.logLabel }, `${label}${provider}`), (0, react.createElement)("span", { className: canvas_module_css_default.logStatus }, String(entry.status)), (0, react.createElement)("span", { className: canvas_module_css_default.logDuration }, formatDuration(entry.durationMs)), (0, react.createElement)("span", { className: canvas_module_css_default.logSize }, formatSize(entry.size)), (0, react.createElement)("span", { className: canvas_module_css_default.logExpand }, this.state.expanded ? "▼" : "▶")), this.state.expanded && (0, react.createElement)("div", { className: canvas_module_css_default.logDetail }, entry.error !== void 0 && (0, react.createElement)("div", { className: canvas_module_css_default.logError }, `${t("logError")}: ${entry.error}`), entry.requestBodyPreview !== void 0 && (0, react.createElement)("div", { className: canvas_module_css_default.logDetailBlock }, (0, react.createElement)("span", { className: canvas_module_css_default.logDetailLabel }, t("logRequestBody")), (0, react.createElement)("pre", { className: canvas_module_css_default.logDetailPre }, entry.requestBodyPreview)), entry.requestHeaders !== void 0 && (0, react.createElement)("div", { className: canvas_module_css_default.logDetailBlock }, (0, react.createElement)("span", { className: canvas_module_css_default.logDetailLabel }, t("logRequestHeaders")), (0, react.createElement)("pre", { className: canvas_module_css_default.logDetailPre }, JSON.stringify(entry.requestHeaders, null, 2))), entry.responseBodyPreview !== void 0 && (0, react.createElement)("div", { className: canvas_module_css_default.logDetailBlock }, (0, react.createElement)("span", { className: canvas_module_css_default.logDetailLabel }, t("logResponseBody")), (0, react.createElement)("pre", { className: canvas_module_css_default.logDetailPre }, entry.responseBodyPreview)), entry.elementPath !== void 0 && (0, react.createElement)("div", { className: canvas_module_css_default.logDetailBlock }, (0, react.createElement)("span", { className: canvas_module_css_default.logDetailLabel }, t("logProducedFile")), (0, react.createElement)("code", { className: canvas_module_css_default.logFilePath }, entry.elementPath), (0, react.createElement)("button", {
|
|
640
|
+
type: "button",
|
|
641
|
+
className: canvas_module_css_default.logLocateButton,
|
|
642
|
+
onClick: () => locateElement(entry.elementPath)
|
|
643
|
+
}, t("logLocate")))));
|
|
644
|
+
}
|
|
645
|
+
};
|
|
646
|
+
/** Error boundary so a render failure in the panel doesn't blank the canvas. */
|
|
647
|
+
var LogBoundary = class extends react.Component {
|
|
648
|
+
state = { error: null };
|
|
649
|
+
static getDerivedStateFromError(error) {
|
|
650
|
+
return { error: error instanceof Error ? error.message : String(error) };
|
|
651
|
+
}
|
|
652
|
+
componentDidCatch(error, info) {
|
|
653
|
+
console.error("[dsh-aigc-canvas] log panel error:", error, info.componentStack);
|
|
654
|
+
}
|
|
655
|
+
render() {
|
|
656
|
+
if (this.state.error !== null) return (0, react.createElement)("div", { className: canvas_module_css_default.boundaryError }, `log panel: ${this.state.error}`);
|
|
657
|
+
return this.props.children;
|
|
658
|
+
}
|
|
659
|
+
};
|
|
660
|
+
/**
|
|
661
|
+
* The request log panel. Renders as a floating panel on the right side of
|
|
662
|
+
* the canvas. Polls the host every 2s for new entries while open.
|
|
663
|
+
*/
|
|
664
|
+
function RequestLogPanel({ sessionId, t, locateElement }) {
|
|
665
|
+
const [entries, setEntries] = (0, react.useState)([]);
|
|
666
|
+
const [loading, setLoading] = (0, react.useState)(false);
|
|
667
|
+
const [error, setError] = (0, react.useState)(null);
|
|
668
|
+
(0, react.useEffect)(() => {
|
|
669
|
+
let cancelled = false;
|
|
670
|
+
const poll = async () => {
|
|
671
|
+
try {
|
|
672
|
+
const result = await fetchRequestLog(sessionId);
|
|
673
|
+
if (!cancelled) {
|
|
674
|
+
setEntries([...result.entries]);
|
|
675
|
+
setError(null);
|
|
676
|
+
}
|
|
677
|
+
} catch (e) {
|
|
678
|
+
if (!cancelled) setError(e instanceof Error ? e.message : String(e));
|
|
679
|
+
} finally {
|
|
680
|
+
if (!cancelled) setLoading(false);
|
|
681
|
+
}
|
|
682
|
+
};
|
|
683
|
+
poll();
|
|
684
|
+
const timer = setInterval(poll, 2e3);
|
|
685
|
+
return () => {
|
|
686
|
+
cancelled = true;
|
|
687
|
+
clearInterval(timer);
|
|
688
|
+
};
|
|
689
|
+
}, [sessionId]);
|
|
690
|
+
const onClear = async () => {
|
|
691
|
+
try {
|
|
692
|
+
await clearRequestLog(sessionId);
|
|
693
|
+
setEntries([]);
|
|
694
|
+
} catch (e) {
|
|
695
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
696
|
+
}
|
|
697
|
+
};
|
|
698
|
+
return (0, react.createElement)(LogBoundary, null, (0, react.createElement)("div", { className: canvas_module_css_default.logPanel }, (0, react.createElement)("div", { className: canvas_module_css_default.logPanelHeader }, (0, react.createElement)("span", { className: canvas_module_css_default.logPanelTitle }, `${t("logTitle")} (${entries.length})`), (0, react.createElement)("button", {
|
|
699
|
+
type: "button",
|
|
700
|
+
className: canvas_module_css_default.logPanelClear,
|
|
701
|
+
onClick: () => {
|
|
702
|
+
onClear();
|
|
703
|
+
},
|
|
704
|
+
disabled: entries.length === 0
|
|
705
|
+
}, t("logClear"))), error !== null && (0, react.createElement)("div", { className: canvas_module_css_default.boundaryError }, `${t("logError")}: ${error}`), loading && entries.length === 0 ? (0, react.createElement)("div", { className: canvas_module_css_default.empty }, t("logLoading")) : entries.length === 0 ? (0, react.createElement)("div", { className: canvas_module_css_default.empty }, t("logEmpty")) : (0, react.createElement)("div", { className: canvas_module_css_default.logList }, ...[...entries].reverse().map((entry) => (0, react.createElement)(LogEntryRow, {
|
|
706
|
+
key: entry.id,
|
|
707
|
+
entry,
|
|
708
|
+
t,
|
|
709
|
+
locateElement
|
|
710
|
+
})))));
|
|
711
|
+
}
|
|
712
|
+
//#endregion
|
|
713
|
+
//#region src/client/compare-helpers.ts
|
|
714
|
+
/**
|
|
715
|
+
* Read a numeric field from a free-form meta object, tolerating:
|
|
716
|
+
* - missing key
|
|
717
|
+
* - non-numeric value (string-encoded numbers are accepted; everything
|
|
718
|
+
* else returns undefined)
|
|
719
|
+
* - `null` / `undefined`
|
|
720
|
+
*
|
|
721
|
+
* Strings are accepted because the agent sometimes writes `"seed": "42"`
|
|
722
|
+
* (JSON-object values sneak through as strings when the model wraps them
|
|
723
|
+
* in quotes).
|
|
724
|
+
*/
|
|
725
|
+
function readNumber(meta, key) {
|
|
726
|
+
if (meta === void 0) return void 0;
|
|
727
|
+
const v = meta[key];
|
|
728
|
+
if (v === null || v === void 0) return void 0;
|
|
729
|
+
if (typeof v === "number" && Number.isFinite(v)) return v;
|
|
730
|
+
if (typeof v === "string") {
|
|
731
|
+
const n = Number(v);
|
|
732
|
+
if (Number.isFinite(n) && v.trim() !== "") return n;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* Extract the generation seed from an element's meta.
|
|
737
|
+
*
|
|
738
|
+
* Looks at `seed` (the canonical key) and falls back to `random_seed`
|
|
739
|
+
* (some providers use that name). Returns undefined when neither is a
|
|
740
|
+
* finite number.
|
|
741
|
+
*/
|
|
742
|
+
function getElementSeed(el) {
|
|
743
|
+
const m = el.meta;
|
|
744
|
+
if (m === void 0) return void 0;
|
|
745
|
+
return readNumber(m, "seed") ?? readNumber(m, "random_seed");
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* Extract the USD cost of producing one element from its meta.
|
|
749
|
+
*
|
|
750
|
+
* Looks at `cost`, then `costUsd`. Returns undefined when neither is a
|
|
751
|
+
* finite number (the agent doesn't always set this — the host's
|
|
752
|
+
* cost-tracker is the source of truth, but it isn't yet written back
|
|
753
|
+
* into meta on placement).
|
|
754
|
+
*/
|
|
755
|
+
function getElementCost(el) {
|
|
756
|
+
const m = el.meta;
|
|
757
|
+
if (m === void 0) return void 0;
|
|
758
|
+
return readNumber(m, "cost") ?? readNumber(m, "costUsd");
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* Extract the wall-clock duration (in ms) of producing one element.
|
|
762
|
+
*
|
|
763
|
+
* Looks at `durationMs` (host convention) and falls back to
|
|
764
|
+
* `durationSeconds` × 1000 (some agents write the duration in seconds).
|
|
765
|
+
* Returns undefined when neither is set.
|
|
766
|
+
*/
|
|
767
|
+
function getElementDurationMs(el) {
|
|
768
|
+
const m = el.meta;
|
|
769
|
+
if (m === void 0) return void 0;
|
|
770
|
+
const ms = readNumber(m, "durationMs");
|
|
771
|
+
if (ms !== void 0) return ms;
|
|
772
|
+
const seconds = readNumber(m, "durationSeconds");
|
|
773
|
+
if (seconds !== void 0) return seconds * 1e3;
|
|
774
|
+
const raw = readNumber(m, "duration");
|
|
775
|
+
if (raw === void 0) return void 0;
|
|
776
|
+
if (el.kind === "video" || el.kind === "audio") return raw * 1e3;
|
|
777
|
+
return raw;
|
|
778
|
+
}
|
|
779
|
+
/** Format a USD cost as a short `$X.XX` string, or "—" when unknown. */
|
|
780
|
+
function formatCost(usd) {
|
|
781
|
+
if (usd === void 0) return "—";
|
|
782
|
+
if (usd === 0) return "$0.00";
|
|
783
|
+
if (usd > 0 && usd < .01) return `$${usd.toFixed(4)}`;
|
|
784
|
+
return `$${usd.toFixed(2)}`;
|
|
785
|
+
}
|
|
786
|
+
/** Format a duration in ms as `X.Xs` (or `XXXms` when < 1s). Returns "—" when undefined. */
|
|
787
|
+
function formatDurationShort(ms) {
|
|
788
|
+
if (ms === void 0) return "—";
|
|
789
|
+
if (ms < 1e3) return `${Math.round(ms)}ms`;
|
|
790
|
+
return `${(ms / 1e3).toFixed(1)}s`;
|
|
791
|
+
}
|
|
792
|
+
/** Format a seed value (integer) as a string, or "—" when undefined. */
|
|
793
|
+
function formatSeed(seed) {
|
|
794
|
+
if (seed === void 0) return "—";
|
|
795
|
+
return String(seed);
|
|
796
|
+
}
|
|
797
|
+
//#endregion
|
|
798
|
+
//#region src/client/CompareView.tsx
|
|
799
|
+
/**
|
|
800
|
+
* CompareView — floating overlay that lets the user compare 2-4 selected
|
|
801
|
+
* canvas elements side by side and pick a winner.
|
|
802
|
+
*
|
|
803
|
+
* Per docs/product/04-ux-reliability.md §2:
|
|
804
|
+
* - All selected elements shown at the same scale, side by side
|
|
805
|
+
* - Each element shows: image/video, prompt, seed, cost, duration (from meta)
|
|
806
|
+
* - "Select as winner" button under each → calls setElementStatus(uuid,
|
|
807
|
+
* 'ready', true) + archives the others
|
|
808
|
+
* - "Reject all" button → calls setElementStatus(uuid, 'rejected') for all
|
|
809
|
+
* - "Close" button → unmounts the overlay (handled by parent)
|
|
810
|
+
*
|
|
811
|
+
* The overlay is rendered as a fixed-position layer above the canvas
|
|
812
|
+
* surface (z-index above the detail panel + log panel + toolbar). It
|
|
813
|
+
* does NOT intercept canvas pointer events outside its own bounds.
|
|
814
|
+
*
|
|
815
|
+
* @module @huanlin/dsh-plugin-aigc-canvas/client/CompareView
|
|
816
|
+
*/
|
|
817
|
+
/**
|
|
818
|
+
* The compare view overlay. Renders as a fixed-position layer above the
|
|
819
|
+
* canvas. Winner/reject actions fire `setElementStatus` calls; the WS
|
|
820
|
+
* push carries the authoritative state back into the canvas snapshot.
|
|
821
|
+
*/
|
|
822
|
+
function CompareView({ sessionId, elements, t, onClose }) {
|
|
823
|
+
return (0, react.createElement)(CompareBoundary, {
|
|
824
|
+
t,
|
|
825
|
+
children: (0, react.createElement)(CompareViewInner, {
|
|
826
|
+
sessionId,
|
|
827
|
+
elements,
|
|
828
|
+
t,
|
|
829
|
+
onClose
|
|
830
|
+
})
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
/** Inner component (wrapped by the boundary so a render error doesn't blank the canvas). */
|
|
834
|
+
function CompareViewInner({ sessionId, elements, t, onClose }) {
|
|
835
|
+
const [pendingWinner, setPendingWinner] = (0, react.useState)(void 0);
|
|
836
|
+
const [pendingReject, setPendingReject] = (0, react.useState)(false);
|
|
837
|
+
const [error, setError] = (0, react.useState)(null);
|
|
838
|
+
(0, react.useEffect)(() => {
|
|
839
|
+
const onKey = (e) => {
|
|
840
|
+
if (e.key === "Escape") onClose();
|
|
841
|
+
};
|
|
842
|
+
window.addEventListener("keydown", onKey);
|
|
843
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
844
|
+
}, [onClose]);
|
|
845
|
+
/**
|
|
846
|
+
* Pick one element as the winner: keep it as `ready` with the winner
|
|
847
|
+
* flag, archive the others. Per doc 04 §2: "选 winner 后其他自动归档
|
|
848
|
+
* (status = archived)".
|
|
849
|
+
*/
|
|
850
|
+
const onPickWinner = async (winner) => {
|
|
851
|
+
if (winner.uuid === void 0) return;
|
|
852
|
+
const winnerUuid = winner.uuid;
|
|
853
|
+
const losers = elements.filter((e) => e.uuid !== void 0 && e.uuid !== winnerUuid);
|
|
854
|
+
setPendingWinner(winnerUuid);
|
|
855
|
+
setError(null);
|
|
856
|
+
try {
|
|
857
|
+
await setElementStatus(sessionId, winnerUuid, "ready", true);
|
|
858
|
+
await Promise.all(losers.map((e) => setElementStatus(sessionId, e.uuid, "archived")));
|
|
859
|
+
} catch (e) {
|
|
860
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
861
|
+
} finally {
|
|
862
|
+
setPendingWinner(void 0);
|
|
863
|
+
}
|
|
864
|
+
};
|
|
865
|
+
/**
|
|
866
|
+
* Reject all: mark every selected element as `rejected`. Per doc 04 §2:
|
|
867
|
+
* "全部否决 → 所有标 rejected".
|
|
868
|
+
*/
|
|
869
|
+
const onRejectAll = async () => {
|
|
870
|
+
setPendingReject(true);
|
|
871
|
+
setError(null);
|
|
872
|
+
try {
|
|
873
|
+
await Promise.all(elements.map((e) => e.uuid !== void 0 ? setElementStatus(sessionId, e.uuid, "rejected") : Promise.resolve()));
|
|
874
|
+
} catch (e) {
|
|
875
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
876
|
+
} finally {
|
|
877
|
+
setPendingReject(false);
|
|
878
|
+
}
|
|
879
|
+
};
|
|
880
|
+
const count = elements.length;
|
|
881
|
+
return (0, react.createElement)("div", {
|
|
882
|
+
className: canvas_module_css_default.compareOverlay,
|
|
883
|
+
onWheel: (e) => e.stopPropagation(),
|
|
884
|
+
onPointerDown: (e) => e.stopPropagation()
|
|
885
|
+
}, (0, react.createElement)("div", { className: canvas_module_css_default.compareHeader }, (0, react.createElement)("span", { className: canvas_module_css_default.compareTitle }, `${t("compareTitle")} (${count})`), (0, react.createElement)("button", {
|
|
886
|
+
type: "button",
|
|
887
|
+
className: canvas_module_css_default.compareCloseButton,
|
|
888
|
+
onClick: onClose,
|
|
889
|
+
"aria-label": t("compareClose")
|
|
890
|
+
}, "×")), error !== null && (0, react.createElement)("div", { className: canvas_module_css_default.boundaryError }, error), (0, react.createElement)("div", { className: canvas_module_css_default.compareGrid }, ...elements.map((el) => (0, react.createElement)(CompareCard, {
|
|
891
|
+
key: el.uuid ?? el.filePath,
|
|
892
|
+
element: el,
|
|
893
|
+
sessionId,
|
|
894
|
+
t,
|
|
895
|
+
pending: pendingWinner === el.uuid || pendingReject,
|
|
896
|
+
onPickWinner: () => {
|
|
897
|
+
onPickWinner(el);
|
|
898
|
+
}
|
|
899
|
+
}))), (0, react.createElement)("div", { className: canvas_module_css_default.compareFooter }, (0, react.createElement)("button", {
|
|
900
|
+
type: "button",
|
|
901
|
+
className: `${canvas_module_css_default.compareButtonDanger}`,
|
|
902
|
+
onClick: () => {
|
|
903
|
+
onRejectAll();
|
|
904
|
+
},
|
|
905
|
+
disabled: pendingReject || pendingWinner !== void 0
|
|
906
|
+
}, t("compareRejectAll")), (0, react.createElement)("button", {
|
|
907
|
+
type: "button",
|
|
908
|
+
className: canvas_module_css_default.compareButtonSecondary,
|
|
909
|
+
onClick: onClose
|
|
910
|
+
}, t("compareClose"))));
|
|
911
|
+
}
|
|
912
|
+
/** One element card in the compare grid. */
|
|
913
|
+
function CompareCard(props) {
|
|
914
|
+
const { element: el, sessionId, t, pending, onPickWinner } = props;
|
|
915
|
+
const seed = getElementSeed(el);
|
|
916
|
+
const cost = getElementCost(el);
|
|
917
|
+
const durationMs = getElementDurationMs(el);
|
|
918
|
+
return (0, react.createElement)("div", { className: canvas_module_css_default.compareCard }, (0, react.createElement)("div", { className: canvas_module_css_default.compareCardMedia }, (0, react.createElement)(CompareMedia, {
|
|
919
|
+
element: el,
|
|
920
|
+
sessionId,
|
|
921
|
+
t
|
|
922
|
+
})), (0, react.createElement)("div", { className: canvas_module_css_default.compareCardBody }, el.promptText !== void 0 && el.promptText !== "" ? (0, react.createElement)("pre", { className: canvas_module_css_default.comparePrompt }, el.promptText) : (0, react.createElement)("div", { className: canvas_module_css_default.comparePromptEmpty }, t("comparePrompt")), (0, react.createElement)("div", { className: canvas_module_css_default.compareMetaRow }, (0, react.createElement)("span", { className: canvas_module_css_default.compareMetaLabel }, `${t("compareSeed")}:`), (0, react.createElement)("span", { className: canvas_module_css_default.compareMetaValue }, formatSeed(seed))), (0, react.createElement)("div", { className: canvas_module_css_default.compareMetaRow }, (0, react.createElement)("span", { className: canvas_module_css_default.compareMetaLabel }, `${t("compareCost")}:`), (0, react.createElement)("span", { className: canvas_module_css_default.compareMetaValue }, formatCost(cost))), (0, react.createElement)("div", { className: canvas_module_css_default.compareMetaRow }, (0, react.createElement)("span", { className: canvas_module_css_default.compareMetaLabel }, `${t("compareDuration")}:`), (0, react.createElement)("span", { className: canvas_module_css_default.compareMetaValue }, formatDurationShort(durationMs)))), (0, react.createElement)("div", { className: canvas_module_css_default.compareCardFooter }, (0, react.createElement)("button", {
|
|
923
|
+
type: "button",
|
|
924
|
+
className: canvas_module_css_default.compareButtonPrimary,
|
|
925
|
+
onClick: onPickWinner,
|
|
926
|
+
disabled: pending || el.uuid === void 0,
|
|
927
|
+
title: el.title
|
|
928
|
+
}, t("compareSelectWinner"))));
|
|
929
|
+
}
|
|
930
|
+
/** Renders the element's media (image / video / audio) at a fixed target size. */
|
|
931
|
+
function CompareMedia(props) {
|
|
932
|
+
const { element: el, sessionId, t } = props;
|
|
933
|
+
if (el.kind === "prompt") return (0, react.createElement)("div", { className: canvas_module_css_default.compareMediaEmpty }, el.title);
|
|
934
|
+
if (el.uuid === void 0) return (0, react.createElement)("div", { className: canvas_module_css_default.compareMediaEmpty }, t("compareNoMedia"));
|
|
935
|
+
const url = mediaUrlOf(sessionId, el.uuid);
|
|
936
|
+
if (el.kind === "image") return (0, react.createElement)("img", {
|
|
937
|
+
className: canvas_module_css_default.compareMediaImage,
|
|
938
|
+
src: url,
|
|
939
|
+
alt: el.title,
|
|
940
|
+
loading: "lazy",
|
|
941
|
+
draggable: false
|
|
942
|
+
});
|
|
943
|
+
if (el.kind === "video") return (0, react.createElement)("video", {
|
|
944
|
+
className: canvas_module_css_default.compareMediaVideo,
|
|
945
|
+
src: url,
|
|
946
|
+
controls: true,
|
|
947
|
+
preload: "metadata"
|
|
948
|
+
});
|
|
949
|
+
return (0, react.createElement)("div", { className: canvas_module_css_default.compareMediaAudioWrap }, (0, react.createElement)("audio", {
|
|
950
|
+
className: canvas_module_css_default.compareMediaAudio,
|
|
951
|
+
src: url,
|
|
952
|
+
controls: true,
|
|
953
|
+
preload: "metadata"
|
|
954
|
+
}));
|
|
955
|
+
}
|
|
956
|
+
/** Error boundary so a render failure shows a strip instead of blanking the overlay. */
|
|
957
|
+
var CompareBoundary = class extends react.Component {
|
|
958
|
+
state = { error: null };
|
|
959
|
+
static getDerivedStateFromError(error) {
|
|
960
|
+
return { error: error instanceof Error ? error.message : String(error) };
|
|
961
|
+
}
|
|
962
|
+
componentDidCatch(error, info) {
|
|
963
|
+
console.error("[dsh-aigc-canvas] compare view error:", error, info.componentStack);
|
|
964
|
+
}
|
|
965
|
+
render() {
|
|
966
|
+
if (this.state.error !== null) return (0, react.createElement)("div", {
|
|
967
|
+
className: canvas_module_css_default.boundaryError,
|
|
968
|
+
style: { margin: "8px" }
|
|
969
|
+
}, `${this.props.t("loadError")}: ${this.state.error}`);
|
|
970
|
+
return this.props.children;
|
|
971
|
+
}
|
|
972
|
+
};
|
|
973
|
+
//#endregion
|
|
974
|
+
//#region src/client/CanvasView.tsx
|
|
975
|
+
/**
|
|
976
|
+
* The infinite canvas view: a free, pannable + zoomable surface where
|
|
977
|
+
* elements live at arbitrary world positions (x, y) and edges render as
|
|
978
|
+
* smooth curves between right/left ports.
|
|
979
|
+
*
|
|
980
|
+
* Interactions:
|
|
981
|
+
* - drag an element: moves it (persisted via the canvas.move API on release)
|
|
982
|
+
* - drag the background: pans the viewport
|
|
983
|
+
* - wheel: zooms around the cursor (clamped 0.2×–4×)
|
|
984
|
+
* - zoom slider / +/- buttons in the header: zoom from center
|
|
985
|
+
* - minimap (bottom-right): click/drag to pan; shows element outlines + viewport frame
|
|
986
|
+
* - double-click an element: opens the detail panel (prompt + params + path)
|
|
987
|
+
*
|
|
988
|
+
* The WS push delivers authoritative snapshots; dragged positions are
|
|
989
|
+
* applied locally as drafts during the gesture and confirmed by the push
|
|
990
|
+
* (the host notifies after persisting the move).
|
|
991
|
+
*/
|
|
992
|
+
/** Error boundary so a render failure shows a strip instead of blanking. */
|
|
993
|
+
var CanvasBoundary = class extends react.Component {
|
|
994
|
+
state = { error: null };
|
|
995
|
+
static getDerivedStateFromError(error) {
|
|
996
|
+
return { error: error instanceof Error ? error.message : String(error) };
|
|
997
|
+
}
|
|
998
|
+
componentDidCatch(error, info) {
|
|
999
|
+
console.error("[dsh-aigc-canvas] render error:", error, info.componentStack);
|
|
1000
|
+
}
|
|
1001
|
+
render() {
|
|
1002
|
+
if (this.state.error !== null) return (0, react.createElement)("div", { className: canvas_module_css_default.boundaryError }, `${this.props.t("loadError")}: ${this.state.error}`);
|
|
1003
|
+
return this.props.children;
|
|
1004
|
+
}
|
|
1005
|
+
};
|
|
1006
|
+
const MIN_SCALE = .2;
|
|
1007
|
+
const MAX_SCALE = 4;
|
|
1008
|
+
/** Fixed node box for edge anchoring (world units). Must match CSS .nodeBox width. */
|
|
1009
|
+
const NODE_W = 240;
|
|
1010
|
+
const NODE_H = 110;
|
|
1011
|
+
/** Zoom at the cursor position, keeping the world point under the cursor fixed. */
|
|
1012
|
+
function zoomAt(viewport, cx, cy, factor) {
|
|
1013
|
+
const scale = Math.min(MAX_SCALE, Math.max(MIN_SCALE, viewport.scale * factor));
|
|
1014
|
+
const worldX = (cx - viewport.x) / viewport.scale;
|
|
1015
|
+
const worldY = (cy - viewport.y) / viewport.scale;
|
|
1016
|
+
return {
|
|
1017
|
+
scale,
|
|
1018
|
+
x: cx - worldX * scale,
|
|
1019
|
+
y: cy - worldY * scale
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
/** Build a uuid → element map. */
|
|
1023
|
+
function elementMap(elements) {
|
|
1024
|
+
const map = /* @__PURE__ */ new Map();
|
|
1025
|
+
for (const el of elements) if (el.uuid !== void 0) map.set(el.uuid, el);
|
|
1026
|
+
return map;
|
|
1027
|
+
}
|
|
1028
|
+
/** Port radius (the small circle drawn at each connection point). */
|
|
1029
|
+
const PORT_R = 5;
|
|
1030
|
+
function lineStyleOf(relation) {
|
|
1031
|
+
switch (relation) {
|
|
1032
|
+
case "reference":
|
|
1033
|
+
case "style":
|
|
1034
|
+
case "mask": return "dashed";
|
|
1035
|
+
case "variation_of":
|
|
1036
|
+
case "remix_of":
|
|
1037
|
+
case "alternative_of": return "dotted";
|
|
1038
|
+
case "edited_from": return "bold";
|
|
1039
|
+
case "input":
|
|
1040
|
+
case "first_frame":
|
|
1041
|
+
case "last_frame":
|
|
1042
|
+
case "audio_track":
|
|
1043
|
+
case void 0:
|
|
1044
|
+
default: return "solid";
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
/** Short human-readable label for one EdgeRelation (rendered at the curve midpoint). */
|
|
1048
|
+
const EDGE_RELATION_LABEL = {
|
|
1049
|
+
input: "",
|
|
1050
|
+
first_frame: "首帧",
|
|
1051
|
+
last_frame: "尾帧",
|
|
1052
|
+
audio_track: "音轨",
|
|
1053
|
+
reference: "参考",
|
|
1054
|
+
style: "风格",
|
|
1055
|
+
mask: "蒙版",
|
|
1056
|
+
variation_of: "变体",
|
|
1057
|
+
remix_of: "再创作",
|
|
1058
|
+
alternative_of: "候选",
|
|
1059
|
+
edited_from: "编辑自"
|
|
1060
|
+
};
|
|
1061
|
+
/** CSS class suffix for one EdgeLineStyle (appended to `edgeLine` / `edgeArrow`). */
|
|
1062
|
+
function edgeLineClass(style) {
|
|
1063
|
+
switch (style) {
|
|
1064
|
+
case "dashed": return canvas_module_css_default.edgeLineDashed ?? "";
|
|
1065
|
+
case "dotted": return canvas_module_css_default.edgeLineDotted ?? "";
|
|
1066
|
+
case "bold": return canvas_module_css_default.edgeLineBold ?? "";
|
|
1067
|
+
default: return canvas_module_css_default.edgeLineSolid ?? "";
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
/**
|
|
1071
|
+
* One smooth-curve edge: exits the source's right-center port, curves
|
|
1072
|
+
* through two control points, enters the target's left-center port.
|
|
1073
|
+
* Drawn as an SVG cubic-bezier path + two port circles + an arrowhead,
|
|
1074
|
+
* with line style + label driven by the edge's `relation`.
|
|
1075
|
+
*
|
|
1076
|
+
* Line styles per relation group:
|
|
1077
|
+
* - solid (default): input / first_frame / last_frame / audio_track
|
|
1078
|
+
* - dashed: reference / style / mask
|
|
1079
|
+
* - dotted: variation_of / remix_of / alternative_of
|
|
1080
|
+
* - bold: edited_from (ffmpeg edit chain)
|
|
1081
|
+
*
|
|
1082
|
+
* Uses the position resolver so the edge follows live drag positions
|
|
1083
|
+
* (drafts) in real time, not just the persisted snapshot.
|
|
1084
|
+
*/
|
|
1085
|
+
function renderEdge(edge, resolvePos) {
|
|
1086
|
+
const srcPos = resolvePos(edge.source);
|
|
1087
|
+
const tgtPos = resolvePos(edge.target);
|
|
1088
|
+
if (srcPos === void 0 || tgtPos === void 0) return null;
|
|
1089
|
+
const sx = srcPos.x + NODE_W;
|
|
1090
|
+
const sy = srcPos.y + NODE_H / 2;
|
|
1091
|
+
const tx = tgtPos.x;
|
|
1092
|
+
const ty = tgtPos.y + NODE_H / 2;
|
|
1093
|
+
const dx = Math.abs(tx - sx);
|
|
1094
|
+
const offset = Math.max(40, Math.min(dx * .5, 160));
|
|
1095
|
+
const c1x = sx + offset;
|
|
1096
|
+
const c1y = sy;
|
|
1097
|
+
const c2x = tx - offset;
|
|
1098
|
+
const c2y = ty;
|
|
1099
|
+
const d = `M ${sx} ${sy} C ${c1x} ${c1y}, ${c2x} ${c2y}, ${tx} ${ty}`;
|
|
1100
|
+
const arrow = 9;
|
|
1101
|
+
const wing = arrow * .55;
|
|
1102
|
+
const baseX = tx - arrow;
|
|
1103
|
+
const arrowPath = `M ${tx} ${ty} L ${baseX} ${ty - wing} L ${baseX} ${ty + wing} Z`;
|
|
1104
|
+
const lineClass = edgeLineClass(lineStyleOf(edge.relation));
|
|
1105
|
+
const label = edge.relation !== void 0 ? EDGE_RELATION_LABEL[edge.relation] : "";
|
|
1106
|
+
const mx = .125 * sx + .375 * c1x + .375 * c2x + .125 * tx;
|
|
1107
|
+
const my = .125 * sy + .375 * c1y + .375 * c2y + .125 * ty;
|
|
1108
|
+
return (0, react.createElement)("g", { key: `${edge.source}:${edge.target}` }, (0, react.createElement)("path", {
|
|
1109
|
+
d,
|
|
1110
|
+
className: `${canvas_module_css_default.edgeLine} ${lineClass}`,
|
|
1111
|
+
fill: "none"
|
|
1112
|
+
}), (0, react.createElement)("path", {
|
|
1113
|
+
d: arrowPath,
|
|
1114
|
+
className: `${canvas_module_css_default.edgeArrow} ${lineClass}`
|
|
1115
|
+
}), (0, react.createElement)("circle", {
|
|
1116
|
+
cx: sx,
|
|
1117
|
+
cy: sy,
|
|
1118
|
+
r: PORT_R,
|
|
1119
|
+
className: canvas_module_css_default.edgePort
|
|
1120
|
+
}), (0, react.createElement)("circle", {
|
|
1121
|
+
cx: tx,
|
|
1122
|
+
cy: ty,
|
|
1123
|
+
r: PORT_R,
|
|
1124
|
+
className: canvas_module_css_default.edgePort
|
|
1125
|
+
}), ...label !== "" ? [(0, react.createElement)("rect", {
|
|
1126
|
+
key: "label-bg",
|
|
1127
|
+
x: mx - 24,
|
|
1128
|
+
y: my - 9,
|
|
1129
|
+
width: 48,
|
|
1130
|
+
height: 18,
|
|
1131
|
+
rx: 9,
|
|
1132
|
+
className: canvas_module_css_default.edgeLabelBg
|
|
1133
|
+
}), (0, react.createElement)("text", {
|
|
1134
|
+
key: "label-text",
|
|
1135
|
+
x: mx,
|
|
1136
|
+
y: my + 4,
|
|
1137
|
+
textAnchor: "middle",
|
|
1138
|
+
className: canvas_module_css_default.edgeLabel
|
|
1139
|
+
}, label)] : []);
|
|
1140
|
+
}
|
|
1141
|
+
/**
|
|
1142
|
+
* The infinite canvas view.
|
|
1143
|
+
* @param props - store + locale translate.
|
|
1144
|
+
* @returns the canvas element.
|
|
1145
|
+
*/
|
|
1146
|
+
function CanvasView({ store, t }) {
|
|
1147
|
+
const state = (0, react.useSyncExternalStore)(store.subscribe, store.getSnapshot, store.getSnapshot);
|
|
1148
|
+
const [viewport, setViewport] = (0, react.useState)({
|
|
1149
|
+
x: 0,
|
|
1150
|
+
y: 0,
|
|
1151
|
+
scale: 1
|
|
1152
|
+
});
|
|
1153
|
+
const [drafts, setDrafts] = (0, react.useState)(/* @__PURE__ */ new Map());
|
|
1154
|
+
const [selected, setSelected] = (0, react.useState)(void 0);
|
|
1155
|
+
/**
|
|
1156
|
+
* Multi-select set for the compare view (per docs/product/04-ux-reliability.md §2).
|
|
1157
|
+
*
|
|
1158
|
+
* Holds 0-4 element uuids. Shift+click on a node toggles its uuid in
|
|
1159
|
+
* this set; click on the empty surface clears it. When the set has
|
|
1160
|
+
* 2-4 entries, the canvas header shows a "Compare" button that opens
|
|
1161
|
+
* the CompareView overlay.
|
|
1162
|
+
*/
|
|
1163
|
+
const [multiSelected, setMultiSelected] = (0, react.useState)(/* @__PURE__ */ new Set());
|
|
1164
|
+
/** Whether the CompareView overlay is currently mounted. */
|
|
1165
|
+
const [showCompare, setShowCompare] = (0, react.useState)(false);
|
|
1166
|
+
const [contextMenu, setContextMenu] = (0, react.useState)(void 0);
|
|
1167
|
+
const [dropTarget, setDropTarget] = (0, react.useState)(void 0);
|
|
1168
|
+
const [uploading, setUploading] = (0, react.useState)(false);
|
|
1169
|
+
const [showLog, setShowLog] = (0, react.useState)(false);
|
|
1170
|
+
const [statusFilter, setStatusFilter] = (0, react.useState)(/* @__PURE__ */ new Set(["ready"]));
|
|
1171
|
+
const [sessionCost, setSessionCost] = (0, react.useState)(null);
|
|
1172
|
+
const surfaceRef = (0, react.useRef)(null);
|
|
1173
|
+
(0, react.useEffect)(() => {
|
|
1174
|
+
if (state.sessionId === "") return;
|
|
1175
|
+
let cancelled = false;
|
|
1176
|
+
const poll = async () => {
|
|
1177
|
+
try {
|
|
1178
|
+
const cost = await fetchSessionCost(state.sessionId);
|
|
1179
|
+
if (!cancelled) setSessionCost(cost);
|
|
1180
|
+
} catch {}
|
|
1181
|
+
};
|
|
1182
|
+
poll();
|
|
1183
|
+
const timer = setInterval(poll, 3e3);
|
|
1184
|
+
return () => {
|
|
1185
|
+
cancelled = true;
|
|
1186
|
+
clearInterval(timer);
|
|
1187
|
+
};
|
|
1188
|
+
}, [state.sessionId]);
|
|
1189
|
+
const prevUuidsRef = (0, react.useRef)(/* @__PURE__ */ new Set());
|
|
1190
|
+
/** Toggle one status in the filter set (checkbox handler). */
|
|
1191
|
+
const toggleStatus = (status) => {
|
|
1192
|
+
setStatusFilter((prev) => {
|
|
1193
|
+
const next = new Set(prev);
|
|
1194
|
+
if (next.has(status)) next.delete(status);
|
|
1195
|
+
else next.add(status);
|
|
1196
|
+
if (next.size === 0) next.add("ready");
|
|
1197
|
+
return next;
|
|
1198
|
+
});
|
|
1199
|
+
};
|
|
1200
|
+
/** Filtered elements based on the status filter checkboxes. */
|
|
1201
|
+
const filteredElements = state.elements.filter((el) => {
|
|
1202
|
+
const status = el.status ?? "ready";
|
|
1203
|
+
return statusFilter.has(status);
|
|
1204
|
+
});
|
|
1205
|
+
/** Filtered edges: only those whose source AND target are in the filtered set. */
|
|
1206
|
+
const filteredElementPaths = new Set(filteredElements.map((e) => e.filePath));
|
|
1207
|
+
const filteredEdges = state.edges.filter((e) => filteredElementPaths.has(e.source) && filteredElementPaths.has(e.target));
|
|
1208
|
+
const panRef = (0, react.useRef)(null);
|
|
1209
|
+
const dragRef = (0, react.useRef)(null);
|
|
1210
|
+
(0, react.useEffect)(() => {
|
|
1211
|
+
const surface = surfaceRef.current;
|
|
1212
|
+
if (surface === null) return;
|
|
1213
|
+
const onWheel = (event) => {
|
|
1214
|
+
event.preventDefault();
|
|
1215
|
+
const rect = surface.getBoundingClientRect();
|
|
1216
|
+
const cx = event.clientX - rect.left;
|
|
1217
|
+
const cy = event.clientY - rect.top;
|
|
1218
|
+
const factor = event.deltaY < 0 ? 1.12 : 1 / 1.12;
|
|
1219
|
+
setViewport((prev) => zoomAt(prev, cx, cy, factor));
|
|
1220
|
+
};
|
|
1221
|
+
surface.addEventListener("wheel", onWheel, { passive: false });
|
|
1222
|
+
return () => surface.removeEventListener("wheel", onWheel);
|
|
1223
|
+
}, []);
|
|
1224
|
+
(0, react.useEffect)(() => {
|
|
1225
|
+
if (drafts.size === 0) return;
|
|
1226
|
+
const lookup = elementMap(state.elements);
|
|
1227
|
+
const stale = [];
|
|
1228
|
+
for (const [uuid, draftPos] of drafts) {
|
|
1229
|
+
const el = lookup.get(uuid);
|
|
1230
|
+
if (el !== void 0 && el.x === draftPos.x && el.y === draftPos.y) stale.push(uuid);
|
|
1231
|
+
}
|
|
1232
|
+
if (stale.length > 0) setDrafts((prev) => {
|
|
1233
|
+
const next = new Map(prev);
|
|
1234
|
+
for (const uuid of stale) next.delete(uuid);
|
|
1235
|
+
return next;
|
|
1236
|
+
});
|
|
1237
|
+
}, [state, drafts]);
|
|
1238
|
+
(0, react.useEffect)(() => {
|
|
1239
|
+
if (panRef.current !== null || dragRef.current !== null) return;
|
|
1240
|
+
const surface = surfaceRef.current;
|
|
1241
|
+
if (surface === null) return;
|
|
1242
|
+
const prev = prevUuidsRef.current;
|
|
1243
|
+
let newest;
|
|
1244
|
+
for (const el of state.elements) if (el.uuid !== void 0 && !prev.has(el.uuid)) newest = el;
|
|
1245
|
+
const nextUuids = /* @__PURE__ */ new Set();
|
|
1246
|
+
for (const el of state.elements) if (el.uuid !== void 0) nextUuids.add(el.uuid);
|
|
1247
|
+
prevUuidsRef.current = nextUuids;
|
|
1248
|
+
if (newest === void 0) return;
|
|
1249
|
+
const rect = surface.getBoundingClientRect();
|
|
1250
|
+
const margin = 32;
|
|
1251
|
+
const screenX = newest.x * viewport.scale + viewport.x;
|
|
1252
|
+
const screenY = newest.y * viewport.scale + viewport.y;
|
|
1253
|
+
const elemW = NODE_W * viewport.scale;
|
|
1254
|
+
const elemH = NODE_H * viewport.scale;
|
|
1255
|
+
let panX = 0;
|
|
1256
|
+
let panY = 0;
|
|
1257
|
+
if (screenY + elemH > rect.height - margin) panY = screenY + elemH - (rect.height - margin);
|
|
1258
|
+
if (screenY < margin) panY = screenY - margin;
|
|
1259
|
+
if (screenX + elemW > rect.width - margin) panX = screenX + elemW - (rect.width - margin);
|
|
1260
|
+
if (screenX < margin) panX = screenX - margin;
|
|
1261
|
+
if (panX !== 0 || panY !== 0) setViewport((prev) => ({
|
|
1262
|
+
...prev,
|
|
1263
|
+
x: prev.x - panX,
|
|
1264
|
+
y: prev.y - panY
|
|
1265
|
+
}));
|
|
1266
|
+
}, [state, viewport.scale]);
|
|
1267
|
+
const [surfaceSize, setSurfaceSize] = (0, react.useState)({
|
|
1268
|
+
width: 0,
|
|
1269
|
+
height: 0
|
|
1270
|
+
});
|
|
1271
|
+
(0, react.useEffect)(() => {
|
|
1272
|
+
const surface = surfaceRef.current;
|
|
1273
|
+
if (surface === null) return;
|
|
1274
|
+
const update = () => {
|
|
1275
|
+
const rect = surface.getBoundingClientRect();
|
|
1276
|
+
setSurfaceSize({
|
|
1277
|
+
width: rect.width,
|
|
1278
|
+
height: rect.height
|
|
1279
|
+
});
|
|
1280
|
+
};
|
|
1281
|
+
update();
|
|
1282
|
+
const observer = new ResizeObserver(update);
|
|
1283
|
+
observer.observe(surface);
|
|
1284
|
+
return () => observer.disconnect();
|
|
1285
|
+
}, []);
|
|
1286
|
+
/** Zoom to a target scale, keeping the center of the viewport fixed. */
|
|
1287
|
+
const zoomToCenter = (newScale) => {
|
|
1288
|
+
const surface = surfaceRef.current;
|
|
1289
|
+
if (surface === null) return;
|
|
1290
|
+
const rect = surface.getBoundingClientRect();
|
|
1291
|
+
const cx = rect.width / 2;
|
|
1292
|
+
const cy = rect.height / 2;
|
|
1293
|
+
setViewport((prev) => {
|
|
1294
|
+
const s = Math.min(MAX_SCALE, Math.max(MIN_SCALE, newScale));
|
|
1295
|
+
const worldX = (cx - prev.x) / prev.scale;
|
|
1296
|
+
const worldY = (cy - prev.y) / prev.scale;
|
|
1297
|
+
return {
|
|
1298
|
+
scale: s,
|
|
1299
|
+
x: cx - worldX * s,
|
|
1300
|
+
y: cy - worldY * s
|
|
1301
|
+
};
|
|
1302
|
+
});
|
|
1303
|
+
};
|
|
1304
|
+
const onSurfacePointerDown = (event) => {
|
|
1305
|
+
if (dragRef.current !== null) return;
|
|
1306
|
+
panRef.current = {
|
|
1307
|
+
pointerId: event.pointerId,
|
|
1308
|
+
startX: event.clientX,
|
|
1309
|
+
startY: event.clientY,
|
|
1310
|
+
orig: viewport
|
|
1311
|
+
};
|
|
1312
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
1313
|
+
};
|
|
1314
|
+
const onSurfacePointerMove = (event) => {
|
|
1315
|
+
const pan = panRef.current;
|
|
1316
|
+
if (pan !== null && pan.pointerId === event.pointerId) {
|
|
1317
|
+
setViewport({
|
|
1318
|
+
...pan.orig,
|
|
1319
|
+
x: pan.orig.x + (event.clientX - pan.startX),
|
|
1320
|
+
y: pan.orig.y + (event.clientY - pan.startY)
|
|
1321
|
+
});
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
const drag = dragRef.current;
|
|
1325
|
+
if (drag !== null && drag.pointerId === event.pointerId) setDrafts((prev) => {
|
|
1326
|
+
const next = new Map(prev);
|
|
1327
|
+
next.set(drag.uuid, {
|
|
1328
|
+
x: drag.origX + (event.clientX - drag.startX) / viewport.scale,
|
|
1329
|
+
y: drag.origY + (event.clientY - drag.startY) / viewport.scale
|
|
1330
|
+
});
|
|
1331
|
+
return next;
|
|
1332
|
+
});
|
|
1333
|
+
};
|
|
1334
|
+
const onSurfacePointerUp = (event) => {
|
|
1335
|
+
const pan = panRef.current;
|
|
1336
|
+
if (pan !== null && pan.pointerId === event.pointerId) {
|
|
1337
|
+
panRef.current = null;
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
const drag = dragRef.current;
|
|
1341
|
+
if (drag !== null && drag.pointerId === event.pointerId) {
|
|
1342
|
+
dragRef.current = null;
|
|
1343
|
+
const pos = drafts.get(drag.uuid);
|
|
1344
|
+
if (pos !== void 0 && (pos.x !== drag.origX || pos.y !== drag.origY)) store.move(drag.uuid, pos.x, pos.y);
|
|
1345
|
+
}
|
|
1346
|
+
};
|
|
1347
|
+
const onNodePointerDown = (event, el) => {
|
|
1348
|
+
if (el.uuid === void 0) return;
|
|
1349
|
+
event.stopPropagation();
|
|
1350
|
+
if (event.shiftKey) {
|
|
1351
|
+
setMultiSelected((prev) => {
|
|
1352
|
+
const next = new Set(prev);
|
|
1353
|
+
if (next.has(el.uuid)) next.delete(el.uuid);
|
|
1354
|
+
else if (next.size < 4) next.add(el.uuid);
|
|
1355
|
+
return next;
|
|
1356
|
+
});
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1359
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
1360
|
+
dragRef.current = {
|
|
1361
|
+
pointerId: event.pointerId,
|
|
1362
|
+
uuid: el.uuid,
|
|
1363
|
+
startX: event.clientX,
|
|
1364
|
+
startY: event.clientY,
|
|
1365
|
+
origX: el.x,
|
|
1366
|
+
origY: el.y
|
|
1367
|
+
};
|
|
1368
|
+
};
|
|
1369
|
+
const onNodeContextMenu = (event, el) => {
|
|
1370
|
+
if (el.uuid === void 0) return;
|
|
1371
|
+
event.preventDefault();
|
|
1372
|
+
event.stopPropagation();
|
|
1373
|
+
setContextMenu({
|
|
1374
|
+
x: event.clientX,
|
|
1375
|
+
y: event.clientY,
|
|
1376
|
+
uuid: el.uuid
|
|
1377
|
+
});
|
|
1378
|
+
};
|
|
1379
|
+
const onSurfaceClick = (event) => {
|
|
1380
|
+
if (contextMenu !== void 0) {
|
|
1381
|
+
event.stopPropagation();
|
|
1382
|
+
setContextMenu(void 0);
|
|
1383
|
+
}
|
|
1384
|
+
if (multiSelected.size > 0) setMultiSelected(/* @__PURE__ */ new Set());
|
|
1385
|
+
};
|
|
1386
|
+
const onDeleteElement = (uuid) => {
|
|
1387
|
+
setContextMenu(void 0);
|
|
1388
|
+
if (selected?.uuid === uuid) setSelected(void 0);
|
|
1389
|
+
store.deleteElement(uuid);
|
|
1390
|
+
};
|
|
1391
|
+
/**
|
|
1392
|
+
* Resolve the element for one context-menu uuid. The menu is only
|
|
1393
|
+
* shown for elements with a uuid (see onNodeContextMenu), so this
|
|
1394
|
+
* always finds a match — but be defensive in case the WS push
|
|
1395
|
+
* removed the element between the right-click and the action.
|
|
1396
|
+
*/
|
|
1397
|
+
const elementForMenu = (uuid) => {
|
|
1398
|
+
return state.elements.find((e) => e.uuid === uuid);
|
|
1399
|
+
};
|
|
1400
|
+
/**
|
|
1401
|
+
* Send a user-role notice to the agent (non-waking). The host's
|
|
1402
|
+
* `canvas.notify` endpoint wraps `agent.inject` so the client does
|
|
1403
|
+
* not need direct access to the agent registry. Best-effort: a
|
|
1404
|
+
* network failure is swallowed (the next WS push carries the
|
|
1405
|
+
* agent's response, which the user will see in the conversation).
|
|
1406
|
+
*/
|
|
1407
|
+
const sendNotice = (uuid, message, summary) => {
|
|
1408
|
+
setContextMenu(void 0);
|
|
1409
|
+
if (state.sessionId === "") return;
|
|
1410
|
+
notifyAgent(state.sessionId, message, summary).catch(() => {});
|
|
1411
|
+
};
|
|
1412
|
+
/** Replace `{filePath}` / `{kind}` / `{title}` placeholders in a notice template. */
|
|
1413
|
+
const fillTemplate = (template, el) => {
|
|
1414
|
+
return template.replaceAll("{filePath}", el.filePath).replaceAll("{kind}", el.kind).replaceAll("{title}", el.title);
|
|
1415
|
+
};
|
|
1416
|
+
/** 重新生成... → ask the agent to reroll the element with aigc_reroll. */
|
|
1417
|
+
const onRegenerate = (el) => {
|
|
1418
|
+
sendNotice(el.uuid ?? "", fillTemplate(t("noticeRegenerate"), el), `regenerate ${el.kind} "${el.title}"`);
|
|
1419
|
+
};
|
|
1420
|
+
/**
|
|
1421
|
+
* 用作参考... → copy the filePath to the clipboard AND send a notice
|
|
1422
|
+
* to the agent. The doc also describes a relation-picker dialog
|
|
1423
|
+
* (first_frame / last_frame / style / mask / reference); for the
|
|
1424
|
+
* initial implementation we send a generic "use as reference"
|
|
1425
|
+
* notice and let the agent pick the relation based on context.
|
|
1426
|
+
* The clipboard copy is the user-facing half (so the user can also
|
|
1427
|
+
* paste the path into a manual prompt).
|
|
1428
|
+
*/
|
|
1429
|
+
const onUseAsReference = (el) => {
|
|
1430
|
+
setContextMenu(void 0);
|
|
1431
|
+
navigator.clipboard?.writeText(el.filePath).catch(() => {});
|
|
1432
|
+
if (state.sessionId !== "") notifyAgent(state.sessionId, fillTemplate(t("noticeUseAsReference"), el), `use ${el.kind} "${el.title}" as reference`).catch(() => {});
|
|
1433
|
+
};
|
|
1434
|
+
/** 发到对话 → send the element's filePath + kind + title to the agent. */
|
|
1435
|
+
const onSendToChat = (el) => {
|
|
1436
|
+
sendNotice(el.uuid ?? "", fillTemplate(t("noticeSendToChat"), el), `use ${el.kind} "${el.title}" as reference`);
|
|
1437
|
+
};
|
|
1438
|
+
/** 下载 → open the media URL in a new tab with download=1. */
|
|
1439
|
+
const onDownload = (el) => {
|
|
1440
|
+
setContextMenu(void 0);
|
|
1441
|
+
if (el.uuid === void 0 || state.sessionId === "" && el.sessionId === void 0) return;
|
|
1442
|
+
const sid = el.sessionId ?? state.sessionId;
|
|
1443
|
+
if (sid === "") return;
|
|
1444
|
+
const url = mediaUrlOf(sid, el.uuid, true);
|
|
1445
|
+
window.open(url, "_blank", "noopener");
|
|
1446
|
+
};
|
|
1447
|
+
/**
|
|
1448
|
+
* 提升到资产库... → call library.promote. The doc describes a
|
|
1449
|
+
* category-picker dialog; for the initial implementation we
|
|
1450
|
+
* promote with the default `final-product` category and let the
|
|
1451
|
+
* user re-tag from the asset library UI later.
|
|
1452
|
+
*/
|
|
1453
|
+
const onPromoteToLibrary = (el) => {
|
|
1454
|
+
setContextMenu(void 0);
|
|
1455
|
+
if (el.uuid === void 0 || state.sessionId === "") return;
|
|
1456
|
+
promoteAsset(state.sessionId, el.uuid, {
|
|
1457
|
+
category: "final-product",
|
|
1458
|
+
title: el.title
|
|
1459
|
+
}).catch(() => {});
|
|
1460
|
+
};
|
|
1461
|
+
/**
|
|
1462
|
+
* Update an element's lifecycle status via `canvas.set_status`.
|
|
1463
|
+
* Best-effort: the WS push carries the authoritative state, so
|
|
1464
|
+
* we don't optimistically patch the local snapshot.
|
|
1465
|
+
*/
|
|
1466
|
+
const onSetStatus = (uuid, status, winner) => {
|
|
1467
|
+
setContextMenu(void 0);
|
|
1468
|
+
if (state.sessionId === "") return;
|
|
1469
|
+
setElementStatus(state.sessionId, uuid, status, winner).catch(() => {});
|
|
1470
|
+
};
|
|
1471
|
+
/**
|
|
1472
|
+
* + 生成 → ask the agent to generate a new asset (provider info →
|
|
1473
|
+
* http_request → canvas_place). The doc describes a t2i/t2v/tts
|
|
1474
|
+
* picker dialog; for the initial implementation we send a generic
|
|
1475
|
+
* "please generate something" notice and let the agent ask the
|
|
1476
|
+
* user for the specifics (prompt, kind).
|
|
1477
|
+
*/
|
|
1478
|
+
const onToolbarGenerate = () => {
|
|
1479
|
+
if (state.sessionId === "") return;
|
|
1480
|
+
notifyAgent(state.sessionId, t("noticeGenerate"), "user requested generation").catch(() => {});
|
|
1481
|
+
};
|
|
1482
|
+
/**
|
|
1483
|
+
* ✂ 编辑选中 → ask the agent to run aigc_media_edit (ffmpeg) on
|
|
1484
|
+
* the currently-selected element. Disabled when nothing is selected.
|
|
1485
|
+
*/
|
|
1486
|
+
const onToolbarEditSelected = () => {
|
|
1487
|
+
if (state.sessionId === "" || selected === void 0) return;
|
|
1488
|
+
notifyAgent(state.sessionId, fillTemplate(t("noticeEditSelected"), selected), `edit ${selected.kind} "${selected.title}"`).catch(() => {});
|
|
1489
|
+
};
|
|
1490
|
+
/**
|
|
1491
|
+
* ▶ 运行工作流 → ask the agent to list + run a pipeline template.
|
|
1492
|
+
* (Per docs/product/02-pipeline.md; the template picker UI is a
|
|
1493
|
+
* future enhancement — for now the agent lists templates in the
|
|
1494
|
+
* conversation and the user picks one.)
|
|
1495
|
+
*/
|
|
1496
|
+
const onToolbarRunWorkflow = () => {
|
|
1497
|
+
if (state.sessionId === "") return;
|
|
1498
|
+
notifyAgent(state.sessionId, t("noticeRunWorkflow"), "user requested workflow run").catch(() => {});
|
|
1499
|
+
};
|
|
1500
|
+
/**
|
|
1501
|
+
* Build the right-click context menu items for one element uuid.
|
|
1502
|
+
* Per docs/product/04-ux-reliability.md §1: 5 action items, a
|
|
1503
|
+
* separator, 3 status items, a separator, and Delete (in the
|
|
1504
|
+
* destructive style).
|
|
1505
|
+
*
|
|
1506
|
+
* The element is resolved from the current snapshot. If it was
|
|
1507
|
+
* removed between the right-click and the menu render (race with
|
|
1508
|
+
* the WS push), every item except Delete is disabled — Delete is
|
|
1509
|
+
* kept enabled because store.deleteElement is idempotent.
|
|
1510
|
+
*/
|
|
1511
|
+
const buildContextMenuItems = (uuid) => {
|
|
1512
|
+
const el = elementForMenu(uuid);
|
|
1513
|
+
const missing = el === void 0;
|
|
1514
|
+
return [
|
|
1515
|
+
{
|
|
1516
|
+
label: t("menuRegenerate"),
|
|
1517
|
+
disabled: missing,
|
|
1518
|
+
onClick: el !== void 0 ? () => onRegenerate(el) : void 0
|
|
1519
|
+
},
|
|
1520
|
+
{
|
|
1521
|
+
label: t("menuUseAsReference"),
|
|
1522
|
+
disabled: missing,
|
|
1523
|
+
onClick: el !== void 0 ? () => onUseAsReference(el) : void 0
|
|
1524
|
+
},
|
|
1525
|
+
{
|
|
1526
|
+
label: t("menuSendToChat"),
|
|
1527
|
+
disabled: missing,
|
|
1528
|
+
onClick: el !== void 0 ? () => onSendToChat(el) : void 0
|
|
1529
|
+
},
|
|
1530
|
+
{
|
|
1531
|
+
label: t("menuDownload"),
|
|
1532
|
+
disabled: missing || el?.uuid === void 0,
|
|
1533
|
+
onClick: el !== void 0 ? () => onDownload(el) : void 0
|
|
1534
|
+
},
|
|
1535
|
+
{
|
|
1536
|
+
label: t("menuPromoteToLibrary"),
|
|
1537
|
+
disabled: missing || el?.uuid === void 0,
|
|
1538
|
+
onClick: el !== void 0 ? () => onPromoteToLibrary(el) : void 0
|
|
1539
|
+
},
|
|
1540
|
+
{ separator: true },
|
|
1541
|
+
{
|
|
1542
|
+
label: t("menuMarkWinner"),
|
|
1543
|
+
disabled: missing,
|
|
1544
|
+
onClick: el !== void 0 ? () => onSetStatus(uuid, "ready", true) : void 0
|
|
1545
|
+
},
|
|
1546
|
+
{
|
|
1547
|
+
label: t("menuMarkRejected"),
|
|
1548
|
+
disabled: missing,
|
|
1549
|
+
onClick: el !== void 0 ? () => onSetStatus(uuid, "rejected") : void 0
|
|
1550
|
+
},
|
|
1551
|
+
{
|
|
1552
|
+
label: t("menuArchive"),
|
|
1553
|
+
disabled: missing,
|
|
1554
|
+
onClick: el !== void 0 ? () => onSetStatus(uuid, "archived") : void 0
|
|
1555
|
+
},
|
|
1556
|
+
{ separator: true },
|
|
1557
|
+
{
|
|
1558
|
+
label: t("delete"),
|
|
1559
|
+
danger: true,
|
|
1560
|
+
onClick: () => onDeleteElement(uuid)
|
|
1561
|
+
}
|
|
1562
|
+
];
|
|
1563
|
+
};
|
|
1564
|
+
const onSurfaceDragOver = (event) => {
|
|
1565
|
+
if (event.dataTransfer.types.includes("Files")) {
|
|
1566
|
+
event.preventDefault();
|
|
1567
|
+
event.stopPropagation();
|
|
1568
|
+
event.dataTransfer.dropEffect = "copy";
|
|
1569
|
+
const rect = surfaceRef.current?.getBoundingClientRect();
|
|
1570
|
+
if (rect !== void 0) {
|
|
1571
|
+
const sx = event.clientX - rect.left;
|
|
1572
|
+
const sy = event.clientY - rect.top;
|
|
1573
|
+
const wx = (sx - viewport.x) / viewport.scale;
|
|
1574
|
+
const wy = (sy - viewport.y) / viewport.scale;
|
|
1575
|
+
setDropTarget({
|
|
1576
|
+
x: wx,
|
|
1577
|
+
y: wy
|
|
1578
|
+
});
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
};
|
|
1582
|
+
const onSurfaceDragLeave = (event) => {
|
|
1583
|
+
if (event.currentTarget === event.target) {
|
|
1584
|
+
event.stopPropagation();
|
|
1585
|
+
setDropTarget(void 0);
|
|
1586
|
+
}
|
|
1587
|
+
};
|
|
1588
|
+
const onSurfaceDrop = async (event) => {
|
|
1589
|
+
event.preventDefault();
|
|
1590
|
+
event.stopPropagation();
|
|
1591
|
+
setDropTarget(void 0);
|
|
1592
|
+
const files = event.dataTransfer.files;
|
|
1593
|
+
if (files.length === 0) return;
|
|
1594
|
+
const rect = surfaceRef.current?.getBoundingClientRect();
|
|
1595
|
+
const sx = event.clientX - (rect?.left ?? 0);
|
|
1596
|
+
const sy = event.clientY - (rect?.top ?? 0);
|
|
1597
|
+
const wx = (sx - viewport.x) / viewport.scale;
|
|
1598
|
+
const wy = (sy - viewport.y) / viewport.scale;
|
|
1599
|
+
setUploading(true);
|
|
1600
|
+
try {
|
|
1601
|
+
for (const file of Array.from(files)) {
|
|
1602
|
+
const buf = await file.arrayBuffer();
|
|
1603
|
+
const bytes = new Uint8Array(buf);
|
|
1604
|
+
let binary = "";
|
|
1605
|
+
const chunk = 32768;
|
|
1606
|
+
for (let i = 0; i < bytes.length; i += chunk) binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
|
|
1607
|
+
const mediaBase64 = btoa(binary);
|
|
1608
|
+
await store.uploadFile(file.name, mediaBase64, {
|
|
1609
|
+
x: wx,
|
|
1610
|
+
y: wy
|
|
1611
|
+
});
|
|
1612
|
+
}
|
|
1613
|
+
} finally {
|
|
1614
|
+
setUploading(false);
|
|
1615
|
+
}
|
|
1616
|
+
};
|
|
1617
|
+
const lookup = elementMap(state.elements);
|
|
1618
|
+
const resolvePos = (uuid) => {
|
|
1619
|
+
const draft = drafts.get(uuid);
|
|
1620
|
+
if (draft !== void 0) return draft;
|
|
1621
|
+
const el = lookup.get(uuid);
|
|
1622
|
+
if (el !== void 0) return {
|
|
1623
|
+
x: el.x,
|
|
1624
|
+
y: el.y
|
|
1625
|
+
};
|
|
1626
|
+
};
|
|
1627
|
+
const posOf = (el) => {
|
|
1628
|
+
if (el.uuid !== void 0) {
|
|
1629
|
+
const draft = drafts.get(el.uuid);
|
|
1630
|
+
if (draft !== void 0) return draft;
|
|
1631
|
+
}
|
|
1632
|
+
return {
|
|
1633
|
+
x: el.x,
|
|
1634
|
+
y: el.y
|
|
1635
|
+
};
|
|
1636
|
+
};
|
|
1637
|
+
return (0, react.createElement)("div", { className: canvas_module_css_default.canvas }, (0, react.createElement)("div", { className: canvas_module_css_default.header }, (0, react.createElement)("span", { className: canvas_module_css_default.title }, t("title")), (0, react.createElement)("span", { className: canvas_module_css_default.count }, `${state.elements.length} ${t("elementCount")}`), (0, react.createElement)("span", { className: canvas_module_css_default.count }, `${state.edges.length} ${t("edgeCount")}`), sessionCost !== null && sessionCost.total > 0 ? (0, react.createElement)("span", { className: canvas_module_css_default.costDisplay }, `$${sessionCost.total.toFixed(2)}`) : null, (0, react.createElement)("span", { className: canvas_module_css_default.zoom }, `${Math.round(viewport.scale * 100)}%`), (0, react.createElement)("button", {
|
|
1638
|
+
type: "button",
|
|
1639
|
+
className: canvas_module_css_default.iconButton,
|
|
1640
|
+
onClick: () => zoomToCenter(viewport.scale * .8),
|
|
1641
|
+
title: t("zoomOut"),
|
|
1642
|
+
"aria-label": t("zoomOut")
|
|
1643
|
+
}, "−"), (0, react.createElement)("input", {
|
|
1644
|
+
type: "range",
|
|
1645
|
+
className: canvas_module_css_default.zoomSlider,
|
|
1646
|
+
min: Math.round(MIN_SCALE * 100),
|
|
1647
|
+
max: Math.round(400),
|
|
1648
|
+
value: Math.round(viewport.scale * 100),
|
|
1649
|
+
onChange: (e) => zoomToCenter(Number(e.target.value) / 100),
|
|
1650
|
+
"aria-label": t("zoom")
|
|
1651
|
+
}), (0, react.createElement)("button", {
|
|
1652
|
+
type: "button",
|
|
1653
|
+
className: canvas_module_css_default.iconButton,
|
|
1654
|
+
onClick: () => zoomToCenter(viewport.scale * 1.25),
|
|
1655
|
+
title: t("zoomIn"),
|
|
1656
|
+
"aria-label": t("zoomIn")
|
|
1657
|
+
}, "+"), (0, react.createElement)("button", {
|
|
1658
|
+
type: "button",
|
|
1659
|
+
className: canvas_module_css_default.iconButton,
|
|
1660
|
+
onClick: () => {
|
|
1661
|
+
store.refresh();
|
|
1662
|
+
},
|
|
1663
|
+
title: t("refresh"),
|
|
1664
|
+
"aria-label": t("refresh")
|
|
1665
|
+
}, "↻"), (0, react.createElement)("button", {
|
|
1666
|
+
type: "button",
|
|
1667
|
+
className: canvas_module_css_default.iconButton,
|
|
1668
|
+
onClick: () => setViewport({
|
|
1669
|
+
x: 0,
|
|
1670
|
+
y: 0,
|
|
1671
|
+
scale: 1
|
|
1672
|
+
}),
|
|
1673
|
+
title: t("resetView"),
|
|
1674
|
+
"aria-label": t("resetView")
|
|
1675
|
+
}, "⤢"), (0, react.createElement)("button", {
|
|
1676
|
+
type: "button",
|
|
1677
|
+
className: `${canvas_module_css_default.iconButton} ${showLog ? canvas_module_css_default.iconButtonActive : ""}`,
|
|
1678
|
+
onClick: () => setShowLog(!showLog),
|
|
1679
|
+
title: t("logButton"),
|
|
1680
|
+
"aria-label": t("logButton")
|
|
1681
|
+
}, "📊"), (0, react.createElement)("span", { className: canvas_module_css_default.statusFilter }, (0, react.createElement)("label", { className: canvas_module_css_default.statusFilterLabel }, (0, react.createElement)("input", {
|
|
1682
|
+
type: "checkbox",
|
|
1683
|
+
checked: statusFilter.has("ready"),
|
|
1684
|
+
onChange: () => toggleStatus("ready")
|
|
1685
|
+
}), t("statusReady")), (0, react.createElement)("label", { className: canvas_module_css_default.statusFilterLabel }, (0, react.createElement)("input", {
|
|
1686
|
+
type: "checkbox",
|
|
1687
|
+
checked: statusFilter.has("draft"),
|
|
1688
|
+
onChange: () => toggleStatus("draft")
|
|
1689
|
+
}), t("statusDraft")), (0, react.createElement)("label", { className: canvas_module_css_default.statusFilterLabel }, (0, react.createElement)("input", {
|
|
1690
|
+
type: "checkbox",
|
|
1691
|
+
checked: statusFilter.has("rejected"),
|
|
1692
|
+
onChange: () => toggleStatus("rejected")
|
|
1693
|
+
}), t("statusRejected")), (0, react.createElement)("label", { className: canvas_module_css_default.statusFilterLabel }, (0, react.createElement)("input", {
|
|
1694
|
+
type: "checkbox",
|
|
1695
|
+
checked: statusFilter.has("archived"),
|
|
1696
|
+
onChange: () => toggleStatus("archived")
|
|
1697
|
+
}), t("statusArchived")))), (0, react.createElement)("div", {
|
|
1698
|
+
className: canvas_module_css_default.surface,
|
|
1699
|
+
ref: surfaceRef,
|
|
1700
|
+
onPointerDown: onSurfacePointerDown,
|
|
1701
|
+
onPointerMove: onSurfacePointerMove,
|
|
1702
|
+
onPointerUp: onSurfacePointerUp,
|
|
1703
|
+
onPointerCancel: onSurfacePointerUp,
|
|
1704
|
+
onDoubleClick: () => setSelected(void 0),
|
|
1705
|
+
onClick: onSurfaceClick,
|
|
1706
|
+
onContextMenu: (event) => {
|
|
1707
|
+
event.preventDefault();
|
|
1708
|
+
},
|
|
1709
|
+
onDragOver: onSurfaceDragOver,
|
|
1710
|
+
onDragLeave: onSurfaceDragLeave,
|
|
1711
|
+
onDrop: (event) => {
|
|
1712
|
+
onSurfaceDrop(event);
|
|
1713
|
+
}
|
|
1714
|
+
}, state.elements.length === 0 ? (0, react.createElement)("div", { className: canvas_module_css_default.empty }, (0, react.createElement)("span", null, t("empty")), (0, react.createElement)("span", { className: canvas_module_css_default.emptyHint }, t("emptyHint"))) : (0, react.createElement)("div", {
|
|
1715
|
+
className: canvas_module_css_default.world,
|
|
1716
|
+
style: {
|
|
1717
|
+
transform: `translate(${viewport.x}px, ${viewport.y}px) scale(${viewport.scale})`,
|
|
1718
|
+
["--canvas-scale"]: viewport.scale
|
|
1719
|
+
}
|
|
1720
|
+
}, (0, react.createElement)("svg", {
|
|
1721
|
+
className: canvas_module_css_default.edgeLayer,
|
|
1722
|
+
"aria-hidden": true
|
|
1723
|
+
}, ...filteredEdges.map((edge) => renderEdge(edge, resolvePos))), ...filteredElements.map((el) => {
|
|
1724
|
+
const pos = posOf(el);
|
|
1725
|
+
const selectedClass = el.uuid !== void 0 && multiSelected.has(el.uuid) ? ` ${canvas_module_css_default.nodeBoxMultiSelected ?? ""}` : "";
|
|
1726
|
+
return (0, react.createElement)("div", {
|
|
1727
|
+
key: el.uuid ?? el.filePath,
|
|
1728
|
+
className: `${canvas_module_css_default.nodeBox} ${el.uuid !== void 0 ? canvas_module_css_default.nodeBoxDraggable : ""}${selectedClass}`,
|
|
1729
|
+
style: { transform: `translate(${pos.x}px, ${pos.y}px)` },
|
|
1730
|
+
onPointerDown: (event) => onNodePointerDown(event, el),
|
|
1731
|
+
onDoubleClick: (event) => {
|
|
1732
|
+
event.stopPropagation();
|
|
1733
|
+
setSelected(el);
|
|
1734
|
+
},
|
|
1735
|
+
onContextMenu: (event) => onNodeContextMenu(event, el)
|
|
1736
|
+
}, (0, react.createElement)(CanvasNode, {
|
|
1737
|
+
element: el,
|
|
1738
|
+
t
|
|
1739
|
+
}));
|
|
1740
|
+
})), dropTarget !== void 0 ? (0, react.createElement)("div", {
|
|
1741
|
+
className: canvas_module_css_default.dropIndicator,
|
|
1742
|
+
style: { transform: `translate(${dropTarget.x * viewport.scale + viewport.x}px, ${dropTarget.y * viewport.scale + viewport.y}px) scale(${viewport.scale})` }
|
|
1743
|
+
}) : null, uploading ? (0, react.createElement)("div", { className: canvas_module_css_default.uploadOverlay }, t("uploading")) : null), selected !== void 0 ? (0, react.createElement)(DetailPanel, {
|
|
1744
|
+
element: selected,
|
|
1745
|
+
t,
|
|
1746
|
+
onClose: () => setSelected(void 0)
|
|
1747
|
+
}) : null, showLog && state.sessionId !== "" ? (0, react.createElement)(RequestLogPanel, {
|
|
1748
|
+
sessionId: state.sessionId,
|
|
1749
|
+
t,
|
|
1750
|
+
locateElement: (filePath) => {
|
|
1751
|
+
const el = state.elements.find((e) => e.filePath === filePath);
|
|
1752
|
+
if (el !== void 0) {
|
|
1753
|
+
const cx = el.x + 120;
|
|
1754
|
+
const cy = el.y + 55;
|
|
1755
|
+
setViewport({
|
|
1756
|
+
x: (surfaceRef.current?.clientWidth ?? 800) / 2 - cx * viewport.scale,
|
|
1757
|
+
y: (surfaceRef.current?.clientHeight ?? 600) / 2 - cy * viewport.scale,
|
|
1758
|
+
scale: viewport.scale
|
|
1759
|
+
});
|
|
1760
|
+
setSelected(el);
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
}) : null, contextMenu !== void 0 ? (0, react.createElement)(ContextMenu, {
|
|
1764
|
+
x: contextMenu.x,
|
|
1765
|
+
y: contextMenu.y,
|
|
1766
|
+
items: buildContextMenuItems(contextMenu.uuid),
|
|
1767
|
+
onClose: () => setContextMenu(void 0)
|
|
1768
|
+
}) : null, state.sessionId !== "" ? (0, react.createElement)("div", { className: canvas_module_css_default.toolbar }, (0, react.createElement)("button", {
|
|
1769
|
+
type: "button",
|
|
1770
|
+
className: canvas_module_css_default.toolbarButton,
|
|
1771
|
+
onClick: onToolbarGenerate,
|
|
1772
|
+
title: t("toolbarGenerateTitle")
|
|
1773
|
+
}, t("toolbarGenerate")), (0, react.createElement)("button", {
|
|
1774
|
+
type: "button",
|
|
1775
|
+
className: canvas_module_css_default.toolbarButton,
|
|
1776
|
+
onClick: onToolbarEditSelected,
|
|
1777
|
+
disabled: selected === void 0,
|
|
1778
|
+
title: selected === void 0 ? t("toolbarNoSelection") : t("toolbarEditSelectedTitle")
|
|
1779
|
+
}, t("toolbarEditSelected")), (0, react.createElement)("button", {
|
|
1780
|
+
type: "button",
|
|
1781
|
+
className: canvas_module_css_default.toolbarButton,
|
|
1782
|
+
onClick: onToolbarRunWorkflow,
|
|
1783
|
+
title: t("toolbarRunWorkflowTitle")
|
|
1784
|
+
}, t("toolbarRunWorkflow"))) : null, multiSelected.size >= 2 ? (0, react.createElement)("div", { className: canvas_module_css_default.multiSelectBar }, (0, react.createElement)("span", { className: canvas_module_css_default.multiSelectCount }, t("compareNSelected").replace("{n}", String(multiSelected.size))), (0, react.createElement)("button", {
|
|
1785
|
+
type: "button",
|
|
1786
|
+
className: canvas_module_css_default.multiSelectCompareButton,
|
|
1787
|
+
onClick: () => setShowCompare(true),
|
|
1788
|
+
disabled: multiSelected.size < 2 || multiSelected.size > 4,
|
|
1789
|
+
title: multiSelected.size > 4 ? t("compareTooMany") : t("compareButton")
|
|
1790
|
+
}, t("compareButton")), (0, react.createElement)("button", {
|
|
1791
|
+
type: "button",
|
|
1792
|
+
className: canvas_module_css_default.multiSelectClearButton,
|
|
1793
|
+
onClick: () => setMultiSelected(/* @__PURE__ */ new Set())
|
|
1794
|
+
}, t("compareClearSelection"))) : null, showCompare && state.sessionId !== "" ? (0, react.createElement)(CompareView, {
|
|
1795
|
+
sessionId: state.sessionId,
|
|
1796
|
+
elements: state.elements.filter((e) => e.uuid !== void 0 && multiSelected.has(e.uuid)),
|
|
1797
|
+
t,
|
|
1798
|
+
onClose: () => setShowCompare(false)
|
|
1799
|
+
}) : null, state.elements.length > 0 ? (0, react.createElement)(Minimap, {
|
|
1800
|
+
elements: state.elements,
|
|
1801
|
+
viewport,
|
|
1802
|
+
surfaceSize,
|
|
1803
|
+
setViewport
|
|
1804
|
+
}) : null);
|
|
1805
|
+
}
|
|
1806
|
+
/** A minimal fixed-position context menu (right-click). */
|
|
1807
|
+
function ContextMenu(props) {
|
|
1808
|
+
(0, react.useEffect)(() => {
|
|
1809
|
+
const onDown = () => props.onClose();
|
|
1810
|
+
const onKey = (e) => {
|
|
1811
|
+
if (e.key === "Escape") props.onClose();
|
|
1812
|
+
};
|
|
1813
|
+
window.addEventListener("pointerdown", onDown, { once: true });
|
|
1814
|
+
window.addEventListener("keydown", onKey, { once: true });
|
|
1815
|
+
return () => {
|
|
1816
|
+
window.removeEventListener("pointerdown", onDown);
|
|
1817
|
+
window.removeEventListener("keydown", onKey);
|
|
1818
|
+
};
|
|
1819
|
+
}, [props]);
|
|
1820
|
+
const style = {
|
|
1821
|
+
left: props.x,
|
|
1822
|
+
top: props.y
|
|
1823
|
+
};
|
|
1824
|
+
return (0, react.createElement)("div", {
|
|
1825
|
+
className: canvas_module_css_default.contextMenu,
|
|
1826
|
+
style,
|
|
1827
|
+
onPointerDown: (e) => e.stopPropagation()
|
|
1828
|
+
}, ...props.items.map((item, i) => {
|
|
1829
|
+
if (item.separator === true) return (0, react.createElement)("hr", {
|
|
1830
|
+
key: `sep-${i}`,
|
|
1831
|
+
className: canvas_module_css_default.contextMenuSeparator
|
|
1832
|
+
});
|
|
1833
|
+
const className = [
|
|
1834
|
+
canvas_module_css_default.contextMenuItem,
|
|
1835
|
+
item.danger === true ? canvas_module_css_default.contextMenuItemDanger ?? "" : "",
|
|
1836
|
+
item.disabled === true ? canvas_module_css_default.contextMenuItemDisabled ?? "" : ""
|
|
1837
|
+
].filter((s) => s !== "").join(" ");
|
|
1838
|
+
return (0, react.createElement)("button", {
|
|
1839
|
+
key: i,
|
|
1840
|
+
type: "button",
|
|
1841
|
+
className,
|
|
1842
|
+
disabled: item.disabled === true,
|
|
1843
|
+
onClick: () => {
|
|
1844
|
+
if (item.disabled !== true) item.onClick?.();
|
|
1845
|
+
}
|
|
1846
|
+
}, item.label);
|
|
1847
|
+
}));
|
|
1848
|
+
}
|
|
1849
|
+
/** The double-click detail panel: prompt + generation params + path. */
|
|
1850
|
+
function DetailPanel({ element, t, onClose }) {
|
|
1851
|
+
const meta = element.meta;
|
|
1852
|
+
const metaEntries = Array.isArray(meta) || meta === null || typeof meta !== "object" ? [] : Object.entries(meta);
|
|
1853
|
+
return (0, react.createElement)("div", { className: canvas_module_css_default.detailPanel }, (0, react.createElement)("div", { className: canvas_module_css_default.detailHeader }, (0, react.createElement)("span", { className: canvas_module_css_default.detailTitle }, element.title), (0, react.createElement)("button", {
|
|
1854
|
+
type: "button",
|
|
1855
|
+
className: canvas_module_css_default.detailClose,
|
|
1856
|
+
onClick: onClose,
|
|
1857
|
+
"aria-label": t("detailClose")
|
|
1858
|
+
}, "×")), (0, react.createElement)("div", { className: canvas_module_css_default.detailBody }, element.promptText !== void 0 ? (0, react.createElement)("div", { className: canvas_module_css_default.detailBlock }, (0, react.createElement)("span", { className: canvas_module_css_default.detailLabel }, t("detailPrompt")), (0, react.createElement)("pre", { className: canvas_module_css_default.detailPrompt }, element.promptText)) : null, metaEntries.length > 0 ? (0, react.createElement)("div", { className: canvas_module_css_default.detailBlock }, (0, react.createElement)("span", { className: canvas_module_css_default.detailLabel }, t("detailParams")), (0, react.createElement)("dl", { className: canvas_module_css_default.metaList }, ...metaEntries.flatMap(([k, v]) => [(0, react.createElement)("dt", {
|
|
1859
|
+
key: `${k}-k`,
|
|
1860
|
+
className: canvas_module_css_default.metaKey
|
|
1861
|
+
}, k), (0, react.createElement)("dd", {
|
|
1862
|
+
key: `${k}-v`,
|
|
1863
|
+
className: canvas_module_css_default.metaValue
|
|
1864
|
+
}, formatMetaValue(v))]))) : null, (0, react.createElement)("div", { className: canvas_module_css_default.detailBlock }, (0, react.createElement)("span", { className: canvas_module_css_default.detailLabel }, t("generatedBy")), (0, react.createElement)("span", { className: canvas_module_css_default.detailValue }, element.producedBy)), (0, react.createElement)("div", { className: canvas_module_css_default.detailBlock }, (0, react.createElement)("span", { className: canvas_module_css_default.detailLabel }, t("detailPosition")), (0, react.createElement)("span", { className: canvas_module_css_default.detailValue }, `(${Math.round(element.x)}, ${Math.round(element.y)})`)), (0, react.createElement)("div", { className: canvas_module_css_default.detailBlock }, (0, react.createElement)("span", { className: canvas_module_css_default.detailLabel }, t("detailPath")), (0, react.createElement)("code", { className: canvas_module_css_default.filePath }, element.filePath))));
|
|
1865
|
+
}
|
|
1866
|
+
function formatMetaValue(v) {
|
|
1867
|
+
if (typeof v === "string") return v;
|
|
1868
|
+
if (typeof v === "number" || typeof v === "boolean") return String(v);
|
|
1869
|
+
if (v === null || v === void 0) return "";
|
|
1870
|
+
try {
|
|
1871
|
+
return JSON.stringify(v);
|
|
1872
|
+
} catch {
|
|
1873
|
+
return String(v);
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
const MINIMAP_W = 168;
|
|
1877
|
+
const MINIMAP_H = 120;
|
|
1878
|
+
/** Color dot per element kind in the minimap. */
|
|
1879
|
+
const KIND_COLOR = {
|
|
1880
|
+
image: "#4caf50",
|
|
1881
|
+
video: "#ff9800",
|
|
1882
|
+
audio: "#ab47bc",
|
|
1883
|
+
prompt: "#6b8cff"
|
|
1884
|
+
};
|
|
1885
|
+
/**
|
|
1886
|
+
* Bottom-right minimap: shows all elements as small colored rectangles and
|
|
1887
|
+
* the current viewport as a frame. Click/drag to pan the viewport.
|
|
1888
|
+
*/
|
|
1889
|
+
function Minimap(props) {
|
|
1890
|
+
const { elements, viewport, surfaceSize, setViewport } = props;
|
|
1891
|
+
const minimapRef = (0, react.useRef)(null);
|
|
1892
|
+
let minX = Infinity;
|
|
1893
|
+
let minY = Infinity;
|
|
1894
|
+
let maxX = -Infinity;
|
|
1895
|
+
let maxY = -Infinity;
|
|
1896
|
+
for (const el of elements) {
|
|
1897
|
+
minX = Math.min(minX, el.x);
|
|
1898
|
+
minY = Math.min(minY, el.y);
|
|
1899
|
+
maxX = Math.max(maxX, el.x + NODE_W);
|
|
1900
|
+
maxY = Math.max(maxY, el.y + NODE_H);
|
|
1901
|
+
}
|
|
1902
|
+
if (surfaceSize.width > 0 && surfaceSize.height > 0) {
|
|
1903
|
+
const vpMinX = -viewport.x / viewport.scale;
|
|
1904
|
+
const vpMinY = -viewport.y / viewport.scale;
|
|
1905
|
+
const vpMaxX = vpMinX + surfaceSize.width / viewport.scale;
|
|
1906
|
+
const vpMaxY = vpMinY + surfaceSize.height / viewport.scale;
|
|
1907
|
+
minX = Math.min(minX, vpMinX);
|
|
1908
|
+
minY = Math.min(minY, vpMinY);
|
|
1909
|
+
maxX = Math.max(maxX, vpMaxX);
|
|
1910
|
+
maxY = Math.max(maxY, vpMaxY);
|
|
1911
|
+
}
|
|
1912
|
+
if (!Number.isFinite(minX) || !Number.isFinite(maxX)) return null;
|
|
1913
|
+
minX -= 40;
|
|
1914
|
+
minY -= 40;
|
|
1915
|
+
maxX += 40;
|
|
1916
|
+
maxY += 40;
|
|
1917
|
+
const worldW = maxX - minX;
|
|
1918
|
+
const worldH = maxY - minY;
|
|
1919
|
+
const miniScale = Math.min(156 / worldW, 108 / worldH);
|
|
1920
|
+
const offsetX = (MINIMAP_W - worldW * miniScale) / 2;
|
|
1921
|
+
const offsetY = (MINIMAP_H - worldH * miniScale) / 2;
|
|
1922
|
+
const toMiniX = (wx) => offsetX + (wx - minX) * miniScale;
|
|
1923
|
+
const toMiniY = (wy) => offsetY + (wy - minY) * miniScale;
|
|
1924
|
+
const vpX = toMiniX(-viewport.x / viewport.scale);
|
|
1925
|
+
const vpY = toMiniY(-viewport.y / viewport.scale);
|
|
1926
|
+
const vpW = surfaceSize.width / viewport.scale * miniScale;
|
|
1927
|
+
const vpH = surfaceSize.height / viewport.scale * miniScale;
|
|
1928
|
+
const onPointerDown = (event) => {
|
|
1929
|
+
event.stopPropagation();
|
|
1930
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
1931
|
+
const pan = (clientX, clientY) => {
|
|
1932
|
+
const rect = minimapRef.current?.getBoundingClientRect();
|
|
1933
|
+
if (rect === void 0) return;
|
|
1934
|
+
const mx = clientX - rect.left;
|
|
1935
|
+
const my = clientY - rect.top;
|
|
1936
|
+
const worldX = (mx - offsetX) / miniScale + minX;
|
|
1937
|
+
const worldY = (my - offsetY) / miniScale + minY;
|
|
1938
|
+
setViewport((prev) => ({
|
|
1939
|
+
...prev,
|
|
1940
|
+
x: surfaceSize.width / 2 - worldX * prev.scale,
|
|
1941
|
+
y: surfaceSize.height / 2 - worldY * prev.scale
|
|
1942
|
+
}));
|
|
1943
|
+
};
|
|
1944
|
+
pan(event.clientX, event.clientY);
|
|
1945
|
+
const onMove = (e) => pan(e.clientX, e.clientY);
|
|
1946
|
+
const onUp = () => {
|
|
1947
|
+
window.removeEventListener("pointermove", onMove);
|
|
1948
|
+
window.removeEventListener("pointerup", onUp);
|
|
1949
|
+
};
|
|
1950
|
+
window.addEventListener("pointermove", onMove);
|
|
1951
|
+
window.addEventListener("pointerup", onUp);
|
|
1952
|
+
};
|
|
1953
|
+
return (0, react.createElement)("div", {
|
|
1954
|
+
className: canvas_module_css_default.minimap,
|
|
1955
|
+
ref: minimapRef,
|
|
1956
|
+
onPointerDown
|
|
1957
|
+
}, (0, react.createElement)("svg", {
|
|
1958
|
+
width: MINIMAP_W,
|
|
1959
|
+
height: MINIMAP_H,
|
|
1960
|
+
className: canvas_module_css_default.minimapSvg
|
|
1961
|
+
}, ...elements.map((el) => (0, react.createElement)("rect", {
|
|
1962
|
+
key: el.uuid ?? el.filePath,
|
|
1963
|
+
x: toMiniX(el.x),
|
|
1964
|
+
y: toMiniY(el.y),
|
|
1965
|
+
width: Math.max(2, NODE_W * miniScale),
|
|
1966
|
+
height: Math.max(2, NODE_H * miniScale),
|
|
1967
|
+
rx: 2,
|
|
1968
|
+
fill: KIND_COLOR[el.kind],
|
|
1969
|
+
fillOpacity: .35,
|
|
1970
|
+
stroke: KIND_COLOR[el.kind],
|
|
1971
|
+
strokeOpacity: .7,
|
|
1972
|
+
strokeWidth: 1
|
|
1973
|
+
})), (0, react.createElement)("rect", {
|
|
1974
|
+
x: vpX,
|
|
1975
|
+
y: vpY,
|
|
1976
|
+
width: vpW,
|
|
1977
|
+
height: vpH,
|
|
1978
|
+
fill: "var(--dsw-alias-label-primary)",
|
|
1979
|
+
fillOpacity: .08,
|
|
1980
|
+
stroke: "var(--dsw-alias-label-primary)",
|
|
1981
|
+
strokeOpacity: .8,
|
|
1982
|
+
strokeWidth: 2,
|
|
1983
|
+
rx: 2
|
|
1984
|
+
})));
|
|
1985
|
+
}
|
|
1986
|
+
/** Wrapped export so the tab component can mount the boundary once. */
|
|
1987
|
+
function CanvasViewWithBoundary(props) {
|
|
1988
|
+
return (0, react.createElement)(CanvasBoundary, {
|
|
1989
|
+
t: props.t,
|
|
1990
|
+
children: (0, react.createElement)(CanvasView, props)
|
|
1991
|
+
});
|
|
1992
|
+
}
|
|
1993
|
+
//#endregion
|
|
1994
|
+
//#region \0dsh-css:D:\Projects\deepseek-harness\dsh-aigc-canvas\src\client\SettingsPage.module.css.mjs
|
|
1995
|
+
const css = "/* AIGC canvas settings section, in the settings-panel design language shared\r\n * with ModelsSection / GeneralSection / yet-another-subagent: 14/22 body,\r\n * 12/18 caption, 16/24 title, capsule controls (h36 r18 primary, h28 r14\r\n * secondary), 32px fields, border-l2 hairlines, and the editor as a filled\r\n * module on the panel fill.\r\n *\r\n * Every color resolves through a --dsw-alias-* token (no literal colors). */\r\n\r\n.o6nt8s_section {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 12px;\r\n max-width: 720px;\r\n color: var(--dsw-alias-label-primary);\r\n}\r\n\r\n.o6nt8s_title {\r\n margin: 0;\r\n font-size: 16px;\r\n line-height: 24px;\r\n font-weight: 500;\r\n color: var(--dsw-alias-label-primary);\r\n}\r\n\r\n.o6nt8s_intro {\r\n margin: 0;\r\n font-size: 14px;\r\n line-height: 22px;\r\n color: var(--dsw-alias-label-tertiary);\r\n}\r\n\r\n.o6nt8s_error {\r\n margin: 0;\r\n padding: 8px 12px;\r\n border: 1px solid var(--dsw-alias-state-error-primary);\r\n border-radius: 8px;\r\n background: var(--dsw-alias-interactive-bg-hover-danger);\r\n font-size: 12px;\r\n line-height: 18px;\r\n color: var(--dsw-alias-state-error-primary);\r\n display: flex;\r\n align-items: center;\r\n justify-content: space-between;\r\n gap: 8px;\r\n}\r\n\r\n.o6nt8s_errorDismiss {\r\n flex: none;\r\n border: none;\r\n background: transparent;\r\n color: inherit;\r\n font-size: 16px;\r\n line-height: 1;\r\n cursor: pointer;\r\n padding: 0 4px;\r\n}\r\n\r\n.o6nt8s_rows {\r\n list-style: none;\r\n margin: 12px 0 0;\r\n padding: 0;\r\n display: flex;\r\n flex-direction: column;\r\n gap: 8px;\r\n}\r\n\r\n/* A configured provider: outlined on the panel fill, matching the rowCard\r\n * chrome in ModelsSection. */\r\n.o6nt8s_rowCard {\r\n border: 1px solid var(--dsw-alias-border-l2);\r\n border-radius: 12px;\r\n padding: 12px 14px;\r\n display: flex;\r\n flex-direction: column;\r\n gap: 12px;\r\n}\r\n\r\n.o6nt8s_rowHead {\r\n display: flex;\r\n align-items: center;\r\n gap: 10px;\r\n}\r\n\r\n/* Chevron toggle: a small square with two borders, rotated to point right\r\n * (collapsed) or down (expanded). Pure CSS, no icon font. */\r\n.o6nt8s_chevronButton {\r\n flex: none;\r\n width: 24px;\r\n height: 24px;\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n border: none;\r\n background: transparent;\r\n color: var(--dsw-alias-label-secondary);\r\n cursor: pointer;\r\n padding: 0;\r\n border-radius: 4px;\r\n}\r\n\r\n.o6nt8s_chevronButton:hover {\r\n background: var(--dsw-alias-interactive-bg-hover-solid);\r\n color: var(--dsw-alias-label-primary);\r\n}\r\n\r\n/* Spacer that occupies the chevron slot on cards without a toggle (e.o6nt8s_g. the\r\n * new-draft card, which is always expanded). Keeps row-head alignment. */\r\n.o6nt8s_chevronSpacer {\r\n flex: none;\r\n width: 24px;\r\n height: 24px;\r\n}\r\n\r\n.o6nt8s_chevron {\r\n width: 7px;\r\n height: 7px;\r\n border-right: 1.5px solid currentColor;\r\n border-bottom: 1.5px solid currentColor;\r\n transform: rotate(-45deg);\r\n transition: transform 0.15s ease;\r\n}\r\n\r\n.o6nt8s_chevronExpanded {\r\n transform: rotate(45deg);\r\n}\r\n\r\n.o6nt8s_rowIdentity {\r\n display: inline-flex;\r\n align-items: center;\r\n gap: 6px;\r\n min-width: 0;\r\n flex: 1 1 auto;\r\n flex-wrap: wrap;\r\n}\r\n\r\n.o6nt8s_rowName {\r\n font-size: 14px;\r\n line-height: 22px;\r\n font-weight: 500;\r\n color: var(--dsw-alias-label-primary);\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n white-space: nowrap;\r\n}\r\n\r\n.o6nt8s_rowNamePlaceholder {\r\n font-size: 14px;\r\n line-height: 22px;\r\n font-weight: 500;\r\n color: var(--dsw-alias-label-tertiary);\r\n font-style: italic;\r\n}\r\n\r\n/* Builtin badge: uses DSH <Pill> with a brand-colored override to mark\r\n * seed providers. */\r\n.o6nt8s_builtinBadge {\r\n height: 18px;\r\n padding: 0 6px;\r\n border-radius: 4px;\r\n font-size: 11px;\r\n line-height: 16px;\r\n font-weight: 500;\r\n background: var(--dsw-alias-brand-primary);\r\n color: var(--dsw-alias-label-primary-foreground);\r\n letter-spacing: 0.02em;\r\n}\r\n\r\n/* Default provider badge. */\r\n.o6nt8s_defaultBadge {\r\n height: 18px;\r\n padding: 0 6px;\r\n border-radius: 4px;\r\n font-size: 11px;\r\n line-height: 16px;\r\n font-weight: 500;\r\n background: var(--dsw-alias-bg-layer-2);\r\n color: var(--dsw-alias-label-secondary);\r\n}\r\n\r\n/* Stub/real mode badge. */\r\n.o6nt8s_stubBadge {\r\n height: 18px;\r\n padding: 0 6px;\r\n border-radius: 4px;\r\n font-size: 11px;\r\n line-height: 16px;\r\n font-weight: 500;\r\n background: var(--dsw-alias-bg-layer-2);\r\n color: var(--dsw-alias-label-tertiary);\r\n}\r\n\r\n.o6nt8s_realBadge {\r\n height: 18px;\r\n padding: 0 6px;\r\n border-radius: 4px;\r\n font-size: 11px;\r\n line-height: 16px;\r\n font-weight: 500;\r\n background: var(--dsw-alias-state-success-primary);\r\n color: var(--dsw-alias-label-primary-foreground);\r\n}\r\n\r\n.o6nt8s_rowId {\r\n flex: none;\r\n padding: 1px 6px;\r\n border: 1px solid var(--dsw-alias-border-l3);\r\n border-radius: 4px;\r\n font-size: 11px;\r\n line-height: 16px;\r\n font-family: var(--dsw-font-markdown-code-block-small, monospace);\r\n color: var(--dsw-alias-label-secondary);\r\n}\r\n\r\n.o6nt8s_rowActions {\r\n display: inline-flex;\r\n align-items: center;\r\n gap: 4px;\r\n margin-left: auto;\r\n flex: none;\r\n}\r\n\r\n/* Editor surface: a filled module on the panel, matching ModelsSection's\r\n * editor chrome (bg-module-platform, r12, p14/16). */\r\n.o6nt8s_editor {\r\n border-radius: 12px;\r\n background: var(--dsw-alias-bg-module-platform);\r\n padding: 14px 16px;\r\n display: flex;\r\n flex-direction: column;\r\n gap: 14px;\r\n}\r\n\r\n.o6nt8s_field {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 6px;\r\n}\r\n\r\n.o6nt8s_fieldLabel {\r\n display: inline-flex;\r\n align-items: center;\r\n gap: 10px;\r\n font-size: 12px;\r\n line-height: 18px;\r\n font-weight: 500;\r\n color: var(--dsw-alias-label-secondary);\r\n}\r\n\r\n/* Input: matching ModelsSection .o6nt8s_input — h32, r8, border-l2, bg-layer-1. */\r\n.o6nt8s_input {\r\n box-sizing: border-box;\r\n width: 100%;\r\n height: 32px;\r\n padding: 0 10px;\r\n border: 1px solid var(--dsw-alias-border-l2);\r\n border-radius: 8px;\r\n font: inherit;\r\n font-size: 14px;\r\n line-height: 22px;\r\n background: var(--dsw-alias-bg-layer-1);\r\n color: var(--dsw-alias-label-primary);\r\n}\r\n\r\n.o6nt8s_input:focus {\r\n outline: none;\r\n border-color: var(--dsw-alias-brand-primary);\r\n}\r\n\r\n.o6nt8s_input::placeholder {\r\n color: var(--dsw-alias-label-dimmed);\r\n}\r\n\r\n.o6nt8s_input:disabled {\r\n opacity: 0.6;\r\n cursor: default;\r\n}\r\n\r\n.o6nt8s_textarea {\r\n box-sizing: border-box;\r\n width: 100%;\r\n min-height: 64px;\r\n padding: 6px 10px;\r\n border: 1px solid var(--dsw-alias-border-l2);\r\n border-radius: 8px;\r\n font: inherit;\r\n font-size: 14px;\r\n line-height: 22px;\r\n background: var(--dsw-alias-bg-layer-1);\r\n color: var(--dsw-alias-label-primary);\r\n resize: vertical;\r\n}\r\n\r\n.o6nt8s_textarea:focus {\r\n outline: none;\r\n border-color: var(--dsw-alias-brand-primary);\r\n}\r\n\r\n.o6nt8s_textarea::placeholder {\r\n color: var(--dsw-alias-label-dimmed);\r\n}\r\n\r\n/* Auth scheme row: scheme select + optional name input side by side. */\r\n.o6nt8s_authRow {\r\n display: flex;\r\n gap: 8px;\r\n}\r\n\r\n.o6nt8s_authRow .o6nt8s_input {\r\n flex: 1 1 auto;\r\n}\r\n\r\n/* Select: same chrome as .o6nt8s_input. */\r\n.o6nt8s_select {\r\n box-sizing: border-box;\r\n flex: 0 0 auto;\r\n min-width: 130px;\r\n height: 32px;\r\n padding: 0 10px;\r\n border: 1px solid var(--dsw-alias-border-l2);\r\n border-radius: 8px;\r\n font: inherit;\r\n font-size: 14px;\r\n line-height: 22px;\r\n background: var(--dsw-alias-bg-layer-1);\r\n color: var(--dsw-alias-label-primary);\r\n}\r\n\r\n.o6nt8s_select:focus {\r\n outline: none;\r\n border-color: var(--dsw-alias-brand-primary);\r\n}\r\n\r\n/* Caption labels under fields. */\r\n.o6nt8s_desc {\r\n font-size: 12px;\r\n line-height: 18px;\r\n color: var(--dsw-alias-label-tertiary);\r\n}\r\n\r\n.o6nt8s_hint {\r\n font-size: 12px;\r\n line-height: 18px;\r\n color: var(--dsw-alias-label-secondary);\r\n}\r\n\r\n/* Buttons: capsule controls matching ModelsSection (h36 r18 primary,\r\n * h28 r14 secondary in row context). */\r\n.o6nt8s_primaryButton,\r\n.o6nt8s_secondaryButton,\r\n.o6nt8s_dangerButton,\r\n.o6nt8s_addBlockButton {\r\n box-sizing: border-box;\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n gap: 4px;\r\n height: 36px;\r\n padding: 0 14px;\r\n border: none;\r\n border-radius: 18px;\r\n font: inherit;\r\n font-size: 14px;\r\n line-height: 22px;\r\n cursor: pointer;\r\n}\r\n\r\n.o6nt8s_primaryButton {\r\n background: var(--dsw-alias-button-primary-fill);\r\n color: var(--dsw-alias-label-primary-foreground);\r\n}\r\n\r\n.o6nt8s_primaryButton:hover:not(:disabled) {\r\n background: var(--dsw-alias-button-primary-hover);\r\n}\r\n\r\n.o6nt8s_secondaryButton,\r\n.o6nt8s_addBlockButton {\r\n border: 1px solid var(--dsw-alias-border-l2);\r\n background: transparent;\r\n color: var(--dsw-alias-label-primary);\r\n}\r\n\r\n.o6nt8s_secondaryButton:hover:not(:disabled),\r\n.o6nt8s_addBlockButton:hover:not(:disabled) {\r\n background: var(--dsw-alias-interactive-bg-hover-solid);\r\n}\r\n\r\n.o6nt8s_dangerButton {\r\n background: transparent;\r\n color: var(--dsw-alias-state-error-primary);\r\n}\r\n\r\n.o6nt8s_dangerButton:hover:not(:disabled) {\r\n background: var(--dsw-alias-interactive-bg-hover-danger);\r\n}\r\n\r\n/* Row-context buttons go dense (h28 r14, 12/18). */\r\n.o6nt8s_rowActions .o6nt8s_secondaryButton,\r\n.o6nt8s_rowActions .o6nt8s_dangerButton {\r\n height: 28px;\r\n padding: 0 10px;\r\n border-radius: 14px;\r\n font-size: 12px;\r\n line-height: 18px;\r\n}\r\n\r\n.o6nt8s_rowActions .o6nt8s_primaryButton {\r\n height: 28px;\r\n padding: 0 10px;\r\n border-radius: 14px;\r\n font-size: 12px;\r\n line-height: 18px;\r\n}\r\n\r\n.o6nt8s_primaryButton:disabled,\r\n.o6nt8s_secondaryButton:disabled,\r\n.o6nt8s_dangerButton:disabled,\r\n.o6nt8s_addBlockButton:disabled {\r\n opacity: 0.4;\r\n cursor: default;\r\n}\r\n\r\n.o6nt8s_primaryButton:focus-visible,\r\n.o6nt8s_secondaryButton:focus-visible,\r\n.o6nt8s_dangerButton:focus-visible,\r\n.o6nt8s_addBlockButton:focus-visible {\r\n outline: none;\r\n box-shadow: 0 0 0 2px var(--dsw-alias-border-l3);\r\n}\r\n\r\n/* Add-provider action: a full-width dashed-outline place card matching\r\n * ModelsSection's addBlock. */\r\n.o6nt8s_addBlockButton {\r\n width: 100%;\r\n margin-top: 12px;\r\n border-style: dashed;\r\n border-radius: 12px;\r\n height: 40px;\r\n color: var(--dsw-alias-label-secondary);\r\n}\r\n\r\n.o6nt8s_addBlockButton:hover:not(:disabled) {\r\n color: var(--dsw-alias-label-primary);\r\n border-color: var(--dsw-alias-brand-primary);\r\n}\r\n\r\n/* Empty state. */\r\n.o6nt8s_empty {\r\n margin: 0;\r\n padding: 24px 12px;\r\n text-align: center;\r\n font-size: 14px;\r\n line-height: 22px;\r\n color: var(--dsw-alias-label-tertiary);\r\n}\r\n\r\n/* Modal confirm body text. */\r\n.o6nt8s_confirmText {\r\n margin: 0;\r\n font-size: 14px;\r\n line-height: 22px;\r\n color: var(--dsw-alias-label-secondary);\r\n}\r\n\r\n.o6nt8s_loading {\r\n padding: 12px;\r\n font-size: 14px;\r\n line-height: 22px;\r\n color: var(--dsw-alias-label-tertiary);\r\n}\r\n\r\n/* ── Structured endpoint catalog editor (per doc 03-provider-catalog.o6nt8s_md §5) ─ */\r\n\r\n/* A row of fields laid out side-by-side (priority + quality, the three\r\n * cost fields, path + method + capability). Wraps on narrow viewports. */\r\n.o6nt8s_fieldRow {\r\n display: flex;\r\n flex-wrap: wrap;\r\n gap: 12px;\r\n align-items: flex-start;\r\n}\r\n\r\n.o6nt8s_fieldRow > .o6nt8s_field {\r\n flex: 1 1 160px;\r\n min-width: 0;\r\n}\r\n\r\n/* The label row inside .o6nt8s_field that also holds a trailing action button\r\n * (the \"Auto-detect\" button on the Endpoints field). */\r\n.o6nt8s_fieldLabelRow {\r\n display: flex;\r\n align-items: center;\r\n justify-content: space-between;\r\n gap: 8px;\r\n}\r\n\r\n.o6nt8s_endpointsAutoDetectButton {\r\n flex: none;\r\n height: 24px;\r\n padding: 0 10px;\r\n border: 1px solid var(--dsw-alias-border-l2);\r\n border-radius: 12px;\r\n background: transparent;\r\n color: var(--dsw-alias-label-secondary);\r\n font: inherit;\r\n font-size: 12px;\r\n line-height: 18px;\r\n cursor: pointer;\r\n}\r\n\r\n.o6nt8s_endpointsAutoDetectButton:hover:not(:disabled) {\r\n background: var(--dsw-alias-interactive-bg-hover-solid);\r\n color: var(--dsw-alias-label-primary);\r\n}\r\n\r\n/* The empty-state hint shown when the endpoints list (or a single\r\n * endpoint's params list) is empty. */\r\n.o6nt8s_endpointsEmpty {\r\n padding: 12px;\r\n border: 1px dashed var(--dsw-alias-border-l2);\r\n border-radius: 8px;\r\n background: var(--dsw-alias-bg-layer-1);\r\n font-size: 12px;\r\n line-height: 18px;\r\n color: var(--dsw-alias-label-tertiary);\r\n text-align: center;\r\n}\r\n\r\n/* Container for the list of endpoint cards. */\r\n.o6nt8s_endpointsList {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 8px;\r\n}\r\n\r\n/* One endpoint card: outlined on the editor fill, matching the rowCard\r\n * chrome but tighter (8px padding, smaller radius). */\r\n.o6nt8s_endpointCard {\r\n border: 1px solid var(--dsw-alias-border-l2);\r\n border-radius: 8px;\r\n padding: 10px 12px;\r\n display: flex;\r\n flex-direction: column;\r\n gap: 10px;\r\n background: var(--dsw-alias-bg-layer-1);\r\n}\r\n\r\n/* Head row of an endpoint card: shows method + path + capability badge\r\n * on the left, remove button on the right. */\r\n.o6nt8s_endpointHead {\r\n display: flex;\r\n align-items: center;\r\n justify-content: space-between;\r\n gap: 8px;\r\n min-width: 0;\r\n}\r\n\r\n.o6nt8s_endpointHeadLabel {\r\n display: inline-flex;\r\n align-items: center;\r\n gap: 6px;\r\n min-width: 0;\r\n font-family: var(--dsw-font-markdown-code-block-small, monospace);\r\n font-size: 12px;\r\n line-height: 18px;\r\n color: var(--dsw-alias-label-primary);\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n white-space: nowrap;\r\n}\r\n\r\n/* Capability badge: a small pill next to the endpoint path (e.o6nt8s_g. \"[t2i]\"). */\r\n.o6nt8s_endpointCapabilityBadge {\r\n height: 16px;\r\n padding: 0 5px;\r\n border-radius: 3px;\r\n font-size: 10px;\r\n line-height: 14px;\r\n font-weight: 500;\r\n background: var(--dsw-alias-brand-primary);\r\n color: var(--dsw-alias-label-primary-foreground);\r\n letter-spacing: 0.02em;\r\n}\r\n\r\n/* Remove button (×) on an endpoint or param row. */\r\n.o6nt8s_endpointRemoveButton,\r\n.o6nt8s_paramRemoveButton {\r\n flex: none;\r\n width: 24px;\r\n height: 24px;\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n border: none;\r\n border-radius: 4px;\r\n background: transparent;\r\n color: var(--dsw-alias-label-tertiary);\r\n cursor: pointer;\r\n font-size: 16px;\r\n line-height: 1;\r\n padding: 0;\r\n}\r\n\r\n.o6nt8s_endpointRemoveButton:hover,\r\n.o6nt8s_paramRemoveButton:hover {\r\n background: var(--dsw-alias-interactive-bg-hover-danger);\r\n color: var(--dsw-alias-state-error-primary);\r\n}\r\n\r\n/* \"+ Add endpoint\" / \"+ Add parameter\" button: a dashed-outline place\r\n * card, matching the .o6nt8s_addBlockButton aesthetic but tighter. */\r\n.o6nt8s_addEndpointButton {\r\n align-self: flex-start;\r\n margin-top: 4px;\r\n padding: 4px 12px;\r\n border: 1px dashed var(--dsw-alias-border-l2);\r\n border-radius: 8px;\r\n background: transparent;\r\n color: var(--dsw-alias-label-secondary);\r\n font: inherit;\r\n font-size: 12px;\r\n line-height: 18px;\r\n cursor: pointer;\r\n}\r\n\r\n.o6nt8s_addEndpointButton:hover:not(:disabled) {\r\n color: var(--dsw-alias-label-primary);\r\n border-color: var(--dsw-alias-brand-primary);\r\n}\r\n\r\n/* Parameter table: a grid of name / type / required / default / remove.\r\n * The columns are sized so the name + default fields get the most room\r\n * (they're the most variable in length); the type + required columns\r\n * are fixed-width. */\r\n.o6nt8s_paramsTable {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 4px;\r\n}\r\n\r\n.o6nt8s_paramRow {\r\n display: grid;\r\n grid-template-columns: 1fr 110px auto 1fr 24px;\r\n gap: 6px;\r\n align-items: center;\r\n}\r\n\r\n/* \"Required\" checkbox cell: a tight label + checkbox. */\r\n.o6nt8s_paramRequired {\r\n display: inline-flex;\r\n align-items: center;\r\n gap: 4px;\r\n font-size: 11px;\r\n line-height: 16px;\r\n color: var(--dsw-alias-label-tertiary);\r\n white-space: nowrap;\r\n}\r\n\r\n.o6nt8s_paramRequired input[type=\"checkbox\"] {\r\n margin: 0;\r\n}\r\n\r\n/* \"Accepts $base64\" checkbox row: full-width label + checkbox. */\r\n.o6nt8s_fieldRow > input[type=\"checkbox\"] {\r\n margin-top: 4px;\r\n}\r\n";
|
|
1996
|
+
const tagId = "@huanlin/dsh-plugin-aigc-canvas/SettingsPage.module.css";
|
|
1997
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
1998
|
+
const tag = document.createElement("style");
|
|
1999
|
+
tag.dataset.plugin = "@huanlin/dsh-plugin-aigc-canvas";
|
|
2000
|
+
tag.dataset.pluginCss = tagId;
|
|
2001
|
+
tag.textContent = css;
|
|
2002
|
+
document.head.appendChild(tag);
|
|
2003
|
+
}
|
|
2004
|
+
var SettingsPage_module_css_default = {
|
|
2005
|
+
"section": "o6nt8s_section",
|
|
2006
|
+
"title": "o6nt8s_title",
|
|
2007
|
+
"intro": "o6nt8s_intro",
|
|
2008
|
+
"error": "o6nt8s_error",
|
|
2009
|
+
"errorDismiss": "o6nt8s_errorDismiss",
|
|
2010
|
+
"rows": "o6nt8s_rows",
|
|
2011
|
+
"rowCard": "o6nt8s_rowCard",
|
|
2012
|
+
"rowHead": "o6nt8s_rowHead",
|
|
2013
|
+
"chevronButton": "o6nt8s_chevronButton",
|
|
2014
|
+
"g": "o6nt8s_g",
|
|
2015
|
+
"chevronSpacer": "o6nt8s_chevronSpacer",
|
|
2016
|
+
"chevron": "o6nt8s_chevron",
|
|
2017
|
+
"chevronExpanded": "o6nt8s_chevronExpanded",
|
|
2018
|
+
"rowIdentity": "o6nt8s_rowIdentity",
|
|
2019
|
+
"rowName": "o6nt8s_rowName",
|
|
2020
|
+
"rowNamePlaceholder": "o6nt8s_rowNamePlaceholder",
|
|
2021
|
+
"builtinBadge": "o6nt8s_builtinBadge",
|
|
2022
|
+
"defaultBadge": "o6nt8s_defaultBadge",
|
|
2023
|
+
"stubBadge": "o6nt8s_stubBadge",
|
|
2024
|
+
"realBadge": "o6nt8s_realBadge",
|
|
2025
|
+
"rowId": "o6nt8s_rowId",
|
|
2026
|
+
"rowActions": "o6nt8s_rowActions",
|
|
2027
|
+
"editor": "o6nt8s_editor",
|
|
2028
|
+
"field": "o6nt8s_field",
|
|
2029
|
+
"fieldLabel": "o6nt8s_fieldLabel",
|
|
2030
|
+
"input": "o6nt8s_input",
|
|
2031
|
+
"textarea": "o6nt8s_textarea",
|
|
2032
|
+
"authRow": "o6nt8s_authRow",
|
|
2033
|
+
"select": "o6nt8s_select",
|
|
2034
|
+
"desc": "o6nt8s_desc",
|
|
2035
|
+
"hint": "o6nt8s_hint",
|
|
2036
|
+
"primaryButton": "o6nt8s_primaryButton",
|
|
2037
|
+
"secondaryButton": "o6nt8s_secondaryButton",
|
|
2038
|
+
"dangerButton": "o6nt8s_dangerButton",
|
|
2039
|
+
"addBlockButton": "o6nt8s_addBlockButton",
|
|
2040
|
+
"empty": "o6nt8s_empty",
|
|
2041
|
+
"confirmText": "o6nt8s_confirmText",
|
|
2042
|
+
"loading": "o6nt8s_loading",
|
|
2043
|
+
"md": "o6nt8s_md",
|
|
2044
|
+
"fieldRow": "o6nt8s_fieldRow",
|
|
2045
|
+
"fieldLabelRow": "o6nt8s_fieldLabelRow",
|
|
2046
|
+
"endpointsAutoDetectButton": "o6nt8s_endpointsAutoDetectButton",
|
|
2047
|
+
"endpointsEmpty": "o6nt8s_endpointsEmpty",
|
|
2048
|
+
"endpointsList": "o6nt8s_endpointsList",
|
|
2049
|
+
"endpointCard": "o6nt8s_endpointCard",
|
|
2050
|
+
"endpointHead": "o6nt8s_endpointHead",
|
|
2051
|
+
"endpointHeadLabel": "o6nt8s_endpointHeadLabel",
|
|
2052
|
+
"endpointCapabilityBadge": "o6nt8s_endpointCapabilityBadge",
|
|
2053
|
+
"endpointRemoveButton": "o6nt8s_endpointRemoveButton",
|
|
2054
|
+
"paramRemoveButton": "o6nt8s_paramRemoveButton",
|
|
2055
|
+
"addEndpointButton": "o6nt8s_addEndpointButton",
|
|
2056
|
+
"paramsTable": "o6nt8s_paramsTable",
|
|
2057
|
+
"paramRow": "o6nt8s_paramRow",
|
|
2058
|
+
"paramRequired": "o6nt8s_paramRequired"
|
|
2059
|
+
};
|
|
2060
|
+
//#endregion
|
|
2061
|
+
//#region src/client/SettingsPage.tsx
|
|
2062
|
+
/**
|
|
2063
|
+
* SettingsPage — the AIGC canvas settings section: provider list CRUD.
|
|
2064
|
+
*
|
|
2065
|
+
* Visual language: matches ModelsSection / GeneralSection / yet-another-subagent —
|
|
2066
|
+
* outlined rowCard per provider (border-l2, r12, p12/14), filled editor surface
|
|
2067
|
+
* (bg-module-platform, r12, p14/16), capsule controls (h36 r18 primary,
|
|
2068
|
+
* h28 r14 secondary), 32px fields with border-l2 / bg-layer-1, 12/18 caption
|
|
2069
|
+
* labels. Every color resolves through --dsw-alias-* tokens.
|
|
2070
|
+
*
|
|
2071
|
+
* Each provider card is collapsible (chevron in the row head); the editor
|
|
2072
|
+
* surface is hidden when collapsed. Builtin providers (cordis.yml seed) carry
|
|
2073
|
+
* a `builtin`/`内置` badge next to the title. The "+ Add provider" button at
|
|
2074
|
+
* the bottom reveals an inline draft card with all fields editable (including
|
|
2075
|
+
* id) and Create / Cancel actions.
|
|
2076
|
+
*
|
|
2077
|
+
* Real providers carry an "initialize" (初始化) action: it sends a prepared
|
|
2078
|
+
* message to the current conversation so the agent probes the API with
|
|
2079
|
+
* aigc_http_request and records the usage instructions via
|
|
2080
|
+
* aigc_provider_set_instructions. The editor also exposes the auth scheme
|
|
2081
|
+
* (bearer / custom header / query param) the aigc_http_request tool uses to
|
|
2082
|
+
* attach the apiKey.
|
|
2083
|
+
*
|
|
2084
|
+
* @module @huanlin/dsh-plugin-aigc-canvas/client/SettingsPage
|
|
2085
|
+
*/
|
|
2086
|
+
/**
|
|
2087
|
+
* Default shape for a brand-new draft (before the user fills in id/name).
|
|
2088
|
+
* The structured-catalog fields default to sane values so the endpoints
|
|
2089
|
+
* editor starts empty (the agent's "Initialize" / "Auto-detect" buttons
|
|
2090
|
+
* populate it after probing the API).
|
|
2091
|
+
*/
|
|
2092
|
+
function emptyDraft() {
|
|
2093
|
+
return {
|
|
2094
|
+
id: "",
|
|
2095
|
+
name: "",
|
|
2096
|
+
endpoint: "stub://aigc-backend",
|
|
2097
|
+
apiKey: "",
|
|
2098
|
+
instructions: "",
|
|
2099
|
+
auth: {
|
|
2100
|
+
scheme: "bearer",
|
|
2101
|
+
name: ""
|
|
2102
|
+
},
|
|
2103
|
+
builtin: false,
|
|
2104
|
+
endpoints: [],
|
|
2105
|
+
priority: 100,
|
|
2106
|
+
costPerCall: 0,
|
|
2107
|
+
costPerKiloToken: 0,
|
|
2108
|
+
costPerSecond: 0,
|
|
2109
|
+
avgLatencyMs: 0,
|
|
2110
|
+
qualityHint: "balanced"
|
|
2111
|
+
};
|
|
2112
|
+
}
|
|
2113
|
+
/** Build a fresh blank endpoint (for the "+ Add endpoint" button). */
|
|
2114
|
+
function emptyEndpoint() {
|
|
2115
|
+
return {
|
|
2116
|
+
path: "",
|
|
2117
|
+
method: "POST",
|
|
2118
|
+
capability: "t2i",
|
|
2119
|
+
params: [],
|
|
2120
|
+
response: {
|
|
2121
|
+
kind: "json_text",
|
|
2122
|
+
path: ""
|
|
2123
|
+
},
|
|
2124
|
+
acceptsCanvasRef: false,
|
|
2125
|
+
notes: ""
|
|
2126
|
+
};
|
|
2127
|
+
}
|
|
2128
|
+
/** Build a fresh blank parameter (for the "+ Add parameter" button). */
|
|
2129
|
+
function emptyParam() {
|
|
2130
|
+
return {
|
|
2131
|
+
name: "",
|
|
2132
|
+
type: "string",
|
|
2133
|
+
required: false,
|
|
2134
|
+
default: "",
|
|
2135
|
+
description: ""
|
|
2136
|
+
};
|
|
2137
|
+
}
|
|
2138
|
+
/** Coerce a possibly-undefined value to a number (default 0 when invalid). */
|
|
2139
|
+
function toNumber(v, fallback = 0) {
|
|
2140
|
+
if (typeof v === "number" && Number.isFinite(v)) return v;
|
|
2141
|
+
if (typeof v === "string") {
|
|
2142
|
+
const n = Number(v);
|
|
2143
|
+
if (Number.isFinite(n) && v.trim() !== "") return n;
|
|
2144
|
+
}
|
|
2145
|
+
return fallback;
|
|
2146
|
+
}
|
|
2147
|
+
/** Coerce a possibly-undefined value to a string (default ''). */
|
|
2148
|
+
function toStr(v) {
|
|
2149
|
+
if (typeof v === "string") return v;
|
|
2150
|
+
if (typeof v === "number" || typeof v === "boolean") return String(v);
|
|
2151
|
+
return "";
|
|
2152
|
+
}
|
|
2153
|
+
/**
|
|
2154
|
+
* Render the AIGC provider settings page.
|
|
2155
|
+
* @param props - settings.section runtime share + locale + inject.
|
|
2156
|
+
* @returns the page element.
|
|
2157
|
+
*/
|
|
2158
|
+
function SettingsPage({ t, send }) {
|
|
2159
|
+
const [providers, setProviders] = (0, react.useState)([]);
|
|
2160
|
+
const [drafts, setDrafts] = (0, react.useState)([]);
|
|
2161
|
+
const [expanded, setExpanded] = (0, react.useState)(/* @__PURE__ */ new Set());
|
|
2162
|
+
const [addingNew, setAddingNew] = (0, react.useState)(false);
|
|
2163
|
+
const [newDraft, setNewDraft] = (0, react.useState)(emptyDraft());
|
|
2164
|
+
const [loading, setLoading] = (0, react.useState)(true);
|
|
2165
|
+
const [error, setError] = (0, react.useState)(void 0);
|
|
2166
|
+
const [confirmDelete, setConfirmDelete] = (0, react.useState)(void 0);
|
|
2167
|
+
const refresh = (0, react.useCallback)(async () => {
|
|
2168
|
+
setLoading(true);
|
|
2169
|
+
setError(void 0);
|
|
2170
|
+
try {
|
|
2171
|
+
const result = await fetchConfig();
|
|
2172
|
+
setProviders(result.providers);
|
|
2173
|
+
setDrafts(result.providers.map((p) => ({
|
|
2174
|
+
...p,
|
|
2175
|
+
auth: { ...p.auth }
|
|
2176
|
+
})));
|
|
2177
|
+
} catch (err) {
|
|
2178
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
2179
|
+
} finally {
|
|
2180
|
+
setLoading(false);
|
|
2181
|
+
}
|
|
2182
|
+
}, []);
|
|
2183
|
+
(0, react.useEffect)(() => {
|
|
2184
|
+
refresh();
|
|
2185
|
+
}, [refresh]);
|
|
2186
|
+
const add = (0, react.useCallback)(async () => {
|
|
2187
|
+
if (newDraft.id === "") return;
|
|
2188
|
+
try {
|
|
2189
|
+
const result = await addProvider(newDraft);
|
|
2190
|
+
setProviders(result.providers);
|
|
2191
|
+
setDrafts(result.providers.map((p) => ({
|
|
2192
|
+
...p,
|
|
2193
|
+
auth: { ...p.auth }
|
|
2194
|
+
})));
|
|
2195
|
+
setExpanded(/* @__PURE__ */ new Set([...expanded, newDraft.id]));
|
|
2196
|
+
setAddingNew(false);
|
|
2197
|
+
setNewDraft(emptyDraft());
|
|
2198
|
+
} catch (err) {
|
|
2199
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
2200
|
+
}
|
|
2201
|
+
}, [expanded, newDraft]);
|
|
2202
|
+
const update = (0, react.useCallback)(async (draft) => {
|
|
2203
|
+
try {
|
|
2204
|
+
const result = await updateProvider(draft);
|
|
2205
|
+
setProviders(result.providers);
|
|
2206
|
+
setDrafts(result.providers.map((p) => ({
|
|
2207
|
+
...p,
|
|
2208
|
+
auth: { ...p.auth }
|
|
2209
|
+
})));
|
|
2210
|
+
} catch (err) {
|
|
2211
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
2212
|
+
}
|
|
2213
|
+
}, []);
|
|
2214
|
+
const remove = (0, react.useCallback)(async (id) => {
|
|
2215
|
+
try {
|
|
2216
|
+
const result = await removeProvider(id);
|
|
2217
|
+
setProviders(result.providers);
|
|
2218
|
+
setDrafts(result.providers.map((p) => ({
|
|
2219
|
+
...p,
|
|
2220
|
+
auth: { ...p.auth }
|
|
2221
|
+
})));
|
|
2222
|
+
const next = new Set(expanded);
|
|
2223
|
+
next.delete(id);
|
|
2224
|
+
setExpanded(next);
|
|
2225
|
+
} catch (err) {
|
|
2226
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
2227
|
+
}
|
|
2228
|
+
}, [expanded]);
|
|
2229
|
+
const init = (0, react.useCallback)(async (provider) => {
|
|
2230
|
+
const label = provider.name === "" ? provider.id : provider.name;
|
|
2231
|
+
const text = t("row.initPrompt").replace("{name}", label).replace("{id}", provider.id);
|
|
2232
|
+
try {
|
|
2233
|
+
await send(text);
|
|
2234
|
+
} catch (err) {
|
|
2235
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
2236
|
+
}
|
|
2237
|
+
}, [send, t]);
|
|
2238
|
+
/**
|
|
2239
|
+
* Auto-detect (per docs/product/03-provider-catalog.md §5): send a
|
|
2240
|
+
* prepared message into the current conversation asking the agent to
|
|
2241
|
+
* call `aigc_probe_endpoint` for each endpoint whose response.kind is
|
|
2242
|
+
* not yet set, then save the detected shapes via
|
|
2243
|
+
* `aigc_provider_set_endpoints`. The agent handles the actual probing
|
|
2244
|
+
* + persistence; the client just sends the prompt (same pattern as
|
|
2245
|
+
* the "Initialize" action).
|
|
2246
|
+
*/
|
|
2247
|
+
const autoDetect = (0, react.useCallback)(async (provider) => {
|
|
2248
|
+
const label = provider.name === "" ? provider.id : provider.name;
|
|
2249
|
+
const text = t("row.autoDetectPrompt").replace("{name}", label).replace("{id}", provider.id);
|
|
2250
|
+
try {
|
|
2251
|
+
await send(text);
|
|
2252
|
+
} catch (err) {
|
|
2253
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
2254
|
+
}
|
|
2255
|
+
}, [send, t]);
|
|
2256
|
+
const patchDraft = (id, patch) => {
|
|
2257
|
+
setDrafts((prev) => prev.map((d) => d.id === id ? {
|
|
2258
|
+
...d,
|
|
2259
|
+
...patch
|
|
2260
|
+
} : d));
|
|
2261
|
+
};
|
|
2262
|
+
const toggleExpand = (id) => {
|
|
2263
|
+
const next = new Set(expanded);
|
|
2264
|
+
if (next.has(id)) next.delete(id);
|
|
2265
|
+
else next.add(id);
|
|
2266
|
+
setExpanded(next);
|
|
2267
|
+
};
|
|
2268
|
+
const cancelNew = () => {
|
|
2269
|
+
setAddingNew(false);
|
|
2270
|
+
setNewDraft(emptyDraft());
|
|
2271
|
+
};
|
|
2272
|
+
const defaultId = providers.length > 0 ? providers[0]?.id : void 0;
|
|
2273
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
2274
|
+
className: SettingsPage_module_css_default.section,
|
|
2275
|
+
children: [
|
|
2276
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
2277
|
+
className: SettingsPage_module_css_default.title,
|
|
2278
|
+
children: t("settingsTitle")
|
|
2279
|
+
}),
|
|
2280
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2281
|
+
className: SettingsPage_module_css_default.intro,
|
|
2282
|
+
children: t("settingsIntro")
|
|
2283
|
+
}),
|
|
2284
|
+
error !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2285
|
+
className: SettingsPage_module_css_default.error,
|
|
2286
|
+
children: [error, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2287
|
+
type: "button",
|
|
2288
|
+
className: SettingsPage_module_css_default.errorDismiss,
|
|
2289
|
+
onClick: () => setError(void 0),
|
|
2290
|
+
children: "×"
|
|
2291
|
+
})]
|
|
2292
|
+
}),
|
|
2293
|
+
loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2294
|
+
className: SettingsPage_module_css_default.loading,
|
|
2295
|
+
children: t("settingsLoading")
|
|
2296
|
+
}) : providers.length === 0 && !addingNew ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2297
|
+
className: SettingsPage_module_css_default.empty,
|
|
2298
|
+
children: t("settingsEmpty")
|
|
2299
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("ul", {
|
|
2300
|
+
className: SettingsPage_module_css_default.rows,
|
|
2301
|
+
children: [drafts.map((draft) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProviderCard, {
|
|
2302
|
+
draft,
|
|
2303
|
+
expanded: expanded.has(draft.id),
|
|
2304
|
+
isDefault: draft.id === defaultId,
|
|
2305
|
+
t,
|
|
2306
|
+
onToggle: () => toggleExpand(draft.id),
|
|
2307
|
+
onPatch: (patch) => patchDraft(draft.id, patch),
|
|
2308
|
+
onSave: () => void update(draft),
|
|
2309
|
+
onDelete: () => setConfirmDelete(draft.id),
|
|
2310
|
+
onInit: () => void init(draft),
|
|
2311
|
+
onAutoDetect: () => void autoDetect(draft)
|
|
2312
|
+
}, draft.id)), addingNew && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProviderCard, {
|
|
2313
|
+
draft: newDraft,
|
|
2314
|
+
expanded: true,
|
|
2315
|
+
isNew: true,
|
|
2316
|
+
isDefault: false,
|
|
2317
|
+
t,
|
|
2318
|
+
onPatch: (patch) => setNewDraft((prev) => ({
|
|
2319
|
+
...prev,
|
|
2320
|
+
...patch
|
|
2321
|
+
})),
|
|
2322
|
+
onCreate: () => void add(),
|
|
2323
|
+
onCancel: cancelNew
|
|
2324
|
+
}, "__new__")]
|
|
2325
|
+
}),
|
|
2326
|
+
!loading && !addingNew && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2327
|
+
type: "button",
|
|
2328
|
+
className: SettingsPage_module_css_default.addBlockButton,
|
|
2329
|
+
onClick: () => setAddingNew(true),
|
|
2330
|
+
children: t("settingsAdd")
|
|
2331
|
+
}),
|
|
2332
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
|
|
2333
|
+
open: confirmDelete !== void 0,
|
|
2334
|
+
onClose: () => {
|
|
2335
|
+
setConfirmDelete(void 0);
|
|
2336
|
+
},
|
|
2337
|
+
title: t("row.deleteConfirm"),
|
|
2338
|
+
footer: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2339
|
+
type: "button",
|
|
2340
|
+
className: SettingsPage_module_css_default.secondaryButton,
|
|
2341
|
+
onClick: () => {
|
|
2342
|
+
setConfirmDelete(void 0);
|
|
2343
|
+
},
|
|
2344
|
+
children: t("row.cancel")
|
|
2345
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2346
|
+
type: "button",
|
|
2347
|
+
className: SettingsPage_module_css_default.dangerButton,
|
|
2348
|
+
onClick: () => {
|
|
2349
|
+
if (confirmDelete !== void 0) remove(confirmDelete);
|
|
2350
|
+
setConfirmDelete(void 0);
|
|
2351
|
+
},
|
|
2352
|
+
children: t("row.delete")
|
|
2353
|
+
})] }),
|
|
2354
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2355
|
+
className: SettingsPage_module_css_default.confirmText,
|
|
2356
|
+
children: t("row.deleteConfirm")
|
|
2357
|
+
})
|
|
2358
|
+
})
|
|
2359
|
+
]
|
|
2360
|
+
});
|
|
2361
|
+
}
|
|
2362
|
+
function ProviderCard({ draft, expanded, isNew, isDefault, t, onToggle, onPatch, onSave, onDelete, onCreate, onCancel, onInit, onAutoDetect }) {
|
|
2363
|
+
const isStub = draft.endpoint === "" || draft.endpoint === "stub://aigc-backend";
|
|
2364
|
+
const patchAuth = (patch) => {
|
|
2365
|
+
onPatch({ auth: {
|
|
2366
|
+
...draft.auth,
|
|
2367
|
+
...patch
|
|
2368
|
+
} });
|
|
2369
|
+
};
|
|
2370
|
+
/**
|
|
2371
|
+
* Patch one endpoint in the draft's endpoints array (by index).
|
|
2372
|
+
* Replaces the whole array so React sees a new reference and re-renders.
|
|
2373
|
+
*/
|
|
2374
|
+
const patchEndpoint = (index, patch) => {
|
|
2375
|
+
const next = [...draft.endpoints ?? []];
|
|
2376
|
+
const existing = next[index];
|
|
2377
|
+
if (existing === void 0) return;
|
|
2378
|
+
next[index] = {
|
|
2379
|
+
...existing,
|
|
2380
|
+
...patch
|
|
2381
|
+
};
|
|
2382
|
+
onPatch({ endpoints: next });
|
|
2383
|
+
};
|
|
2384
|
+
/** Append a fresh blank endpoint to the endpoints array. */
|
|
2385
|
+
const addEndpoint = () => {
|
|
2386
|
+
onPatch({ endpoints: [...draft.endpoints ?? [], emptyEndpoint()] });
|
|
2387
|
+
};
|
|
2388
|
+
/** Remove the endpoint at one index. */
|
|
2389
|
+
const removeEndpoint = (index) => {
|
|
2390
|
+
const next = [...draft.endpoints ?? []];
|
|
2391
|
+
next.splice(index, 1);
|
|
2392
|
+
onPatch({ endpoints: next });
|
|
2393
|
+
};
|
|
2394
|
+
/**
|
|
2395
|
+
* Patch one parameter on one endpoint (by endpoint index + param index).
|
|
2396
|
+
*/
|
|
2397
|
+
const patchParam = (epIndex, paramIndex, patch) => {
|
|
2398
|
+
const ep = (draft.endpoints ?? [])[epIndex];
|
|
2399
|
+
if (ep === void 0) return;
|
|
2400
|
+
const params = [...ep.params ?? []];
|
|
2401
|
+
const existing = params[paramIndex];
|
|
2402
|
+
if (existing === void 0) return;
|
|
2403
|
+
params[paramIndex] = {
|
|
2404
|
+
...existing,
|
|
2405
|
+
...patch
|
|
2406
|
+
};
|
|
2407
|
+
patchEndpoint(epIndex, { params });
|
|
2408
|
+
};
|
|
2409
|
+
/** Append a fresh blank parameter to one endpoint. */
|
|
2410
|
+
const addParam = (epIndex) => {
|
|
2411
|
+
const ep = (draft.endpoints ?? [])[epIndex];
|
|
2412
|
+
if (ep === void 0) return;
|
|
2413
|
+
const params = [...ep.params ?? [], emptyParam()];
|
|
2414
|
+
patchEndpoint(epIndex, { params });
|
|
2415
|
+
};
|
|
2416
|
+
/** Remove the parameter at one index on one endpoint. */
|
|
2417
|
+
const removeParam = (epIndex, paramIndex) => {
|
|
2418
|
+
const ep = (draft.endpoints ?? [])[epIndex];
|
|
2419
|
+
if (ep === void 0) return;
|
|
2420
|
+
const params = [...ep.params ?? []];
|
|
2421
|
+
params.splice(paramIndex, 1);
|
|
2422
|
+
patchEndpoint(epIndex, { params });
|
|
2423
|
+
};
|
|
2424
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
2425
|
+
className: SettingsPage_module_css_default.rowCard,
|
|
2426
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2427
|
+
className: SettingsPage_module_css_default.rowHead,
|
|
2428
|
+
children: [
|
|
2429
|
+
!isNew && onToggle !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2430
|
+
type: "button",
|
|
2431
|
+
className: SettingsPage_module_css_default.chevronButton,
|
|
2432
|
+
onClick: onToggle,
|
|
2433
|
+
"aria-label": expanded ? t("row.collapse") : t("row.expand"),
|
|
2434
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2435
|
+
className: expanded ? `${SettingsPage_module_css_default.chevron} ${SettingsPage_module_css_default.chevronExpanded}` : SettingsPage_module_css_default.chevron,
|
|
2436
|
+
"aria-hidden": "true"
|
|
2437
|
+
})
|
|
2438
|
+
}),
|
|
2439
|
+
isNew && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2440
|
+
className: SettingsPage_module_css_default.chevronSpacer,
|
|
2441
|
+
"aria-hidden": "true"
|
|
2442
|
+
}),
|
|
2443
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2444
|
+
className: SettingsPage_module_css_default.rowIdentity,
|
|
2445
|
+
children: [
|
|
2446
|
+
isNew ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2447
|
+
className: SettingsPage_module_css_default.rowNamePlaceholder,
|
|
2448
|
+
children: t("settingsAdd")
|
|
2449
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2450
|
+
className: SettingsPage_module_css_default.rowName,
|
|
2451
|
+
children: draft.name === "" ? draft.id : draft.name
|
|
2452
|
+
}),
|
|
2453
|
+
draft.builtin && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Pill, {
|
|
2454
|
+
className: SettingsPage_module_css_default.builtinBadge,
|
|
2455
|
+
children: t("badge.builtin")
|
|
2456
|
+
}),
|
|
2457
|
+
isDefault && !isNew && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Pill, {
|
|
2458
|
+
className: SettingsPage_module_css_default.defaultBadge,
|
|
2459
|
+
children: t("badge.default")
|
|
2460
|
+
}),
|
|
2461
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Pill, {
|
|
2462
|
+
className: isStub ? SettingsPage_module_css_default.stubBadge : SettingsPage_module_css_default.realBadge,
|
|
2463
|
+
children: isStub ? t("badge.stub") : t("badge.real")
|
|
2464
|
+
}),
|
|
2465
|
+
!isNew && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
|
|
2466
|
+
className: SettingsPage_module_css_default.rowId,
|
|
2467
|
+
children: draft.id
|
|
2468
|
+
})
|
|
2469
|
+
]
|
|
2470
|
+
}),
|
|
2471
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2472
|
+
className: SettingsPage_module_css_default.rowActions,
|
|
2473
|
+
children: isNew ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2474
|
+
type: "button",
|
|
2475
|
+
className: SettingsPage_module_css_default.primaryButton,
|
|
2476
|
+
onClick: onCreate,
|
|
2477
|
+
disabled: draft.id === "",
|
|
2478
|
+
children: t("row.create")
|
|
2479
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2480
|
+
type: "button",
|
|
2481
|
+
className: SettingsPage_module_css_default.secondaryButton,
|
|
2482
|
+
onClick: onCancel,
|
|
2483
|
+
children: t("row.cancel")
|
|
2484
|
+
})] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
2485
|
+
!isStub && onInit !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2486
|
+
type: "button",
|
|
2487
|
+
className: SettingsPage_module_css_default.secondaryButton,
|
|
2488
|
+
onClick: onInit,
|
|
2489
|
+
children: t("row.init")
|
|
2490
|
+
}),
|
|
2491
|
+
!isStub && onAutoDetect !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2492
|
+
type: "button",
|
|
2493
|
+
className: SettingsPage_module_css_default.secondaryButton,
|
|
2494
|
+
onClick: onAutoDetect,
|
|
2495
|
+
title: t("row.autoDetectTitle"),
|
|
2496
|
+
children: t("row.autoDetect")
|
|
2497
|
+
}),
|
|
2498
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2499
|
+
type: "button",
|
|
2500
|
+
className: SettingsPage_module_css_default.secondaryButton,
|
|
2501
|
+
onClick: onSave,
|
|
2502
|
+
children: t("row.save")
|
|
2503
|
+
}),
|
|
2504
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2505
|
+
type: "button",
|
|
2506
|
+
className: SettingsPage_module_css_default.dangerButton,
|
|
2507
|
+
onClick: onDelete,
|
|
2508
|
+
children: t("row.delete")
|
|
2509
|
+
})
|
|
2510
|
+
] })
|
|
2511
|
+
})
|
|
2512
|
+
]
|
|
2513
|
+
}), expanded && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2514
|
+
className: SettingsPage_module_css_default.editor,
|
|
2515
|
+
children: [
|
|
2516
|
+
isNew && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2517
|
+
className: SettingsPage_module_css_default.field,
|
|
2518
|
+
children: [
|
|
2519
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2520
|
+
className: SettingsPage_module_css_default.fieldLabel,
|
|
2521
|
+
children: t("row.id")
|
|
2522
|
+
}),
|
|
2523
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2524
|
+
className: SettingsPage_module_css_default.input,
|
|
2525
|
+
value: draft.id,
|
|
2526
|
+
placeholder: t("row.idPlaceholder"),
|
|
2527
|
+
onChange: (e) => onPatch({ id: e.target.value })
|
|
2528
|
+
}),
|
|
2529
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2530
|
+
className: SettingsPage_module_css_default.hint,
|
|
2531
|
+
children: t("row.idHint")
|
|
2532
|
+
})
|
|
2533
|
+
]
|
|
2534
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2535
|
+
className: SettingsPage_module_css_default.field,
|
|
2536
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2537
|
+
className: SettingsPage_module_css_default.fieldLabel,
|
|
2538
|
+
children: t("row.name")
|
|
2539
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2540
|
+
className: SettingsPage_module_css_default.input,
|
|
2541
|
+
value: draft.name,
|
|
2542
|
+
placeholder: t("row.namePlaceholder"),
|
|
2543
|
+
onChange: (e) => onPatch({ name: e.target.value })
|
|
2544
|
+
})]
|
|
2545
|
+
})] }),
|
|
2546
|
+
!isNew && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2547
|
+
className: SettingsPage_module_css_default.field,
|
|
2548
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2549
|
+
className: SettingsPage_module_css_default.fieldLabel,
|
|
2550
|
+
children: t("row.name")
|
|
2551
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2552
|
+
className: SettingsPage_module_css_default.input,
|
|
2553
|
+
value: draft.name,
|
|
2554
|
+
placeholder: t("row.namePlaceholder"),
|
|
2555
|
+
onChange: (e) => onPatch({ name: e.target.value })
|
|
2556
|
+
})]
|
|
2557
|
+
}),
|
|
2558
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2559
|
+
className: SettingsPage_module_css_default.field,
|
|
2560
|
+
children: [
|
|
2561
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2562
|
+
className: SettingsPage_module_css_default.fieldLabel,
|
|
2563
|
+
children: t("row.endpoint")
|
|
2564
|
+
}),
|
|
2565
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2566
|
+
className: SettingsPage_module_css_default.input,
|
|
2567
|
+
value: draft.endpoint,
|
|
2568
|
+
placeholder: t("row.endpointPlaceholder"),
|
|
2569
|
+
onChange: (e) => onPatch({ endpoint: e.target.value })
|
|
2570
|
+
}),
|
|
2571
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2572
|
+
className: SettingsPage_module_css_default.desc,
|
|
2573
|
+
children: t("row.endpointDesc")
|
|
2574
|
+
})
|
|
2575
|
+
]
|
|
2576
|
+
}),
|
|
2577
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2578
|
+
className: SettingsPage_module_css_default.field,
|
|
2579
|
+
children: [
|
|
2580
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2581
|
+
className: SettingsPage_module_css_default.fieldLabel,
|
|
2582
|
+
children: t("row.apiKey")
|
|
2583
|
+
}),
|
|
2584
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2585
|
+
className: SettingsPage_module_css_default.input,
|
|
2586
|
+
type: "password",
|
|
2587
|
+
autoComplete: "off",
|
|
2588
|
+
value: draft.apiKey,
|
|
2589
|
+
placeholder: t("row.apiKeyPlaceholder"),
|
|
2590
|
+
onChange: (e) => onPatch({ apiKey: e.target.value })
|
|
2591
|
+
}),
|
|
2592
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2593
|
+
className: SettingsPage_module_css_default.desc,
|
|
2594
|
+
children: t("row.apiKeyDesc")
|
|
2595
|
+
})
|
|
2596
|
+
]
|
|
2597
|
+
}),
|
|
2598
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2599
|
+
className: SettingsPage_module_css_default.field,
|
|
2600
|
+
children: [
|
|
2601
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2602
|
+
className: SettingsPage_module_css_default.fieldLabel,
|
|
2603
|
+
children: t("row.auth")
|
|
2604
|
+
}),
|
|
2605
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2606
|
+
className: SettingsPage_module_css_default.authRow,
|
|
2607
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
2608
|
+
className: SettingsPage_module_css_default.select,
|
|
2609
|
+
value: draft.auth.scheme,
|
|
2610
|
+
onChange: (e) => patchAuth({ scheme: e.target.value }),
|
|
2611
|
+
children: [
|
|
2612
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
2613
|
+
value: "bearer",
|
|
2614
|
+
children: t("row.authBearer")
|
|
2615
|
+
}),
|
|
2616
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
2617
|
+
value: "header",
|
|
2618
|
+
children: t("row.authHeader")
|
|
2619
|
+
}),
|
|
2620
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
2621
|
+
value: "query",
|
|
2622
|
+
children: t("row.authQuery")
|
|
2623
|
+
})
|
|
2624
|
+
]
|
|
2625
|
+
}), draft.auth.scheme !== "bearer" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2626
|
+
className: SettingsPage_module_css_default.input,
|
|
2627
|
+
value: draft.auth.name,
|
|
2628
|
+
placeholder: draft.auth.scheme === "header" ? "x-api-key" : "api_key",
|
|
2629
|
+
onChange: (e) => patchAuth({ name: e.target.value })
|
|
2630
|
+
})]
|
|
2631
|
+
}),
|
|
2632
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2633
|
+
className: SettingsPage_module_css_default.desc,
|
|
2634
|
+
children: t("row.authDesc")
|
|
2635
|
+
})
|
|
2636
|
+
]
|
|
2637
|
+
}),
|
|
2638
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2639
|
+
className: SettingsPage_module_css_default.fieldRow,
|
|
2640
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2641
|
+
className: SettingsPage_module_css_default.field,
|
|
2642
|
+
children: [
|
|
2643
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2644
|
+
className: SettingsPage_module_css_default.fieldLabel,
|
|
2645
|
+
children: t("row.priority")
|
|
2646
|
+
}),
|
|
2647
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2648
|
+
className: SettingsPage_module_css_default.input,
|
|
2649
|
+
type: "number",
|
|
2650
|
+
min: 0,
|
|
2651
|
+
step: 1,
|
|
2652
|
+
value: toNumber(draft.priority, 100),
|
|
2653
|
+
onChange: (e) => onPatch({ priority: toNumber(e.target.value, 100) })
|
|
2654
|
+
}),
|
|
2655
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2656
|
+
className: SettingsPage_module_css_default.desc,
|
|
2657
|
+
children: t("row.priorityDesc")
|
|
2658
|
+
})
|
|
2659
|
+
]
|
|
2660
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2661
|
+
className: SettingsPage_module_css_default.field,
|
|
2662
|
+
children: [
|
|
2663
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2664
|
+
className: SettingsPage_module_css_default.fieldLabel,
|
|
2665
|
+
children: t("row.qualityHint")
|
|
2666
|
+
}),
|
|
2667
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
|
|
2668
|
+
className: SettingsPage_module_css_default.select,
|
|
2669
|
+
value: draft.qualityHint ?? "balanced",
|
|
2670
|
+
onChange: (e) => onPatch({ qualityHint: e.target.value }),
|
|
2671
|
+
children: RUNTIME_QUALITY_HINTS.map((q) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
2672
|
+
value: q,
|
|
2673
|
+
children: q
|
|
2674
|
+
}, q))
|
|
2675
|
+
}),
|
|
2676
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2677
|
+
className: SettingsPage_module_css_default.desc,
|
|
2678
|
+
children: t("row.qualityHintDesc")
|
|
2679
|
+
})
|
|
2680
|
+
]
|
|
2681
|
+
})]
|
|
2682
|
+
}),
|
|
2683
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2684
|
+
className: SettingsPage_module_css_default.fieldRow,
|
|
2685
|
+
children: [
|
|
2686
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2687
|
+
className: SettingsPage_module_css_default.field,
|
|
2688
|
+
children: [
|
|
2689
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2690
|
+
className: SettingsPage_module_css_default.fieldLabel,
|
|
2691
|
+
children: t("row.costPerCall")
|
|
2692
|
+
}),
|
|
2693
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2694
|
+
className: SettingsPage_module_css_default.input,
|
|
2695
|
+
type: "number",
|
|
2696
|
+
min: 0,
|
|
2697
|
+
step: 1e-4,
|
|
2698
|
+
value: toNumber(draft.costPerCall, 0),
|
|
2699
|
+
onChange: (e) => onPatch({ costPerCall: toNumber(e.target.value, 0) })
|
|
2700
|
+
}),
|
|
2701
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2702
|
+
className: SettingsPage_module_css_default.desc,
|
|
2703
|
+
children: t("row.costPerCallDesc")
|
|
2704
|
+
})
|
|
2705
|
+
]
|
|
2706
|
+
}),
|
|
2707
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2708
|
+
className: SettingsPage_module_css_default.field,
|
|
2709
|
+
children: [
|
|
2710
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2711
|
+
className: SettingsPage_module_css_default.fieldLabel,
|
|
2712
|
+
children: t("row.costPerKiloToken")
|
|
2713
|
+
}),
|
|
2714
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2715
|
+
className: SettingsPage_module_css_default.input,
|
|
2716
|
+
type: "number",
|
|
2717
|
+
min: 0,
|
|
2718
|
+
step: 1e-4,
|
|
2719
|
+
value: toNumber(draft.costPerKiloToken, 0),
|
|
2720
|
+
onChange: (e) => onPatch({ costPerKiloToken: toNumber(e.target.value, 0) })
|
|
2721
|
+
}),
|
|
2722
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2723
|
+
className: SettingsPage_module_css_default.desc,
|
|
2724
|
+
children: t("row.costPerKiloTokenDesc")
|
|
2725
|
+
})
|
|
2726
|
+
]
|
|
2727
|
+
}),
|
|
2728
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2729
|
+
className: SettingsPage_module_css_default.field,
|
|
2730
|
+
children: [
|
|
2731
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2732
|
+
className: SettingsPage_module_css_default.fieldLabel,
|
|
2733
|
+
children: t("row.costPerSecond")
|
|
2734
|
+
}),
|
|
2735
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2736
|
+
className: SettingsPage_module_css_default.input,
|
|
2737
|
+
type: "number",
|
|
2738
|
+
min: 0,
|
|
2739
|
+
step: 1e-4,
|
|
2740
|
+
value: toNumber(draft.costPerSecond, 0),
|
|
2741
|
+
onChange: (e) => onPatch({ costPerSecond: toNumber(e.target.value, 0) })
|
|
2742
|
+
}),
|
|
2743
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2744
|
+
className: SettingsPage_module_css_default.desc,
|
|
2745
|
+
children: t("row.costPerSecondDesc")
|
|
2746
|
+
})
|
|
2747
|
+
]
|
|
2748
|
+
})
|
|
2749
|
+
]
|
|
2750
|
+
}),
|
|
2751
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2752
|
+
className: SettingsPage_module_css_default.field,
|
|
2753
|
+
children: [
|
|
2754
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2755
|
+
className: SettingsPage_module_css_default.fieldLabelRow,
|
|
2756
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2757
|
+
className: SettingsPage_module_css_default.fieldLabel,
|
|
2758
|
+
children: t("row.endpoints")
|
|
2759
|
+
}), !isStub && onAutoDetect !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2760
|
+
type: "button",
|
|
2761
|
+
className: SettingsPage_module_css_default.endpointsAutoDetectButton,
|
|
2762
|
+
onClick: onAutoDetect,
|
|
2763
|
+
title: t("row.autoDetectTitle"),
|
|
2764
|
+
children: t("row.autoDetect")
|
|
2765
|
+
})]
|
|
2766
|
+
}),
|
|
2767
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2768
|
+
className: SettingsPage_module_css_default.desc,
|
|
2769
|
+
children: t("row.endpointsDesc")
|
|
2770
|
+
}),
|
|
2771
|
+
(draft.endpoints ?? []).length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2772
|
+
className: SettingsPage_module_css_default.endpointsEmpty,
|
|
2773
|
+
children: t("row.endpointsEmpty")
|
|
2774
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2775
|
+
className: SettingsPage_module_css_default.endpointsList,
|
|
2776
|
+
children: (draft.endpoints ?? []).map((ep, epIndex) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EndpointCard, {
|
|
2777
|
+
endpoint: ep,
|
|
2778
|
+
t,
|
|
2779
|
+
onPatch: (patch) => patchEndpoint(epIndex, patch),
|
|
2780
|
+
onRemove: () => removeEndpoint(epIndex),
|
|
2781
|
+
onAddParam: () => addParam(epIndex),
|
|
2782
|
+
onPatchParam: (paramIndex, patch) => patchParam(epIndex, paramIndex, patch),
|
|
2783
|
+
onRemoveParam: (paramIndex) => removeParam(epIndex, paramIndex)
|
|
2784
|
+
}, epIndex))
|
|
2785
|
+
}),
|
|
2786
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2787
|
+
type: "button",
|
|
2788
|
+
className: SettingsPage_module_css_default.addEndpointButton,
|
|
2789
|
+
onClick: addEndpoint,
|
|
2790
|
+
children: t("row.addEndpoint")
|
|
2791
|
+
})
|
|
2792
|
+
]
|
|
2793
|
+
}),
|
|
2794
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2795
|
+
className: SettingsPage_module_css_default.field,
|
|
2796
|
+
children: [
|
|
2797
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2798
|
+
className: SettingsPage_module_css_default.fieldLabel,
|
|
2799
|
+
children: t("row.instructions")
|
|
2800
|
+
}),
|
|
2801
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
2802
|
+
className: SettingsPage_module_css_default.textarea,
|
|
2803
|
+
value: draft.instructions,
|
|
2804
|
+
placeholder: t("row.instructionsPlaceholder"),
|
|
2805
|
+
rows: 8,
|
|
2806
|
+
onChange: (e) => onPatch({ instructions: e.target.value })
|
|
2807
|
+
}),
|
|
2808
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2809
|
+
className: SettingsPage_module_css_default.desc,
|
|
2810
|
+
children: t("row.instructionsDesc")
|
|
2811
|
+
}),
|
|
2812
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2813
|
+
className: SettingsPage_module_css_default.hint,
|
|
2814
|
+
children: t("row.instructionsHint")
|
|
2815
|
+
})
|
|
2816
|
+
]
|
|
2817
|
+
})
|
|
2818
|
+
]
|
|
2819
|
+
})]
|
|
2820
|
+
});
|
|
2821
|
+
}
|
|
2822
|
+
/**
|
|
2823
|
+
* One endpoint in the catalog editor: path / method / capability /
|
|
2824
|
+
* response.kind / response.path + acceptsCanvasRef + notes + parameter
|
|
2825
|
+
* list. Per docs/product/03-provider-catalog.md §5.
|
|
2826
|
+
*
|
|
2827
|
+
* No collapsible state — the card is always expanded so the user can
|
|
2828
|
+
* see all fields. The parameter list is a simple grid (name / type /
|
|
2829
|
+
* required / default) with add/remove buttons.
|
|
2830
|
+
*/
|
|
2831
|
+
function EndpointCard({ endpoint, t, onPatch, onRemove, onAddParam, onPatchParam, onRemoveParam }) {
|
|
2832
|
+
const response = endpoint.response ?? {
|
|
2833
|
+
kind: "json_text",
|
|
2834
|
+
path: ""
|
|
2835
|
+
};
|
|
2836
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2837
|
+
className: SettingsPage_module_css_default.endpointCard,
|
|
2838
|
+
children: [
|
|
2839
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2840
|
+
className: SettingsPage_module_css_default.endpointHead,
|
|
2841
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2842
|
+
className: SettingsPage_module_css_default.endpointHeadLabel,
|
|
2843
|
+
children: [
|
|
2844
|
+
endpoint.method,
|
|
2845
|
+
" ",
|
|
2846
|
+
endpoint.path === "" ? "<path>" : endpoint.path,
|
|
2847
|
+
endpoint.capability !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Pill, {
|
|
2848
|
+
className: SettingsPage_module_css_default.endpointCapabilityBadge,
|
|
2849
|
+
children: endpoint.capability
|
|
2850
|
+
})
|
|
2851
|
+
]
|
|
2852
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2853
|
+
type: "button",
|
|
2854
|
+
className: SettingsPage_module_css_default.endpointRemoveButton,
|
|
2855
|
+
onClick: onRemove,
|
|
2856
|
+
"aria-label": t("row.removeEndpoint"),
|
|
2857
|
+
children: "×"
|
|
2858
|
+
})]
|
|
2859
|
+
}),
|
|
2860
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2861
|
+
className: SettingsPage_module_css_default.fieldRow,
|
|
2862
|
+
children: [
|
|
2863
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2864
|
+
className: SettingsPage_module_css_default.field,
|
|
2865
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2866
|
+
className: SettingsPage_module_css_default.fieldLabel,
|
|
2867
|
+
children: t("row.endpointPath")
|
|
2868
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2869
|
+
className: SettingsPage_module_css_default.input,
|
|
2870
|
+
value: endpoint.path,
|
|
2871
|
+
placeholder: "/v1/images/generations",
|
|
2872
|
+
onChange: (e) => onPatch({ path: e.target.value })
|
|
2873
|
+
})]
|
|
2874
|
+
}),
|
|
2875
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2876
|
+
className: SettingsPage_module_css_default.field,
|
|
2877
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2878
|
+
className: SettingsPage_module_css_default.fieldLabel,
|
|
2879
|
+
children: t("row.endpointMethod")
|
|
2880
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
|
|
2881
|
+
className: SettingsPage_module_css_default.select,
|
|
2882
|
+
value: endpoint.method,
|
|
2883
|
+
onChange: (e) => onPatch({ method: e.target.value }),
|
|
2884
|
+
children: RUNTIME_HTTP_METHODS.map((m) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
2885
|
+
value: m,
|
|
2886
|
+
children: m
|
|
2887
|
+
}, m))
|
|
2888
|
+
})]
|
|
2889
|
+
}),
|
|
2890
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2891
|
+
className: SettingsPage_module_css_default.field,
|
|
2892
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2893
|
+
className: SettingsPage_module_css_default.fieldLabel,
|
|
2894
|
+
children: t("row.endpointCapability")
|
|
2895
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
|
|
2896
|
+
className: SettingsPage_module_css_default.select,
|
|
2897
|
+
value: endpoint.capability,
|
|
2898
|
+
onChange: (e) => onPatch({ capability: e.target.value }),
|
|
2899
|
+
children: RUNTIME_CAPABILITIES.map((c) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
2900
|
+
value: c,
|
|
2901
|
+
children: c
|
|
2902
|
+
}, c))
|
|
2903
|
+
})]
|
|
2904
|
+
})
|
|
2905
|
+
]
|
|
2906
|
+
}),
|
|
2907
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2908
|
+
className: SettingsPage_module_css_default.fieldRow,
|
|
2909
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2910
|
+
className: SettingsPage_module_css_default.field,
|
|
2911
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2912
|
+
className: SettingsPage_module_css_default.fieldLabel,
|
|
2913
|
+
children: t("row.endpointResponseKind")
|
|
2914
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
|
|
2915
|
+
className: SettingsPage_module_css_default.select,
|
|
2916
|
+
value: response.kind,
|
|
2917
|
+
onChange: (e) => onPatch({ response: {
|
|
2918
|
+
kind: e.target.value,
|
|
2919
|
+
path: response.path
|
|
2920
|
+
} }),
|
|
2921
|
+
children: RUNTIME_RESPONSE_KINDS.map((k) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
2922
|
+
value: k,
|
|
2923
|
+
children: k
|
|
2924
|
+
}, k))
|
|
2925
|
+
})]
|
|
2926
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2927
|
+
className: SettingsPage_module_css_default.field,
|
|
2928
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2929
|
+
className: SettingsPage_module_css_default.fieldLabel,
|
|
2930
|
+
children: t("row.endpointResponsePath")
|
|
2931
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2932
|
+
className: SettingsPage_module_css_default.input,
|
|
2933
|
+
value: toStr(response.path),
|
|
2934
|
+
placeholder: "data[0].b64_json",
|
|
2935
|
+
onChange: (e) => onPatch({ response: {
|
|
2936
|
+
kind: response.kind,
|
|
2937
|
+
path: e.target.value
|
|
2938
|
+
} })
|
|
2939
|
+
})]
|
|
2940
|
+
})]
|
|
2941
|
+
}),
|
|
2942
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2943
|
+
className: SettingsPage_module_css_default.fieldRow,
|
|
2944
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2945
|
+
type: "checkbox",
|
|
2946
|
+
checked: endpoint.acceptsCanvasRef === true,
|
|
2947
|
+
onChange: (e) => onPatch({ acceptsCanvasRef: e.target.checked })
|
|
2948
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2949
|
+
className: SettingsPage_module_css_default.desc,
|
|
2950
|
+
children: t("row.endpointAcceptsCanvasRef")
|
|
2951
|
+
})]
|
|
2952
|
+
}),
|
|
2953
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2954
|
+
className: SettingsPage_module_css_default.field,
|
|
2955
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2956
|
+
className: SettingsPage_module_css_default.fieldLabel,
|
|
2957
|
+
children: t("row.endpointNotes")
|
|
2958
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2959
|
+
className: SettingsPage_module_css_default.input,
|
|
2960
|
+
value: toStr(endpoint.notes),
|
|
2961
|
+
placeholder: "size must be 1024x1024 or 1792x1024",
|
|
2962
|
+
onChange: (e) => onPatch({ notes: e.target.value })
|
|
2963
|
+
})]
|
|
2964
|
+
}),
|
|
2965
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2966
|
+
className: SettingsPage_module_css_default.field,
|
|
2967
|
+
children: [
|
|
2968
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2969
|
+
className: SettingsPage_module_css_default.fieldLabel,
|
|
2970
|
+
children: t("row.endpointParams")
|
|
2971
|
+
}),
|
|
2972
|
+
(endpoint.params ?? []).length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2973
|
+
className: SettingsPage_module_css_default.endpointsEmpty,
|
|
2974
|
+
children: t("row.endpointParamsEmpty")
|
|
2975
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2976
|
+
className: SettingsPage_module_css_default.paramsTable,
|
|
2977
|
+
children: (endpoint.params ?? []).map((param, paramIndex) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2978
|
+
className: SettingsPage_module_css_default.paramRow,
|
|
2979
|
+
children: [
|
|
2980
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2981
|
+
className: SettingsPage_module_css_default.input,
|
|
2982
|
+
value: param.name,
|
|
2983
|
+
placeholder: t("row.endpointParamName"),
|
|
2984
|
+
onChange: (e) => onPatchParam(paramIndex, { name: e.target.value })
|
|
2985
|
+
}),
|
|
2986
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
|
|
2987
|
+
className: SettingsPage_module_css_default.select,
|
|
2988
|
+
value: param.type,
|
|
2989
|
+
onChange: (e) => onPatchParam(paramIndex, { type: e.target.value }),
|
|
2990
|
+
children: RUNTIME_PARAM_TYPES.map((tp) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
2991
|
+
value: tp,
|
|
2992
|
+
children: tp
|
|
2993
|
+
}, tp))
|
|
2994
|
+
}),
|
|
2995
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2996
|
+
className: SettingsPage_module_css_default.paramRequired,
|
|
2997
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2998
|
+
type: "checkbox",
|
|
2999
|
+
checked: param.required,
|
|
3000
|
+
onChange: (e) => onPatchParam(paramIndex, { required: e.target.checked })
|
|
3001
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("row.endpointParamRequired") })]
|
|
3002
|
+
}),
|
|
3003
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
3004
|
+
className: SettingsPage_module_css_default.input,
|
|
3005
|
+
value: toStr(param.default),
|
|
3006
|
+
placeholder: t("row.endpointParamDefault"),
|
|
3007
|
+
onChange: (e) => onPatchParam(paramIndex, { default: e.target.value })
|
|
3008
|
+
}),
|
|
3009
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3010
|
+
type: "button",
|
|
3011
|
+
className: SettingsPage_module_css_default.paramRemoveButton,
|
|
3012
|
+
onClick: () => onRemoveParam(paramIndex),
|
|
3013
|
+
"aria-label": t("row.endpointRemoveParam"),
|
|
3014
|
+
children: "×"
|
|
3015
|
+
})
|
|
3016
|
+
]
|
|
3017
|
+
}, paramIndex))
|
|
3018
|
+
}),
|
|
3019
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3020
|
+
type: "button",
|
|
3021
|
+
className: SettingsPage_module_css_default.addEndpointButton,
|
|
3022
|
+
onClick: onAddParam,
|
|
3023
|
+
children: t("row.endpointAddParam")
|
|
3024
|
+
})
|
|
3025
|
+
]
|
|
3026
|
+
})
|
|
3027
|
+
]
|
|
3028
|
+
});
|
|
3029
|
+
}
|
|
3030
|
+
//#endregion
|
|
3031
|
+
//#region src/client/locales.ts
|
|
3032
|
+
/**
|
|
3033
|
+
* i18n dictionaries for the AIGC canvas plugin.
|
|
3034
|
+
*
|
|
3035
|
+
* @module @huanlin/dsh-plugin-aigc-canvas/client/locales
|
|
3036
|
+
*/
|
|
3037
|
+
const NS = "dsh-aigc-canvas";
|
|
3038
|
+
const zh = {
|
|
3039
|
+
tabTitle: "AIGC 画布",
|
|
3040
|
+
title: "AIGC 画布",
|
|
3041
|
+
empty: "画布是空的。模型通过 aigc_http_request 调用供应商 API 生成素材,再用 aigc_canvas_place 把文件放到画布的任意位置。",
|
|
3042
|
+
emptyHint: "可在右侧设置页配置供应商,然后让模型开始生成。",
|
|
3043
|
+
prompt: "提示词",
|
|
3044
|
+
image: "图片",
|
|
3045
|
+
video: "视频",
|
|
3046
|
+
audio: "音频",
|
|
3047
|
+
meta: "元信息",
|
|
3048
|
+
generatedBy: "生成方式",
|
|
3049
|
+
edgeCount: "条连线",
|
|
3050
|
+
elementCount: "个元素",
|
|
3051
|
+
loadError: "加载画布失败",
|
|
3052
|
+
disconnected: "已断开,正在重连…",
|
|
3053
|
+
reconnecting: "正在重连…",
|
|
3054
|
+
refresh: "刷新",
|
|
3055
|
+
resetView: "重置视图",
|
|
3056
|
+
zoom: "缩放",
|
|
3057
|
+
zoomIn: "放大",
|
|
3058
|
+
zoomOut: "缩小",
|
|
3059
|
+
detailClose: "关闭",
|
|
3060
|
+
detailPrompt: "提示词",
|
|
3061
|
+
detailParams: "生成参数",
|
|
3062
|
+
detailPosition: "位置",
|
|
3063
|
+
detailPath: "文件路径",
|
|
3064
|
+
delete: "删除",
|
|
3065
|
+
deleteElement: "删除元素",
|
|
3066
|
+
dropHint: "拖放文件到画布",
|
|
3067
|
+
uploading: "上传中…",
|
|
3068
|
+
menuRegenerate: "重新生成...",
|
|
3069
|
+
menuUseAsReference: "用作参考...",
|
|
3070
|
+
menuSendToChat: "发到对话",
|
|
3071
|
+
menuDownload: "下载",
|
|
3072
|
+
menuPromoteToLibrary: "提升到资产库...",
|
|
3073
|
+
menuMarkWinner: "标记为 winner",
|
|
3074
|
+
menuMarkRejected: "标记为否决",
|
|
3075
|
+
menuArchive: "归档",
|
|
3076
|
+
menuSeparator: "─────────────",
|
|
3077
|
+
toolbarGenerate: "+ 生成",
|
|
3078
|
+
toolbarEditSelected: "✂ 编辑选中",
|
|
3079
|
+
toolbarRunWorkflow: "▶ 运行工作流",
|
|
3080
|
+
toolbarGenerateTitle: "打开快速生成弹窗(t2i/t2v/tts)",
|
|
3081
|
+
toolbarEditSelectedTitle: "对选中元素执行 ffmpeg 操作",
|
|
3082
|
+
toolbarRunWorkflowTitle: "打开 pipeline 模板选择器",
|
|
3083
|
+
toolbarNoSelection: "请先在画布上选中一个元素",
|
|
3084
|
+
noticeRegenerate: "请用 aigc_reroll 重新生成元素 {filePath}",
|
|
3085
|
+
noticeUseAsReference: "请把以下元素用作后续生成的参考: {filePath}",
|
|
3086
|
+
noticeSendToChat: "请使用这个元素作为参考: [filePath: {filePath}, kind: {kind}, title: {title}]",
|
|
3087
|
+
noticeGenerate: "请帮我生成一个新的 AIGC 素材(先调用 aigc_get_provider_info 查看可用供应商,再调用 aigc_http_request 发起生成,最后用 aigc_canvas_place 把产物放到画布上)。",
|
|
3088
|
+
noticeEditSelected: "请用 aigc_media_edit(ffmpeg)对选中元素进行编辑: {filePath}(kind: {kind}, title: {title})",
|
|
3089
|
+
noticeRunWorkflow: "请列出可用的 pipeline 模板并运行其中一个(如果只有一个模板就直接运行它)。",
|
|
3090
|
+
settingsNav: "AIGC 画布",
|
|
3091
|
+
settingsTitle: "AIGC 供应商",
|
|
3092
|
+
settingsIntro: "配置一个或多个 AIGC 供应商。每个供应商可独立设置名称、API 地址、密钥、鉴权方式和调用说明。模型通过 aigc_get_provider_info 读取供应商列表,用 aigc_http_request 调用 API(自动携带 endpoint 和 apiKey),生成的文件用 aigc_canvas_place 放到画布上。",
|
|
3093
|
+
settingsEmpty: "暂无供应商,请在下方添加。",
|
|
3094
|
+
settingsAdd: "+ 添加供应商",
|
|
3095
|
+
settingsLoading: "加载中…",
|
|
3096
|
+
settingsError: "错误",
|
|
3097
|
+
"row.id": "ID",
|
|
3098
|
+
"row.name": "名称",
|
|
3099
|
+
"row.endpoint": "API 地址",
|
|
3100
|
+
"row.apiKey": "API Key",
|
|
3101
|
+
"row.instructions": "调用说明",
|
|
3102
|
+
"row.idPlaceholder": "volcano / jimeng / minimax",
|
|
3103
|
+
"row.namePlaceholder": "显示名(如\"火山引擎\")",
|
|
3104
|
+
"row.endpointPlaceholder": "stub://aigc-backend",
|
|
3105
|
+
"row.apiKeyPlaceholder": "sk-...",
|
|
3106
|
+
"row.instructionsPlaceholder": "调用说明由 Agent 初始化供应商时自动撰写(点击卡片上的\"初始化\"按钮)...",
|
|
3107
|
+
"row.idHint": "小写字母、数字、连字符;必须以字母开头。作为 provider_id 传给 aigc_http_request",
|
|
3108
|
+
"row.endpointDesc": "供应商 API 地址。填 stub://aigc-backend 使用内置 stub(合成测试媒体,不调真实 API)",
|
|
3109
|
+
"row.apiKeyDesc": "供应商 API 密钥。stub 后端不需要。模型看不到密钥,由 aigc_http_request 自动附加",
|
|
3110
|
+
"row.instructionsDesc": "Agent 通过 aigc_get_provider_info 工具读取此字段,决定如何调用该供应商的 API",
|
|
3111
|
+
"row.instructionsHint": "💡 点击卡片上的\"初始化\"按钮,Agent 会用 aigc_http_request 探测 API 并自动撰写调用说明",
|
|
3112
|
+
"row.save": "保存",
|
|
3113
|
+
"row.delete": "删除",
|
|
3114
|
+
"row.deleteConfirm": "确定删除此供应商?",
|
|
3115
|
+
"row.expand": "展开",
|
|
3116
|
+
"row.collapse": "收起",
|
|
3117
|
+
"row.create": "创建",
|
|
3118
|
+
"row.cancel": "取消",
|
|
3119
|
+
"row.init": "初始化",
|
|
3120
|
+
"row.initPrompt": "请帮我初始化 AIGC 供应商「{name}」(id: {id}):先用 aigc_get_provider_info 查看配置,再用 aigc_http_request 探测它的 API(apiKey 会自动附加,无需手动传入),最后调用 aigc_provider_set_instructions 把调用说明保存下来,方便以后直接使用。",
|
|
3121
|
+
"row.auth": "鉴权方式",
|
|
3122
|
+
"row.authBearer": "Bearer 头",
|
|
3123
|
+
"row.authHeader": "自定义 Header",
|
|
3124
|
+
"row.authQuery": "URL 参数",
|
|
3125
|
+
"row.authDesc": "aigc_http_request 自动附加 apiKey 的方式。默认 Authorization: Bearer <key>;选择自定义 Header 或 URL 参数时需填写名称",
|
|
3126
|
+
"badge.builtin": "内置",
|
|
3127
|
+
"badge.stub": "stub 模式",
|
|
3128
|
+
"badge.real": "真实 API",
|
|
3129
|
+
"badge.default": "默认",
|
|
3130
|
+
logButton: "日志",
|
|
3131
|
+
logTitle: "请求日志",
|
|
3132
|
+
logClear: "清空",
|
|
3133
|
+
logEmpty: "暂无请求记录。",
|
|
3134
|
+
logLoading: "加载中…",
|
|
3135
|
+
logError: "错误",
|
|
3136
|
+
logRequestBody: "请求体",
|
|
3137
|
+
logRequestHeaders: "请求头",
|
|
3138
|
+
logResponseBody: "响应预览",
|
|
3139
|
+
logProducedFile: "产物文件",
|
|
3140
|
+
logLocate: "在画布上定位",
|
|
3141
|
+
winner: "优胜",
|
|
3142
|
+
statusFilter: "状态",
|
|
3143
|
+
statusReady: "就绪",
|
|
3144
|
+
statusDraft: "草稿",
|
|
3145
|
+
statusRejected: "否决",
|
|
3146
|
+
statusArchived: "归档",
|
|
3147
|
+
compareButton: "对比",
|
|
3148
|
+
compareTitle: "对比视图",
|
|
3149
|
+
compareClose: "关闭",
|
|
3150
|
+
compareSelectWinner: "选为 winner",
|
|
3151
|
+
compareRejectAll: "全部否决",
|
|
3152
|
+
compareCancelSelection: "取消选择",
|
|
3153
|
+
compareSeed: "seed",
|
|
3154
|
+
compareCost: "成本",
|
|
3155
|
+
compareDuration: "耗时",
|
|
3156
|
+
comparePrompt: "提示词",
|
|
3157
|
+
compareNoMedia: "该元素无可显示的媒体",
|
|
3158
|
+
compareNotEnough: "请选择 2-4 个元素进行对比",
|
|
3159
|
+
compareTooMany: "最多只能同时对比 4 个元素",
|
|
3160
|
+
compareClearSelection: "清除选择",
|
|
3161
|
+
compareNSelected: "已选 {n} 个",
|
|
3162
|
+
"row.priority": "优先级",
|
|
3163
|
+
"row.qualityHint": "质量",
|
|
3164
|
+
"row.costPerCall": "单次成本 ($)",
|
|
3165
|
+
"row.costPerKiloToken": "千 token 成本 ($)",
|
|
3166
|
+
"row.costPerSecond": "每秒成本 ($)",
|
|
3167
|
+
"row.priorityDesc": "数字越小优先级越高(默认 100)。多 provider 同 capability 时按此排序",
|
|
3168
|
+
"row.qualityHintDesc": "fast / balanced / quality,供 Agent 选择快速或高质量 provider",
|
|
3169
|
+
"row.costPerCallDesc": "单次调用成本(美元),用于成本追踪",
|
|
3170
|
+
"row.costPerKiloTokenDesc": "按 token 计费时(chat / transcribe)的千 token 成本",
|
|
3171
|
+
"row.costPerSecondDesc": "按秒计费时(t2v / tts)的每秒成本",
|
|
3172
|
+
"row.endpoints": "Endpoints",
|
|
3173
|
+
"row.endpointsDesc": "结构化能力表。Agent 通过 aigc_get_provider_info 读取,无需解析自然语言",
|
|
3174
|
+
"row.endpointsEmpty": "暂无 endpoint。点击下方\"自动探测\"或\"+ 添加 endpoint\"",
|
|
3175
|
+
"row.addEndpoint": "+ 添加 endpoint",
|
|
3176
|
+
"row.editEndpoint": "编辑 endpoint",
|
|
3177
|
+
"row.removeEndpoint": "删除",
|
|
3178
|
+
"row.endpointPath": "Path",
|
|
3179
|
+
"row.endpointMethod": "Method",
|
|
3180
|
+
"row.endpointCapability": "Capability",
|
|
3181
|
+
"row.endpointResponseKind": "响应类型",
|
|
3182
|
+
"row.endpointResponsePath": "响应字段路径",
|
|
3183
|
+
"row.endpointAcceptsCanvasRef": "支持 $base64 占位符",
|
|
3184
|
+
"row.endpointNotes": "备注",
|
|
3185
|
+
"row.endpointParams": "参数",
|
|
3186
|
+
"row.endpointParamsEmpty": "暂无参数",
|
|
3187
|
+
"row.endpointParamName": "名称",
|
|
3188
|
+
"row.endpointParamType": "类型",
|
|
3189
|
+
"row.endpointParamRequired": "必填",
|
|
3190
|
+
"row.endpointParamDefault": "默认值",
|
|
3191
|
+
"row.endpointAddParam": "+ 添加参数",
|
|
3192
|
+
"row.endpointRemoveParam": "删除参数",
|
|
3193
|
+
"row.endpointCancel": "取消",
|
|
3194
|
+
"row.endpointSave": "保存",
|
|
3195
|
+
"row.autoDetect": "自动探测",
|
|
3196
|
+
"row.autoDetectTitle": "让 Agent 用 aigc_probe_endpoint 自动探测响应格式",
|
|
3197
|
+
"row.autoDetectPrompt": "请帮我自动探测 AIGC 供应商「{name}」(id: {id}) 的 endpoint 响应格式:对每个未配置响应类型的 endpoint 调用 aigc_probe_endpoint(会发送一次最小测试请求,apiKey 自动附加),把探测到的 response.kind + response.path 通过 aigc_provider_set_endpoints 保存到 EndpointSpec。如果该 provider 还没有任何 endpoint,请先用 aigc_http_request 探测常见的 endpoint(如 /v1/images/generations、/v1/videos/generations、/v1/audio/speech),再用 aigc_probe_endpoint 探测响应格式。"
|
|
3198
|
+
};
|
|
3199
|
+
const en = {
|
|
3200
|
+
tabTitle: "AIGC Canvas",
|
|
3201
|
+
title: "AIGC Canvas",
|
|
3202
|
+
empty: "Canvas is empty. The agent calls provider APIs via aigc_http_request and places the generated files anywhere on the canvas with aigc_canvas_place.",
|
|
3203
|
+
emptyHint: "Configure a provider in the settings tab on the right, then ask the agent to generate something.",
|
|
3204
|
+
prompt: "Prompt",
|
|
3205
|
+
image: "Image",
|
|
3206
|
+
video: "Video",
|
|
3207
|
+
audio: "Audio",
|
|
3208
|
+
meta: "Metadata",
|
|
3209
|
+
generatedBy: "Generated by",
|
|
3210
|
+
edgeCount: "edges",
|
|
3211
|
+
elementCount: "elements",
|
|
3212
|
+
loadError: "Failed to load canvas",
|
|
3213
|
+
disconnected: "Disconnected, reconnecting…",
|
|
3214
|
+
reconnecting: "Reconnecting…",
|
|
3215
|
+
refresh: "Refresh",
|
|
3216
|
+
resetView: "Reset view",
|
|
3217
|
+
zoom: "Zoom",
|
|
3218
|
+
zoomIn: "Zoom in",
|
|
3219
|
+
zoomOut: "Zoom out",
|
|
3220
|
+
detailClose: "Close",
|
|
3221
|
+
detailPrompt: "Prompt",
|
|
3222
|
+
detailParams: "Generation params",
|
|
3223
|
+
detailPosition: "Position",
|
|
3224
|
+
detailPath: "File path",
|
|
3225
|
+
delete: "Delete",
|
|
3226
|
+
deleteElement: "Delete element",
|
|
3227
|
+
dropHint: "Drop files onto canvas",
|
|
3228
|
+
uploading: "Uploading…",
|
|
3229
|
+
menuRegenerate: "Regenerate...",
|
|
3230
|
+
menuUseAsReference: "Use as reference...",
|
|
3231
|
+
menuSendToChat: "Send to chat",
|
|
3232
|
+
menuDownload: "Download",
|
|
3233
|
+
menuPromoteToLibrary: "Promote to library...",
|
|
3234
|
+
menuMarkWinner: "Mark as winner",
|
|
3235
|
+
menuMarkRejected: "Mark as rejected",
|
|
3236
|
+
menuArchive: "Archive",
|
|
3237
|
+
menuSeparator: "─────────────",
|
|
3238
|
+
toolbarGenerate: "+ Generate",
|
|
3239
|
+
toolbarEditSelected: "✂ Edit selected",
|
|
3240
|
+
toolbarRunWorkflow: "▶ Run workflow",
|
|
3241
|
+
toolbarGenerateTitle: "Open the quick-generate dialog (t2i/t2v/tts)",
|
|
3242
|
+
toolbarEditSelectedTitle: "Run an ffmpeg operation on the selected element",
|
|
3243
|
+
toolbarRunWorkflowTitle: "Open the pipeline template picker",
|
|
3244
|
+
toolbarNoSelection: "Select an element on the canvas first",
|
|
3245
|
+
noticeRegenerate: "Please regenerate the element {filePath} using aigc_reroll",
|
|
3246
|
+
noticeUseAsReference: "Please use the following element as a reference for the next generation: {filePath}",
|
|
3247
|
+
noticeSendToChat: "Please use this element as a reference: [filePath: {filePath}, kind: {kind}, title: {title}]",
|
|
3248
|
+
noticeGenerate: "Please generate a new AIGC asset (call aigc_get_provider_info to list available providers, then aigc_http_request to generate, and finally aigc_canvas_place to put the result on the canvas).",
|
|
3249
|
+
noticeEditSelected: "Please edit the selected element with aigc_media_edit (ffmpeg): {filePath} (kind: {kind}, title: {title})",
|
|
3250
|
+
noticeRunWorkflow: "Please list the available pipeline templates and run one (if there is only one, run it directly).",
|
|
3251
|
+
settingsNav: "AIGC Canvas",
|
|
3252
|
+
settingsTitle: "AIGC Providers",
|
|
3253
|
+
settingsIntro: "Configure one or more AIGC providers. Each provider has its own name, API endpoint, key, auth scheme, and usage instructions. The agent reads the provider list via aigc_get_provider_info, calls the API via aigc_http_request (endpoint + apiKey attached automatically), and places generated files onto the canvas with aigc_canvas_place.",
|
|
3254
|
+
settingsEmpty: "No providers configured. Add one below.",
|
|
3255
|
+
settingsAdd: "+ Add provider",
|
|
3256
|
+
settingsLoading: "Loading…",
|
|
3257
|
+
settingsError: "Error",
|
|
3258
|
+
"row.id": "ID",
|
|
3259
|
+
"row.name": "Name",
|
|
3260
|
+
"row.endpoint": "Endpoint",
|
|
3261
|
+
"row.apiKey": "API Key",
|
|
3262
|
+
"row.instructions": "Instructions",
|
|
3263
|
+
"row.idPlaceholder": "volcano / jimeng / minimax",
|
|
3264
|
+
"row.namePlaceholder": "Display name (e.g. \"Volcano Engine\")",
|
|
3265
|
+
"row.endpointPlaceholder": "stub://aigc-backend",
|
|
3266
|
+
"row.apiKeyPlaceholder": "sk-...",
|
|
3267
|
+
"row.instructionsPlaceholder": "The agent writes these when you initialize the provider (click \"Initialize\" on the card)...",
|
|
3268
|
+
"row.idHint": "Lowercase letters, digits, hyphens; must start with a letter. Used as the provider_id parameter to aigc_http_request",
|
|
3269
|
+
"row.endpointDesc": "Provider API URL. Use stub://aigc-backend for the built-in stub (synthetic test media, no real API calls)",
|
|
3270
|
+
"row.apiKeyDesc": "Provider API key. Not needed for the stub backend. The agent never sees it — aigc_http_request attaches it automatically",
|
|
3271
|
+
"row.instructionsDesc": "The agent reads this field via the aigc_get_provider_info tool to decide how to call the provider API",
|
|
3272
|
+
"row.instructionsHint": "💡 Click \"Initialize\" on the card: the agent probes the API with aigc_http_request and writes the instructions itself",
|
|
3273
|
+
"row.save": "Save",
|
|
3274
|
+
"row.delete": "Delete",
|
|
3275
|
+
"row.deleteConfirm": "Delete this provider?",
|
|
3276
|
+
"row.expand": "Expand",
|
|
3277
|
+
"row.collapse": "Collapse",
|
|
3278
|
+
"row.create": "Create",
|
|
3279
|
+
"row.cancel": "Cancel",
|
|
3280
|
+
"row.init": "Initialize",
|
|
3281
|
+
"row.initPrompt": "Please initialize the AIGC provider \"{name}\" (id: {id}): first call aigc_get_provider_info to see its config, then probe its API with aigc_http_request (the apiKey is attached automatically — do not pass it yourself), and finally call aigc_provider_set_instructions to save the usage instructions so it can be used directly later.",
|
|
3282
|
+
"row.auth": "Auth scheme",
|
|
3283
|
+
"row.authBearer": "Bearer header",
|
|
3284
|
+
"row.authHeader": "Custom header",
|
|
3285
|
+
"row.authQuery": "URL query param",
|
|
3286
|
+
"row.authDesc": "How aigc_http_request attaches the apiKey. Default: Authorization: Bearer <key>. For custom header or URL query param, fill in the name",
|
|
3287
|
+
"badge.builtin": "builtin",
|
|
3288
|
+
"badge.stub": "stub mode",
|
|
3289
|
+
"badge.real": "real API",
|
|
3290
|
+
"badge.default": "default",
|
|
3291
|
+
logButton: "Logs",
|
|
3292
|
+
logTitle: "Request Log",
|
|
3293
|
+
logClear: "Clear",
|
|
3294
|
+
logEmpty: "No requests logged yet.",
|
|
3295
|
+
logLoading: "Loading…",
|
|
3296
|
+
logError: "Error",
|
|
3297
|
+
logRequestBody: "Request body",
|
|
3298
|
+
logRequestHeaders: "Request headers",
|
|
3299
|
+
logResponseBody: "Response preview",
|
|
3300
|
+
logProducedFile: "Produced file",
|
|
3301
|
+
logLocate: "Locate on canvas",
|
|
3302
|
+
winner: "Winner",
|
|
3303
|
+
statusFilter: "Status",
|
|
3304
|
+
statusReady: "Ready",
|
|
3305
|
+
statusDraft: "Draft",
|
|
3306
|
+
statusRejected: "Rejected",
|
|
3307
|
+
statusArchived: "Archived",
|
|
3308
|
+
compareButton: "Compare",
|
|
3309
|
+
compareTitle: "Compare view",
|
|
3310
|
+
compareClose: "Close",
|
|
3311
|
+
compareSelectWinner: "Select as winner",
|
|
3312
|
+
compareRejectAll: "Reject all",
|
|
3313
|
+
compareCancelSelection: "Cancel selection",
|
|
3314
|
+
compareSeed: "seed",
|
|
3315
|
+
compareCost: "cost",
|
|
3316
|
+
compareDuration: "duration",
|
|
3317
|
+
comparePrompt: "Prompt",
|
|
3318
|
+
compareNoMedia: "No media to display for this element",
|
|
3319
|
+
compareNotEnough: "Select 2-4 elements to compare",
|
|
3320
|
+
compareTooMany: "You can compare at most 4 elements at once",
|
|
3321
|
+
compareClearSelection: "Clear selection",
|
|
3322
|
+
compareNSelected: "{n} selected",
|
|
3323
|
+
"row.priority": "Priority",
|
|
3324
|
+
"row.qualityHint": "Quality",
|
|
3325
|
+
"row.costPerCall": "Cost per call ($)",
|
|
3326
|
+
"row.costPerKiloToken": "Cost per 1k tokens ($)",
|
|
3327
|
+
"row.costPerSecond": "Cost per second ($)",
|
|
3328
|
+
"row.priorityDesc": "Smaller = higher priority (default 100). When multiple providers serve the same capability, they are sorted by this",
|
|
3329
|
+
"row.qualityHintDesc": "fast / balanced / quality — lets the agent pick fast vs. quality providers",
|
|
3330
|
+
"row.costPerCallDesc": "Cost per call in USD (for cost tracking)",
|
|
3331
|
+
"row.costPerKiloTokenDesc": "Per-1k-token cost in USD (for chat / transcribe cost tracking)",
|
|
3332
|
+
"row.costPerSecondDesc": "Per-second cost in USD (for t2v / tts cost tracking)",
|
|
3333
|
+
"row.endpoints": "Endpoints",
|
|
3334
|
+
"row.endpointsDesc": "Structured capability catalog. The agent reads it via aigc_get_provider_info — no natural-language parsing needed",
|
|
3335
|
+
"row.endpointsEmpty": "No endpoints yet. Click \"Auto-detect\" or \"+ Add endpoint\" below",
|
|
3336
|
+
"row.addEndpoint": "+ Add endpoint",
|
|
3337
|
+
"row.editEndpoint": "Edit endpoint",
|
|
3338
|
+
"row.removeEndpoint": "Remove",
|
|
3339
|
+
"row.endpointPath": "Path",
|
|
3340
|
+
"row.endpointMethod": "Method",
|
|
3341
|
+
"row.endpointCapability": "Capability",
|
|
3342
|
+
"row.endpointResponseKind": "Response kind",
|
|
3343
|
+
"row.endpointResponsePath": "Response field path",
|
|
3344
|
+
"row.endpointAcceptsCanvasRef": "Accepts $base64 placeholder",
|
|
3345
|
+
"row.endpointNotes": "Notes",
|
|
3346
|
+
"row.endpointParams": "Parameters",
|
|
3347
|
+
"row.endpointParamsEmpty": "No parameters",
|
|
3348
|
+
"row.endpointParamName": "Name",
|
|
3349
|
+
"row.endpointParamType": "Type",
|
|
3350
|
+
"row.endpointParamRequired": "Required",
|
|
3351
|
+
"row.endpointParamDefault": "Default",
|
|
3352
|
+
"row.endpointAddParam": "+ Add parameter",
|
|
3353
|
+
"row.endpointRemoveParam": "Remove parameter",
|
|
3354
|
+
"row.endpointCancel": "Cancel",
|
|
3355
|
+
"row.endpointSave": "Save",
|
|
3356
|
+
"row.autoDetect": "Auto-detect",
|
|
3357
|
+
"row.autoDetectTitle": "Ask the agent to auto-detect the response shape via aigc_probe_endpoint",
|
|
3358
|
+
"row.autoDetectPrompt": "Please auto-detect the response shape for the AIGC provider \"{name}\" (id: {id}): for every endpoint whose response.kind is not yet set, call aigc_probe_endpoint (it sends ONE minimal test request — apiKey is attached automatically) and save the detected response.kind + response.path into the EndpointSpec via aigc_provider_set_endpoints. If this provider has no endpoints at all yet, first probe common endpoints with aigc_http_request (e.g. /v1/images/generations, /v1/videos/generations, /v1/audio/speech), then probe the response shape with aigc_probe_endpoint."
|
|
3359
|
+
};
|
|
3360
|
+
//#endregion
|
|
3361
|
+
//#region src/client/index.tsx
|
|
3362
|
+
/** Services required before mounting. */
|
|
3363
|
+
const inject = [
|
|
3364
|
+
"betterSidebar",
|
|
3365
|
+
"slots",
|
|
3366
|
+
"locale",
|
|
3367
|
+
"conversation"
|
|
3368
|
+
];
|
|
3369
|
+
function apply(ctx) {
|
|
3370
|
+
ctx.effect(() => ctx.locale.register(NS, {
|
|
3371
|
+
zh,
|
|
3372
|
+
en
|
|
3373
|
+
}), "dsh-aigc-canvas: dictionaries");
|
|
3374
|
+
const t = ctx.locale.bind(NS);
|
|
3375
|
+
const betterSidebar = ctx.betterSidebar;
|
|
3376
|
+
if (betterSidebar !== void 0) ctx.effect(() => betterSidebar.registerTab({
|
|
3377
|
+
id: "aigc-canvas:main",
|
|
3378
|
+
title: () => t("tabTitle"),
|
|
3379
|
+
order: 50,
|
|
3380
|
+
dedupeKey: () => "aigc-canvas:main",
|
|
3381
|
+
component: ({ scope }) => {
|
|
3382
|
+
const storeRef = (0, react.useRef)(null);
|
|
3383
|
+
if (storeRef.current === null || storeRef.current.sessionId !== scope.sessionId) {
|
|
3384
|
+
storeRef.current?.dispose();
|
|
3385
|
+
storeRef.current = new CanvasStore({ sessionId: scope.sessionId });
|
|
3386
|
+
}
|
|
3387
|
+
(0, react.useEffect)(() => {
|
|
3388
|
+
return () => {
|
|
3389
|
+
storeRef.current?.dispose();
|
|
3390
|
+
storeRef.current = null;
|
|
3391
|
+
};
|
|
3392
|
+
}, []);
|
|
3393
|
+
return (0, react.createElement)(CanvasViewWithBoundary, {
|
|
3394
|
+
store: storeRef.current,
|
|
3395
|
+
t
|
|
3396
|
+
});
|
|
3397
|
+
}
|
|
3398
|
+
}));
|
|
3399
|
+
const settingsInjected = () => ({
|
|
3400
|
+
t,
|
|
3401
|
+
send: (text) => ctx.conversation.send(text)
|
|
3402
|
+
});
|
|
3403
|
+
ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
3404
|
+
name: "settings.section",
|
|
3405
|
+
id: "aigc-canvas",
|
|
3406
|
+
order: 60,
|
|
3407
|
+
label: () => t("settingsNav"),
|
|
3408
|
+
locale: NS,
|
|
3409
|
+
inject: settingsInjected
|
|
3410
|
+
}, SettingsPage));
|
|
3411
|
+
}
|
|
3412
|
+
//#endregion
|
|
3413
|
+
exports.apply = apply;
|
|
3414
|
+
exports.inject = inject;
|
|
3415
|
+
return module.exports;
|
|
3416
|
+
}
|
|
3417
|
+
});
|
|
3418
|
+
|
|
3419
|
+
//# sourceMappingURL=client.js.map
|