@wisdoverse/dsh-inline-media-viewer 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/LICENSE +21 -0
- package/README.md +166 -0
- package/README.zh-CN.md +155 -0
- package/SECURITY.md +67 -0
- package/client/client.js +554 -0
- package/cordis.patch.yml +3 -0
- package/index.js +185 -0
- package/lib.js +178 -0
- package/package.json +74 -0
package/client/client.js
ADDED
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "dsh-inline-media-viewer",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
|
|
8
|
+
const React = require("react");
|
|
9
|
+
const { createElement: h, useEffect, useMemo, useState } = React;
|
|
10
|
+
const CHANNEL = "/inline-media";
|
|
11
|
+
const ENDPOINT = "read";
|
|
12
|
+
const DATA_KEY = "inlineMedia";
|
|
13
|
+
const DISPLAY_CAP = 12;
|
|
14
|
+
const SETTINGS_NAMESPACE = "inline-media";
|
|
15
|
+
// Empty config = the host-side built-in default
|
|
16
|
+
// (http://127.0.0.1:8188, ComfyUI's standard local address).
|
|
17
|
+
const COMFY_DEFAULT_URL = "";
|
|
18
|
+
const DEFAULT_SETTINGS = Object.freeze({ autoRender: true, displayCap: DISPLAY_CAP, imageMaxPx: 380, comfyUrl: COMFY_DEFAULT_URL });
|
|
19
|
+
let settingsScope = null;
|
|
20
|
+
|
|
21
|
+
function clampInt(raw, min, max, fallback) {
|
|
22
|
+
const n = Number.parseInt(String(raw), 10);
|
|
23
|
+
if (!Number.isFinite(n)) return fallback;
|
|
24
|
+
return Math.min(max, Math.max(min, n));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function readSettings() {
|
|
28
|
+
if (settingsScope === null) return DEFAULT_SETTINGS;
|
|
29
|
+
const snapshot = settingsScope.getSnapshot();
|
|
30
|
+
const value = snapshot && snapshot.value;
|
|
31
|
+
if (!value || typeof value !== "object") return DEFAULT_SETTINGS;
|
|
32
|
+
return {
|
|
33
|
+
autoRender: value.autoRender !== false,
|
|
34
|
+
displayCap: clampInt(value.displayCap, 1, 30, DEFAULT_SETTINGS.displayCap),
|
|
35
|
+
imageMaxPx: clampInt(value.imageMaxPx, 160, 1200, DEFAULT_SETTINGS.imageMaxPx),
|
|
36
|
+
comfyUrl: typeof value.comfyUrl === "string" && value.comfyUrl.trim() !== ""
|
|
37
|
+
? value.comfyUrl.trim().slice(0, 512)
|
|
38
|
+
: DEFAULT_SETTINGS.comfyUrl,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const MEDIA_EXTENSIONS = new Set([
|
|
42
|
+
"png", "jpg", "jpeg", "webp", "gif", "avif", "bmp",
|
|
43
|
+
"mp4", "webm", "mov", "m4v", "mkv", "avi", "ogv",
|
|
44
|
+
"mp3", "wav", "m4a", "aac", "ogg", "oga", "flac", "opus",
|
|
45
|
+
]);
|
|
46
|
+
const VIDEO_EXTENSIONS = new Set(["mp4", "webm", "mov", "m4v", "mkv", "avi", "ogv"]);
|
|
47
|
+
const AUDIO_EXTENSIONS = new Set(["mp3", "wav", "m4a", "aac", "ogg", "oga", "flac", "opus"]);
|
|
48
|
+
const COMFY_HOSTS = new Set(["127.0.0.1", "localhost"]);
|
|
49
|
+
const COMFY_PORTS = new Set(["8188"]);
|
|
50
|
+
|
|
51
|
+
function extensionOf(source) {
|
|
52
|
+
try {
|
|
53
|
+
const url = new URL(source);
|
|
54
|
+
const filename = url.searchParams.get("filename");
|
|
55
|
+
const target = filename || url.pathname;
|
|
56
|
+
const dot = target.lastIndexOf(".");
|
|
57
|
+
return dot < 0 ? "" : target.slice(dot + 1).toLowerCase();
|
|
58
|
+
} catch (_error) {
|
|
59
|
+
const target = source.split(/[?#]/, 1)[0];
|
|
60
|
+
const dot = target.lastIndexOf(".");
|
|
61
|
+
return dot < 0 ? "" : target.slice(dot + 1).toLowerCase();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function mediaKind(source) {
|
|
66
|
+
const ext = extensionOf(source);
|
|
67
|
+
if (!MEDIA_EXTENSIONS.has(ext)) return null;
|
|
68
|
+
if (VIDEO_EXTENSIONS.has(ext)) return "video";
|
|
69
|
+
if (AUDIO_EXTENSIONS.has(ext)) return "audio";
|
|
70
|
+
return "image";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function cleanCandidate(value) {
|
|
74
|
+
return value
|
|
75
|
+
.trim()
|
|
76
|
+
.replace(/^[`'"(<\[]+/, "")
|
|
77
|
+
.replace(/[`'">)\],;:.!?]+$/, "");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function extractCandidates(text) {
|
|
81
|
+
const found = [];
|
|
82
|
+
const seen = new Set();
|
|
83
|
+
const add = (raw) => {
|
|
84
|
+
const source = cleanCandidate(raw);
|
|
85
|
+
const kind = mediaKind(source);
|
|
86
|
+
if (!kind || seen.has(source)) return;
|
|
87
|
+
seen.add(source);
|
|
88
|
+
found.push({ source, kind });
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
for (const match of text.matchAll(/!?\[[^\]]*\]\(([^)]+)\)/g)) add(match[1]);
|
|
92
|
+
for (const match of text.matchAll(/`([^`\n]+)`/g)) add(match[1]);
|
|
93
|
+
for (const match of text.matchAll(/https?:\/\/[^\s<>"'`]+/gi)) add(match[0]);
|
|
94
|
+
|
|
95
|
+
const ext = Array.from(MEDIA_EXTENSIONS).join("|");
|
|
96
|
+
const pathPattern = new RegExp(
|
|
97
|
+
String.raw`(?:\/|\.\.?\/|[\w.-]+\/)[^\s<>"'\x60()\[\]{}]+?\.(?:${ext})(?:\?[^\s<>"'\x60]*)?`,
|
|
98
|
+
"gi",
|
|
99
|
+
);
|
|
100
|
+
for (const match of text.matchAll(pathPattern)) add(match[0]);
|
|
101
|
+
return found;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function assistantText(event) {
|
|
105
|
+
if (event.type !== "assistant/message") return "";
|
|
106
|
+
const content = event.data && event.data.message && event.data.message.content;
|
|
107
|
+
if (!Array.isArray(content)) return "";
|
|
108
|
+
return content
|
|
109
|
+
.filter((block) => block && block.type === "text" && typeof block.text === "string")
|
|
110
|
+
.map((block) => block.text)
|
|
111
|
+
.join("\n");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const mediaDefinition = {
|
|
115
|
+
kind: DATA_KEY,
|
|
116
|
+
match(event) {
|
|
117
|
+
if (event.type === "turn/start") {
|
|
118
|
+
return { id: String(event.data.turn), role: "start" };
|
|
119
|
+
}
|
|
120
|
+
if (event.type === "assistant/message" && Number.isInteger(event.data && event.data.turn)) {
|
|
121
|
+
return { id: String(event.data.turn), role: "update" };
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
},
|
|
125
|
+
start(_context, match) {
|
|
126
|
+
return { turn: match.event.data.turn, candidates: [] };
|
|
127
|
+
},
|
|
128
|
+
update(context, match) {
|
|
129
|
+
const additions = extractCandidates(assistantText(match.event)).map((candidate) => ({
|
|
130
|
+
...candidate,
|
|
131
|
+
seq: match.event.seq,
|
|
132
|
+
}));
|
|
133
|
+
if (additions.length === 0) return context.state;
|
|
134
|
+
const seen = new Set(context.state.candidates.map((candidate) => candidate.source));
|
|
135
|
+
const fresh = additions.filter((candidate) => {
|
|
136
|
+
if (seen.has(candidate.source)) return false;
|
|
137
|
+
seen.add(candidate.source);
|
|
138
|
+
return true;
|
|
139
|
+
});
|
|
140
|
+
if (fresh.length === 0) return context.state;
|
|
141
|
+
return {
|
|
142
|
+
...context.state,
|
|
143
|
+
candidates: [...context.state.candidates, ...fresh].slice(0, DISPLAY_CAP),
|
|
144
|
+
};
|
|
145
|
+
},
|
|
146
|
+
buildLocationData(context, scope) {
|
|
147
|
+
if (scope !== "turn" || context.state === undefined) return null;
|
|
148
|
+
return {
|
|
149
|
+
kind: "turn",
|
|
150
|
+
turn: context.state.turn,
|
|
151
|
+
key: DATA_KEY,
|
|
152
|
+
value: { candidates: context.state.candidates },
|
|
153
|
+
};
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
function selectMedia(owner) {
|
|
158
|
+
const settings = readSettings();
|
|
159
|
+
if (!settings.autoRender) return null;
|
|
160
|
+
const data = owner.turn.data.get(DATA_KEY);
|
|
161
|
+
if (!data || !Array.isArray(data.candidates)) return null;
|
|
162
|
+
const candidates = data.candidates.filter((candidate) => candidate.seq <= owner.seq);
|
|
163
|
+
return candidates.length === 0 ? null : candidates.slice(0, settings.displayCap);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function normalizeComfyOrigin(input) {
|
|
167
|
+
if (typeof input !== "string") return null;
|
|
168
|
+
let raw = input.trim();
|
|
169
|
+
if (!raw) return null;
|
|
170
|
+
if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(raw)) raw = `http://${raw}`;
|
|
171
|
+
let url;
|
|
172
|
+
try {
|
|
173
|
+
url = new URL(raw);
|
|
174
|
+
} catch (_error) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
if ((url.protocol !== "http:" && url.protocol !== "https:") || !url.hostname) return null;
|
|
178
|
+
if (url.username || url.password) return null;
|
|
179
|
+
const host = url.hostname;
|
|
180
|
+
if (!/^[a-z0-9.-]+$/i.test(host) && !/^\[[0-9a-f:.%]+\]$/i.test(host)) return null;
|
|
181
|
+
if ((url.pathname !== "" && url.pathname !== "/") || url.search || url.hash) return null;
|
|
182
|
+
const port = url.port || (url.protocol === "https:" ? "443" : "8188");
|
|
183
|
+
return new URL(`${url.protocol}//${host}:${port}`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function isComfySource(source, comfyUrl) {
|
|
187
|
+
try {
|
|
188
|
+
const url = new URL(source);
|
|
189
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return false;
|
|
190
|
+
const port = url.port || (url.protocol === "https:" ? "443" : "80");
|
|
191
|
+
if (COMFY_HOSTS.has(url.hostname) && COMFY_PORTS.has(port)) return true;
|
|
192
|
+
const canonical = normalizeComfyOrigin(comfyUrl || COMFY_DEFAULT_URL);
|
|
193
|
+
const canonicalPort = canonical ? (canonical.port || (canonical.protocol === "https:" ? "443" : "8188")) : "";
|
|
194
|
+
return !!canonical && url.hostname === canonical.hostname && port === canonicalPort;
|
|
195
|
+
} catch (_error) {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function basename(source) {
|
|
201
|
+
try {
|
|
202
|
+
const url = new URL(source);
|
|
203
|
+
const filename = url.searchParams.get("filename");
|
|
204
|
+
if (filename) return filename.split(/[\\/]/).pop();
|
|
205
|
+
return decodeURIComponent(url.pathname.split("/").pop() || source);
|
|
206
|
+
} catch (_error) {
|
|
207
|
+
return source.split(/[\\/]/).pop() || source;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function MediaCard({ candidate, connection, sessionId, imageMaxPx, comfyUrl }) {
|
|
212
|
+
const proxied = !/^https?:\/\//i.test(candidate.source) || isComfySource(candidate.source, comfyUrl);
|
|
213
|
+
const [state, setState] = useState(() => proxied
|
|
214
|
+
? { status: "loading", src: "" }
|
|
215
|
+
: { status: "ready", src: candidate.source });
|
|
216
|
+
const [expanded, setExpanded] = useState(false);
|
|
217
|
+
|
|
218
|
+
useEffect(() => {
|
|
219
|
+
if (!proxied) return undefined;
|
|
220
|
+
const controller = new AbortController();
|
|
221
|
+
let active = true;
|
|
222
|
+
setState({ status: "loading", src: "" });
|
|
223
|
+
connection.rpc.call(
|
|
224
|
+
CHANNEL,
|
|
225
|
+
ENDPOINT,
|
|
226
|
+
{ source: candidate.source, sessionId },
|
|
227
|
+
controller.signal,
|
|
228
|
+
).then((result) => {
|
|
229
|
+
if (!active) return;
|
|
230
|
+
if (!result.ok || !result.value || typeof result.value.dataUrl !== "string") {
|
|
231
|
+
setState({ status: "failed", src: "" });
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
setState({ status: "ready", src: result.value.dataUrl });
|
|
235
|
+
}).catch(() => {
|
|
236
|
+
if (active) setState({ status: "failed", src: "" });
|
|
237
|
+
});
|
|
238
|
+
return () => {
|
|
239
|
+
active = false;
|
|
240
|
+
controller.abort();
|
|
241
|
+
};
|
|
242
|
+
}, [candidate.source, connection, proxied, sessionId]);
|
|
243
|
+
|
|
244
|
+
useEffect(() => {
|
|
245
|
+
if (!expanded) return undefined;
|
|
246
|
+
const close = (event) => {
|
|
247
|
+
if (event.key === "Escape") setExpanded(false);
|
|
248
|
+
};
|
|
249
|
+
window.addEventListener("keydown", close);
|
|
250
|
+
return () => window.removeEventListener("keydown", close);
|
|
251
|
+
}, [expanded]);
|
|
252
|
+
|
|
253
|
+
if (state.status === "failed") return null;
|
|
254
|
+
if (state.status === "loading") {
|
|
255
|
+
return h("div", {
|
|
256
|
+
style: {
|
|
257
|
+
color: "var(--dsw-alias-label-tertiary)",
|
|
258
|
+
fontSize: 12,
|
|
259
|
+
padding: "8px 0",
|
|
260
|
+
},
|
|
261
|
+
}, `正在加载 ${basename(candidate.source)}…`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const common = {
|
|
265
|
+
src: state.src,
|
|
266
|
+
title: candidate.source,
|
|
267
|
+
onError: () => setState({ status: "failed", src: "" }),
|
|
268
|
+
style: {
|
|
269
|
+
display: "block",
|
|
270
|
+
width: "100%",
|
|
271
|
+
maxHeight: imageMaxPx || 380,
|
|
272
|
+
borderRadius: 10,
|
|
273
|
+
background: "var(--dsw-alias-bg-layer-2)",
|
|
274
|
+
objectFit: "contain",
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
let media;
|
|
279
|
+
if (candidate.kind === "video") {
|
|
280
|
+
media = h("video", { ...common, controls: true, preload: "metadata" });
|
|
281
|
+
} else if (candidate.kind === "audio") {
|
|
282
|
+
media = h("audio", {
|
|
283
|
+
...common,
|
|
284
|
+
controls: true,
|
|
285
|
+
preload: "metadata",
|
|
286
|
+
style: { ...common.style, minHeight: 42 },
|
|
287
|
+
});
|
|
288
|
+
} else {
|
|
289
|
+
media = h("button", {
|
|
290
|
+
type: "button",
|
|
291
|
+
onClick: () => setExpanded(true),
|
|
292
|
+
"aria-label": `放大 ${basename(candidate.source)}`,
|
|
293
|
+
style: {
|
|
294
|
+
display: "block",
|
|
295
|
+
width: "100%",
|
|
296
|
+
cursor: "zoom-in",
|
|
297
|
+
background: "transparent",
|
|
298
|
+
border: 0,
|
|
299
|
+
padding: 0,
|
|
300
|
+
},
|
|
301
|
+
}, h("img", { ...common, alt: basename(candidate.source), loading: "lazy" }));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return h("div", {
|
|
305
|
+
style: {
|
|
306
|
+
minWidth: 0,
|
|
307
|
+
overflow: "hidden",
|
|
308
|
+
border: "1px solid var(--dsw-alias-border-l3)",
|
|
309
|
+
borderRadius: 12,
|
|
310
|
+
padding: 8,
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
media,
|
|
314
|
+
h("div", {
|
|
315
|
+
title: candidate.source,
|
|
316
|
+
style: {
|
|
317
|
+
color: "var(--dsw-alias-label-tertiary)",
|
|
318
|
+
fontSize: 12,
|
|
319
|
+
overflow: "hidden",
|
|
320
|
+
padding: "6px 2px 0",
|
|
321
|
+
textOverflow: "ellipsis",
|
|
322
|
+
whiteSpace: "nowrap",
|
|
323
|
+
},
|
|
324
|
+
}, basename(candidate.source)),
|
|
325
|
+
expanded && candidate.kind === "image" ? h("div", {
|
|
326
|
+
role: "dialog",
|
|
327
|
+
"aria-modal": "true",
|
|
328
|
+
onClick: () => setExpanded(false),
|
|
329
|
+
style: {
|
|
330
|
+
alignItems: "center",
|
|
331
|
+
background: "rgba(0,0,0,.86)",
|
|
332
|
+
cursor: "zoom-out",
|
|
333
|
+
display: "flex",
|
|
334
|
+
inset: 0,
|
|
335
|
+
justifyContent: "center",
|
|
336
|
+
padding: 24,
|
|
337
|
+
position: "fixed",
|
|
338
|
+
zIndex: 9999,
|
|
339
|
+
},
|
|
340
|
+
}, h("img", {
|
|
341
|
+
src: state.src,
|
|
342
|
+
alt: basename(candidate.source),
|
|
343
|
+
style: { maxHeight: "calc(100vh - 48px)", maxWidth: "calc(100vw - 48px)", objectFit: "contain" },
|
|
344
|
+
})) : null);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function MediaTail({ matched, connection, sessionId }) {
|
|
348
|
+
const settings = readSettings();
|
|
349
|
+
const candidates = useMemo(() => matched.slice(0, settings.displayCap), [matched, settings.displayCap]);
|
|
350
|
+
return h("section", {
|
|
351
|
+
"aria-label": "媒体预览",
|
|
352
|
+
style: { marginTop: 14 },
|
|
353
|
+
},
|
|
354
|
+
h("div", {
|
|
355
|
+
style: {
|
|
356
|
+
color: "var(--dsw-alias-label-tertiary)",
|
|
357
|
+
fontSize: 12,
|
|
358
|
+
marginBottom: 6,
|
|
359
|
+
},
|
|
360
|
+
}, "媒体预览"),
|
|
361
|
+
h("div", {
|
|
362
|
+
style: {
|
|
363
|
+
display: "grid",
|
|
364
|
+
gap: 10,
|
|
365
|
+
gridTemplateColumns: "repeat(auto-fit, minmax(min(280px, 100%), 1fr))",
|
|
366
|
+
},
|
|
367
|
+
}, candidates.map((candidate) => h(MediaCard, {
|
|
368
|
+
candidate,
|
|
369
|
+
comfyUrl: settings.comfyUrl,
|
|
370
|
+
connection,
|
|
371
|
+
imageMaxPx: settings.imageMaxPx,
|
|
372
|
+
key: candidate.source,
|
|
373
|
+
sessionId,
|
|
374
|
+
}))));
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const SETTINGS_NS = "inlineMedia";
|
|
378
|
+
const SETTINGS_DICT = {
|
|
379
|
+
zh: {
|
|
380
|
+
nav: "媒体预览",
|
|
381
|
+
intro: "调整聊天中内联媒体预览的行为。设置会持久保存到本机设置文档。",
|
|
382
|
+
autoRender: "自动渲染检测到的媒体",
|
|
383
|
+
autoRenderHint: "关闭后仅扫描不展示(仍会扫描消息)。",
|
|
384
|
+
displayCap: "每回合最多显示",
|
|
385
|
+
displayCapHint: "1–30",
|
|
386
|
+
imageMaxPx: "媒体最大高度 (px)",
|
|
387
|
+
imageMaxPxHint: "160–1200",
|
|
388
|
+
comfyUrl: "ComfyUI 地址",
|
|
389
|
+
comfyUrlHint: "主机代理拉流的服务器地址,格式 http(s)://host[:port];留空使用内置默认地址。",
|
|
390
|
+
comfyUrlError: "地址无效,请使用 http://host:8188 这类格式。",
|
|
391
|
+
reset: "恢复默认",
|
|
392
|
+
writable: "当前连接不可回写:设置仅临时保存在本会话,刷新后恢复默认。",
|
|
393
|
+
},
|
|
394
|
+
en: {
|
|
395
|
+
nav: "Media preview",
|
|
396
|
+
intro: "Tune how inline media is previewed in conversations. Changes persist to the local settings document.",
|
|
397
|
+
autoRender: "Auto-render detected media",
|
|
398
|
+
autoRenderHint: "When off, mentions are still scanned but nothing is shown.",
|
|
399
|
+
displayCap: "Max items per turn",
|
|
400
|
+
displayCapHint: "1–30",
|
|
401
|
+
imageMaxPx: "Max media height (px)",
|
|
402
|
+
imageMaxPxHint: "160–1200",
|
|
403
|
+
comfyUrl: "ComfyUI address",
|
|
404
|
+
comfyUrlHint: "Origin the host proxy fetches media from: http(s)://host[:port]; empty uses the built-in default.",
|
|
405
|
+
comfyUrlError: "Invalid address; use the http://host:8188 form.",
|
|
406
|
+
reset: "Reset to defaults",
|
|
407
|
+
writable: "This connection cannot write settings: values are session-local until a loopback connection is used.",
|
|
408
|
+
},
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
function MediaSettingsSection({ scope, t }) {
|
|
412
|
+
const [snap, setSnap] = useState(scope ? scope.getSnapshot() : null);
|
|
413
|
+
useEffect(() => (scope ? scope.subscribe(() => setSnap(scope.getSnapshot())) : undefined), [scope]);
|
|
414
|
+
const stored = snap && snap.value && typeof snap.value === "object" ? snap.value : DEFAULT_SETTINGS;
|
|
415
|
+
const writable = snap ? snap.writable === true : false;
|
|
416
|
+
const comfyUrlValue = String(typeof stored.comfyUrl === "string" ? stored.comfyUrl : DEFAULT_SETTINGS.comfyUrl);
|
|
417
|
+
const comfyUrlInvalid = comfyUrlValue.trim() !== "" && !normalizeComfyOrigin(comfyUrlValue);
|
|
418
|
+
const change = (field, next) => {
|
|
419
|
+
setSnap((prev) => (prev
|
|
420
|
+
? { ...prev, value: { ...(prev.value || DEFAULT_SETTINGS), [field]: next } }
|
|
421
|
+
: prev));
|
|
422
|
+
if (scope && writable) scope.set(field, next);
|
|
423
|
+
};
|
|
424
|
+
const reset = () => {
|
|
425
|
+
if (!scope || !writable) return;
|
|
426
|
+
setSnap((prev) => (prev ? { ...prev, value: DEFAULT_SETTINGS } : prev));
|
|
427
|
+
scope.unset("autoRender");
|
|
428
|
+
scope.unset("displayCap");
|
|
429
|
+
scope.unset("imageMaxPx");
|
|
430
|
+
scope.unset("comfyUrl");
|
|
431
|
+
};
|
|
432
|
+
const field = (label, hint, node) => h("label", {
|
|
433
|
+
style: { display: "grid", gap: 4, margin: "14px 0 0" },
|
|
434
|
+
},
|
|
435
|
+
h("span", {}, label),
|
|
436
|
+
typeof hint === "string" && h("span", {
|
|
437
|
+
style: { color: "var(--dsw-alias-label-tertiary)", fontSize: 12 },
|
|
438
|
+
}, hint),
|
|
439
|
+
node);
|
|
440
|
+
const inputStyle = {
|
|
441
|
+
background: "var(--dsw-alias-bg-layer-2)",
|
|
442
|
+
border: "1px solid var(--dsw-alias-border-l3)",
|
|
443
|
+
borderRadius: 8,
|
|
444
|
+
color: "inherit",
|
|
445
|
+
font: "inherit",
|
|
446
|
+
maxWidth: 280,
|
|
447
|
+
padding: "6px 10px",
|
|
448
|
+
};
|
|
449
|
+
const buttonStyle = {
|
|
450
|
+
background: "var(--dsw-alias-interactive-bg-hover)",
|
|
451
|
+
border: "1px solid var(--dsw-alias-border-l3)",
|
|
452
|
+
borderRadius: 8,
|
|
453
|
+
color: "inherit",
|
|
454
|
+
cursor: "pointer",
|
|
455
|
+
font: "inherit",
|
|
456
|
+
padding: "6px 14px",
|
|
457
|
+
};
|
|
458
|
+
return h("section", { style: { width: "100%" } },
|
|
459
|
+
h("p", { style: { color: "var(--dsw-alias-label-secondary)", fontSize: 13, margin: "0 0 4px" } }, t("intro")),
|
|
460
|
+
field(
|
|
461
|
+
t("autoRender"),
|
|
462
|
+
t("autoRenderHint"),
|
|
463
|
+
h("input", {
|
|
464
|
+
type: "checkbox",
|
|
465
|
+
checked: stored.autoRender !== false,
|
|
466
|
+
disabled: !writable,
|
|
467
|
+
onChange: (event) => change("autoRender", event.target.checked),
|
|
468
|
+
style: { justifySelf: "start" },
|
|
469
|
+
}),
|
|
470
|
+
),
|
|
471
|
+
field(
|
|
472
|
+
t("displayCap"),
|
|
473
|
+
t("displayCapHint"),
|
|
474
|
+
h("input", {
|
|
475
|
+
type: "number",
|
|
476
|
+
min: 1,
|
|
477
|
+
max: 30,
|
|
478
|
+
step: 1,
|
|
479
|
+
value: String(clampInt(stored.displayCap, 1, 30, DEFAULT_SETTINGS.displayCap)),
|
|
480
|
+
disabled: !writable,
|
|
481
|
+
onChange: (event) => change("displayCap", clampInt(event.target.value, 1, 30, DEFAULT_SETTINGS.displayCap)),
|
|
482
|
+
style: inputStyle,
|
|
483
|
+
}),
|
|
484
|
+
),
|
|
485
|
+
field(
|
|
486
|
+
t("imageMaxPx"),
|
|
487
|
+
t("imageMaxPxHint"),
|
|
488
|
+
h("input", {
|
|
489
|
+
type: "number",
|
|
490
|
+
min: 160,
|
|
491
|
+
max: 1200,
|
|
492
|
+
step: 1,
|
|
493
|
+
value: String(clampInt(stored.imageMaxPx, 160, 1200, DEFAULT_SETTINGS.imageMaxPx)),
|
|
494
|
+
disabled: !writable,
|
|
495
|
+
onChange: (event) => change("imageMaxPx", clampInt(event.target.value, 160, 1200, DEFAULT_SETTINGS.imageMaxPx)),
|
|
496
|
+
style: inputStyle,
|
|
497
|
+
}),
|
|
498
|
+
),
|
|
499
|
+
field(
|
|
500
|
+
t("comfyUrl"),
|
|
501
|
+
t("comfyUrlHint"),
|
|
502
|
+
h(React.Fragment, null,
|
|
503
|
+
h("input", {
|
|
504
|
+
type: "text",
|
|
505
|
+
value: comfyUrlValue,
|
|
506
|
+
disabled: !writable,
|
|
507
|
+
maxLength: 512,
|
|
508
|
+
spellCheck: false,
|
|
509
|
+
autoComplete: "off",
|
|
510
|
+
placeholder: "http://host:8188",
|
|
511
|
+
onChange: (event) => change("comfyUrl", String(event.target.value)),
|
|
512
|
+
style: inputStyle,
|
|
513
|
+
}),
|
|
514
|
+
comfyUrlInvalid && h("span", {
|
|
515
|
+
style: { color: "var(--dsw-alias-state-error-primary)", fontSize: 12 },
|
|
516
|
+
}, t("comfyUrlError")),
|
|
517
|
+
),
|
|
518
|
+
),
|
|
519
|
+
h("div", { style: { alignItems: "center", display: "flex", gap: 12, marginTop: 18 } },
|
|
520
|
+
h("button", { type: "button", onClick: reset, disabled: !writable, style: buttonStyle }, t("reset")),
|
|
521
|
+
!writable && h("span", { style: { color: "var(--dsw-alias-label-tertiary)", fontSize: 12 } }, t("writable")),
|
|
522
|
+
));
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const name = "dsh-inline-media-viewer";
|
|
526
|
+
const inject = ["slots", "conversationEvents", "connection", "settingsScope", "locale"];
|
|
527
|
+
|
|
528
|
+
function apply(ctx) {
|
|
529
|
+
const connection = ctx.get("connection");
|
|
530
|
+
ctx.effect(() => ctx.locale.register(SETTINGS_NS, SETTINGS_DICT), "inline-media: dictionaries");
|
|
531
|
+
const t = ctx.locale.bind(SETTINGS_NS);
|
|
532
|
+
const scope = ctx.settingsScope.bind({ namespace: SETTINGS_NAMESPACE });
|
|
533
|
+
settingsScope = scope;
|
|
534
|
+
ctx.conversationEvents.register(mediaDefinition);
|
|
535
|
+
ctx.slots.inject("conversation.chat.turnTail", () => ctx.slots.register({
|
|
536
|
+
name: "conversation.chat.turnTail",
|
|
537
|
+
select: selectMedia,
|
|
538
|
+
inject: () => ({ connection }),
|
|
539
|
+
}, MediaTail));
|
|
540
|
+
ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
541
|
+
name: "settings.section",
|
|
542
|
+
id: "inline-media",
|
|
543
|
+
order: 25,
|
|
544
|
+
label: () => t("nav"),
|
|
545
|
+
inject: () => ({ scope, t }),
|
|
546
|
+
}, MediaSettingsSection));
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
exports.apply = apply;
|
|
550
|
+
exports.inject = inject;
|
|
551
|
+
exports.name = name;
|
|
552
|
+
return module.exports;
|
|
553
|
+
},
|
|
554
|
+
});
|
package/cordis.patch.yml
ADDED