@ai-setting/roy-plugin-task-show 2.4.1 → 2.5.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/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-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 +31 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +228 -4
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/public/chat-panel.js +260 -0
- package/public/mermaid-renderer.js +81 -26
- package/public/style.css +141 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Home chat panel — client controller.
|
|
3
|
+
*
|
|
4
|
+
* v2.5.0+ REQ-2: lives on the index page (top of body). Hooks the
|
|
5
|
+
* following DOM contract (pinned by `test/home-chat-v25.test.ts`):
|
|
6
|
+
*
|
|
7
|
+
* [data-chat-shell] <section> wrapper
|
|
8
|
+
* [data-chat-messages] <div> message log (role="log")
|
|
9
|
+
* [data-chat-form] <form> submit handler
|
|
10
|
+
* [data-chat-input] <input> text input
|
|
11
|
+
* [data-chat-send] <button> send button (type="submit")
|
|
12
|
+
*
|
|
13
|
+
* sessionId is persisted in `localStorage["taskShow.chat.sessionId"]`
|
|
14
|
+
* across page loads so the conversation survives reloads. The first
|
|
15
|
+
* run (no stored id) POSTs `/api/chat/start` to allocate one.
|
|
16
|
+
*
|
|
17
|
+
* The server speaks SSE for streaming chunks. We use `fetch` + an
|
|
18
|
+
* `ReadableStream` reader instead of `EventSource` because the chat
|
|
19
|
+
* endpoint is POST (EventSource can't POST) and we need to send the
|
|
20
|
+
* `message` in the request body.
|
|
21
|
+
*
|
|
22
|
+
* If the chat feature is disabled server-side (503), the panel hides
|
|
23
|
+
* itself with a banner instead of throwing.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
(function () {
|
|
27
|
+
const STORAGE_KEY = "taskShow.chat.sessionId";
|
|
28
|
+
const SHELL_SEL = "[data-chat-shell]";
|
|
29
|
+
const MESSAGES_SEL = "[data-chat-messages]";
|
|
30
|
+
const FORM_SEL = "[data-chat-form]";
|
|
31
|
+
const INPUT_SEL = "[data-chat-input]";
|
|
32
|
+
const SEND_SEL = "[data-chat-send]";
|
|
33
|
+
|
|
34
|
+
function $(sel) { return document.querySelector(sel); }
|
|
35
|
+
|
|
36
|
+
function readSessionId() {
|
|
37
|
+
try { return window.localStorage.getItem(STORAGE_KEY) || ""; }
|
|
38
|
+
catch { return ""; }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function writeSessionId(id) {
|
|
42
|
+
try { window.localStorage.setItem(STORAGE_KEY, id); } catch { /* ignore */ }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function clearMessages(container) {
|
|
46
|
+
while (container.firstChild) container.removeChild(container.firstChild);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function appendMessage(container, role, text) {
|
|
50
|
+
const empty = container.querySelector(".chat-empty");
|
|
51
|
+
if (empty) empty.remove();
|
|
52
|
+
const el = document.createElement("div");
|
|
53
|
+
el.className = "chat-message chat-message--" + role;
|
|
54
|
+
el.dataset.role = role;
|
|
55
|
+
const label = role === "user" ? "You" : role === "assistant" ? "Assistant" : "Error";
|
|
56
|
+
el.innerHTML =
|
|
57
|
+
`<div class="chat-message-meta">${label}</div>` +
|
|
58
|
+
`<div class="chat-message-text"></div>`;
|
|
59
|
+
el.querySelector(".chat-message-text").textContent = text;
|
|
60
|
+
container.appendChild(el);
|
|
61
|
+
container.scrollTop = container.scrollHeight;
|
|
62
|
+
return el;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Append a streaming chunk to the last assistant message. If no
|
|
67
|
+
* assistant message exists yet (first chunk of a turn), create one.
|
|
68
|
+
*/
|
|
69
|
+
function appendChunk(container, text) {
|
|
70
|
+
let last = container.querySelector(".chat-message--assistant:last-of-type");
|
|
71
|
+
if (!last) last = appendMessage(container, "assistant", "");
|
|
72
|
+
const textEl = last.querySelector(".chat-message-text");
|
|
73
|
+
textEl.textContent += text;
|
|
74
|
+
container.scrollTop = container.scrollHeight;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function setBusy(form, busy) {
|
|
78
|
+
const input = form.querySelector(INPUT_SEL);
|
|
79
|
+
const send = form.querySelector(SEND_SEL);
|
|
80
|
+
if (input) input.disabled = busy;
|
|
81
|
+
if (send) {
|
|
82
|
+
send.disabled = busy;
|
|
83
|
+
send.textContent = busy ? "Sending…" : "Send";
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function showDisabledBanner(shell, msg) {
|
|
88
|
+
const banner = document.createElement("p");
|
|
89
|
+
banner.className = "chat-banner";
|
|
90
|
+
banner.dataset.role = "banner";
|
|
91
|
+
banner.textContent = msg;
|
|
92
|
+
shell.appendChild(banner);
|
|
93
|
+
const form = shell.querySelector(FORM_SEL);
|
|
94
|
+
if (form) form.remove();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function ensureSessionId() {
|
|
98
|
+
let id = readSessionId();
|
|
99
|
+
if (id) return id;
|
|
100
|
+
const res = await fetch("/api/chat/start", {
|
|
101
|
+
method: "POST",
|
|
102
|
+
headers: { "Content-Type": "application/json" },
|
|
103
|
+
body: "{}",
|
|
104
|
+
});
|
|
105
|
+
if (!res.ok) throw new Error("chat_start_failed: " + res.status);
|
|
106
|
+
const j = await res.json();
|
|
107
|
+
id = j.sessionId;
|
|
108
|
+
writeSessionId(id);
|
|
109
|
+
return id;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function sendMessage(form, container, message) {
|
|
113
|
+
let sessionId;
|
|
114
|
+
try {
|
|
115
|
+
sessionId = await ensureSessionId();
|
|
116
|
+
} catch (e) {
|
|
117
|
+
appendMessage(container, "error", String(e && e.message || e));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
appendMessage(container, "user", message);
|
|
121
|
+
setBusy(form, true);
|
|
122
|
+
|
|
123
|
+
let res;
|
|
124
|
+
try {
|
|
125
|
+
res = await fetch(`/api/chat/${encodeURIComponent(sessionId)}/message`, {
|
|
126
|
+
method: "POST",
|
|
127
|
+
headers: {
|
|
128
|
+
"Content-Type": "application/json",
|
|
129
|
+
Accept: "text/event-stream",
|
|
130
|
+
},
|
|
131
|
+
body: JSON.stringify({ message }),
|
|
132
|
+
});
|
|
133
|
+
} catch (e) {
|
|
134
|
+
appendMessage(container, "error", "network_error: " + (e && e.message || e));
|
|
135
|
+
setBusy(form, false);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (!res.ok || !res.body) {
|
|
140
|
+
let detail = "";
|
|
141
|
+
try { detail = await res.text(); } catch { /* ignore */ }
|
|
142
|
+
appendMessage(container, "error", `server_error: ${res.status} ${detail.slice(0, 200)}`);
|
|
143
|
+
setBusy(form, false);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const reader = res.body.getReader();
|
|
148
|
+
const decoder = new TextDecoder("utf-8");
|
|
149
|
+
let buf = "";
|
|
150
|
+
let sawError = false;
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
while (true) {
|
|
154
|
+
const { value, done } = await reader.read();
|
|
155
|
+
if (done) break;
|
|
156
|
+
buf += decoder.decode(value, { stream: true });
|
|
157
|
+
let nl;
|
|
158
|
+
while ((nl = buf.indexOf("\n\n")) >= 0) {
|
|
159
|
+
const frame = buf.slice(0, nl);
|
|
160
|
+
buf = buf.slice(nl + 2);
|
|
161
|
+
handleFrame(frame, container, (err) => { sawError = sawError || !!err; });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// Flush trailing frame if any.
|
|
165
|
+
if (buf.trim().length > 0) handleFrame(buf, container, () => {});
|
|
166
|
+
} catch (e) {
|
|
167
|
+
appendMessage(container, "error", "stream_error: " + (e && e.message || e));
|
|
168
|
+
} finally {
|
|
169
|
+
setBusy(form, false);
|
|
170
|
+
// Suppress the final empty-message that may have been left if the
|
|
171
|
+
// assistant produced no text. We intentionally leave error messages
|
|
172
|
+
// alone so the user can see what went wrong.
|
|
173
|
+
if (!sawError) {
|
|
174
|
+
const last = container.querySelector(".chat-message--assistant:last-of-type");
|
|
175
|
+
if (last && !last.querySelector(".chat-message-text").textContent.trim()) {
|
|
176
|
+
last.remove();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Parse one SSE frame (event:/data:/id: lines). Returns the parsed frame or null. */
|
|
183
|
+
function handleFrame(raw, container, onError) {
|
|
184
|
+
let event = "";
|
|
185
|
+
let data = "";
|
|
186
|
+
let id = "";
|
|
187
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
188
|
+
if (line.startsWith(":")) continue;
|
|
189
|
+
if (line.startsWith("event:")) event = line.slice(6).trim();
|
|
190
|
+
else if (line.startsWith("data:")) data += (data ? "\n" : "") + line.slice(5).trim();
|
|
191
|
+
else if (line.startsWith("id:")) id = line.slice(3).trim();
|
|
192
|
+
}
|
|
193
|
+
if (!event && !data) return null;
|
|
194
|
+
let parsed = null;
|
|
195
|
+
if (data) {
|
|
196
|
+
try { parsed = JSON.parse(data); } catch { parsed = data; }
|
|
197
|
+
}
|
|
198
|
+
if (event === "chunk" && parsed && typeof parsed === "object") {
|
|
199
|
+
if (parsed.type === "text" && typeof parsed.text === "string") {
|
|
200
|
+
appendChunk(container, parsed.text);
|
|
201
|
+
} else if (parsed.type === "reasoning" && typeof parsed.text === "string") {
|
|
202
|
+
// Reasoning is hidden in the chat UI by design; we still log it
|
|
203
|
+
// so a developer can inspect via DevTools.
|
|
204
|
+
if (window.console && window.console.debug) {
|
|
205
|
+
window.console.debug("[chat] reasoning:", parsed.text);
|
|
206
|
+
}
|
|
207
|
+
} else if (parsed.type === "start") {
|
|
208
|
+
// No-op: the assistant bubble is created lazily on first text chunk.
|
|
209
|
+
} else if (parsed.type === "error" && parsed.error) {
|
|
210
|
+
onError(parsed);
|
|
211
|
+
appendMessage(container, "error", parsed.error.message || "unknown error");
|
|
212
|
+
}
|
|
213
|
+
} else if (event === "done") {
|
|
214
|
+
// Terminal marker — nothing to render.
|
|
215
|
+
} else if (event === "error") {
|
|
216
|
+
onError(parsed);
|
|
217
|
+
const msg = parsed && parsed.error && parsed.error.message
|
|
218
|
+
? parsed.error.message
|
|
219
|
+
: "stream error";
|
|
220
|
+
appendMessage(container, "error", msg);
|
|
221
|
+
} else if (event === "ready") {
|
|
222
|
+
// SSE handshake — nothing to render.
|
|
223
|
+
}
|
|
224
|
+
return { event, data, id, parsed };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function init() {
|
|
228
|
+
const shell = document.querySelector(SHELL_SEL);
|
|
229
|
+
if (!shell) return;
|
|
230
|
+
const form = shell.querySelector(FORM_SEL);
|
|
231
|
+
const container = shell.querySelector(MESSAGES_SEL);
|
|
232
|
+
const input = shell.querySelector(INPUT_SEL);
|
|
233
|
+
if (!form || !container || !input) return;
|
|
234
|
+
|
|
235
|
+
// Probe feature availability with a HEAD request. If 503, hide UI.
|
|
236
|
+
fetch("/api/chat/start", { method: "HEAD" }).then((res) => {
|
|
237
|
+
if (res.status === 503) {
|
|
238
|
+
showDisabledBanner(shell, "Chat disabled (server returned 503 — chatOptions not configured).");
|
|
239
|
+
}
|
|
240
|
+
}).catch(() => { /* offline — leave UI alone */ });
|
|
241
|
+
|
|
242
|
+
form.addEventListener("submit", (e) => {
|
|
243
|
+
e.preventDefault();
|
|
244
|
+
const message = (input.value || "").trim();
|
|
245
|
+
if (!message) return;
|
|
246
|
+
input.value = "";
|
|
247
|
+
sendMessage(form, container, message);
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (document.readyState === "loading") {
|
|
252
|
+
document.addEventListener("DOMContentLoaded", init);
|
|
253
|
+
} else {
|
|
254
|
+
init();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Expose a tiny test hook so headless harness can drive the panel
|
|
258
|
+
// without firing real events.
|
|
259
|
+
window.taskShowChatPanel = { init, sendMessage, STORAGE_KEY };
|
|
260
|
+
})();
|
|
@@ -293,10 +293,17 @@
|
|
|
293
293
|
if (cw === 0 && ch === 0 && sw === 0 && sh === 0) {
|
|
294
294
|
return { tx: safeTx, ty: safeTy };
|
|
295
295
|
}
|
|
296
|
+
// v2.4.2: bidirectional pan — when the diagram is wider/taller
|
|
297
|
+
// than the container, allow tx/ty in [-(dim-stage-dim-container),
|
|
298
|
+
// +(dim-stage-dim-container)] so the user can drag in any
|
|
299
|
+
// direction and see any part of the diagram. When the diagram
|
|
300
|
+
// fits, pin to 0 on that axis.
|
|
296
301
|
const minTx = sw > cw ? -(sw - cw) : 0;
|
|
302
|
+
const maxTx = sw > cw ? (sw - cw) : 0;
|
|
297
303
|
const minTy = sh > ch ? -(sh - ch) : 0;
|
|
298
|
-
const
|
|
299
|
-
const
|
|
304
|
+
const maxTy = sh > ch ? (sh - ch) : 0;
|
|
305
|
+
const nextTx = Math.max(minTx, Math.min(maxTx, safeTx));
|
|
306
|
+
const nextTy = Math.max(minTy, Math.min(maxTy, safeTy));
|
|
300
307
|
return { tx: nextTx, ty: nextTy };
|
|
301
308
|
}
|
|
302
309
|
|
|
@@ -477,9 +484,23 @@
|
|
|
477
484
|
return { tx: clamped.tx, ty: clamped.ty };
|
|
478
485
|
}
|
|
479
486
|
|
|
480
|
-
// v2.4.
|
|
481
|
-
//
|
|
482
|
-
//
|
|
487
|
+
// v2.4.2: pointer-driven drag handlers. The pointerdown
|
|
488
|
+
// listener is wired on the CONTAINER (not the stage) because
|
|
489
|
+
// real mermaid SVGs include child elements (mermaid's
|
|
490
|
+
// `bindFunctions` injects click handlers on `<g>` and
|
|
491
|
+
// `<path>`) that may call `event.stopPropagation()`. If the
|
|
492
|
+
// listener lived on the stage, those child handlers would
|
|
493
|
+
// silently eat the pointerdown and the pan would never start
|
|
494
|
+
// — but the CSS-driven `cursor: grab` on the stage would
|
|
495
|
+
// still show, fooling the user into thinking drag was wired
|
|
496
|
+
// up. Listening on the container (a parent of the stage)
|
|
497
|
+
// bypasses any stopPropagation that originates inside the
|
|
498
|
+
// SVG and still lets us reject clicks on the zoom toolbar
|
|
499
|
+
// (the toolbar is a sibling of the stage, not a descendant).
|
|
500
|
+
//
|
|
501
|
+
// pointermove / pointerup / pointercancel still attach to
|
|
502
|
+
// the stage so `setPointerCapture` keeps the events flowing
|
|
503
|
+
// to the stage even when the pointer leaves the SVG bounds.
|
|
483
504
|
function attachPanHandlers(container) {
|
|
484
505
|
if (!container || !container.ownerDocument) return;
|
|
485
506
|
// Already wired — don't double-bind.
|
|
@@ -492,9 +513,25 @@
|
|
|
492
513
|
// the current pan offsets so subsequent pointermove events
|
|
493
514
|
// can compute the delta.
|
|
494
515
|
function onPointerDown(ev) {
|
|
495
|
-
// Only respond to the primary pointer button
|
|
496
|
-
//
|
|
497
|
-
|
|
516
|
+
// Only respond to the primary pointer button. We accept
|
|
517
|
+
// button === 0 (left click / primary touch) AND
|
|
518
|
+
// button === -1 (Wacom / iPad Pencil hover events that
|
|
519
|
+
// report `button: -1` in PointerEvent spec). We reject
|
|
520
|
+
// button > 0 (right / middle / x1 / x2). The previous
|
|
521
|
+
// `!== 0` check rejected -1 and broke touch/pen entirely.
|
|
522
|
+
if (ev && typeof ev.button === "number" && ev.button > 0) return;
|
|
523
|
+
// Ignore secondary pointers: if a drag is already in
|
|
524
|
+
// progress (e.g. multi-touch), a second pointerdown
|
|
525
|
+
// should NOT overwrite the captured drag state. Without
|
|
526
|
+
// this guard the primary drag's startX/startTx are
|
|
527
|
+
// silently lost and the next pointermove from the
|
|
528
|
+
// primary pointer is filtered out by the pointerId
|
|
529
|
+
// mismatch check.
|
|
530
|
+
if (dragState.get(container)) return;
|
|
531
|
+
// The container holds both the stage (pan target) and
|
|
532
|
+
// the zoom toolbar (which we must NOT pan). Only start
|
|
533
|
+
// a pan when the pointerdown target is inside the stage.
|
|
534
|
+
if (ev && ev.target && typeof stage.contains === "function" && !stage.contains(ev.target)) return;
|
|
498
535
|
// Allow the user to drag using touch / pen too.
|
|
499
536
|
const pid = ev && typeof ev.pointerId === "number" ? ev.pointerId : 1;
|
|
500
537
|
const cur = getPan(container);
|
|
@@ -509,7 +546,9 @@
|
|
|
509
546
|
try {
|
|
510
547
|
stage.setPointerCapture && stage.setPointerCapture(pid);
|
|
511
548
|
} catch (_) {
|
|
512
|
-
// ignore — setPointerCapture can throw
|
|
549
|
+
// ignore — setPointerCapture can throw when the
|
|
550
|
+
// element is detached, has zero size, or the pointer
|
|
551
|
+
// is already captured by another element.
|
|
513
552
|
}
|
|
514
553
|
if (ev && typeof ev.preventDefault === "function") {
|
|
515
554
|
try { ev.preventDefault(); } catch (_) { /* ignore */ }
|
|
@@ -544,18 +583,28 @@
|
|
|
544
583
|
}
|
|
545
584
|
}
|
|
546
585
|
|
|
547
|
-
//
|
|
548
|
-
//
|
|
549
|
-
//
|
|
550
|
-
//
|
|
551
|
-
//
|
|
552
|
-
//
|
|
553
|
-
//
|
|
554
|
-
//
|
|
555
|
-
|
|
556
|
-
stage.
|
|
557
|
-
|
|
558
|
-
|
|
586
|
+
// pointerdown attaches to the CONTAINER in CAPTURE phase
|
|
587
|
+
// so that any stopPropagation inside the SVG (mermaid's
|
|
588
|
+
// bindFunctions, custom `<path>` handlers, etc.) cannot
|
|
589
|
+
// block the pan handler. Capture-phase listeners fire on
|
|
590
|
+
// the way DOWN to the target, BEFORE the target's bubble
|
|
591
|
+
// phase handlers run — so even if a `<g>` child calls
|
|
592
|
+
// `e.stopPropagation()`, our container listener has
|
|
593
|
+
// already fired. The `stage.contains(target)` check above
|
|
594
|
+
// still rejects clicks on the toolbar (a sibling of the
|
|
595
|
+
// stage).
|
|
596
|
+
container.addEventListener("pointerdown", onPointerDown, true);
|
|
597
|
+
// v2.4.2: pointermove + pointerup + pointercancel also
|
|
598
|
+
// attach to the CONTAINER in capture phase for the same
|
|
599
|
+
// reason. Real mermaid SVGs may call `stopPropagation` on
|
|
600
|
+
// pointermove (e.g. when binding drag-to-pan on a node),
|
|
601
|
+
// which would otherwise silently drop every move event
|
|
602
|
+
// after the drag started. We still call setPointerCapture
|
|
603
|
+
// on the stage below so the drag survives the pointer
|
|
604
|
+
// leaving the container entirely.
|
|
605
|
+
container.addEventListener("pointermove", onPointerMove, true);
|
|
606
|
+
container.addEventListener("pointerup", endDrag, true);
|
|
607
|
+
container.addEventListener("pointercancel", endDrag, true);
|
|
559
608
|
// Also attach to document/window as a safety net — some
|
|
560
609
|
// browsers route pointercancel through the document even
|
|
561
610
|
// when capture is set. We swallow duplicates in endDrag.
|
|
@@ -565,7 +614,7 @@
|
|
|
565
614
|
}
|
|
566
615
|
// We need a way to remove these listeners on destroy();
|
|
567
616
|
// stash a teardown closure on the container.
|
|
568
|
-
container.__panHandlers = { onPointerDown, onPointerMove, endDrag, doc, stage };
|
|
617
|
+
container.__panHandlers = { onPointerDown, onPointerMove, endDrag, doc, stage, container };
|
|
569
618
|
container.__panHandlersAttached = true;
|
|
570
619
|
}
|
|
571
620
|
|
|
@@ -573,10 +622,16 @@
|
|
|
573
622
|
if (!container || !container.__panHandlers) return;
|
|
574
623
|
const h = container.__panHandlers;
|
|
575
624
|
try {
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
625
|
+
// v2.4.2: pointerdown was moved to the container (capture
|
|
626
|
+
// phase) so it can survive stopPropagation in SVG
|
|
627
|
+
// children. pointermove/up/cancel also moved. Mirror
|
|
628
|
+
// those changes here so destroy() actually unbinds.
|
|
629
|
+
if (h.container && h.container.removeEventListener) {
|
|
630
|
+
h.container.removeEventListener("pointerdown", h.onPointerDown, true);
|
|
631
|
+
h.container.removeEventListener("pointermove", h.onPointerMove, true);
|
|
632
|
+
h.container.removeEventListener("pointerup", h.endDrag, true);
|
|
633
|
+
h.container.removeEventListener("pointercancel", h.endDrag, true);
|
|
634
|
+
}
|
|
580
635
|
if (h.doc && h.doc.removeEventListener) {
|
|
581
636
|
h.doc.removeEventListener("pointerup", h.endDrag);
|
|
582
637
|
h.doc.removeEventListener("pointercancel", h.endDrag);
|
package/public/style.css
CHANGED
|
@@ -1987,3 +1987,144 @@ details summary {
|
|
|
1987
1987
|
font-size: 11px;
|
|
1988
1988
|
color: var(--fg-muted, #6b7280);
|
|
1989
1989
|
}
|
|
1990
|
+
|
|
1991
|
+
/* ============================================================
|
|
1992
|
+
* v2.5.0+ REQ-2: home-page chat panel
|
|
1993
|
+
* ============================================================ */
|
|
1994
|
+
|
|
1995
|
+
.chat-panel {
|
|
1996
|
+
display: flex;
|
|
1997
|
+
flex-direction: column;
|
|
1998
|
+
gap: 12px;
|
|
1999
|
+
margin-top: 12px;
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
.chat-panel-header h2 {
|
|
2003
|
+
margin: 0 0 4px;
|
|
2004
|
+
font-size: 18px;
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
.chat-panel-meta {
|
|
2008
|
+
margin: 0;
|
|
2009
|
+
font-size: 12px;
|
|
2010
|
+
color: var(--fg-muted, #6b7280);
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
.chat-messages {
|
|
2014
|
+
display: flex;
|
|
2015
|
+
flex-direction: column;
|
|
2016
|
+
gap: 8px;
|
|
2017
|
+
min-height: 80px;
|
|
2018
|
+
max-height: 360px;
|
|
2019
|
+
overflow-y: auto;
|
|
2020
|
+
padding: 12px;
|
|
2021
|
+
border: 1px solid var(--border, #e5e7eb);
|
|
2022
|
+
border-radius: 8px;
|
|
2023
|
+
background: #fff;
|
|
2024
|
+
font-size: 14px;
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
.chat-empty {
|
|
2028
|
+
margin: 0;
|
|
2029
|
+
color: var(--fg-muted, #9ca3af);
|
|
2030
|
+
font-style: italic;
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
.chat-message {
|
|
2034
|
+
padding: 8px 10px;
|
|
2035
|
+
border-radius: 6px;
|
|
2036
|
+
border: 1px solid transparent;
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
.chat-message-meta {
|
|
2040
|
+
font-size: 11px;
|
|
2041
|
+
color: var(--fg-muted, #6b7280);
|
|
2042
|
+
margin-bottom: 2px;
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
.chat-message-text {
|
|
2046
|
+
white-space: pre-wrap;
|
|
2047
|
+
word-break: break-word;
|
|
2048
|
+
line-height: 1.4;
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
.chat-message--user {
|
|
2052
|
+
align-self: flex-end;
|
|
2053
|
+
background: #dbeafe;
|
|
2054
|
+
border-color: #93c5fd;
|
|
2055
|
+
max-width: 80%;
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
.chat-message--assistant {
|
|
2059
|
+
align-self: flex-start;
|
|
2060
|
+
background: #f3f4f6;
|
|
2061
|
+
border-color: #d1d5db;
|
|
2062
|
+
max-width: 80%;
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
.chat-message--error {
|
|
2066
|
+
align-self: stretch;
|
|
2067
|
+
background: #fee2e2;
|
|
2068
|
+
border-color: #fca5a5;
|
|
2069
|
+
color: #991b1b;
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
.chat-form {
|
|
2073
|
+
display: flex;
|
|
2074
|
+
gap: 8px;
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
.chat-input {
|
|
2078
|
+
flex: 1 1 auto;
|
|
2079
|
+
padding: 8px 10px;
|
|
2080
|
+
border: 1px solid var(--border, #d1d5db);
|
|
2081
|
+
border-radius: 6px;
|
|
2082
|
+
font-size: 14px;
|
|
2083
|
+
font-family: inherit;
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
.chat-input:focus {
|
|
2087
|
+
outline: 2px solid #3b82f6;
|
|
2088
|
+
outline-offset: -1px;
|
|
2089
|
+
border-color: #3b82f6;
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
.chat-send {
|
|
2093
|
+
padding: 8px 16px;
|
|
2094
|
+
background: #2563eb;
|
|
2095
|
+
color: #fff;
|
|
2096
|
+
border: 1px solid #1d4ed8;
|
|
2097
|
+
border-radius: 6px;
|
|
2098
|
+
cursor: pointer;
|
|
2099
|
+
font-weight: 500;
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
.chat-send:hover:not(:disabled) {
|
|
2103
|
+
background: #1d4ed8;
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
.chat-send:disabled {
|
|
2107
|
+
opacity: 0.6;
|
|
2108
|
+
cursor: not-allowed;
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
.chat-banner {
|
|
2112
|
+
padding: 8px 12px;
|
|
2113
|
+
margin: 0;
|
|
2114
|
+
background: #fef3c7;
|
|
2115
|
+
border: 1px solid #fcd34d;
|
|
2116
|
+
border-radius: 6px;
|
|
2117
|
+
color: #78350f;
|
|
2118
|
+
font-size: 13px;
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
.visually-hidden {
|
|
2122
|
+
position: absolute;
|
|
2123
|
+
width: 1px;
|
|
2124
|
+
height: 1px;
|
|
2125
|
+
padding: 0;
|
|
2126
|
+
margin: -1px;
|
|
2127
|
+
overflow: hidden;
|
|
2128
|
+
clip: rect(0, 0, 0, 0);
|
|
2129
|
+
border: 0;
|
|
2130
|
+
}
|