@bdocs/plugin-ask-ai 0.3.0 → 0.4.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/client/index.d.mts +146 -10
- package/dist/client/index.mjs +701 -561
- package/dist/node/index.d.mts +276 -114
- package/dist/node/index.mjs +227 -253
- package/dist/rate-limit-eZ04302C.mjs +217 -0
- package/dist/server/index.d.mts +73 -1
- package/dist/server/index.mjs +340 -23
- package/package.json +12 -5
package/dist/client/index.mjs
CHANGED
|
@@ -1,8 +1,109 @@
|
|
|
1
|
-
import { useCallback, useEffect, useRef, useState } from "react";
|
|
1
|
+
import { memo, useCallback, useEffect, useId, useRef, useState } from "react";
|
|
2
2
|
import { useConfig } from "boltdocs/client";
|
|
3
|
-
import {
|
|
3
|
+
import { clsx } from "clsx";
|
|
4
|
+
import { twMerge } from "tailwind-merge";
|
|
4
5
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
import { Streamdown } from "streamdown";
|
|
7
|
+
//#region src/client/abort.ts
|
|
8
|
+
/**
|
|
9
|
+
* Creates a cooperative cancellation controller for a single submission.
|
|
10
|
+
*
|
|
11
|
+
* Prefers the native `AbortController` (real `AbortSignal` for `fetch`).
|
|
12
|
+
* When it is unavailable we degrade gracefully: `abort()` cancels the stream
|
|
13
|
+
* reader and sets a flag that the read loop checks. Never throws.
|
|
14
|
+
*/
|
|
15
|
+
function createClientController() {
|
|
16
|
+
let holder = null;
|
|
17
|
+
let aborted = false;
|
|
18
|
+
const attachReader = (reader) => {
|
|
19
|
+
holder = reader;
|
|
20
|
+
if (aborted) try {
|
|
21
|
+
holder?.cancel?.();
|
|
22
|
+
} catch {}
|
|
23
|
+
};
|
|
24
|
+
const abort = () => {
|
|
25
|
+
if (aborted) return;
|
|
26
|
+
aborted = true;
|
|
27
|
+
try {
|
|
28
|
+
holder?.cancel?.();
|
|
29
|
+
} catch {}
|
|
30
|
+
};
|
|
31
|
+
try {
|
|
32
|
+
if (typeof AbortController !== "undefined") {
|
|
33
|
+
const native = new AbortController();
|
|
34
|
+
return {
|
|
35
|
+
get signal() {
|
|
36
|
+
return native.signal;
|
|
37
|
+
},
|
|
38
|
+
get aborted() {
|
|
39
|
+
return aborted;
|
|
40
|
+
},
|
|
41
|
+
attachReader,
|
|
42
|
+
abort() {
|
|
43
|
+
abort();
|
|
44
|
+
try {
|
|
45
|
+
native.abort();
|
|
46
|
+
} catch {}
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
} catch {}
|
|
51
|
+
return {
|
|
52
|
+
signal: void 0,
|
|
53
|
+
get aborted() {
|
|
54
|
+
return aborted;
|
|
55
|
+
},
|
|
56
|
+
attachReader,
|
|
57
|
+
abort
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
//#endregion
|
|
5
61
|
//#region src/client/use-ask-ai.ts
|
|
62
|
+
const DEFAULT_ASK_AI_UI_COPY = {
|
|
63
|
+
title: "Ask Assistant",
|
|
64
|
+
placeholder: "Ask about this page…",
|
|
65
|
+
emptyTitle: "How can I help you today?",
|
|
66
|
+
emptyDescription: "Ask anything about the current documentation page. The assistant only answers using the page you are viewing.",
|
|
67
|
+
buttonTooltip: "Ask AI assistant",
|
|
68
|
+
composerHint: "Enter to send · Shift+Enter for a new line"
|
|
69
|
+
};
|
|
70
|
+
function readUiCopy(meta) {
|
|
71
|
+
if (!meta) return DEFAULT_ASK_AI_UI_COPY;
|
|
72
|
+
const str = (key) => {
|
|
73
|
+
const v = meta[key];
|
|
74
|
+
return typeof v === "string" && v.length > 0 ? v : DEFAULT_ASK_AI_UI_COPY[key];
|
|
75
|
+
};
|
|
76
|
+
return {
|
|
77
|
+
title: str("title"),
|
|
78
|
+
placeholder: str("placeholder"),
|
|
79
|
+
emptyTitle: str("emptyTitle"),
|
|
80
|
+
emptyDescription: str("emptyDescription"),
|
|
81
|
+
buttonTooltip: str("buttonTooltip"),
|
|
82
|
+
composerHint: str("composerHint")
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function readClassNames(meta, overrides = {}) {
|
|
86
|
+
const configured = meta?.classNames && typeof meta.classNames === "object" ? meta.classNames : {};
|
|
87
|
+
const result = {};
|
|
88
|
+
for (const key of [
|
|
89
|
+
"root",
|
|
90
|
+
"panel",
|
|
91
|
+
"trigger",
|
|
92
|
+
"header",
|
|
93
|
+
"messages",
|
|
94
|
+
"message",
|
|
95
|
+
"userMessage",
|
|
96
|
+
"assistantMessage",
|
|
97
|
+
"emptyState",
|
|
98
|
+
"composer",
|
|
99
|
+
"input",
|
|
100
|
+
"markdown"
|
|
101
|
+
]) {
|
|
102
|
+
const value = overrides[key] ?? configured[key];
|
|
103
|
+
if (typeof value === "string" && value) result[key] = value;
|
|
104
|
+
}
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
6
107
|
function useAskAi(options = {}) {
|
|
7
108
|
const [messages, setMessages] = useState([]);
|
|
8
109
|
const [input, setInput] = useState("");
|
|
@@ -11,9 +112,12 @@ function useAskAi(options = {}) {
|
|
|
11
112
|
const submitAbortRef = useRef(null);
|
|
12
113
|
const pendingTextRef = useRef({ value: "" });
|
|
13
114
|
const pendingRafRef = useRef(null);
|
|
115
|
+
const messageIdRef = useRef(0);
|
|
14
116
|
const askAiPluginMeta = useConfig()?.plugins?.find((p) => p.name === "boltdocs-plugin-ask-ai")?.metadata;
|
|
15
117
|
const customEndpoint = options.endpoint || askAiPluginMeta?.endpoint || "/api/ask-ai";
|
|
16
118
|
const devMode = askAiPluginMeta?.devMode ?? false;
|
|
119
|
+
const ui = readUiCopy(askAiPluginMeta);
|
|
120
|
+
const classNames = readClassNames(askAiPluginMeta, options.classNames);
|
|
17
121
|
useEffect(() => {
|
|
18
122
|
const handleOpen = () => setIsOpen(true);
|
|
19
123
|
const handleClose = () => setIsOpen(false);
|
|
@@ -27,6 +131,10 @@ function useAskAi(options = {}) {
|
|
|
27
131
|
window.removeEventListener("boltdocs:ask-ai:toggle", handleToggle);
|
|
28
132
|
};
|
|
29
133
|
}, []);
|
|
134
|
+
useEffect(() => () => {
|
|
135
|
+
submitAbortRef.current?.abort();
|
|
136
|
+
if (pendingRafRef.current !== null) cancelAnimationFrame(pendingRafRef.current);
|
|
137
|
+
}, []);
|
|
30
138
|
const startTimeRef = useRef(0);
|
|
31
139
|
const usageRef = useRef(null);
|
|
32
140
|
const flushPendingText = useCallback(() => {
|
|
@@ -38,9 +146,13 @@ function useAskAi(options = {}) {
|
|
|
38
146
|
const chunk = pendingTextRef.current.value;
|
|
39
147
|
pendingTextRef.current.value = "";
|
|
40
148
|
setMessages((prev) => {
|
|
41
|
-
const next =
|
|
42
|
-
const
|
|
43
|
-
|
|
149
|
+
const next = prev.slice();
|
|
150
|
+
const i = next.length - 1;
|
|
151
|
+
const last = next[i];
|
|
152
|
+
if (last && last.role === "assistant") next[i] = {
|
|
153
|
+
...last,
|
|
154
|
+
content: last.content + chunk
|
|
155
|
+
};
|
|
44
156
|
return next;
|
|
45
157
|
});
|
|
46
158
|
}, []);
|
|
@@ -56,16 +168,16 @@ function useAskAi(options = {}) {
|
|
|
56
168
|
setMessages((prev) => {
|
|
57
169
|
const next = [...prev];
|
|
58
170
|
const last = next[next.length - 1];
|
|
59
|
-
if (last && last.role === "assistant" && last.status !== "done") {
|
|
60
|
-
last
|
|
61
|
-
|
|
62
|
-
|
|
171
|
+
if (last && last.role === "assistant" && last.status !== "done" && last.status !== "cancelled") next[next.length - 1] = {
|
|
172
|
+
...last,
|
|
173
|
+
status: opts.kind === "cancel" ? "cancelled" : "error",
|
|
174
|
+
...opts.kind === "error" && opts.message ? { errorMessage: opts.message } : {}
|
|
175
|
+
};
|
|
63
176
|
return next;
|
|
64
177
|
});
|
|
65
178
|
}, [flushPendingText]);
|
|
66
179
|
const stopStreaming = useCallback(() => {
|
|
67
180
|
if (submitAbortRef.current) submitAbortRef.current.abort();
|
|
68
|
-
setIsLoading(false);
|
|
69
181
|
finalizeAssistantTerminal({ kind: "cancel" });
|
|
70
182
|
}, [finalizeAssistantTerminal]);
|
|
71
183
|
useEffect(() => {
|
|
@@ -89,13 +201,18 @@ function useAskAi(options = {}) {
|
|
|
89
201
|
if (!trimmed || isLoading) return;
|
|
90
202
|
pendingTextRef.current = { value: "" };
|
|
91
203
|
pendingRafRef.current = null;
|
|
204
|
+
usageRef.current = null;
|
|
205
|
+
startTimeRef.current = Date.now();
|
|
206
|
+
const turnId = ++messageIdRef.current;
|
|
92
207
|
setMessages((prev) => [
|
|
93
208
|
...prev,
|
|
94
209
|
{
|
|
210
|
+
id: `message-${turnId}-user`,
|
|
95
211
|
role: "user",
|
|
96
212
|
content: trimmed
|
|
97
213
|
},
|
|
98
214
|
{
|
|
215
|
+
id: `message-${turnId}-assistant`,
|
|
99
216
|
role: "assistant",
|
|
100
217
|
content: "",
|
|
101
218
|
status: "reading"
|
|
@@ -103,26 +220,45 @@ function useAskAi(options = {}) {
|
|
|
103
220
|
]);
|
|
104
221
|
setInput("");
|
|
105
222
|
setIsLoading(true);
|
|
106
|
-
const
|
|
223
|
+
const replaceLastAssistant = (prev, apply) => {
|
|
224
|
+
const next = prev.slice();
|
|
225
|
+
const i = next.length - 1;
|
|
226
|
+
const last = next[i];
|
|
227
|
+
if (last && last.role === "assistant") next[i] = apply(last);
|
|
228
|
+
return next;
|
|
229
|
+
};
|
|
230
|
+
const controller = createClientController();
|
|
107
231
|
submitAbortRef.current = controller;
|
|
108
232
|
try {
|
|
109
233
|
const currentPage = options.currentPage || (typeof window !== "undefined" ? window.location.pathname : "/");
|
|
110
|
-
const
|
|
234
|
+
const context = options.getContext?.() ?? options.context;
|
|
235
|
+
const init = {
|
|
111
236
|
method: "POST",
|
|
112
237
|
headers: { "Content-Type": "application/json" },
|
|
113
238
|
body: JSON.stringify({
|
|
114
239
|
question: trimmed,
|
|
115
|
-
currentPage
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
119
|
-
if (
|
|
240
|
+
currentPage,
|
|
241
|
+
...context ? { context } : {}
|
|
242
|
+
})
|
|
243
|
+
};
|
|
244
|
+
if (controller.signal) init.signal = controller.signal;
|
|
245
|
+
const response = await fetch(customEndpoint, init);
|
|
246
|
+
if (!response.ok) {
|
|
247
|
+
const errorBody = await response.text();
|
|
248
|
+
let errorMessage = `Server returned ${response.status}`;
|
|
249
|
+
try {
|
|
250
|
+
const parsed = JSON.parse(errorBody);
|
|
251
|
+
if (typeof parsed.error === "string") errorMessage = parsed.error;
|
|
252
|
+
} catch {}
|
|
253
|
+
throw new Error(errorMessage);
|
|
254
|
+
}
|
|
120
255
|
if (!response.body) throw new Error("No streaming body from endpoint");
|
|
121
256
|
const reader = response.body.getReader();
|
|
257
|
+
controller.attachReader(reader);
|
|
122
258
|
const decoder = new TextDecoder();
|
|
123
259
|
let lineBuffer = "";
|
|
124
260
|
let firstTextSeen = false;
|
|
125
|
-
while (
|
|
261
|
+
while (!controller.aborted) {
|
|
126
262
|
const { value, done } = await reader.read();
|
|
127
263
|
if (done) break;
|
|
128
264
|
lineBuffer += decoder.decode(value, { stream: true });
|
|
@@ -139,56 +275,61 @@ function useAskAi(options = {}) {
|
|
|
139
275
|
} catch {
|
|
140
276
|
continue;
|
|
141
277
|
}
|
|
142
|
-
if (parsed.context) setMessages((prev) => {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
return next;
|
|
150
|
-
});
|
|
278
|
+
if (parsed.context) setMessages((prev) => replaceLastAssistant(prev, (last) => {
|
|
279
|
+
startTimeRef.current = Date.now();
|
|
280
|
+
return {
|
|
281
|
+
...last,
|
|
282
|
+
contextChip: parsed.context
|
|
283
|
+
};
|
|
284
|
+
}));
|
|
151
285
|
else if (parsed.usage) usageRef.current = parsed.usage;
|
|
152
286
|
else if (typeof parsed.text === "string") {
|
|
153
287
|
if (!firstTextSeen) {
|
|
154
288
|
firstTextSeen = true;
|
|
155
|
-
setMessages((prev) => {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
return next;
|
|
160
|
-
});
|
|
289
|
+
setMessages((prev) => replaceLastAssistant(prev, (last) => ({
|
|
290
|
+
...last,
|
|
291
|
+
status: "streaming"
|
|
292
|
+
})));
|
|
161
293
|
}
|
|
162
294
|
pendingTextRef.current.value += parsed.text;
|
|
163
295
|
scheduleFlush();
|
|
164
|
-
} else if (parsed.error) throw new Error(parsed.error);
|
|
296
|
+
} else if (typeof parsed.error === "string") throw new Error(parsed.error);
|
|
165
297
|
}
|
|
166
298
|
}
|
|
299
|
+
if (controller.aborted) {
|
|
300
|
+
finalizeAssistantTerminal({ kind: "cancel" });
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
167
303
|
flushPendingText();
|
|
168
|
-
setMessages((prev) => {
|
|
169
|
-
const
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
});
|
|
304
|
+
setMessages((prev) => replaceLastAssistant(prev, (last) => {
|
|
305
|
+
const status = last.content ? "done" : "error";
|
|
306
|
+
const usage = usageRef.current && devMode ? {
|
|
307
|
+
...usageRef.current,
|
|
308
|
+
elapsedMs: startTimeRef.current ? Date.now() - startTimeRef.current : usageRef.current.elapsedMs
|
|
309
|
+
} : last.usage;
|
|
310
|
+
return {
|
|
311
|
+
...last,
|
|
312
|
+
status,
|
|
313
|
+
errorMessage: status === "error" && !last.errorMessage ? "No response received." : last.errorMessage,
|
|
314
|
+
usage
|
|
315
|
+
};
|
|
316
|
+
}));
|
|
181
317
|
} catch (error) {
|
|
182
318
|
if (error instanceof DOMException && error.name === "AbortError") finalizeAssistantTerminal({
|
|
183
319
|
kind: "cancel",
|
|
184
320
|
message: "Request cancelled."
|
|
185
321
|
});
|
|
186
322
|
else {
|
|
323
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
324
|
+
const userMessage = message.includes("AI_NOT_CONFIGURED") ? "The AI assistant is not configured on this server." : message.includes("RATE_LIMITED") ? "Too many requests. Please try again shortly." : message.includes("UNAUTHORIZED") ? "This assistant requires authorization." : message.includes("CLIENT_CONTEXT_NOT_ALLOWED") ? "This deployment does not accept client-provided context." : "The AI assistant could not complete this request.";
|
|
187
325
|
finalizeAssistantTerminal({
|
|
188
326
|
kind: "error",
|
|
189
|
-
message:
|
|
327
|
+
message: userMessage
|
|
190
328
|
});
|
|
191
|
-
|
|
329
|
+
setMessages((prev) => replaceLastAssistant(prev, (last) => last.status === "error" ? {
|
|
330
|
+
...last,
|
|
331
|
+
errorMessage: userMessage
|
|
332
|
+
} : last));
|
|
192
333
|
}
|
|
193
334
|
} finally {
|
|
194
335
|
if (pendingRafRef.current !== null) {
|
|
@@ -201,20 +342,225 @@ function useAskAi(options = {}) {
|
|
|
201
342
|
}, [
|
|
202
343
|
customEndpoint,
|
|
203
344
|
options.currentPage,
|
|
345
|
+
options.context,
|
|
346
|
+
options.getContext,
|
|
204
347
|
isLoading,
|
|
205
348
|
scheduleFlush,
|
|
206
|
-
finalizeAssistantTerminal
|
|
349
|
+
finalizeAssistantTerminal,
|
|
350
|
+
devMode,
|
|
351
|
+
flushPendingText
|
|
207
352
|
]),
|
|
208
353
|
stopStreaming,
|
|
209
354
|
clearChat,
|
|
210
355
|
isOpen,
|
|
211
356
|
setIsOpen,
|
|
212
|
-
devMode
|
|
357
|
+
devMode,
|
|
358
|
+
ui,
|
|
359
|
+
classNames
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
//#endregion
|
|
363
|
+
//#region src/client/cn.ts
|
|
364
|
+
function cn(...inputs) {
|
|
365
|
+
return twMerge(clsx(inputs));
|
|
366
|
+
}
|
|
367
|
+
//#endregion
|
|
368
|
+
//#region src/client/components/icons.tsx
|
|
369
|
+
function IconBase({ size = 16, className, strokeWidth = 2, children }) {
|
|
370
|
+
return /* @__PURE__ */ jsx("svg", {
|
|
371
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
372
|
+
width: size,
|
|
373
|
+
height: size,
|
|
374
|
+
viewBox: "0 0 24 24",
|
|
375
|
+
fill: "none",
|
|
376
|
+
stroke: "currentColor",
|
|
377
|
+
strokeWidth,
|
|
378
|
+
strokeLinecap: "round",
|
|
379
|
+
strokeLinejoin: "round",
|
|
380
|
+
className,
|
|
381
|
+
"aria-hidden": "true",
|
|
382
|
+
children
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
function SparkleIcon(props) {
|
|
386
|
+
return /* @__PURE__ */ jsx(IconBase, {
|
|
387
|
+
strokeWidth: 2.5,
|
|
388
|
+
...props,
|
|
389
|
+
children: /* @__PURE__ */ jsx("path", { d: "m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z" })
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
function ChatIcon(props) {
|
|
393
|
+
return /* @__PURE__ */ jsx(IconBase, {
|
|
394
|
+
...props,
|
|
395
|
+
children: /* @__PURE__ */ jsx("path", { d: "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" })
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
function TrashIcon(props) {
|
|
399
|
+
return /* @__PURE__ */ jsxs(IconBase, {
|
|
400
|
+
...props,
|
|
401
|
+
children: [
|
|
402
|
+
/* @__PURE__ */ jsx("path", { d: "M3 6h18" }),
|
|
403
|
+
/* @__PURE__ */ jsx("path", { d: "M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" }),
|
|
404
|
+
/* @__PURE__ */ jsx("path", { d: "M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" })
|
|
405
|
+
]
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
function CloseIcon(props) {
|
|
409
|
+
return /* @__PURE__ */ jsxs(IconBase, {
|
|
410
|
+
...props,
|
|
411
|
+
children: [/* @__PURE__ */ jsx("path", { d: "M18 6 6 18" }), /* @__PURE__ */ jsx("path", { d: "m6 6 12 12" })]
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
function SendIcon(props) {
|
|
415
|
+
return /* @__PURE__ */ jsxs(IconBase, {
|
|
416
|
+
strokeWidth: 2.5,
|
|
417
|
+
...props,
|
|
418
|
+
children: [/* @__PURE__ */ jsx("path", { d: "m22 2-7 20-4-9-9-4Z" }), /* @__PURE__ */ jsx("path", { d: "M22 2 11 13" })]
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
function StopIcon({ size = 16, className }) {
|
|
422
|
+
return /* @__PURE__ */ jsx("svg", {
|
|
423
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
424
|
+
width: size,
|
|
425
|
+
height: size,
|
|
426
|
+
viewBox: "0 0 24 24",
|
|
427
|
+
fill: "currentColor",
|
|
428
|
+
className,
|
|
429
|
+
"aria-hidden": "true",
|
|
430
|
+
children: /* @__PURE__ */ jsx("rect", {
|
|
431
|
+
x: "6",
|
|
432
|
+
y: "6",
|
|
433
|
+
width: "12",
|
|
434
|
+
height: "12",
|
|
435
|
+
rx: "2"
|
|
436
|
+
})
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
function FileIcon(props) {
|
|
440
|
+
return /* @__PURE__ */ jsxs(IconBase, {
|
|
441
|
+
...props,
|
|
442
|
+
children: [/* @__PURE__ */ jsx("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" }), /* @__PURE__ */ jsx("polyline", { points: "14 2 14 8 20 8" })]
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
//#endregion
|
|
446
|
+
//#region src/client/components/chat-empty-state.tsx
|
|
447
|
+
function ChatEmptyState({ description, title = "How can I help you today?", variant = "bubble", className }) {
|
|
448
|
+
const compact = variant === "dialog";
|
|
449
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
450
|
+
className: cn("flex flex-1 flex-col items-center justify-center text-center", compact ? "p-2" : "p-6", className),
|
|
451
|
+
children: [
|
|
452
|
+
/* @__PURE__ */ jsx("div", {
|
|
453
|
+
className: `${compact ? "w-10 h-10 rounded-xl" : "w-12 h-12 rounded-full"} bg-primary-500/10 flex items-center justify-center text-primary-500 mb-3`,
|
|
454
|
+
children: /* @__PURE__ */ jsx(SparkleIcon, { size: compact ? 20 : 24 })
|
|
455
|
+
}),
|
|
456
|
+
!compact && /* @__PURE__ */ jsx("h3", {
|
|
457
|
+
className: "text-sm font-semibold text-body mb-1",
|
|
458
|
+
children: title
|
|
459
|
+
}),
|
|
460
|
+
/* @__PURE__ */ jsx("p", {
|
|
461
|
+
className: compact ? "text-xs text-muted leading-relaxed" : "text-xs text-muted max-w-[240px]",
|
|
462
|
+
children: description
|
|
463
|
+
})
|
|
464
|
+
]
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
//#endregion
|
|
468
|
+
//#region src/client/components/chat-header.tsx
|
|
469
|
+
function ChatHeader({ title, variant = "bubble", canClear, onClear, onClose, className, titleId }) {
|
|
470
|
+
const compact = variant === "dialog";
|
|
471
|
+
return /* @__PURE__ */ jsx("header", {
|
|
472
|
+
className: cn("shrink-0 border-b border-subtle bg-gradient-to-r from-primary-500/8 to-transparent px-4 py-3", className),
|
|
473
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
474
|
+
className: "flex items-center justify-between gap-3",
|
|
475
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
476
|
+
className: "flex min-w-0 items-center gap-2.5",
|
|
477
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
478
|
+
className: "flex size-8 shrink-0 items-center justify-center rounded-xl bg-primary-500/12 text-primary-500",
|
|
479
|
+
children: /* @__PURE__ */ jsx(SparkleIcon, { size: compact ? 15 : 17 })
|
|
480
|
+
}), /* @__PURE__ */ jsx("h2", {
|
|
481
|
+
id: titleId,
|
|
482
|
+
className: cn("truncate font-semibold tracking-tight text-body", compact ? "text-xs" : "text-sm"),
|
|
483
|
+
children: title
|
|
484
|
+
})]
|
|
485
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
486
|
+
className: "flex items-center gap-1",
|
|
487
|
+
children: [canClear ? /* @__PURE__ */ jsx("button", {
|
|
488
|
+
type: "button",
|
|
489
|
+
onClick: onClear,
|
|
490
|
+
className: "cursor-pointer rounded-lg p-1.5 text-muted transition-colors hover:bg-soft hover:text-danger-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500/30",
|
|
491
|
+
title: "Clear chat",
|
|
492
|
+
"aria-label": "Clear chat",
|
|
493
|
+
children: /* @__PURE__ */ jsx(TrashIcon, { size: compact ? 13 : 15 })
|
|
494
|
+
}) : null, /* @__PURE__ */ jsx("button", {
|
|
495
|
+
type: "button",
|
|
496
|
+
onClick: onClose,
|
|
497
|
+
className: "cursor-pointer rounded-lg p-1.5 text-muted transition-colors hover:bg-soft hover:text-body focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500/30",
|
|
498
|
+
title: "Close assistant",
|
|
499
|
+
"aria-label": "Close assistant",
|
|
500
|
+
children: /* @__PURE__ */ jsx(CloseIcon, { size: compact ? 13 : 16 })
|
|
501
|
+
})]
|
|
502
|
+
})]
|
|
503
|
+
})
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
//#endregion
|
|
507
|
+
//#region src/client/components/chat-input.tsx
|
|
508
|
+
function ChatInput({ input, onInputChange, onSubmit, onStop, isLoading, placeholder = "Ask about this page…", hint = "Enter to send · Shift+Enter for a new line", variant = "bubble", inputRef, className, inputClassName }) {
|
|
509
|
+
const compact = variant === "dialog";
|
|
510
|
+
const hintId = useId();
|
|
511
|
+
const canSubmit = Boolean(input.trim()) && !isLoading;
|
|
512
|
+
const rows = Math.min(Math.max(input.split("\n").length, 1), 6);
|
|
513
|
+
const handleKeyDown = (event) => {
|
|
514
|
+
if (event.key !== "Enter" || event.shiftKey) return;
|
|
515
|
+
event.preventDefault();
|
|
516
|
+
if (canSubmit) onSubmit(input);
|
|
213
517
|
};
|
|
518
|
+
const handleSubmit = (event) => {
|
|
519
|
+
event.preventDefault();
|
|
520
|
+
if (canSubmit) onSubmit(input);
|
|
521
|
+
};
|
|
522
|
+
const buttonSize = compact ? "size-8" : "size-9";
|
|
523
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
524
|
+
className: cn("flex flex-col gap-1.5", compact ? "shrink-0" : "shrink-0", className),
|
|
525
|
+
children: [/* @__PURE__ */ jsxs("form", {
|
|
526
|
+
onSubmit: handleSubmit,
|
|
527
|
+
className: cn("flex items-end gap-2 rounded-2xl border border-subtle bg-surface/80 p-1.5 shadow-sm transition-colors focus-within:border-primary-500/60 focus-within:ring-2 focus-within:ring-primary-500/10", inputClassName),
|
|
528
|
+
children: [/* @__PURE__ */ jsx("textarea", {
|
|
529
|
+
ref: inputRef,
|
|
530
|
+
rows,
|
|
531
|
+
value: input,
|
|
532
|
+
onChange: (event) => onInputChange(event.target.value),
|
|
533
|
+
onKeyDown: handleKeyDown,
|
|
534
|
+
placeholder,
|
|
535
|
+
disabled: isLoading,
|
|
536
|
+
"aria-label": "Ask the assistant",
|
|
537
|
+
"aria-describedby": hintId,
|
|
538
|
+
className: cn("min-h-8 flex-1 resize-none bg-transparent px-2 py-1.5 text-body leading-relaxed outline-none placeholder:text-muted", compact ? "text-xs" : "text-sm")
|
|
539
|
+
}), isLoading ? /* @__PURE__ */ jsx("button", {
|
|
540
|
+
type: "button",
|
|
541
|
+
onClick: onStop,
|
|
542
|
+
title: "Stop generating",
|
|
543
|
+
"aria-label": "Stop generating",
|
|
544
|
+
className: cn(buttonSize, "flex shrink-0 cursor-pointer select-none items-center justify-center rounded-full bg-danger-500 text-white transition-opacity hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-danger-500/40"),
|
|
545
|
+
children: /* @__PURE__ */ jsx(StopIcon, { size: compact ? 12 : 16 })
|
|
546
|
+
}) : /* @__PURE__ */ jsx("button", {
|
|
547
|
+
type: "submit",
|
|
548
|
+
disabled: !input.trim(),
|
|
549
|
+
title: "Send question",
|
|
550
|
+
"aria-label": "Send question",
|
|
551
|
+
className: cn(buttonSize, "flex shrink-0 cursor-pointer select-none items-center justify-center rounded-full bg-primary-500 text-white transition-all hover:bg-primary-600 disabled:cursor-not-allowed disabled:opacity-40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500/40"),
|
|
552
|
+
children: /* @__PURE__ */ jsx(SendIcon, { size: compact ? 12 : 16 })
|
|
553
|
+
})]
|
|
554
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
555
|
+
id: hintId,
|
|
556
|
+
className: cn("select-none px-1 text-muted", compact ? "text-[10px]" : "text-[11px]"),
|
|
557
|
+
children: hint
|
|
558
|
+
})]
|
|
559
|
+
});
|
|
214
560
|
}
|
|
215
561
|
//#endregion
|
|
216
562
|
//#region src/client/render-markdown.tsx
|
|
217
|
-
function
|
|
563
|
+
function MarkdownRendererImpl({ content, className, parseIncompleteMarkdown = true }) {
|
|
218
564
|
if (!content) return null;
|
|
219
565
|
return /* @__PURE__ */ jsx("div", {
|
|
220
566
|
className,
|
|
@@ -224,313 +570,283 @@ function MarkdownRenderer({ content, className, parseIncompleteMarkdown = true }
|
|
|
224
570
|
})
|
|
225
571
|
});
|
|
226
572
|
}
|
|
573
|
+
/**
|
|
574
|
+
* Memoized: while the message streams the same buffered content can be
|
|
575
|
+
* re-rendered (e.g. a status flip reading → streaming) — this skips the
|
|
576
|
+
* heavy markdown re-parse when nothing changed.
|
|
577
|
+
*/
|
|
578
|
+
const MarkdownRenderer = memo(MarkdownRendererImpl);
|
|
579
|
+
//#endregion
|
|
580
|
+
//#region src/client/components/typing-indicator.tsx
|
|
581
|
+
function Dot({ delay }) {
|
|
582
|
+
return /* @__PURE__ */ jsx("span", {
|
|
583
|
+
className: "w-1.5 h-1.5 rounded-full bg-primary-500 animate-bounce",
|
|
584
|
+
style: { animationDelay: delay }
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
function TypingIndicator({ label = "Waiting…" }) {
|
|
588
|
+
return /* @__PURE__ */ jsxs("span", {
|
|
589
|
+
className: "inline-flex items-center gap-2 text-xs text-muted",
|
|
590
|
+
children: [/* @__PURE__ */ jsxs("span", {
|
|
591
|
+
className: "flex items-center gap-1",
|
|
592
|
+
children: [
|
|
593
|
+
/* @__PURE__ */ jsx(Dot, { delay: "0ms" }),
|
|
594
|
+
/* @__PURE__ */ jsx(Dot, { delay: "120ms" }),
|
|
595
|
+
/* @__PURE__ */ jsx(Dot, { delay: "240ms" })
|
|
596
|
+
]
|
|
597
|
+
}), label]
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
//#endregion
|
|
601
|
+
//#region src/client/components/chat-message.tsx
|
|
602
|
+
function UsageChip({ usage, compact }) {
|
|
603
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
604
|
+
className: "flex items-center gap-1.5 px-2 py-1 mb-1 text-[11px] text-muted font-mono",
|
|
605
|
+
children: [
|
|
606
|
+
/* @__PURE__ */ jsx("span", {
|
|
607
|
+
className: "px-1 py-0.5 rounded bg-warning-500/15 text-warning-500 text-[10px] font-bold uppercase tracking-wide",
|
|
608
|
+
children: "DEV"
|
|
609
|
+
}),
|
|
610
|
+
/* @__PURE__ */ jsxs("span", {
|
|
611
|
+
title: "Provider / model",
|
|
612
|
+
children: [
|
|
613
|
+
usage.provider,
|
|
614
|
+
"/",
|
|
615
|
+
usage.model
|
|
616
|
+
]
|
|
617
|
+
}),
|
|
618
|
+
/* @__PURE__ */ jsx("span", {
|
|
619
|
+
className: "text-muted",
|
|
620
|
+
children: "·"
|
|
621
|
+
}),
|
|
622
|
+
/* @__PURE__ */ jsxs("span", {
|
|
623
|
+
title: "Prompt tokens",
|
|
624
|
+
children: [usage.promptTokens, "↑"]
|
|
625
|
+
}),
|
|
626
|
+
/* @__PURE__ */ jsxs("span", {
|
|
627
|
+
title: "Completion tokens",
|
|
628
|
+
children: [usage.completionTokens, "↓"]
|
|
629
|
+
}),
|
|
630
|
+
/* @__PURE__ */ jsx("span", {
|
|
631
|
+
className: "text-muted",
|
|
632
|
+
children: "·"
|
|
633
|
+
}),
|
|
634
|
+
/* @__PURE__ */ jsxs("span", {
|
|
635
|
+
title: "Total tokens",
|
|
636
|
+
className: "text-primary-500 font-semibold",
|
|
637
|
+
children: [usage.totalTokens, " tok"]
|
|
638
|
+
}),
|
|
639
|
+
!compact && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
|
|
640
|
+
className: "text-muted",
|
|
641
|
+
children: "·"
|
|
642
|
+
}), /* @__PURE__ */ jsxs("span", {
|
|
643
|
+
title: "Elapsed",
|
|
644
|
+
children: [usage.elapsedMs, "ms"]
|
|
645
|
+
})] })
|
|
646
|
+
]
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
function ContextChip({ chip, compact }) {
|
|
650
|
+
return /* @__PURE__ */ jsx("div", {
|
|
651
|
+
className: "flex items-center gap-1.5 px-2 py-1 mb-1 text-[11px] text-muted",
|
|
652
|
+
children: chip.missing ? /* @__PURE__ */ jsx("span", { children: compact ? "No docs in scope" : "No docs page in scope" }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
653
|
+
/* @__PURE__ */ jsx(FileIcon, { size: 12 }),
|
|
654
|
+
/* @__PURE__ */ jsx("span", { children: "Reading" }),
|
|
655
|
+
/* @__PURE__ */ jsx("code", {
|
|
656
|
+
className: "px-1 py-0.5 rounded bg-surface text-primary-500 text-[10px] font-mono",
|
|
657
|
+
children: chip.page
|
|
658
|
+
}),
|
|
659
|
+
/* @__PURE__ */ jsx("span", {
|
|
660
|
+
className: "text-muted",
|
|
661
|
+
children: "·"
|
|
662
|
+
}),
|
|
663
|
+
/* @__PURE__ */ jsxs("span", { children: [chip.chars, compact ? "c" : " chars"] }),
|
|
664
|
+
!compact && typeof chip.elapsedMs === "number" && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
|
|
665
|
+
className: "text-muted",
|
|
666
|
+
children: "·"
|
|
667
|
+
}), /* @__PURE__ */ jsxs("span", { children: [chip.elapsedMs, "ms"] })] })
|
|
668
|
+
] })
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
function ChatMessageImpl({ message: msg, variant = "bubble", devMode, className, userClassName, assistantClassName, markdownClassName }) {
|
|
672
|
+
const compact = variant === "dialog";
|
|
673
|
+
const isUser = msg.role === "user";
|
|
674
|
+
const containerClass = cn("flex flex-col", compact ? "max-w-full" : "max-w-[85%]", isUser ? "ml-auto items-end" : "items-start", className);
|
|
675
|
+
const bubbleClass = cn("rounded-2xl px-3.5 py-2.5 text-left", isUser ? cn("rounded-br-md bg-primary-500 text-white shadow-sm", compact && "max-w-[90%]", userClassName) : msg.status === "error" ? cn("rounded-bl-md border border-danger-500/30 bg-danger-500/10 text-danger-500", assistantClassName) : cn("rounded-bl-md border border-subtle bg-surface text-body shadow-sm", assistantClassName));
|
|
676
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
677
|
+
className: containerClass,
|
|
678
|
+
children: [
|
|
679
|
+
msg.role === "assistant" && msg.usage && devMode && /* @__PURE__ */ jsx(UsageChip, {
|
|
680
|
+
usage: msg.usage,
|
|
681
|
+
compact
|
|
682
|
+
}),
|
|
683
|
+
msg.role === "assistant" && msg.contextChip && /* @__PURE__ */ jsx(ContextChip, {
|
|
684
|
+
chip: msg.contextChip,
|
|
685
|
+
compact
|
|
686
|
+
}),
|
|
687
|
+
/* @__PURE__ */ jsx("div", {
|
|
688
|
+
className: bubbleClass,
|
|
689
|
+
children: isUser ? /* @__PURE__ */ jsx("p", {
|
|
690
|
+
className: `whitespace-pre-wrap ${compact ? "text-xs" : "text-sm"}`,
|
|
691
|
+
children: msg.content
|
|
692
|
+
}) : msg.status === "cancelled" ? msg.content ? /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("p", {
|
|
693
|
+
className: "whitespace-pre-wrap text-sm text-body",
|
|
694
|
+
children: msg.content
|
|
695
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
696
|
+
className: "mt-2 text-[11px] text-muted",
|
|
697
|
+
children: "Generation stopped."
|
|
698
|
+
})] }) : /* @__PURE__ */ jsx("p", {
|
|
699
|
+
className: cn("text-muted", compact ? "text-xs" : "text-sm"),
|
|
700
|
+
children: "Generation stopped."
|
|
701
|
+
}) : msg.status === "error" ? msg.content ? /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("p", {
|
|
702
|
+
className: "whitespace-pre-wrap text-sm text-body",
|
|
703
|
+
children: msg.content
|
|
704
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
705
|
+
className: "mt-2 text-xs text-danger-500",
|
|
706
|
+
children: msg.errorMessage || "Something went wrong."
|
|
707
|
+
})] }) : /* @__PURE__ */ jsxs("p", {
|
|
708
|
+
className: compact ? "text-xs" : "text-sm",
|
|
709
|
+
children: [
|
|
710
|
+
/* @__PURE__ */ jsx("strong", { children: "Error:" }),
|
|
711
|
+
" ",
|
|
712
|
+
msg.errorMessage || "Something went wrong."
|
|
713
|
+
]
|
|
714
|
+
}) : /* @__PURE__ */ jsx("div", {
|
|
715
|
+
className: "ask-ai-streamdown",
|
|
716
|
+
children: msg.content ? /* @__PURE__ */ jsx(MarkdownRenderer, {
|
|
717
|
+
content: msg.content,
|
|
718
|
+
className: cn("text-sm leading-relaxed", markdownClassName)
|
|
719
|
+
}) : msg.status === "reading" ? /* @__PURE__ */ jsx(TypingIndicator, { label: "Reading page…" }) : /* @__PURE__ */ jsx(TypingIndicator, { label: "Waiting…" })
|
|
720
|
+
})
|
|
721
|
+
})
|
|
722
|
+
]
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Memoized so that during streaming only the message whose content actually
|
|
727
|
+
* changed re-renders. The hook structurally shares the array and replaces the
|
|
728
|
+
* final assistant message with a new object, so all older siblings keep their
|
|
729
|
+
* identity and are skipped by the default shallow comparison.
|
|
730
|
+
*/
|
|
731
|
+
const ChatMessage = memo(ChatMessageImpl);
|
|
227
732
|
//#endregion
|
|
228
733
|
//#region src/client/components/ask-ai-bubble.tsx
|
|
229
|
-
function AskAiBubble() {
|
|
230
|
-
const { messages, input, setInput, isLoading, submitQuestion, stopStreaming, clearChat, isOpen, setIsOpen, devMode } = useAskAi();
|
|
734
|
+
function AskAiBubble(props = {}) {
|
|
735
|
+
const { messages, input, setInput, isLoading, submitQuestion, stopStreaming, clearChat, isOpen, setIsOpen, devMode, ui, classNames = {} } = useAskAi(props);
|
|
231
736
|
const messagesEndRef = useRef(null);
|
|
737
|
+
const inputRef = useRef(null);
|
|
738
|
+
const triggerRef = useRef(null);
|
|
739
|
+
const wasOpenRef = useRef(false);
|
|
740
|
+
const titleId = useId();
|
|
741
|
+
const panelId = useId();
|
|
742
|
+
const lastMessageContent = messages.at(-1)?.content;
|
|
232
743
|
useEffect(() => {
|
|
233
|
-
if (isOpen)
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
744
|
+
if (isOpen) {
|
|
745
|
+
wasOpenRef.current = true;
|
|
746
|
+
inputRef.current?.focus();
|
|
747
|
+
} else if (wasOpenRef.current) {
|
|
748
|
+
wasOpenRef.current = false;
|
|
749
|
+
triggerRef.current?.focus();
|
|
750
|
+
}
|
|
751
|
+
}, [isOpen]);
|
|
752
|
+
useEffect(() => {
|
|
753
|
+
if (isOpen && lastMessageContent !== void 0) messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
754
|
+
}, [isOpen, lastMessageContent]);
|
|
240
755
|
return /* @__PURE__ */ jsxs("div", {
|
|
241
|
-
className: "fixed
|
|
242
|
-
children: [isOpen
|
|
243
|
-
|
|
756
|
+
className: cn("pointer-events-none fixed inset-x-0 bottom-0 z-50 flex flex-col items-stretch md:inset-x-auto md:bottom-6 md:right-6 md:items-end", classNames.root),
|
|
757
|
+
children: [isOpen ? /* @__PURE__ */ jsxs("section", {
|
|
758
|
+
id: panelId,
|
|
759
|
+
role: "dialog",
|
|
760
|
+
"aria-modal": "false",
|
|
761
|
+
"aria-labelledby": titleId,
|
|
762
|
+
tabIndex: -1,
|
|
763
|
+
onKeyDown: (event) => {
|
|
764
|
+
if (event.key === "Escape") {
|
|
765
|
+
setIsOpen(false);
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
if (event.key !== "Tab") return;
|
|
769
|
+
const focusable = Array.from(event.currentTarget.querySelectorAll("button:not([disabled]), textarea:not([disabled]), [href], input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex=\"-1\"])"));
|
|
770
|
+
if (focusable.length === 0) return;
|
|
771
|
+
const first = focusable[0];
|
|
772
|
+
const last = focusable[focusable.length - 1];
|
|
773
|
+
if (event.shiftKey && document.activeElement === first) {
|
|
774
|
+
event.preventDefault();
|
|
775
|
+
last.focus();
|
|
776
|
+
} else if (!event.shiftKey && document.activeElement === last) {
|
|
777
|
+
event.preventDefault();
|
|
778
|
+
first.focus();
|
|
779
|
+
}
|
|
780
|
+
},
|
|
781
|
+
className: cn("pointer-events-auto mb-0 flex h-[min(100dvh,680px)] w-full flex-col overflow-hidden rounded-t-3xl border border-subtle bg-main/95 shadow-2xl shadow-black/10 backdrop-blur-xl motion-safe:animate-in motion-safe:fade-in motion-safe:slide-in-from-bottom-4 md:mb-4 md:h-[min(560px,calc(100vh-8rem))] md:w-[400px] md:max-w-[calc(100vw-2rem)] md:rounded-3xl", classNames.panel),
|
|
244
782
|
children: [
|
|
245
|
-
/* @__PURE__ */
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
height: "16",
|
|
253
|
-
viewBox: "0 0 24 24",
|
|
254
|
-
fill: "none",
|
|
255
|
-
stroke: "currentColor",
|
|
256
|
-
strokeWidth: "2.5",
|
|
257
|
-
strokeLinecap: "round",
|
|
258
|
-
strokeLinejoin: "round",
|
|
259
|
-
className: "text-primary-500",
|
|
260
|
-
children: /* @__PURE__ */ jsx("path", { d: "m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z" })
|
|
261
|
-
}), /* @__PURE__ */ jsx("span", {
|
|
262
|
-
className: "text-sm font-semibold text-body",
|
|
263
|
-
children: "Ask Assistant"
|
|
264
|
-
})]
|
|
265
|
-
}), /* @__PURE__ */ jsxs("div", {
|
|
266
|
-
className: "flex items-center gap-1",
|
|
267
|
-
children: [messages.length > 0 && /* @__PURE__ */ jsx("button", {
|
|
268
|
-
onClick: clearChat,
|
|
269
|
-
className: "p-1 text-muted hover:text-red-500 hover:bg-surface rounded-lg transition-colors cursor-pointer",
|
|
270
|
-
title: "Clear chat",
|
|
271
|
-
"aria-label": "Clear chat",
|
|
272
|
-
children: /* @__PURE__ */ jsxs("svg", {
|
|
273
|
-
xmlns: "http://www.w3.org/2000/svg",
|
|
274
|
-
width: "14",
|
|
275
|
-
height: "14",
|
|
276
|
-
viewBox: "0 0 24 24",
|
|
277
|
-
fill: "none",
|
|
278
|
-
stroke: "currentColor",
|
|
279
|
-
strokeWidth: "2",
|
|
280
|
-
strokeLinecap: "round",
|
|
281
|
-
strokeLinejoin: "round",
|
|
282
|
-
children: [
|
|
283
|
-
/* @__PURE__ */ jsx("path", { d: "M3 6h18" }),
|
|
284
|
-
/* @__PURE__ */ jsx("path", { d: "M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" }),
|
|
285
|
-
/* @__PURE__ */ jsx("path", { d: "M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" })
|
|
286
|
-
]
|
|
287
|
-
})
|
|
288
|
-
}), /* @__PURE__ */ jsx("button", {
|
|
289
|
-
onClick: () => setIsOpen(false),
|
|
290
|
-
className: "p-1 text-muted hover:text-body hover:bg-surface rounded-lg transition-colors cursor-pointer",
|
|
291
|
-
title: "Close assistant",
|
|
292
|
-
"aria-label": "Close assistant",
|
|
293
|
-
children: /* @__PURE__ */ jsxs("svg", {
|
|
294
|
-
xmlns: "http://www.w3.org/2000/svg",
|
|
295
|
-
width: "16",
|
|
296
|
-
height: "16",
|
|
297
|
-
viewBox: "0 0 24 24",
|
|
298
|
-
fill: "none",
|
|
299
|
-
stroke: "currentColor",
|
|
300
|
-
strokeWidth: "2",
|
|
301
|
-
strokeLinecap: "round",
|
|
302
|
-
strokeLinejoin: "round",
|
|
303
|
-
children: [/* @__PURE__ */ jsx("path", { d: "M18 6 6 18" }), /* @__PURE__ */ jsx("path", { d: "m6 6 12 12" })]
|
|
304
|
-
})
|
|
305
|
-
})]
|
|
306
|
-
})]
|
|
783
|
+
/* @__PURE__ */ jsx(ChatHeader, {
|
|
784
|
+
title: ui.title,
|
|
785
|
+
canClear: messages.length > 0,
|
|
786
|
+
onClear: clearChat,
|
|
787
|
+
onClose: () => setIsOpen(false),
|
|
788
|
+
className: classNames.header,
|
|
789
|
+
titleId
|
|
307
790
|
}),
|
|
308
791
|
/* @__PURE__ */ jsxs("div", {
|
|
309
|
-
|
|
792
|
+
role: "log",
|
|
793
|
+
"aria-live": "polite",
|
|
794
|
+
"aria-relevant": "additions text",
|
|
795
|
+
"aria-label": "Assistant messages",
|
|
796
|
+
className: cn("flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto overscroll-contain p-4", classNames.messages),
|
|
310
797
|
children: [
|
|
311
|
-
messages.length === 0
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
strokeLinecap: "round",
|
|
325
|
-
strokeLinejoin: "round",
|
|
326
|
-
children: /* @__PURE__ */ jsx("path", { d: "m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z" })
|
|
327
|
-
})
|
|
328
|
-
}),
|
|
329
|
-
/* @__PURE__ */ jsx("h3", {
|
|
330
|
-
className: "text-sm font-semibold text-body mb-1",
|
|
331
|
-
children: "How can I help you today?"
|
|
332
|
-
}),
|
|
333
|
-
/* @__PURE__ */ jsx("p", {
|
|
334
|
-
className: "text-xs text-muted max-w-[240px]",
|
|
335
|
-
children: "Ask questions about the current documentation page. The assistant only answers using the page you're viewing."
|
|
336
|
-
})
|
|
337
|
-
]
|
|
338
|
-
}),
|
|
339
|
-
messages.map((msg, i) => /* @__PURE__ */ jsxs("div", {
|
|
340
|
-
className: `flex flex-col max-w-[85%] ${msg.role === "user" ? "align-self-end items-end ml-auto" : "align-self-start items-start"}`,
|
|
341
|
-
children: [
|
|
342
|
-
msg.role === "assistant" && msg.usage && devMode && /* @__PURE__ */ jsxs("div", {
|
|
343
|
-
className: "flex items-center gap-1.5 px-2 py-1 mb-1 text-[11px] text-muted font-mono",
|
|
344
|
-
children: [
|
|
345
|
-
/* @__PURE__ */ jsx("span", {
|
|
346
|
-
className: "px-1 py-0.5 rounded bg-amber-500/10 text-amber-600 dark:text-amber-400 text-[10px] font-bold uppercase tracking-wide",
|
|
347
|
-
children: "DEV"
|
|
348
|
-
}),
|
|
349
|
-
/* @__PURE__ */ jsxs("span", {
|
|
350
|
-
title: "Provider / model",
|
|
351
|
-
children: [
|
|
352
|
-
msg.usage.provider,
|
|
353
|
-
"/",
|
|
354
|
-
msg.usage.model
|
|
355
|
-
]
|
|
356
|
-
}),
|
|
357
|
-
/* @__PURE__ */ jsx("span", {
|
|
358
|
-
className: "text-muted",
|
|
359
|
-
children: "·"
|
|
360
|
-
}),
|
|
361
|
-
/* @__PURE__ */ jsxs("span", {
|
|
362
|
-
title: "Prompt tokens",
|
|
363
|
-
children: [msg.usage.promptTokens, "↑"]
|
|
364
|
-
}),
|
|
365
|
-
/* @__PURE__ */ jsxs("span", {
|
|
366
|
-
title: "Completion tokens",
|
|
367
|
-
children: [msg.usage.completionTokens, "↓"]
|
|
368
|
-
}),
|
|
369
|
-
/* @__PURE__ */ jsx("span", {
|
|
370
|
-
className: "text-muted",
|
|
371
|
-
children: "·"
|
|
372
|
-
}),
|
|
373
|
-
/* @__PURE__ */ jsxs("span", {
|
|
374
|
-
title: "Total tokens",
|
|
375
|
-
className: "text-primary-500 font-semibold",
|
|
376
|
-
children: [msg.usage.totalTokens, " tok"]
|
|
377
|
-
}),
|
|
378
|
-
/* @__PURE__ */ jsx("span", {
|
|
379
|
-
className: "text-muted",
|
|
380
|
-
children: "·"
|
|
381
|
-
}),
|
|
382
|
-
/* @__PURE__ */ jsxs("span", {
|
|
383
|
-
title: "Elapsed",
|
|
384
|
-
children: [msg.usage.elapsedMs, "ms"]
|
|
385
|
-
})
|
|
386
|
-
]
|
|
387
|
-
}),
|
|
388
|
-
msg.role === "assistant" && msg.contextChip && /* @__PURE__ */ jsx("div", {
|
|
389
|
-
className: "flex items-center gap-1.5 px-2 py-1 mb-1 text-[11px] text-muted",
|
|
390
|
-
children: msg.contextChip.missing ? /* @__PURE__ */ jsx("span", { children: "No docs page in scope" }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
391
|
-
/* @__PURE__ */ jsxs("svg", {
|
|
392
|
-
xmlns: "http://www.w3.org/2000/svg",
|
|
393
|
-
width: "12",
|
|
394
|
-
height: "12",
|
|
395
|
-
viewBox: "0 0 24 24",
|
|
396
|
-
fill: "none",
|
|
397
|
-
stroke: "currentColor",
|
|
398
|
-
strokeWidth: "2",
|
|
399
|
-
strokeLinecap: "round",
|
|
400
|
-
strokeLinejoin: "round",
|
|
401
|
-
children: [/* @__PURE__ */ jsx("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" }), /* @__PURE__ */ jsx("polyline", { points: "14 2 14 8 20 8" })]
|
|
402
|
-
}),
|
|
403
|
-
/* @__PURE__ */ jsx("span", { children: "Reading" }),
|
|
404
|
-
/* @__PURE__ */ jsx("code", {
|
|
405
|
-
className: "px-1 py-0.5 rounded bg-surface text-primary-500 text-[10px] font-mono",
|
|
406
|
-
children: msg.contextChip.page
|
|
407
|
-
}),
|
|
408
|
-
/* @__PURE__ */ jsx("span", {
|
|
409
|
-
className: "text-muted",
|
|
410
|
-
children: "·"
|
|
411
|
-
}),
|
|
412
|
-
/* @__PURE__ */ jsxs("span", { children: [msg.contextChip.chars, " chars"] }),
|
|
413
|
-
typeof msg.contextChip.elapsedMs === "number" && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
|
|
414
|
-
className: "text-muted",
|
|
415
|
-
children: "·"
|
|
416
|
-
}), /* @__PURE__ */ jsxs("span", { children: [msg.contextChip.elapsedMs, "ms"] })] })
|
|
417
|
-
] })
|
|
418
|
-
}),
|
|
419
|
-
/* @__PURE__ */ jsx("div", {
|
|
420
|
-
className: `px-3 py-2 rounded-xl ${msg.role === "user" ? "bg-primary-500 text-white rounded-br-none" : msg.status === "error" ? "bg-red-500/10 border border-red-500/30 text-red-600 dark:text-red-400 rounded-bl-none" : "bg-surface border border-subtle text-body rounded-bl-none"}`,
|
|
421
|
-
children: msg.role === "user" ? /* @__PURE__ */ jsx("p", {
|
|
422
|
-
className: "whitespace-pre-wrap text-sm",
|
|
423
|
-
children: msg.content
|
|
424
|
-
}) : msg.status === "error" ? /* @__PURE__ */ jsxs("p", {
|
|
425
|
-
className: "text-sm",
|
|
426
|
-
children: [
|
|
427
|
-
/* @__PURE__ */ jsx("strong", { children: "Error:" }),
|
|
428
|
-
" ",
|
|
429
|
-
msg.errorMessage || "Something went wrong."
|
|
430
|
-
]
|
|
431
|
-
}) : /* @__PURE__ */ jsx("div", {
|
|
432
|
-
className: "ask-ai-streamdown",
|
|
433
|
-
children: msg.content ? /* @__PURE__ */ jsx(MarkdownRenderer, { content: msg.content }) : msg.status === "reading" ? /* @__PURE__ */ jsxs("span", {
|
|
434
|
-
className: "inline-flex items-center gap-1.5 text-xs text-muted",
|
|
435
|
-
children: [/* @__PURE__ */ jsx("span", { className: "w-1.5 h-1.5 rounded-full bg-primary-500 animate-pulse" }), /* @__PURE__ */ jsx("span", { children: "Reading page…" })]
|
|
436
|
-
}) : /* @__PURE__ */ jsxs("span", {
|
|
437
|
-
className: "inline-flex items-center gap-1.5 text-xs text-muted",
|
|
438
|
-
children: [/* @__PURE__ */ jsx("span", { className: "w-1.5 h-1.5 rounded-full bg-primary-500 animate-pulse" }), /* @__PURE__ */ jsx("span", { children: "Waiting…" })]
|
|
439
|
-
})
|
|
440
|
-
})
|
|
441
|
-
})
|
|
442
|
-
]
|
|
443
|
-
}, i)),
|
|
798
|
+
messages.length === 0 ? /* @__PURE__ */ jsx(ChatEmptyState, {
|
|
799
|
+
title: ui.emptyTitle,
|
|
800
|
+
description: ui.emptyDescription,
|
|
801
|
+
className: classNames.emptyState
|
|
802
|
+
}) : null,
|
|
803
|
+
messages.map((message, index) => /* @__PURE__ */ jsx(ChatMessage, {
|
|
804
|
+
message,
|
|
805
|
+
devMode,
|
|
806
|
+
className: classNames.message,
|
|
807
|
+
userClassName: classNames.userMessage,
|
|
808
|
+
assistantClassName: classNames.assistantMessage,
|
|
809
|
+
markdownClassName: classNames.markdown
|
|
810
|
+
}, message.id ?? `legacy-message-${index}`)),
|
|
444
811
|
/* @__PURE__ */ jsx("div", { ref: messagesEndRef })
|
|
445
812
|
]
|
|
446
813
|
}),
|
|
447
|
-
/* @__PURE__ */
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
type: "button",
|
|
459
|
-
onClick: stopStreaming,
|
|
460
|
-
className: "px-3 py-1.5 bg-red-500 hover:bg-red-600 text-white text-sm font-semibold rounded-xl flex items-center justify-center transition-colors cursor-pointer select-none",
|
|
461
|
-
title: "Stop generating",
|
|
462
|
-
"aria-label": "Stop generating",
|
|
463
|
-
children: /* @__PURE__ */ jsx("svg", {
|
|
464
|
-
xmlns: "http://www.w3.org/2000/svg",
|
|
465
|
-
width: "16",
|
|
466
|
-
height: "16",
|
|
467
|
-
viewBox: "0 0 24 24",
|
|
468
|
-
fill: "currentColor",
|
|
469
|
-
children: /* @__PURE__ */ jsx("rect", {
|
|
470
|
-
x: "6",
|
|
471
|
-
y: "6",
|
|
472
|
-
width: "12",
|
|
473
|
-
height: "12",
|
|
474
|
-
rx: "2"
|
|
475
|
-
})
|
|
476
|
-
})
|
|
477
|
-
}) : /* @__PURE__ */ jsx("button", {
|
|
478
|
-
type: "submit",
|
|
479
|
-
disabled: !input.trim(),
|
|
480
|
-
className: "px-3 py-1.5 bg-primary-500 hover:bg-primary-600 disabled:opacity-50 text-white text-sm font-semibold rounded-xl flex items-center justify-center transition-colors cursor-pointer select-none",
|
|
481
|
-
"aria-label": "Send question",
|
|
482
|
-
children: /* @__PURE__ */ jsxs("svg", {
|
|
483
|
-
xmlns: "http://www.w3.org/2000/svg",
|
|
484
|
-
width: "16",
|
|
485
|
-
height: "16",
|
|
486
|
-
viewBox: "0 0 24 24",
|
|
487
|
-
fill: "none",
|
|
488
|
-
stroke: "currentColor",
|
|
489
|
-
strokeWidth: "2.5",
|
|
490
|
-
strokeLinecap: "round",
|
|
491
|
-
strokeLinejoin: "round",
|
|
492
|
-
children: [/* @__PURE__ */ jsx("path", { d: "m22 2-7 20-4-9-9-4Z" }), /* @__PURE__ */ jsx("path", { d: "M22 2 11 13" })]
|
|
493
|
-
})
|
|
494
|
-
})]
|
|
814
|
+
/* @__PURE__ */ jsx(ChatInput, {
|
|
815
|
+
input,
|
|
816
|
+
onInputChange: setInput,
|
|
817
|
+
onSubmit: submitQuestion,
|
|
818
|
+
onStop: stopStreaming,
|
|
819
|
+
isLoading,
|
|
820
|
+
inputRef,
|
|
821
|
+
placeholder: ui.placeholder,
|
|
822
|
+
hint: ui.composerHint,
|
|
823
|
+
className: cn("px-3 pb-3", classNames.composer),
|
|
824
|
+
inputClassName: classNames.input
|
|
495
825
|
})
|
|
496
826
|
]
|
|
497
|
-
}), /* @__PURE__ */ jsx("button", {
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
"
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
strokeLinecap: "round",
|
|
511
|
-
strokeLinejoin: "round",
|
|
512
|
-
children: [/* @__PURE__ */ jsx("path", { d: "M18 6 6 18" }), /* @__PURE__ */ jsx("path", { d: "m6 6 12 12" })]
|
|
513
|
-
}) : /* @__PURE__ */ jsx("svg", {
|
|
514
|
-
xmlns: "http://www.w3.org/2000/svg",
|
|
515
|
-
width: "20",
|
|
516
|
-
height: "20",
|
|
517
|
-
viewBox: "0 0 24 24",
|
|
518
|
-
fill: "none",
|
|
519
|
-
stroke: "currentColor",
|
|
520
|
-
strokeWidth: "2",
|
|
521
|
-
strokeLinecap: "round",
|
|
522
|
-
strokeLinejoin: "round",
|
|
523
|
-
children: /* @__PURE__ */ jsx("path", { d: "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" })
|
|
524
|
-
})
|
|
827
|
+
}) : null, /* @__PURE__ */ jsx("button", {
|
|
828
|
+
ref: triggerRef,
|
|
829
|
+
type: "button",
|
|
830
|
+
onClick: () => setIsOpen((open) => !open),
|
|
831
|
+
className: cn("pointer-events-auto ml-auto flex size-14 cursor-pointer select-none items-center justify-center rounded-2xl bg-primary-500 text-white shadow-xl shadow-primary-500/25 transition-all hover:-translate-y-0.5 hover:bg-primary-600 hover:shadow-2xl active:translate-y-0 active:scale-95 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500/40 focus-visible:ring-offset-2 focus-visible:ring-offset-main motion-reduce:transform-none motion-reduce:transition-none", classNames.trigger),
|
|
832
|
+
title: ui.buttonTooltip,
|
|
833
|
+
"aria-label": ui.buttonTooltip,
|
|
834
|
+
"aria-expanded": isOpen,
|
|
835
|
+
"aria-controls": panelId,
|
|
836
|
+
children: isOpen ? /* @__PURE__ */ jsx(CloseIcon, {
|
|
837
|
+
size: 21,
|
|
838
|
+
strokeWidth: 2.5
|
|
839
|
+
}) : /* @__PURE__ */ jsx(ChatIcon, { size: 21 })
|
|
525
840
|
})]
|
|
526
841
|
});
|
|
527
842
|
}
|
|
528
843
|
//#endregion
|
|
529
844
|
//#region src/client/components/ask-ai-dialog.tsx
|
|
530
|
-
function AskAiDialog() {
|
|
531
|
-
const { messages, input, setInput, isLoading, submitQuestion, stopStreaming, clearChat, isOpen, setIsOpen, devMode } = useAskAi();
|
|
845
|
+
function AskAiDialog(props = {}) {
|
|
846
|
+
const { messages, input, setInput, isLoading, submitQuestion, stopStreaming, clearChat, isOpen, setIsOpen, devMode, ui, classNames = {} } = useAskAi(props);
|
|
532
847
|
const messagesEndRef = useRef(null);
|
|
533
848
|
const inputRef = useRef(null);
|
|
849
|
+
const titleId = useId();
|
|
534
850
|
useEffect(() => {
|
|
535
851
|
if (isOpen) {
|
|
536
852
|
window.dispatchEvent(new CustomEvent("boltdocs:ask-ai:open"));
|
|
@@ -538,239 +854,63 @@ function AskAiDialog() {
|
|
|
538
854
|
inputRef.current?.focus();
|
|
539
855
|
} else window.dispatchEvent(new CustomEvent("boltdocs:ask-ai:close"));
|
|
540
856
|
}, [isOpen]);
|
|
541
|
-
const handleSubmit = (e) => {
|
|
542
|
-
e.preventDefault();
|
|
543
|
-
if (!input.trim() || isLoading) return;
|
|
544
|
-
submitQuestion(input);
|
|
545
|
-
};
|
|
546
857
|
if (!isOpen) return null;
|
|
547
|
-
return /* @__PURE__ */ jsxs("
|
|
548
|
-
|
|
858
|
+
return /* @__PURE__ */ jsxs("aside", {
|
|
859
|
+
"aria-labelledby": titleId,
|
|
860
|
+
onKeyDown: (event) => {
|
|
861
|
+
if (event.key === "Escape") setIsOpen(false);
|
|
862
|
+
},
|
|
863
|
+
className: cn("hidden w-[340px] shrink-0 flex-col overflow-hidden border-l border-subtle bg-main xl:flex", classNames.root),
|
|
549
864
|
children: [
|
|
550
|
-
/* @__PURE__ */
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
viewBox: "0 0 24 24",
|
|
559
|
-
fill: "none",
|
|
560
|
-
stroke: "currentColor",
|
|
561
|
-
strokeWidth: "2.5",
|
|
562
|
-
strokeLinecap: "round",
|
|
563
|
-
strokeLinejoin: "round",
|
|
564
|
-
className: "text-primary-500 shrink-0",
|
|
565
|
-
children: /* @__PURE__ */ jsx("path", { d: "m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z" })
|
|
566
|
-
}), /* @__PURE__ */ jsx("span", {
|
|
567
|
-
className: "text-xs font-bold text-body",
|
|
568
|
-
children: "AI Assistant"
|
|
569
|
-
})]
|
|
570
|
-
}), /* @__PURE__ */ jsxs("div", {
|
|
571
|
-
className: "flex items-center gap-1",
|
|
572
|
-
children: [messages.length > 0 && /* @__PURE__ */ jsx("button", {
|
|
573
|
-
onClick: clearChat,
|
|
574
|
-
className: "p-1 text-muted hover:text-red-500 hover:bg-surface rounded-lg transition-colors cursor-pointer",
|
|
575
|
-
title: "Clear chat",
|
|
576
|
-
"aria-label": "Clear chat",
|
|
577
|
-
children: /* @__PURE__ */ jsxs("svg", {
|
|
578
|
-
xmlns: "http://www.w3.org/2000/svg",
|
|
579
|
-
width: "12",
|
|
580
|
-
height: "12",
|
|
581
|
-
viewBox: "0 0 24 24",
|
|
582
|
-
fill: "none",
|
|
583
|
-
stroke: "currentColor",
|
|
584
|
-
strokeWidth: "2",
|
|
585
|
-
strokeLinecap: "round",
|
|
586
|
-
strokeLinejoin: "round",
|
|
587
|
-
children: [
|
|
588
|
-
/* @__PURE__ */ jsx("path", { d: "M3 6h18" }),
|
|
589
|
-
/* @__PURE__ */ jsx("path", { d: "M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" }),
|
|
590
|
-
/* @__PURE__ */ jsx("path", { d: "M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" })
|
|
591
|
-
]
|
|
592
|
-
})
|
|
593
|
-
}), /* @__PURE__ */ jsx("button", {
|
|
594
|
-
onClick: () => setIsOpen(false),
|
|
595
|
-
className: "p-1 text-muted hover:text-body hover:bg-surface rounded-lg transition-colors cursor-pointer",
|
|
596
|
-
title: "Close assistant",
|
|
597
|
-
"aria-label": "Close assistant",
|
|
598
|
-
children: /* @__PURE__ */ jsxs("svg", {
|
|
599
|
-
xmlns: "http://www.w3.org/2000/svg",
|
|
600
|
-
width: "12",
|
|
601
|
-
height: "12",
|
|
602
|
-
viewBox: "0 0 24 24",
|
|
603
|
-
fill: "none",
|
|
604
|
-
stroke: "currentColor",
|
|
605
|
-
strokeWidth: "2",
|
|
606
|
-
strokeLinecap: "round",
|
|
607
|
-
strokeLinejoin: "round",
|
|
608
|
-
children: [/* @__PURE__ */ jsx("path", { d: "M18 6 6 18" }), /* @__PURE__ */ jsx("path", { d: "m6 6 12 12" })]
|
|
609
|
-
})
|
|
610
|
-
})]
|
|
611
|
-
})]
|
|
865
|
+
/* @__PURE__ */ jsx(ChatHeader, {
|
|
866
|
+
title: ui.title,
|
|
867
|
+
variant: "dialog",
|
|
868
|
+
canClear: messages.length > 0,
|
|
869
|
+
onClear: clearChat,
|
|
870
|
+
onClose: () => setIsOpen(false),
|
|
871
|
+
className: classNames.header,
|
|
872
|
+
titleId
|
|
612
873
|
}),
|
|
613
874
|
/* @__PURE__ */ jsxs("div", {
|
|
614
|
-
|
|
875
|
+
role: "log",
|
|
876
|
+
"aria-live": "polite",
|
|
877
|
+
"aria-relevant": "additions text",
|
|
878
|
+
"aria-label": "Assistant messages",
|
|
879
|
+
className: cn("flex min-h-0 flex-1 flex-col gap-3 overflow-y-auto p-4", classNames.messages),
|
|
615
880
|
children: [
|
|
616
|
-
messages.length === 0
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
})
|
|
632
|
-
}), /* @__PURE__ */ jsx("p", {
|
|
633
|
-
className: "text-xs text-muted leading-relaxed",
|
|
634
|
-
children: "Ask anything about the current documentation page"
|
|
635
|
-
})]
|
|
636
|
-
}),
|
|
637
|
-
messages.map((msg, i) => /* @__PURE__ */ jsxs("div", {
|
|
638
|
-
className: `flex flex-col max-w-full ${msg.role === "user" ? "items-end" : "items-start"}`,
|
|
639
|
-
children: [
|
|
640
|
-
msg.role === "assistant" && msg.usage && devMode && /* @__PURE__ */ jsxs("div", {
|
|
641
|
-
className: "flex items-center gap-1.5 px-2 py-1 mb-1 text-[11px] text-muted font-mono",
|
|
642
|
-
children: [
|
|
643
|
-
/* @__PURE__ */ jsx("span", {
|
|
644
|
-
className: "px-1 py-0.5 rounded bg-amber-500/10 text-amber-600 dark:text-amber-400 text-[10px] font-bold uppercase tracking-wide",
|
|
645
|
-
children: "DEV"
|
|
646
|
-
}),
|
|
647
|
-
/* @__PURE__ */ jsxs("span", { children: [
|
|
648
|
-
msg.usage.provider,
|
|
649
|
-
"/",
|
|
650
|
-
msg.usage.model
|
|
651
|
-
] }),
|
|
652
|
-
/* @__PURE__ */ jsx("span", { children: "·" }),
|
|
653
|
-
/* @__PURE__ */ jsxs("span", { children: [msg.usage.promptTokens, "↑"] }),
|
|
654
|
-
/* @__PURE__ */ jsxs("span", { children: [msg.usage.completionTokens, "↓"] }),
|
|
655
|
-
/* @__PURE__ */ jsx("span", { children: "·" }),
|
|
656
|
-
/* @__PURE__ */ jsxs("span", {
|
|
657
|
-
className: "text-primary-500 font-semibold",
|
|
658
|
-
children: [msg.usage.totalTokens, " tok"]
|
|
659
|
-
}),
|
|
660
|
-
/* @__PURE__ */ jsx("span", { children: "·" }),
|
|
661
|
-
/* @__PURE__ */ jsxs("span", { children: [msg.usage.elapsedMs, "ms"] })
|
|
662
|
-
]
|
|
663
|
-
}),
|
|
664
|
-
msg.role === "assistant" && msg.contextChip && /* @__PURE__ */ jsx("div", {
|
|
665
|
-
className: "flex items-center gap-1.5 px-2 py-1 mb-1 text-[11px] text-muted",
|
|
666
|
-
children: msg.contextChip.missing ? /* @__PURE__ */ jsx("span", { children: "No docs in scope" }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
667
|
-
/* @__PURE__ */ jsxs("svg", {
|
|
668
|
-
xmlns: "http://www.w3.org/2000/svg",
|
|
669
|
-
width: "12",
|
|
670
|
-
height: "12",
|
|
671
|
-
viewBox: "0 0 24 24",
|
|
672
|
-
fill: "none",
|
|
673
|
-
stroke: "currentColor",
|
|
674
|
-
strokeWidth: "2",
|
|
675
|
-
strokeLinecap: "round",
|
|
676
|
-
strokeLinejoin: "round",
|
|
677
|
-
children: [/* @__PURE__ */ jsx("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" }), /* @__PURE__ */ jsx("polyline", { points: "14 2 14 8 20 8" })]
|
|
678
|
-
}),
|
|
679
|
-
/* @__PURE__ */ jsx("span", { children: "Reading" }),
|
|
680
|
-
/* @__PURE__ */ jsx("code", {
|
|
681
|
-
className: "px-1 py-0.5 rounded bg-surface text-primary-500 text-[10px] font-mono",
|
|
682
|
-
children: msg.contextChip.page
|
|
683
|
-
}),
|
|
684
|
-
/* @__PURE__ */ jsx("span", {
|
|
685
|
-
className: "text-muted",
|
|
686
|
-
children: "·"
|
|
687
|
-
}),
|
|
688
|
-
/* @__PURE__ */ jsxs("span", { children: [msg.contextChip.chars, "c"] })
|
|
689
|
-
] })
|
|
690
|
-
}),
|
|
691
|
-
/* @__PURE__ */ jsx("div", {
|
|
692
|
-
className: `px-3 py-2 rounded-xl ${msg.role === "user" ? "bg-primary-500 text-white rounded-br-none max-w-[90%]" : msg.status === "error" ? "bg-red-500/10 border border-red-500/30 text-red-600 dark:text-red-400 rounded-bl-none" : "bg-surface border border-subtle text-body rounded-bl-none"}`,
|
|
693
|
-
children: msg.role === "user" ? /* @__PURE__ */ jsx("p", {
|
|
694
|
-
className: "whitespace-pre-wrap text-xs",
|
|
695
|
-
children: msg.content
|
|
696
|
-
}) : msg.status === "error" ? /* @__PURE__ */ jsxs("p", {
|
|
697
|
-
className: "text-xs",
|
|
698
|
-
children: [
|
|
699
|
-
/* @__PURE__ */ jsx("strong", { children: "Error:" }),
|
|
700
|
-
" ",
|
|
701
|
-
msg.errorMessage || "Something went wrong."
|
|
702
|
-
]
|
|
703
|
-
}) : /* @__PURE__ */ jsx("div", {
|
|
704
|
-
className: "ask-ai-streamdown",
|
|
705
|
-
children: msg.content ? /* @__PURE__ */ jsx(MarkdownRenderer, { content: msg.content }) : msg.status === "reading" ? /* @__PURE__ */ jsxs("span", {
|
|
706
|
-
className: "inline-flex items-center gap-1.5 text-xs text-muted",
|
|
707
|
-
children: [/* @__PURE__ */ jsx("span", { className: "w-1.5 h-1.5 rounded-full bg-primary-500 animate-pulse" }), /* @__PURE__ */ jsx("span", { children: "Reading page…" })]
|
|
708
|
-
}) : /* @__PURE__ */ jsxs("span", {
|
|
709
|
-
className: "inline-flex items-center gap-1.5 text-xs text-muted",
|
|
710
|
-
children: [/* @__PURE__ */ jsx("span", { className: "w-1.5 h-1.5 rounded-full bg-primary-500 animate-pulse" }), /* @__PURE__ */ jsx("span", { children: "Waiting…" })]
|
|
711
|
-
})
|
|
712
|
-
})
|
|
713
|
-
})
|
|
714
|
-
]
|
|
715
|
-
}, i)),
|
|
881
|
+
messages.length === 0 ? /* @__PURE__ */ jsx(ChatEmptyState, {
|
|
882
|
+
variant: "dialog",
|
|
883
|
+
title: ui.emptyTitle,
|
|
884
|
+
description: ui.emptyDescription,
|
|
885
|
+
className: classNames.emptyState
|
|
886
|
+
}) : null,
|
|
887
|
+
messages.map((message, index) => /* @__PURE__ */ jsx(ChatMessage, {
|
|
888
|
+
message,
|
|
889
|
+
variant: "dialog",
|
|
890
|
+
devMode,
|
|
891
|
+
className: classNames.message,
|
|
892
|
+
userClassName: classNames.userMessage,
|
|
893
|
+
assistantClassName: classNames.assistantMessage,
|
|
894
|
+
markdownClassName: classNames.markdown
|
|
895
|
+
}, message.id ?? `legacy-message-${index}`)),
|
|
716
896
|
/* @__PURE__ */ jsx("div", { ref: messagesEndRef })
|
|
717
897
|
]
|
|
718
898
|
}),
|
|
719
|
-
/* @__PURE__ */
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
type: "button",
|
|
732
|
-
onClick: stopStreaming,
|
|
733
|
-
className: "px-2.5 py-1.5 bg-red-500 hover:bg-red-600 text-white text-xs font-semibold rounded-lg flex items-center justify-center transition-colors cursor-pointer shrink-0",
|
|
734
|
-
title: "Stop generating",
|
|
735
|
-
"aria-label": "Stop generating",
|
|
736
|
-
children: /* @__PURE__ */ jsx("svg", {
|
|
737
|
-
xmlns: "http://www.w3.org/2000/svg",
|
|
738
|
-
width: "12",
|
|
739
|
-
height: "12",
|
|
740
|
-
viewBox: "0 0 24 24",
|
|
741
|
-
fill: "currentColor",
|
|
742
|
-
children: /* @__PURE__ */ jsx("rect", {
|
|
743
|
-
x: "6",
|
|
744
|
-
y: "6",
|
|
745
|
-
width: "12",
|
|
746
|
-
height: "12",
|
|
747
|
-
rx: "2"
|
|
748
|
-
})
|
|
749
|
-
})
|
|
750
|
-
}) : /* @__PURE__ */ jsx("button", {
|
|
751
|
-
type: "submit",
|
|
752
|
-
disabled: !input.trim(),
|
|
753
|
-
className: "px-2.5 py-1.5 bg-primary-500 hover:bg-primary-600 disabled:opacity-50 text-white text-xs font-semibold rounded-lg flex items-center justify-center transition-colors cursor-pointer shrink-0",
|
|
754
|
-
"aria-label": "Send question",
|
|
755
|
-
children: /* @__PURE__ */ jsxs("svg", {
|
|
756
|
-
xmlns: "http://www.w3.org/2000/svg",
|
|
757
|
-
width: "12",
|
|
758
|
-
height: "12",
|
|
759
|
-
viewBox: "0 0 24 24",
|
|
760
|
-
fill: "none",
|
|
761
|
-
stroke: "currentColor",
|
|
762
|
-
strokeWidth: "2.5",
|
|
763
|
-
strokeLinecap: "round",
|
|
764
|
-
strokeLinejoin: "round",
|
|
765
|
-
children: [/* @__PURE__ */ jsx("path", { d: "m22 2-7 20-4-9-9-4Z" }), /* @__PURE__ */ jsx("path", { d: "M22 2 11 13" })]
|
|
766
|
-
})
|
|
767
|
-
})]
|
|
899
|
+
/* @__PURE__ */ jsx(ChatInput, {
|
|
900
|
+
variant: "dialog",
|
|
901
|
+
input,
|
|
902
|
+
onInputChange: setInput,
|
|
903
|
+
onSubmit: submitQuestion,
|
|
904
|
+
onStop: stopStreaming,
|
|
905
|
+
isLoading,
|
|
906
|
+
inputRef,
|
|
907
|
+
placeholder: ui.placeholder,
|
|
908
|
+
hint: ui.composerHint,
|
|
909
|
+
className: cn("border-t border-subtle p-3", classNames.composer),
|
|
910
|
+
inputClassName: classNames.input
|
|
768
911
|
})
|
|
769
912
|
]
|
|
770
913
|
});
|
|
771
914
|
}
|
|
772
915
|
//#endregion
|
|
773
|
-
|
|
774
|
-
var client_default = null;
|
|
775
|
-
//#endregion
|
|
776
|
-
export { AskAiBubble, AskAiDialog, MarkdownRenderer, client_default as default, useAskAi };
|
|
916
|
+
export { AskAiBubble, AskAiDialog, ChatEmptyState, ChatHeader, ChatInput, ChatMessage, MarkdownRenderer, TypingIndicator, useAskAi };
|