@ai-setting/roy-plugin-task-show 2.4.2 → 2.5.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/dist/chat-output-parser.d.ts +26 -0
- package/dist/chat-output-parser.d.ts.map +1 -0
- package/dist/chat-output-parser.js +36 -0
- package/dist/chat-output-parser.js.map +1 -0
- package/dist/chat-panel-html.d.ts +76 -0
- package/dist/chat-panel-html.d.ts.map +1 -0
- package/dist/chat-panel-html.js +117 -0
- package/dist/chat-panel-html.js.map +1 -0
- package/dist/chat-process-manager.d.ts +93 -0
- package/dist/chat-process-manager.d.ts.map +1 -0
- package/dist/chat-process-manager.js +197 -0
- package/dist/chat-process-manager.js.map +1 -0
- package/dist/server.d.ts +55 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +408 -4
- package/dist/server.js.map +1 -1
- package/dist/task-chat-context-builder.d.ts +94 -0
- package/dist/task-chat-context-builder.d.ts.map +1 -0
- package/dist/task-chat-context-builder.js +130 -0
- package/dist/task-chat-context-builder.js.map +1 -0
- package/dist/task-session-store.d.ts.map +1 -1
- package/dist/task-session-store.js +5 -0
- package/dist/task-session-store.js.map +1 -1
- package/package.json +4 -2
- package/plugin.json +1 -1
- package/public/chat-panel.js +565 -0
- package/public/dompurify.min.js +3 -0
- package/public/index.html +11 -0
- package/public/markdown-renderer.js +176 -0
- package/public/marked.min.js +6 -0
- package/public/style.css +316 -0
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Chat panel — client controller.
|
|
3
|
+
*
|
|
4
|
+
* v2.5.0+ REQ-2 (home) + REQ-3 (task-aware) + REQ-4 (markdown).
|
|
5
|
+
*
|
|
6
|
+
* The module exposes a single `attachChatPanel(root, opts)` factory so
|
|
7
|
+
* the same JS file can drive both the home chat panel and the
|
|
8
|
+
* task-detail chat panel. The legacy `init()` entry-point is preserved
|
|
9
|
+
* for backward compat with any page that wires the home chat via
|
|
10
|
+
* `<script src="chat-panel.js">` alone (no opts needed).
|
|
11
|
+
*
|
|
12
|
+
* DOM contract (pinned by `test/home-chat-v25.test.ts` and
|
|
13
|
+
* `test/task-chat-v25.test.ts`):
|
|
14
|
+
*
|
|
15
|
+
* [data-chat-shell] <section> wrapper
|
|
16
|
+
* [data-chat-scope] "home" | "task" (read by factory)
|
|
17
|
+
* [data-task-id] task id (when scope=task) (read by factory)
|
|
18
|
+
* [data-chat-messages] <div> message log (role="log")
|
|
19
|
+
* [data-chat-form] <form> submit handler
|
|
20
|
+
* [data-chat-input] <input> text input
|
|
21
|
+
* [data-chat-send] <button> send button (type="submit")
|
|
22
|
+
*
|
|
23
|
+
* sessionId is persisted in localStorage under a scope-aware key:
|
|
24
|
+
* - scope=home: `taskShow.chat.sessionId` (one-off uuid)
|
|
25
|
+
* - scope=task: `taskShow.chat.task.${taskId}.sid` (long-lived, per task)
|
|
26
|
+
* The task-detail key intentionally differs from the home key so the
|
|
27
|
+
* two scopes don't clobber each other.
|
|
28
|
+
*
|
|
29
|
+
* The server speaks SSE for streaming chunks. We use `fetch` + an
|
|
30
|
+
* `ReadableStream` reader instead of `EventSource` because the chat
|
|
31
|
+
* endpoint is POST (EventSource can't POST) and we need to send the
|
|
32
|
+
* `message` in the request body.
|
|
33
|
+
*
|
|
34
|
+
* If the chat feature is disabled server-side (503), the panel hides
|
|
35
|
+
* itself with a banner instead of throwing.
|
|
36
|
+
*
|
|
37
|
+
* v2.5.x REQ-4: assistant messages are rendered through
|
|
38
|
+
* `markdownRenderer.renderMarkdown()` (vendored `marked` +
|
|
39
|
+
* `DOMPurify`). User messages deliberately stay `textContent` —
|
|
40
|
+
* users must not be able to inject markdown / HTML into their own
|
|
41
|
+
* bubbles.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Module dual-mode
|
|
46
|
+
//
|
|
47
|
+
// - In the browser this file is loaded as a classic <script>. The
|
|
48
|
+
// trailing bootstrap calls `init()` and the panel becomes live.
|
|
49
|
+
// - In tests (bun:test) it is imported as an ES module; the bootstrap
|
|
50
|
+
// branch is skipped (no `document`/`window` globals), and the named
|
|
51
|
+
// exports below are what the harness drives.
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
const HOME_STORAGE_KEY = "taskShow.chat.sessionId";
|
|
55
|
+
const SHELL_SEL = "[data-chat-shell]";
|
|
56
|
+
const MESSAGES_SEL = "[data-chat-messages]";
|
|
57
|
+
const FORM_SEL = "[data-chat-form]";
|
|
58
|
+
const INPUT_SEL = "[data-chat-input]";
|
|
59
|
+
const SEND_SEL = "[data-chat-send]";
|
|
60
|
+
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// Portable DOM access. happy-dom tests give us a Window via
|
|
63
|
+
// `container.ownerDocument`; the browser gives us a global `document`.
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
function getDoc(container) {
|
|
67
|
+
if (container && container.ownerDocument) return container.ownerDocument;
|
|
68
|
+
if (typeof document !== "undefined") return document;
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function getWindow(doc) {
|
|
73
|
+
if (doc && doc.defaultView) return doc.defaultView;
|
|
74
|
+
if (typeof window !== "undefined") return window;
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function $(container, sel) {
|
|
79
|
+
const doc = getDoc(container);
|
|
80
|
+
if (doc) return doc.querySelector(sel);
|
|
81
|
+
// Fallback — should never happen for the production code path.
|
|
82
|
+
return typeof document !== "undefined" ? document.querySelector(sel) : null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
// Markdown renderer resolution. In production the browser script tag
|
|
87
|
+
// order is:
|
|
88
|
+
// marked.min.js → dompurify.min.js → markdown-renderer.js → chat-panel.js
|
|
89
|
+
// so by the time chat-panel.js runs, `window.markdownRenderer` is set.
|
|
90
|
+
//
|
|
91
|
+
// If a test harness imported chat-panel.js without loading
|
|
92
|
+
// markdown-renderer.js first, we lazily require it (Node-only path).
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
function resolveRenderer() {
|
|
96
|
+
if (typeof window !== "undefined" && window.markdownRenderer) {
|
|
97
|
+
return window.markdownRenderer;
|
|
98
|
+
}
|
|
99
|
+
if (typeof globalThis !== "undefined" && globalThis.markdownRenderer) {
|
|
100
|
+
return globalThis.markdownRenderer;
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
104
|
+
const mod = require("./markdown-renderer.js");
|
|
105
|
+
return { renderMarkdown: mod.renderMarkdown, attachStreamingRenderer: mod.attachStreamingRenderer };
|
|
106
|
+
} catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
// Scope-aware storage key helpers (REQ-3). The home key is the legacy
|
|
113
|
+
// single global; the task key is per-task-id so different tasks don't
|
|
114
|
+
// clobber each other across reloads.
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
function storageKey(scope, taskId) {
|
|
118
|
+
if (scope === "task" && Number.isInteger(taskId)) {
|
|
119
|
+
return `taskShow.chat.task.${taskId}.sid`;
|
|
120
|
+
}
|
|
121
|
+
return HOME_STORAGE_KEY;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function readScopedSessionId(scope, taskId) {
|
|
125
|
+
try {
|
|
126
|
+
const w = typeof window !== "undefined" ? window : null;
|
|
127
|
+
if (!w || !w.localStorage) return "";
|
|
128
|
+
return w.localStorage.getItem(storageKey(scope, taskId)) || "";
|
|
129
|
+
} catch {
|
|
130
|
+
return "";
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function writeScopedSessionId(scope, taskId, id) {
|
|
135
|
+
try {
|
|
136
|
+
const w = typeof window !== "undefined" ? window : null;
|
|
137
|
+
if (!w || !w.localStorage) return;
|
|
138
|
+
w.localStorage.setItem(storageKey(scope, taskId), id);
|
|
139
|
+
} catch {
|
|
140
|
+
/* ignore */
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
// Persistence helpers (legacy single-key API; REQ-2 home path uses
|
|
146
|
+
// these directly). REQ-3 task scope goes through readScopedSessionId /
|
|
147
|
+
// writeScopedSessionId above. Guarded try/catch so headless test envs
|
|
148
|
+
// without localStorage don't blow up.
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
export function readSessionId(win) {
|
|
152
|
+
try {
|
|
153
|
+
const w = win || (typeof window !== "undefined" ? window : null);
|
|
154
|
+
if (!w || !w.localStorage) return "";
|
|
155
|
+
return w.localStorage.getItem(HOME_STORAGE_KEY) || "";
|
|
156
|
+
} catch {
|
|
157
|
+
return "";
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function writeSessionId(id, win) {
|
|
162
|
+
try {
|
|
163
|
+
const w = win || (typeof window !== "undefined" ? window : null);
|
|
164
|
+
if (!w || !w.localStorage) return;
|
|
165
|
+
w.localStorage.setItem(HOME_STORAGE_KEY, id);
|
|
166
|
+
} catch {
|
|
167
|
+
/* ignore */
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
// DOM mutation helpers
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
|
|
175
|
+
export function clearMessages(container) {
|
|
176
|
+
while (container.firstChild) container.removeChild(container.firstChild);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Append a message bubble to the chat log.
|
|
181
|
+
*
|
|
182
|
+
* - `role === "assistant"` → text is rendered via
|
|
183
|
+
* `markdownRenderer.renderMarkdown()` (marked + DOMPurify).
|
|
184
|
+
* - `role === "user"` → text stays `textContent`. Users must not
|
|
185
|
+
* be able to inject HTML / markdown into their own bubble.
|
|
186
|
+
* - `role === "error"` → also `textContent` — error text should
|
|
187
|
+
* be readable as-is, no formatting.
|
|
188
|
+
*
|
|
189
|
+
* Returns the created element so the test harness can inspect it.
|
|
190
|
+
*/
|
|
191
|
+
export function appendMessage(container, role, text) {
|
|
192
|
+
const doc = getDoc(container);
|
|
193
|
+
if (!doc) throw new Error("chat-panel.appendMessage: no document available");
|
|
194
|
+
const empty = container.querySelector(".chat-empty");
|
|
195
|
+
if (empty) empty.remove();
|
|
196
|
+
const el = doc.createElement("div");
|
|
197
|
+
el.className = "chat-message chat-message--" + role;
|
|
198
|
+
el.dataset.role = role;
|
|
199
|
+
const label = role === "user" ? "You" : role === "assistant" ? "Assistant" : "Error";
|
|
200
|
+
el.innerHTML =
|
|
201
|
+
`<div class="chat-message-meta">${label}</div>` +
|
|
202
|
+
`<div class="chat-message-text"></div>`;
|
|
203
|
+
const textEl = el.querySelector(".chat-message-text");
|
|
204
|
+
if (role === "assistant") {
|
|
205
|
+
const renderer = resolveRenderer();
|
|
206
|
+
if (renderer) {
|
|
207
|
+
textEl.innerHTML = renderer.renderMarkdown(text);
|
|
208
|
+
} else {
|
|
209
|
+
// Defensive fallback — should never hit in production.
|
|
210
|
+
textEl.textContent = text;
|
|
211
|
+
}
|
|
212
|
+
} else {
|
|
213
|
+
// user / error — textContent literal.
|
|
214
|
+
textEl.textContent = text;
|
|
215
|
+
}
|
|
216
|
+
container.appendChild(el);
|
|
217
|
+
if (container.scrollHeight != null) {
|
|
218
|
+
container.scrollTop = container.scrollHeight;
|
|
219
|
+
}
|
|
220
|
+
return el;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Streaming chunk → debounced markdown render. The first chunk of a
|
|
225
|
+
* turn creates the assistant bubble lazily; subsequent chunks feed
|
|
226
|
+
* the bubble's streaming renderer.
|
|
227
|
+
*
|
|
228
|
+
* Returns the controller (with `flush` / `dispose`) so the caller
|
|
229
|
+
* can force a final render on `event: done`.
|
|
230
|
+
*/
|
|
231
|
+
export function appendChunk(container, text) {
|
|
232
|
+
const win = getWindow(getDoc(container));
|
|
233
|
+
let last = container.querySelector(".chat-message--assistant:last-of-type");
|
|
234
|
+
if (!last) last = appendMessage(container, "assistant", "");
|
|
235
|
+
const textEl = last.querySelector(".chat-message-text");
|
|
236
|
+
|
|
237
|
+
// Cache the streaming controller on the element so successive
|
|
238
|
+
// chunks reuse the same buffer + timer.
|
|
239
|
+
let ctrl = last.__markdownStreamCtrl;
|
|
240
|
+
const renderer = resolveRenderer();
|
|
241
|
+
if (!ctrl && renderer) {
|
|
242
|
+
ctrl = renderer.attachStreamingRenderer(textEl, { debounceMs: 50 });
|
|
243
|
+
last.__markdownStreamCtrl = ctrl;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (ctrl) {
|
|
247
|
+
ctrl.append(text);
|
|
248
|
+
} else {
|
|
249
|
+
// No renderer available — fall back to textContent (textContent
|
|
250
|
+
// of a chunk is the safest path when DOMPurify isn't loaded).
|
|
251
|
+
textEl.textContent += text;
|
|
252
|
+
}
|
|
253
|
+
if (win && container.scrollHeight != null) {
|
|
254
|
+
container.scrollTop = container.scrollHeight;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ---------------------------------------------------------------------------
|
|
259
|
+
// Form / network helpers (kept outside any IIFE so they can be reached
|
|
260
|
+
// in the browser; tests don't exercise these).
|
|
261
|
+
// ---------------------------------------------------------------------------
|
|
262
|
+
|
|
263
|
+
function setBusy(form, busy) {
|
|
264
|
+
const input = form.querySelector(INPUT_SEL);
|
|
265
|
+
const send = form.querySelector(SEND_SEL);
|
|
266
|
+
if (input) input.disabled = busy;
|
|
267
|
+
if (send) {
|
|
268
|
+
send.disabled = busy;
|
|
269
|
+
send.textContent = busy ? "Sending…" : "Send";
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function showDisabledBanner(shell, msg) {
|
|
274
|
+
const doc = getDoc(shell);
|
|
275
|
+
if (!doc) return;
|
|
276
|
+
const banner = doc.createElement("p");
|
|
277
|
+
banner.className = "chat-banner";
|
|
278
|
+
banner.dataset.role = "banner";
|
|
279
|
+
banner.textContent = msg;
|
|
280
|
+
shell.appendChild(banner);
|
|
281
|
+
const form = shell.querySelector(FORM_SEL);
|
|
282
|
+
if (form) form.remove();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Resolve a fresh session id for the current scope.
|
|
287
|
+
* - scope=home: POST /api/chat/start → { sessionId }
|
|
288
|
+
* - scope=task: deterministic `task-${taskId}`; we don't need to
|
|
289
|
+
* round-trip the server because the task-detail endpoint keys
|
|
290
|
+
* off the taskId directly. This guarantees multi-turn
|
|
291
|
+
* conversations survive reloads even when the CLI never saw a
|
|
292
|
+
* /start call.
|
|
293
|
+
*/
|
|
294
|
+
async function ensureSessionId(scope, taskId) {
|
|
295
|
+
if (scope === "task" && Number.isInteger(taskId)) {
|
|
296
|
+
return `task-${taskId}`;
|
|
297
|
+
}
|
|
298
|
+
const cached = readScopedSessionId(scope, taskId);
|
|
299
|
+
if (cached) return cached;
|
|
300
|
+
const res = await fetch("/api/chat/start", {
|
|
301
|
+
method: "POST",
|
|
302
|
+
headers: { "Content-Type": "application/json" },
|
|
303
|
+
body: "{}",
|
|
304
|
+
});
|
|
305
|
+
if (!res.ok) throw new Error("chat_start_failed: " + res.status);
|
|
306
|
+
const j = await res.json();
|
|
307
|
+
writeScopedSessionId(scope, taskId, j.sessionId);
|
|
308
|
+
return j.sessionId;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Compute the message-endpoint URL for the current scope.
|
|
313
|
+
* - scope=home: /api/chat/<sessionId>/message
|
|
314
|
+
* - scope=task: /api/task-chat/<taskId>/message (server derives
|
|
315
|
+
* sessionId from taskId, so we never need it on the client side)
|
|
316
|
+
*
|
|
317
|
+
* If `opts.endpoint` is supplied, it's used as the base prefix
|
|
318
|
+
* (mostly for tests / proxies); the resource sub-path is still
|
|
319
|
+
* computed from scope + (taskId|sessionId).
|
|
320
|
+
*/
|
|
321
|
+
function endpointFor(opts, sessionId) {
|
|
322
|
+
const base = (opts && opts.endpoint) || "";
|
|
323
|
+
const trimBase = base.replace(/\/+$/, "");
|
|
324
|
+
if (opts.scope === "task" && Number.isInteger(opts.taskId)) {
|
|
325
|
+
const url = `/api/task-chat/${opts.taskId}/message`;
|
|
326
|
+
return trimBase ? `${trimBase}${url}` : url;
|
|
327
|
+
}
|
|
328
|
+
const url = `/api/chat/${encodeURIComponent(sessionId)}/message`;
|
|
329
|
+
return trimBase ? `${trimBase}${url}` : url;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Public hook for the test harness — only used by the browser init
|
|
334
|
+
* flow in tests that simulate a full submit. Kept as a named export
|
|
335
|
+
* so home-chat-v25's fake ACT test can call into the same code path.
|
|
336
|
+
*
|
|
337
|
+
* Legacy signature: sendMessage(form, container, message) — used by
|
|
338
|
+
* `init()` for the home scope. REQ-3 goes through attachChatPanel()
|
|
339
|
+
* instead, which calls into streamSession() below.
|
|
340
|
+
*/
|
|
341
|
+
export async function sendMessage(form, container, message) {
|
|
342
|
+
return streamSession({ scope: "home", endpoint: "" }, form, container, message);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Stream one user message to the server, render chunks back into the
|
|
347
|
+
* container. Shared by both legacy `sendMessage` (home) and the
|
|
348
|
+
* REQ-3 `attachChatPanel` (home + task).
|
|
349
|
+
*/
|
|
350
|
+
async function streamSession(opts, form, container, message) {
|
|
351
|
+
let sessionId;
|
|
352
|
+
try {
|
|
353
|
+
sessionId = await ensureSessionId(opts.scope, opts.taskId);
|
|
354
|
+
} catch (e) {
|
|
355
|
+
appendMessage(container, "error", String(e && e.message || e));
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
appendMessage(container, "user", message);
|
|
359
|
+
setBusy(form, true);
|
|
360
|
+
|
|
361
|
+
const url = endpointFor(opts, sessionId);
|
|
362
|
+
let res;
|
|
363
|
+
try {
|
|
364
|
+
res = await fetch(url, {
|
|
365
|
+
method: "POST",
|
|
366
|
+
headers: {
|
|
367
|
+
"Content-Type": "application/json",
|
|
368
|
+
Accept: "text/event-stream",
|
|
369
|
+
},
|
|
370
|
+
body: JSON.stringify({ message }),
|
|
371
|
+
});
|
|
372
|
+
} catch (e) {
|
|
373
|
+
appendMessage(container, "error", "network_error: " + (e && e.message || e));
|
|
374
|
+
setBusy(form, false);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (!res.ok || !res.body) {
|
|
379
|
+
let detail = "";
|
|
380
|
+
try { detail = await res.text(); } catch { /* ignore */ }
|
|
381
|
+
appendMessage(container, "error", `server_error: ${res.status} ${detail.slice(0, 200)}`);
|
|
382
|
+
setBusy(form, false);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const reader = res.body.getReader();
|
|
387
|
+
const decoder = new TextDecoder("utf-8");
|
|
388
|
+
let buf = "";
|
|
389
|
+
let sawError = false;
|
|
390
|
+
|
|
391
|
+
try {
|
|
392
|
+
while (true) {
|
|
393
|
+
const { value, done } = await reader.read();
|
|
394
|
+
if (done) break;
|
|
395
|
+
buf += decoder.decode(value, { stream: true });
|
|
396
|
+
let nl;
|
|
397
|
+
while ((nl = buf.indexOf("\n\n")) >= 0) {
|
|
398
|
+
const frame = buf.slice(0, nl);
|
|
399
|
+
buf = buf.slice(nl + 2);
|
|
400
|
+
handleFrame(frame, container, (err) => { sawError = sawError || !!err; });
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
// Flush trailing frame if any.
|
|
404
|
+
if (buf.trim().length > 0) handleFrame(buf, container, () => {});
|
|
405
|
+
} catch (e) {
|
|
406
|
+
appendMessage(container, "error", "stream_error: " + (e && e.message || e));
|
|
407
|
+
} finally {
|
|
408
|
+
setBusy(form, false);
|
|
409
|
+
// Final markdown flush so the last debounced chunk renders before
|
|
410
|
+
// the busy spinner goes away.
|
|
411
|
+
const last = container.querySelector(".chat-message--assistant:last-of-type");
|
|
412
|
+
if (last && last.__markdownStreamCtrl && last.__markdownStreamCtrl.flush) {
|
|
413
|
+
last.__markdownStreamCtrl.flush();
|
|
414
|
+
}
|
|
415
|
+
// Suppress the final empty-message that may have been left if the
|
|
416
|
+
// assistant produced no text. We intentionally leave error messages
|
|
417
|
+
// alone so the user can see what went wrong.
|
|
418
|
+
if (!sawError) {
|
|
419
|
+
const last2 = container.querySelector(".chat-message--assistant:last-of-type");
|
|
420
|
+
if (last2 && !last2.querySelector(".chat-message-text").textContent.trim()) {
|
|
421
|
+
last2.remove();
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/** Parse one SSE frame (event:/data:/id: lines). Returns the parsed frame or null. */
|
|
428
|
+
function handleFrame(raw, container, onError) {
|
|
429
|
+
let event = "";
|
|
430
|
+
let data = "";
|
|
431
|
+
let id = "";
|
|
432
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
433
|
+
if (line.startsWith(":")) continue;
|
|
434
|
+
if (line.startsWith("event:")) event = line.slice(6).trim();
|
|
435
|
+
else if (line.startsWith("data:")) data += (data ? "\n" : "") + line.slice(5).trim();
|
|
436
|
+
else if (line.startsWith("id:")) id = line.slice(3).trim();
|
|
437
|
+
}
|
|
438
|
+
if (!event && !data) return null;
|
|
439
|
+
let parsed = null;
|
|
440
|
+
if (data) {
|
|
441
|
+
try { parsed = JSON.parse(data); } catch { parsed = data; }
|
|
442
|
+
}
|
|
443
|
+
if (event === "chunk" && parsed && typeof parsed === "object") {
|
|
444
|
+
if (parsed.type === "text" && typeof parsed.text === "string") {
|
|
445
|
+
appendChunk(container, parsed.text);
|
|
446
|
+
} else if (parsed.type === "reasoning" && typeof parsed.text === "string") {
|
|
447
|
+
// Reasoning is hidden in the chat UI by design; we still log it
|
|
448
|
+
// so a developer can inspect via DevTools.
|
|
449
|
+
if (typeof console !== "undefined" && console.debug) {
|
|
450
|
+
console.debug("[chat] reasoning:", parsed.text);
|
|
451
|
+
}
|
|
452
|
+
} else if (parsed.type === "start") {
|
|
453
|
+
// No-op: the assistant bubble is created lazily on first text chunk.
|
|
454
|
+
} else if (parsed.type === "error" && parsed.error) {
|
|
455
|
+
onError(parsed);
|
|
456
|
+
appendMessage(container, "error", parsed.error.message || "unknown error");
|
|
457
|
+
}
|
|
458
|
+
} else if (event === "done") {
|
|
459
|
+
// Terminal marker — final flush is handled by the caller's finally.
|
|
460
|
+
} else if (event === "error") {
|
|
461
|
+
onError(parsed);
|
|
462
|
+
const msg = parsed && parsed.error && parsed.error.message
|
|
463
|
+
? parsed.error.message
|
|
464
|
+
: "stream error";
|
|
465
|
+
appendMessage(container, "error", msg);
|
|
466
|
+
} else if (event === "ready") {
|
|
467
|
+
// SSE handshake — nothing to render.
|
|
468
|
+
}
|
|
469
|
+
return { event, data, id, parsed };
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Resolve the chat options from DOM data-attrs when the caller
|
|
474
|
+
* didn't pass them explicitly. Lets the server emit a single
|
|
475
|
+
* `<section data-chat-scope="task" data-task-id="4242">` and have
|
|
476
|
+
* the JS pick up the right endpoint / storage key automatically.
|
|
477
|
+
*/
|
|
478
|
+
function readOptsFromRoot(root, opts) {
|
|
479
|
+
const merged = Object.assign({}, opts || {});
|
|
480
|
+
if (!merged.scope) {
|
|
481
|
+
const scopeAttr = root.getAttribute && root.getAttribute("data-chat-scope");
|
|
482
|
+
merged.scope = scopeAttr === "task" ? "task" : "home";
|
|
483
|
+
}
|
|
484
|
+
if (merged.scope === "task" && !Number.isInteger(merged.taskId)) {
|
|
485
|
+
const idAttr = root.getAttribute && root.getAttribute("data-task-id");
|
|
486
|
+
const n = Number(idAttr);
|
|
487
|
+
if (Number.isInteger(n)) merged.taskId = n;
|
|
488
|
+
}
|
|
489
|
+
return merged;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Factory — wire a chat panel inside `root` using the supplied
|
|
494
|
+
* options. Returns the controller handle (for tests / programmatic
|
|
495
|
+
* sends). Backward-compatible with the legacy `init()` entry point
|
|
496
|
+
* which auto-discovers the home shell.
|
|
497
|
+
*
|
|
498
|
+
* @param {HTMLElement} root the [data-chat-shell] element
|
|
499
|
+
* @param {{
|
|
500
|
+
* endpoint?: string; // optional base prefix (e.g. "" or "https://proxy")
|
|
501
|
+
* scope?: "home"|"task"; // which API endpoint to use (default: from DOM data-chat-scope)
|
|
502
|
+
* taskId?: number; // required when scope === "task"
|
|
503
|
+
* }} [opts]
|
|
504
|
+
*/
|
|
505
|
+
export function attachChatPanel(root, opts) {
|
|
506
|
+
if (!root) return null;
|
|
507
|
+
const finalOpts = readOptsFromRoot(root, opts);
|
|
508
|
+
const form = root.querySelector(FORM_SEL);
|
|
509
|
+
const container = root.querySelector(MESSAGES_SEL);
|
|
510
|
+
const input = root.querySelector(INPUT_SEL);
|
|
511
|
+
if (!form || !container || !input) return null;
|
|
512
|
+
|
|
513
|
+
// Probe feature availability with a HEAD request. If 503, hide UI.
|
|
514
|
+
// For task scope we don't have a generic /api/chat/start — but
|
|
515
|
+
// the same server returns 503 on /api/task-chat/0/message when
|
|
516
|
+
// the chat is disabled (any numeric id triggers the chatManager
|
|
517
|
+
// check), so we use that as a feature probe.
|
|
518
|
+
const probeUrl = finalOpts.scope === "task"
|
|
519
|
+
? `/api/task-chat/0/message`
|
|
520
|
+
: "/api/chat/start";
|
|
521
|
+
fetch(probeUrl, { method: "HEAD" }).then((res) => {
|
|
522
|
+
if (res.status === 503) {
|
|
523
|
+
showDisabledBanner(root, "Chat disabled (server returned 503 — chatOptions not configured).");
|
|
524
|
+
}
|
|
525
|
+
}).catch(() => { /* offline — leave UI alone */ });
|
|
526
|
+
|
|
527
|
+
form.addEventListener("submit", (e) => {
|
|
528
|
+
e.preventDefault();
|
|
529
|
+
const message = (input.value || "").trim();
|
|
530
|
+
if (!message) return;
|
|
531
|
+
input.value = "";
|
|
532
|
+
streamSession(finalOpts, form, container, message);
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
return { root, opts: finalOpts, send: (msg) => streamSession(finalOpts, form, container, msg) };
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
export function init() {
|
|
539
|
+
const shell = $(null, SHELL_SEL);
|
|
540
|
+
if (!shell) return null;
|
|
541
|
+
return attachChatPanel(shell, {});
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// ---------------------------------------------------------------------------
|
|
545
|
+
// Browser-only bootstrap. Tests run this file under bun:test where
|
|
546
|
+
// `document`/`window` are not globals, so the if-branch is skipped and
|
|
547
|
+
// only the named exports are available.
|
|
548
|
+
// ---------------------------------------------------------------------------
|
|
549
|
+
|
|
550
|
+
if (typeof document !== "undefined" && typeof window !== "undefined") {
|
|
551
|
+
if (document.readyState === "loading") {
|
|
552
|
+
document.addEventListener("DOMContentLoaded", init);
|
|
553
|
+
} else {
|
|
554
|
+
init();
|
|
555
|
+
}
|
|
556
|
+
// Expose a tiny test hook so headless harness can drive the panel
|
|
557
|
+
// without firing real events.
|
|
558
|
+
window.taskShowChatPanel = {
|
|
559
|
+
init,
|
|
560
|
+
attachChatPanel,
|
|
561
|
+
sendMessage,
|
|
562
|
+
storageKey,
|
|
563
|
+
HOME_STORAGE_KEY,
|
|
564
|
+
};
|
|
565
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
/*! @license DOMPurify 3.4.12 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.4.12/LICENSE */
|
|
2
|
+
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).DOMPurify=t()}(this,function(){"use strict";function e(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,o=Array(t);n<t;n++)o[n]=e[n];return o}function t(t,n){return function(e){if(Array.isArray(e))return e}(t)||function(e,t){var n=null==e?null:"undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(null!=n){var o,r,i,a,l=[],c=!0,s=!1;try{if(i=(n=n.call(e)).next,0===t);else for(;!(c=(o=i.call(n)).done)&&(l.push(o.value),l.length!==t);c=!0);}catch(e){s=!0,r=e}finally{try{if(!c&&null!=n.return&&(a=n.return(),Object(a)!==a))return}finally{if(s)throw r}}return l}}(t,n)||function(t,n){if(t){if("string"==typeof t)return e(t,n);var o={}.toString.call(t).slice(8,-1);return"Object"===o&&t.constructor&&(o=t.constructor.name),"Map"===o||"Set"===o?Array.from(t):"Arguments"===o||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(o)?e(t,n):void 0}}(t,n)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}const n=Object.entries,o=Object.setPrototypeOf,r=Object.isFrozen,i=Object.getPrototypeOf,a=Object.getOwnPropertyDescriptor;let l=Object.freeze,c=Object.seal,s=Object.create,u="undefined"!=typeof Reflect&&Reflect,f=u.apply,p=u.construct;l||(l=function(e){return e}),c||(c=function(e){return e}),f||(f=function(e,t){for(var n=arguments.length,o=new Array(n>2?n-2:0),r=2;r<n;r++)o[r-2]=arguments[r];return e.apply(t,o)}),p||(p=function(e){for(var t=arguments.length,n=new Array(t>1?t-1:0),o=1;o<t;o++)n[o-1]=arguments[o];return new e(...n)});const m=L(Array.prototype.forEach),d=L(Array.prototype.lastIndexOf),h=L(Array.prototype.pop),g=L(Array.prototype.push),y=L(Array.prototype.splice),b=Array.isArray,T=L(String.prototype.toLowerCase),S=L(String.prototype.toString),E=L(String.prototype.match),A=L(String.prototype.replace),N=L(String.prototype.indexOf),_=L(String.prototype.trim),w=L(Number.prototype.toString),O=L(Boolean.prototype.toString),v="undefined"==typeof BigInt?null:L(BigInt.prototype.toString),D="undefined"==typeof Symbol?null:L(Symbol.prototype.toString),R=L(Object.prototype.hasOwnProperty),C=L(Object.prototype.toString),I=L(RegExp.prototype.test),x=(k=TypeError,function(){for(var e=arguments.length,t=new Array(e),n=0;n<e;n++)t[n]=arguments[n];return p(k,t)});var k;function L(e){return function(t){t instanceof RegExp&&(t.lastIndex=0);for(var n=arguments.length,o=new Array(n>1?n-1:0),r=1;r<n;r++)o[r-1]=arguments[r];return f(e,t,o)}}function M(e,t){let n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:T;if(o&&o(e,null),!b(t))return e;let i=t.length;for(;i--;){let o=t[i];if("string"==typeof o){const e=n(o);e!==o&&(r(t)||(t[i]=e),o=e)}e[o]=!0}return e}function z(e){for(let t=0;t<e.length;t++){R(e,t)||(e[t]=null)}return e}function P(e){const o=s(null);for(const i of n(e)){var r=t(i,2);const n=r[0],a=r[1];R(e,n)&&(b(a)?o[n]=z(a):a&&"object"==typeof a&&a.constructor===Object?o[n]=P(a):o[n]=a)}return o}function U(e,t){for(;null!==e;){const n=a(e,t);if(n){if(n.get)return L(n.get);if("function"==typeof n.value)return L(n.value)}e=i(e)}return function(){return null}}const F=l(["a","abbr","acronym","address","area","article","aside","audio","b","bdi","bdo","big","blink","blockquote","body","br","button","canvas","caption","center","cite","code","col","colgroup","content","data","datalist","dd","decorator","del","details","dfn","dialog","dir","div","dl","dt","element","em","fieldset","figcaption","figure","font","footer","form","h1","h2","h3","h4","h5","h6","head","header","hgroup","hr","html","i","img","input","ins","kbd","label","legend","li","main","map","mark","marquee","menu","menuitem","meter","nav","nobr","ol","optgroup","option","output","p","picture","pre","progress","q","rp","rt","ruby","s","samp","search","section","select","shadow","slot","small","source","spacer","span","strike","strong","style","sub","summary","sup","table","tbody","td","template","textarea","tfoot","th","thead","time","tr","track","tt","u","ul","var","video","wbr"]),H=l(["svg","a","altglyph","altglyphdef","altglyphitem","animatecolor","animatemotion","animatetransform","circle","clippath","defs","desc","ellipse","enterkeyhint","exportparts","filter","font","g","glyph","glyphref","hkern","image","inputmode","line","lineargradient","marker","mask","metadata","mpath","part","path","pattern","polygon","polyline","radialgradient","rect","stop","style","switch","symbol","text","textpath","title","tref","tspan","view","vkern"]),j=l(["feBlend","feColorMatrix","feComponentTransfer","feComposite","feConvolveMatrix","feDiffuseLighting","feDisplacementMap","feDistantLight","feDropShadow","feFlood","feFuncA","feFuncB","feFuncG","feFuncR","feGaussianBlur","feImage","feMerge","feMergeNode","feMorphology","feOffset","fePointLight","feSpecularLighting","feSpotLight","feTile","feTurbulence"]),B=l(["animate","color-profile","cursor","discard","font-face","font-face-format","font-face-name","font-face-src","font-face-uri","foreignobject","hatch","hatchpath","mesh","meshgradient","meshpatch","meshrow","missing-glyph","script","set","solidcolor","unknown","use"]),G=l(["math","menclose","merror","mfenced","mfrac","mglyph","mi","mlabeledtr","mmultiscripts","mn","mo","mover","mpadded","mphantom","mroot","mrow","ms","mspace","msqrt","mstyle","msub","msup","msubsup","mtable","mtd","mtext","mtr","munder","munderover","mprescripts"]),W=l(["maction","maligngroup","malignmark","mlongdiv","mscarries","mscarry","msgroup","mstack","msline","msrow","semantics","annotation","annotation-xml","mprescripts","none"]),Y=l(["#text"]),q=l(["accept","action","align","alt","autocapitalize","autocomplete","autopictureinpicture","autoplay","background","bgcolor","border","capture","cellpadding","cellspacing","checked","cite","class","clear","color","cols","colspan","command","commandfor","controls","controlslist","coords","crossorigin","datetime","decoding","default","dir","disabled","disablepictureinpicture","disableremoteplayback","download","draggable","enctype","enterkeyhint","exportparts","face","for","headers","height","hidden","high","href","hreflang","id","inert","inputmode","integrity","ismap","kind","label","lang","list","loading","loop","low","max","maxlength","media","method","min","minlength","multiple","muted","name","nonce","noshade","novalidate","nowrap","open","optimum","part","pattern","placeholder","playsinline","popover","popovertarget","popovertargetaction","poster","preload","pubdate","radiogroup","readonly","rel","required","rev","reversed","role","rows","rowspan","spellcheck","scope","selected","shape","size","sizes","slot","span","srclang","start","src","srcset","step","style","summary","tabindex","title","translate","type","usemap","valign","value","width","wrap","xmlns"]),X=l(["accent-height","accumulate","additive","alignment-baseline","amplitude","ascent","attributename","attributetype","azimuth","basefrequency","baseline-shift","begin","bias","by","class","clip","clippathunits","clip-path","clip-rule","color","color-interpolation","color-interpolation-filters","color-profile","color-rendering","cx","cy","d","dx","dy","diffuseconstant","direction","display","divisor","dominant-baseline","dur","edgemode","elevation","end","exponent","fill","fill-opacity","fill-rule","filter","filterunits","flood-color","flood-opacity","font-family","font-size","font-size-adjust","font-stretch","font-style","font-variant","font-weight","fx","fy","g1","g2","glyph-name","glyphref","gradientunits","gradienttransform","height","href","id","image-rendering","in","in2","intercept","k","k1","k2","k3","k4","kerning","keypoints","keysplines","keytimes","lang","lengthadjust","letter-spacing","kernelmatrix","kernelunitlength","lighting-color","local","marker-end","marker-mid","marker-start","markerheight","markerunits","markerwidth","maskcontentunits","maskunits","max","mask","mask-type","media","method","mode","min","name","numoctaves","offset","operator","opacity","order","orient","orientation","origin","overflow","paint-order","path","pathlength","patterncontentunits","patterntransform","patternunits","points","preservealpha","preserveaspectratio","primitiveunits","r","rx","ry","radius","refx","refy","repeatcount","repeatdur","restart","result","rotate","scale","seed","shape-rendering","slope","specularconstant","specularexponent","spreadmethod","startoffset","stddeviation","stitchtiles","stop-color","stop-opacity","stroke-dasharray","stroke-dashoffset","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke","stroke-width","style","surfacescale","systemlanguage","tabindex","tablevalues","targetx","targety","transform","transform-origin","text-anchor","text-decoration","text-orientation","text-rendering","textlength","type","u1","u2","unicode","values","viewbox","visibility","version","vert-adv-y","vert-origin-x","vert-origin-y","width","word-spacing","wrap","writing-mode","xchannelselector","ychannelselector","x","x1","x2","xmlns","y","y1","y2","z","zoomandpan"]),$=l(["accent","accentunder","align","bevelled","close","columnalign","columnlines","columnspacing","columnspan","denomalign","depth","dir","display","displaystyle","encoding","fence","frame","height","href","id","largeop","length","linethickness","lquote","lspace","mathbackground","mathcolor","mathsize","mathvariant","maxsize","minsize","movablelimits","notation","numalign","open","rowalign","rowlines","rowspacing","rowspan","rspace","rquote","scriptlevel","scriptminsize","scriptsizemultiplier","selection","separator","separators","stretchy","subscriptshift","supscriptshift","symmetric","voffset","width","xmlns"]),K=l(["xlink:href","xml:id","xlink:title","xml:space","xmlns:xlink"]),V=c(/{{[\w\W]*|^[\w\W]*}}/g),Z=c(/<%[\w\W]*|^[\w\W]*%>/g),J=c(/\${[\w\W]*/g),Q=c(/^data-[\-\w.\u00B7-\uFFFF]+$/),ee=c(/^aria-[\-\w]+$/),te=c(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),ne=c(/^(?:\w+script|data):/i),oe=c(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),re=c(/^html$/i),ie=c(/^[a-z][.\w]*(-[.\w]+)+$/i),ae=c(/<[/\w!]/g),le=c(/<[/\w]/g),ce=c(/<\/no(script|embed|frames)/i),se=c(/\/>/i),ue=1,fe=3,pe=7,me=8,de=9,he=11,ge=function(){return"undefined"==typeof window?null:window},ye=function(e,t,n,o){return R(e,t)&&b(e[t])?M(o.base?P(o.base):{},e[t],o.transform):n};var be=function e(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:ge();const o=t=>e(t);if(o.version="3.4.12",o.removed=[],!t||!t.document||t.document.nodeType!==de||!t.Element)return o.isSupported=!1,o;let r=t.document;const i=r,a=i.currentScript;t.DocumentFragment;const u=t.HTMLTemplateElement,f=t.Node,p=t.Element,k=t.NodeFilter,L=t.NamedNodeMap;void 0===L&&(t.NamedNodeMap||t.MozNamedAttrMap),t.HTMLFormElement;const z=t.DOMParser,be=t.trustedTypes,Te=p.prototype,Se=U(Te,"cloneNode"),Ee=U(Te,"remove"),Ae=U(Te,"nextSibling"),Ne=U(Te,"childNodes"),_e=U(Te,"parentNode"),we=U(Te,"shadowRoot"),Oe=U(Te,"attributes"),ve=f&&f.prototype?U(f.prototype,"nodeType"):null,De=f&&f.prototype?U(f.prototype,"nodeName"):null;if("function"==typeof u){const e=r.createElement("template");e.content&&e.content.ownerDocument&&(r=e.content.ownerDocument)}let Re,Ce,Ie="",xe=!1,ke=0;const Le=function(){if(ke>0)throw x('A configured TRUSTED_TYPES_POLICY callback (createHTML or createScriptURL) must not call DOMPurify.sanitize, as that causes infinite recursion. Do not pass a policy whose callbacks wrap DOMPurify as TRUSTED_TYPES_POLICY; see the "DOMPurify and Trusted Types" section of the README.')},Me=function(e){Le(),ke++;try{return Re.createHTML(e)}finally{ke--}},ze=function(){return xe||(Ce=function(e,t){if("object"!=typeof e||"function"!=typeof e.createPolicy)return null;let n=null;const o="data-tt-policy-suffix";t&&t.hasAttribute(o)&&(n=t.getAttribute(o));const r="dompurify"+(n?"#"+n:"");try{return e.createPolicy(r,{createHTML:e=>e,createScriptURL:e=>e})}catch(e){return console.warn("TrustedTypes policy "+r+" could not be created."),null}}(be,a),xe=!0),Ce},Pe=r,Ue=Pe.implementation,Fe=Pe.createNodeIterator,He=Pe.createDocumentFragment,je=Pe.getElementsByTagName,Be=i.importNode;let Ge={afterSanitizeAttributes:[],afterSanitizeElements:[],afterSanitizeShadowDOM:[],beforeSanitizeAttributes:[],beforeSanitizeElements:[],beforeSanitizeShadowDOM:[],uponSanitizeAttribute:[],uponSanitizeElement:[],uponSanitizeShadowNode:[]};o.isSupported="function"==typeof n&&"function"==typeof _e&&Ue&&void 0!==Ue.createHTMLDocument;const We=V,Ye=Z,qe=J,Xe=Q,$e=ee,Ke=ne,Ve=oe,Ze=ie;let Je=te,Qe=null;const et=M({},[...F,...H,...j,...G,...Y]);let tt=null;const nt=M({},[...q,...X,...$,...K]);let ot=Object.seal(s(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),rt=null,it=null;const at=Object.seal(s(null,{tagCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeCheck:{writable:!0,configurable:!1,enumerable:!0,value:null}}));let lt=!0,ct=!0,st=!1,ut=!0,ft=!1,pt=!0,mt=!1,dt=!1,ht=null,gt=null,yt=!1,bt=!1,Tt=!1,St=!1,Et=!0,At=!1;const Nt="user-content-";let _t=!0,wt=!1,Ot={},vt=null;const Dt=M({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","selectedcontent","style","svg","template","thead","title","video","xmp"]);let Rt=null;const Ct=M({},["audio","video","img","source","image","track"]);let It=null;const xt=M({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),kt="http://www.w3.org/1998/Math/MathML",Lt="http://www.w3.org/2000/svg",Mt="http://www.w3.org/1999/xhtml";let zt=Mt,Pt=!1,Ut=null;const Ft=M({},[kt,Lt,Mt],S),Ht=l(["mi","mo","mn","ms","mtext"]);let jt=M({},Ht);const Bt=l(["annotation-xml"]);let Gt=M({},Bt);const Wt=M({},["title","style","font","a","script"]);let Yt=null;const qt=["application/xhtml+xml","text/html"];let Xt=null,$t=null;const Kt=r.createElement("form"),Vt=function(e){return e instanceof RegExp||e instanceof Function},Zt=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};if($t&&$t===e)return;e&&"object"==typeof e||(e={}),e=P(e),Yt=-1===qt.indexOf(e.PARSER_MEDIA_TYPE)?"text/html":e.PARSER_MEDIA_TYPE,Xt="application/xhtml+xml"===Yt?S:T,Qe=ye(e,"ALLOWED_TAGS",et,{transform:Xt}),tt=ye(e,"ALLOWED_ATTR",nt,{transform:Xt}),Ut=ye(e,"ALLOWED_NAMESPACES",Ft,{transform:S}),It=ye(e,"ADD_URI_SAFE_ATTR",xt,{transform:Xt,base:xt}),Rt=ye(e,"ADD_DATA_URI_TAGS",Ct,{transform:Xt,base:Ct}),vt=ye(e,"FORBID_CONTENTS",Dt,{transform:Xt}),rt=ye(e,"FORBID_TAGS",P({}),{transform:Xt}),it=ye(e,"FORBID_ATTR",P({}),{transform:Xt}),Ot=!!R(e,"USE_PROFILES")&&(e.USE_PROFILES&&"object"==typeof e.USE_PROFILES?P(e.USE_PROFILES):e.USE_PROFILES),lt=!1!==e.ALLOW_ARIA_ATTR,ct=!1!==e.ALLOW_DATA_ATTR,st=e.ALLOW_UNKNOWN_PROTOCOLS||!1,ut=!1!==e.ALLOW_SELF_CLOSE_IN_ATTR,ft=e.SAFE_FOR_TEMPLATES||!1,pt=!1!==e.SAFE_FOR_XML,mt=e.WHOLE_DOCUMENT||!1,bt=e.RETURN_DOM||!1,Tt=e.RETURN_DOM_FRAGMENT||!1,St=e.RETURN_TRUSTED_TYPE||!1,yt=e.FORCE_BODY||!1,Et=!1!==e.SANITIZE_DOM,At=e.SANITIZE_NAMED_PROPS||!1,_t=!1!==e.KEEP_CONTENT,wt=e.IN_PLACE||!1,Je=function(e){try{return I(e,""),!0}catch(e){return!1}}(e.ALLOWED_URI_REGEXP)?e.ALLOWED_URI_REGEXP:te,zt="string"==typeof e.NAMESPACE?e.NAMESPACE:Mt,jt=R(e,"MATHML_TEXT_INTEGRATION_POINTS")&&e.MATHML_TEXT_INTEGRATION_POINTS&&"object"==typeof e.MATHML_TEXT_INTEGRATION_POINTS?P(e.MATHML_TEXT_INTEGRATION_POINTS):M({},Ht),Gt=R(e,"HTML_INTEGRATION_POINTS")&&e.HTML_INTEGRATION_POINTS&&"object"==typeof e.HTML_INTEGRATION_POINTS?P(e.HTML_INTEGRATION_POINTS):M({},Bt);const t=R(e,"CUSTOM_ELEMENT_HANDLING")&&e.CUSTOM_ELEMENT_HANDLING&&"object"==typeof e.CUSTOM_ELEMENT_HANDLING?P(e.CUSTOM_ELEMENT_HANDLING):s(null);if(ot=s(null),R(t,"tagNameCheck")&&Vt(t.tagNameCheck)&&(ot.tagNameCheck=t.tagNameCheck),R(t,"attributeNameCheck")&&Vt(t.attributeNameCheck)&&(ot.attributeNameCheck=t.attributeNameCheck),R(t,"allowCustomizedBuiltInElements")&&"boolean"==typeof t.allowCustomizedBuiltInElements&&(ot.allowCustomizedBuiltInElements=t.allowCustomizedBuiltInElements),c(ot),ft&&(ct=!1),Tt&&(bt=!0),Ot&&(Qe=M({},Y),tt=s(null),!0===Ot.html&&(M(Qe,F),M(tt,q)),!0===Ot.svg&&(M(Qe,H),M(tt,X),M(tt,K)),!0===Ot.svgFilters&&(M(Qe,j),M(tt,X),M(tt,K)),!0===Ot.mathMl&&(M(Qe,G),M(tt,$),M(tt,K))),at.tagCheck=null,at.attributeCheck=null,R(e,"ADD_TAGS")&&("function"==typeof e.ADD_TAGS?at.tagCheck=e.ADD_TAGS:b(e.ADD_TAGS)&&(Qe===et&&(Qe=P(Qe)),M(Qe,e.ADD_TAGS,Xt))),R(e,"ADD_ATTR")&&("function"==typeof e.ADD_ATTR?at.attributeCheck=e.ADD_ATTR:b(e.ADD_ATTR)&&(tt===nt&&(tt=P(tt)),M(tt,e.ADD_ATTR,Xt))),R(e,"ADD_URI_SAFE_ATTR")&&b(e.ADD_URI_SAFE_ATTR)&&M(It,e.ADD_URI_SAFE_ATTR,Xt),R(e,"FORBID_CONTENTS")&&b(e.FORBID_CONTENTS)&&(vt===Dt&&(vt=P(vt)),M(vt,e.FORBID_CONTENTS,Xt)),R(e,"ADD_FORBID_CONTENTS")&&b(e.ADD_FORBID_CONTENTS)&&(vt===Dt&&(vt=P(vt)),M(vt,e.ADD_FORBID_CONTENTS,Xt)),_t&&(Qe["#text"]=!0),mt&&M(Qe,["html","head","body"]),Qe.table&&(M(Qe,["tbody"]),delete rt.tbody),e.TRUSTED_TYPES_POLICY){if("function"!=typeof e.TRUSTED_TYPES_POLICY.createHTML)throw x('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');if("function"!=typeof e.TRUSTED_TYPES_POLICY.createScriptURL)throw x('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');const t=Re;Re=e.TRUSTED_TYPES_POLICY;try{Ie=Me("")}catch(e){throw Re=t,e}}else null===e.TRUSTED_TYPES_POLICY?(Re=void 0,Ie=""):(void 0===Re&&(Re=ze()),Re&&"string"==typeof Ie&&(Ie=Me("")));l&&l(e),$t=e},Jt=M({},[...H,...j,...B]),Qt=M({},[...G,...W]),en=function(e){let t=_e(e);t&&t.tagName||(t={namespaceURI:zt,tagName:"template"});const n=T(e.tagName),o=T(t.tagName);return!!Ut[e.namespaceURI]&&(e.namespaceURI===Lt?function(e,t,n){return t.namespaceURI===Mt?"svg"===e:t.namespaceURI===kt?"svg"===e&&("annotation-xml"===n||jt[n]):Boolean(Jt[e])}(n,t,o):e.namespaceURI===kt?function(e,t,n){return t.namespaceURI===Mt?"math"===e:t.namespaceURI===Lt?"math"===e&&Gt[n]:Boolean(Qt[e])}(n,t,o):e.namespaceURI===Mt?function(e,t,n){return!(t.namespaceURI===Lt&&!Gt[n])&&!(t.namespaceURI===kt&&!jt[n])&&!Qt[e]&&(Wt[e]||!Jt[e])}(n,t,o):!("application/xhtml+xml"!==Yt||!Ut[e.namespaceURI]))},tn=function(e){g(o.removed,{element:e});try{_e(e).removeChild(e)}catch(t){if(Ee(e),!_e(e))throw x("a node selected for removal could not be detached from its tree and cannot be safely returned; refusing to sanitize in place")}},nn=function(e){an(e);const t=Ne(e);if(t){const e=[];m(t,t=>{g(e,t)}),m(e,e=>{try{Ee(e)}catch(e){}})}const n=Oe(e);if(n)for(let t=n.length-1;t>=0;--t){const o=n[t],r=o&&o.name;if("string"==typeof r)try{e.removeAttribute(r)}catch(e){}}},on=function(e,t){try{g(o.removed,{attribute:t.getAttributeNode(e),from:t})}catch(e){g(o.removed,{attribute:null,from:t})}if(t.removeAttribute(e),"is"===e)if(bt||Tt)try{tn(t)}catch(e){}else try{t.setAttribute(e,"")}catch(e){}},rn=function(e){const t=Oe(e);if(t)for(let n=t.length-1;n>=0;--n){const o=t[n],r=o&&o.name;if("string"==typeof r&&!tt[Xt(r)])try{e.removeAttribute(r)}catch(e){}}},an=function(e){const t=[e];for(;t.length>0;){const e=t.pop();(ve?ve(e):e.nodeType)===ue&&rn(e);const n=Ne(e);if(n)for(let e=n.length-1;e>=0;--e)t.push(n[e])}},ln=function(e){let t=null,n=null;if(yt)e="<remove></remove>"+e;else{const t=E(e,/^[\r\n\t ]+/);n=t&&t[0]}"application/xhtml+xml"===Yt&&zt===Mt&&(e='<html xmlns="http://www.w3.org/1999/xhtml"><head></head><body>'+e+"</body></html>");const o=Re?Me(e):e;if(zt===Mt)try{t=(new z).parseFromString(o,Yt)}catch(e){}if(!t||!t.documentElement){t=Ue.createDocument(zt,"template",null);try{t.documentElement.innerHTML=Pt?Ie:o}catch(e){}}const i=t.body||t.documentElement;return e&&n&&i.insertBefore(r.createTextNode(n),i.childNodes[0]||null),zt===Mt?je.call(t,mt?"html":"body")[0]:mt?t.documentElement:i},cn=function(e){return Fe.call(e.ownerDocument||e,e,k.SHOW_ELEMENT|k.SHOW_COMMENT|k.SHOW_TEXT|k.SHOW_PROCESSING_INSTRUCTION|k.SHOW_CDATA_SECTION,null)},sn=function(e){return e=A(e,We," "),e=A(e,Ye," "),e=A(e,qe," ")},un=function(e){var t;e.normalize();const n=Fe.call(e.ownerDocument||e,e,k.SHOW_TEXT|k.SHOW_COMMENT|k.SHOW_CDATA_SECTION|k.SHOW_PROCESSING_INSTRUCTION,null);let o=n.nextNode();for(;o;)o.data=sn(o.data),o=n.nextNode();const r=null===(t=e.querySelectorAll)||void 0===t?void 0:t.call(e,"template");r&&m(r,e=>{pn(e.content)&&un(e.content)})},fn=function(e){const t=De?De(e):null;return"string"==typeof t&&("form"===Xt(t)&&("string"!=typeof e.nodeName||"string"!=typeof e.textContent||"function"!=typeof e.removeChild||e.attributes!==Oe(e)||"function"!=typeof e.removeAttribute||"function"!=typeof e.setAttribute||"string"!=typeof e.namespaceURI||"function"!=typeof e.insertBefore||"function"!=typeof e.hasChildNodes||e.nodeType!==ve(e)||e.childNodes!==Ne(e)))},pn=function(e){if(!ve||"object"!=typeof e||null===e)return!1;try{return ve(e)===he}catch(e){return!1}},mn=function(e){if(!ve||"object"!=typeof e||null===e)return!1;try{return"number"==typeof ve(e)}catch(e){return!1}};function dn(e,t,n){0!==e.length&&m(e,e=>{e.call(o,t,n,$t)})}const hn=function(e,t){if(dn(Ge.beforeSanitizeElements,e,null),e!==t&&null===_e(e))return!0;if(fn(e))return tn(e),!0;const n=Xt(De?De(e):e.nodeName);if(dn(Ge.uponSanitizeElement,e,{tagName:n,allowedTags:Qe}),e!==t&&null===_e(e))return!0;if(function(e,t){return!!(pt&&e.hasChildNodes()&&!mn(e.firstElementChild)&&I(ae,e.textContent)&&I(ae,e.innerHTML))||!(!pt||e.namespaceURI!==Mt||"style"!==t||!mn(e.firstElementChild))||e.nodeType===pe||!(!pt||e.nodeType!==me||!I(le,e.data))}(e,n))return tn(e),!0;if(rt[n]||!(at.tagCheck instanceof Function&&at.tagCheck(n))&&!Qe[n]){const t=function(e,t){if(!rt[t]&&bn(t)){if(ot.tagNameCheck instanceof RegExp&&I(ot.tagNameCheck,t))return!1;if(ot.tagNameCheck instanceof Function&&ot.tagNameCheck(t))return!1}if(_t&&!vt[t]){const t=_e(e),n=Ne(e);if(n&&t)for(let o=n.length-1;o>=0;--o){const r=wt?n[o]:Se(n[o],!0);t.insertBefore(r,Ae(e))}}return tn(e),!0}(e,n);return!1===t&&dn(Ge.afterSanitizeElements,e,null),t}if((ve?ve(e):e.nodeType)===ue&&!en(e))return tn(e),!0;if(("noscript"===n||"noembed"===n||"noframes"===n)&&I(ce,e.innerHTML))return tn(e),!0;if(ft&&e.nodeType===fe){const t=sn(e.textContent);e.textContent!==t&&(g(o.removed,{element:e.cloneNode()}),e.textContent=t)}return dn(Ge.afterSanitizeElements,e,null),!1},gn=function(e,t,n){if(it[t])return!1;if(pt&&"patchsrc"===t)return!1;if(pt&&"for"===t&&"label"!==e&&"output"!==e)return!1;if(Et&&("id"===t||"name"===t)&&(n in r||n in Kt))return!1;const o=tt[t]||at.attributeCheck instanceof Function&&at.attributeCheck(t,e);if(ct&&I(Xe,t));else if(lt&&I($e,t));else if(o)if(It[t]);else if(I(Je,A(n,Ve,"")));else if("src"!==t&&"xlink:href"!==t&&"href"!==t||"script"===e||0!==N(n,"data:")||!Rt[e]){if(st&&!I(Ke,A(n,Ve,"")));else if(n)return!1}else;else if(!(bn(e)&&(ot.tagNameCheck instanceof RegExp&&I(ot.tagNameCheck,e)||ot.tagNameCheck instanceof Function&&ot.tagNameCheck(e))&&(ot.attributeNameCheck instanceof RegExp&&I(ot.attributeNameCheck,t)||ot.attributeNameCheck instanceof Function&&ot.attributeNameCheck(t,e))||"is"===t&&ot.allowCustomizedBuiltInElements&&(ot.tagNameCheck instanceof RegExp&&I(ot.tagNameCheck,n)||ot.tagNameCheck instanceof Function&&ot.tagNameCheck(n))))return!1;return!0},yn=M({},["annotation-xml","color-profile","font-face","font-face-format","font-face-name","font-face-src","font-face-uri","missing-glyph"]),bn=function(e){return!yn[T(e)]&&I(Ze,e)},Tn=function(e,t,n,o){if(Re&&"object"==typeof be&&"function"==typeof be.getAttributeType&&!n)switch(be.getAttributeType(e,t)){case"TrustedHTML":return Me(o);case"TrustedScriptURL":return function(e){Le(),ke++;try{return Re.createScriptURL(e)}finally{ke--}}(o)}return o},Sn=function(e,t,n,r){try{n?e.setAttributeNS(n,t,r):e.setAttribute(t,r),fn(e)?tn(e):h(o.removed)}catch(n){on(t,e)}},En=function(e){dn(Ge.beforeSanitizeAttributes,e,null);const t=e.attributes;if(!t||fn(e))return;const n={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:tt,forceKeepAttr:void 0};let o=t.length;const r=Xt(e.nodeName);for(;o--;){const i=t[o],a=i.name,l=i.namespaceURI,c=i.value,s=Xt(a),u=c;let f="value"===a?u:_(u);n.attrName=s,n.attrValue=f,n.keepAttr=!0,n.forceKeepAttr=void 0,dn(Ge.uponSanitizeAttribute,e,n),f=n.attrValue,!At||"id"!==s&&"name"!==s||0===N(f,Nt)||(on(a,e),f=Nt+f),pt&&I(/((--!?|])>)|<\/(style|script|title|xmp|textarea|noscript|iframe|noembed|noframes)/i,f)?on(a,e):"attributename"===s&&E(f,"href")?on(a,e):n.forceKeepAttr||(n.keepAttr&&(ut||!I(se,f))?(ft&&(f=sn(f)),gn(r,s,f)?(f=Tn(r,s,l,f),f!==u&&Sn(e,a,l,f)):on(a,e)):on(a,e))}dn(Ge.afterSanitizeAttributes,e,null)},An=function(e){let t=null;const n=cn(e);for(dn(Ge.beforeSanitizeShadowDOM,e,null);t=n.nextNode();){dn(Ge.uponSanitizeShadowNode,t,null),hn(t,e),En(t),pn(t.content)&&An(t.content);if((ve?ve(t):t.nodeType)===ue){const e=we(t);pn(e)&&(Nn(e),An(e))}}dn(Ge.afterSanitizeShadowDOM,e,null)},Nn=function(e){const t=[{node:e,shadow:null}];for(;t.length>0;){const e=t.pop();if(e.shadow){An(e.shadow);continue}const n=e.node,o=(ve?ve(n):n.nodeType)===ue,r=Ne(n);if(r)for(let e=r.length-1;e>=0;--e)t.push({node:r[e],shadow:null});if(o){const e=De?De(n):null;if("string"==typeof e&&"template"===Xt(e)){const e=n.content;pn(e)&&t.push({node:e,shadow:null})}}if(o){const e=we(n);pn(e)&&t.push({node:null,shadow:e},{node:e,shadow:null})}}};return o.sanitize=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=null,r=null,a=null,l=null;if(Pt=!e,Pt&&(e="\x3c!--\x3e"),"string"!=typeof e&&!mn(e)&&"string"!=typeof(e=function(e){switch(typeof e){case"string":return e;case"number":return w(e);case"boolean":return O(e);case"bigint":return v?v(e):"0";case"symbol":return D?D(e):"Symbol()";case"undefined":default:return C(e);case"function":case"object":{if(null===e)return C(e);const t=e,n=U(t,"toString");if("function"==typeof n){const e=n(t);return"string"==typeof e?e:C(e)}return C(e)}}}(e)))throw x("dirty is not a string, aborting");if(!o.isSupported)return e;dt?(Qe=ht,tt=gt):Zt(t),(Ge.uponSanitizeElement.length>0||Ge.uponSanitizeAttribute.length>0)&&(Qe=P(Qe)),Ge.uponSanitizeAttribute.length>0&&(tt=P(tt)),o.removed=[];const c=wt&&"string"!=typeof e&&mn(e);if(c){!function(e){if(!pt)return;const t=[e];for(;t.length>0;){const e=t.pop(),n=ve?ve(e):e.nodeType;if(n===pe||n===me&&I(le,e.data)){try{Ee(e)}catch(e){}continue}if(n===ue){const t=e,n=Xt(De?De(e):e.nodeName);try{t.hasAttribute&&t.hasAttribute("patchsrc")&&t.removeAttribute("patchsrc"),t.hasAttribute&&t.hasAttribute("for")&&"label"!==n&&"output"!==n&&t.removeAttribute("for")}catch(e){}}const o=Ne(e);if(o)for(let e=o.length-1;e>=0;--e)t.push(o[e])}}(e);const t=De?De(e):e.nodeName;if("string"==typeof t){const n=Xt(t);if(!Qe[n]||rt[n])throw nn(e),x("root node is forbidden and cannot be sanitized in-place")}if(fn(e))throw nn(e),x("root node is clobbered and cannot be sanitized in-place");try{Nn(e)}catch(t){throw nn(e),t}}else if(mn(e))n=ln("\x3c!----\x3e"),r=n.ownerDocument.importNode(e,!0),r.nodeType===ue&&"BODY"===r.nodeName||"HTML"===r.nodeName?n=r:n.appendChild(r),Nn(r);else{if(!bt&&!ft&&!mt&&-1===e.indexOf("<"))return Re&&St?Me(e):e;if(n=ln(e),!n)return bt?null:St?Ie:""}n&&yt&&tn(n.firstChild);const s=c?e:n,u=cn(s);try{for(;a=u.nextNode();)hn(a,s),En(a),pn(a.content)&&An(a.content)}catch(t){throw c&&(nn(e),m(o.removed,e=>{e.element&&an(e.element)})),t}if(c)return m(o.removed,e=>{e.element&&an(e.element)}),ft&&un(e),e;if(bt){if(ft&&un(n),Tt)for(l=He.call(n.ownerDocument);n.firstChild;)l.appendChild(n.firstChild);else l=n;return(tt.shadowroot||tt.shadowrootmode)&&(l=Be.call(i,l,!0)),l}let f=mt?n.outerHTML:n.innerHTML;return mt&&Qe["!doctype"]&&n.ownerDocument&&n.ownerDocument.doctype&&n.ownerDocument.doctype.name&&I(re,n.ownerDocument.doctype.name)&&(f="<!DOCTYPE "+n.ownerDocument.doctype.name+">\n"+f),ft&&(f=sn(f)),Re&&St?Me(f):f},o.setConfig=function(){Zt(arguments.length>0&&void 0!==arguments[0]?arguments[0]:{}),dt=!0,ht=Qe,gt=tt},o.clearConfig=function(){$t=null,dt=!1,ht=null,gt=null,Re=Ce,Ie=""},o.isValidAttribute=function(e,t,n){$t||Zt({});const o=Xt(e),r=Xt(t);return gn(o,r,n)},o.addHook=function(e,t){"function"==typeof t&&R(Ge,e)&&g(Ge[e],t)},o.removeHook=function(e,t){if(R(Ge,e)){if(void 0!==t){const n=d(Ge[e],t);return-1===n?void 0:y(Ge[e],n,1)[0]}return h(Ge[e])}},o.removeHooks=function(e){R(Ge,e)&&(Ge[e]=[])},o.removeAllHooks=function(){Ge={afterSanitizeAttributes:[],afterSanitizeElements:[],afterSanitizeShadowDOM:[],beforeSanitizeAttributes:[],beforeSanitizeElements:[],beforeSanitizeShadowDOM:[],uponSanitizeAttribute:[],uponSanitizeElement:[],uponSanitizeShadowNode:[]}},o}();return be});
|
|
3
|
+
//# sourceMappingURL=purify.min.js.map
|
package/public/index.html
CHANGED
|
@@ -107,5 +107,16 @@
|
|
|
107
107
|
<script src="/static/tasks-tree.js"></script>
|
|
108
108
|
<script src="/static/task-operations.js"></script>
|
|
109
109
|
<script src="/static/session-forest.js"></script>
|
|
110
|
+
<!--
|
|
111
|
+
v2.5.x REQ-4: vendor markdown rendering for the home chat panel.
|
|
112
|
+
Load order matters - marked must register globals before DOMPurify
|
|
113
|
+
is constructed, and both must register before markdown-renderer.js
|
|
114
|
+
resolves them at import time. chat-panel.js only depends on
|
|
115
|
+
window.markdownRenderer, which is set by markdown-renderer.js.
|
|
116
|
+
-->
|
|
117
|
+
<script src="/static/marked.min.js" defer></script>
|
|
118
|
+
<script src="/static/dompurify.min.js" defer></script>
|
|
119
|
+
<script src="/static/markdown-renderer.js" defer></script>
|
|
120
|
+
<script src="/static/chat-panel.js" defer></script>
|
|
110
121
|
</body>
|
|
111
122
|
</html>
|