@bdocs/plugin-ask-ai 0.3.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.
@@ -0,0 +1,776 @@
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
+ import { useConfig } from "boltdocs/client";
3
+ import { Streamdown } from "streamdown";
4
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
5
+ //#region src/client/use-ask-ai.ts
6
+ function useAskAi(options = {}) {
7
+ const [messages, setMessages] = useState([]);
8
+ const [input, setInput] = useState("");
9
+ const [isLoading, setIsLoading] = useState(false);
10
+ const [isOpen, setIsOpen] = useState(false);
11
+ const submitAbortRef = useRef(null);
12
+ const pendingTextRef = useRef({ value: "" });
13
+ const pendingRafRef = useRef(null);
14
+ const askAiPluginMeta = useConfig()?.plugins?.find((p) => p.name === "boltdocs-plugin-ask-ai")?.metadata;
15
+ const customEndpoint = options.endpoint || askAiPluginMeta?.endpoint || "/api/ask-ai";
16
+ const devMode = askAiPluginMeta?.devMode ?? false;
17
+ useEffect(() => {
18
+ const handleOpen = () => setIsOpen(true);
19
+ const handleClose = () => setIsOpen(false);
20
+ const handleToggle = () => setIsOpen((prev) => !prev);
21
+ window.addEventListener("boltdocs:ask-ai:open", handleOpen);
22
+ window.addEventListener("boltdocs:ask-ai:close", handleClose);
23
+ window.addEventListener("boltdocs:ask-ai:toggle", handleToggle);
24
+ return () => {
25
+ window.removeEventListener("boltdocs:ask-ai:open", handleOpen);
26
+ window.removeEventListener("boltdocs:ask-ai:close", handleClose);
27
+ window.removeEventListener("boltdocs:ask-ai:toggle", handleToggle);
28
+ };
29
+ }, []);
30
+ const startTimeRef = useRef(0);
31
+ const usageRef = useRef(null);
32
+ const flushPendingText = useCallback(() => {
33
+ if (pendingRafRef.current !== null) {
34
+ cancelAnimationFrame(pendingRafRef.current);
35
+ pendingRafRef.current = null;
36
+ }
37
+ if (!pendingTextRef.current.value) return;
38
+ const chunk = pendingTextRef.current.value;
39
+ pendingTextRef.current.value = "";
40
+ setMessages((prev) => {
41
+ const next = [...prev];
42
+ const last = next[next.length - 1];
43
+ if (last && last.role === "assistant") last.content += chunk;
44
+ return next;
45
+ });
46
+ }, []);
47
+ const scheduleFlush = useCallback(() => {
48
+ if (pendingRafRef.current !== null) return;
49
+ pendingRafRef.current = requestAnimationFrame(() => {
50
+ pendingRafRef.current = null;
51
+ flushPendingText();
52
+ });
53
+ }, [flushPendingText]);
54
+ const finalizeAssistantTerminal = useCallback((opts) => {
55
+ flushPendingText();
56
+ setMessages((prev) => {
57
+ const next = [...prev];
58
+ const last = next[next.length - 1];
59
+ if (last && last.role === "assistant" && last.status !== "done") {
60
+ last.status = last.content ? "done" : "error";
61
+ if (last.status === "error" && opts.message) last.errorMessage = opts.message;
62
+ }
63
+ return next;
64
+ });
65
+ }, [flushPendingText]);
66
+ const stopStreaming = useCallback(() => {
67
+ if (submitAbortRef.current) submitAbortRef.current.abort();
68
+ setIsLoading(false);
69
+ finalizeAssistantTerminal({ kind: "cancel" });
70
+ }, [finalizeAssistantTerminal]);
71
+ useEffect(() => {
72
+ if (!isOpen && isLoading) stopStreaming();
73
+ }, [
74
+ isOpen,
75
+ isLoading,
76
+ stopStreaming
77
+ ]);
78
+ const clearChat = useCallback(() => {
79
+ stopStreaming();
80
+ setMessages([]);
81
+ }, [stopStreaming]);
82
+ return {
83
+ messages,
84
+ input,
85
+ setInput,
86
+ isLoading,
87
+ submitQuestion: useCallback(async (text) => {
88
+ const trimmed = text.trim();
89
+ if (!trimmed || isLoading) return;
90
+ pendingTextRef.current = { value: "" };
91
+ pendingRafRef.current = null;
92
+ setMessages((prev) => [
93
+ ...prev,
94
+ {
95
+ role: "user",
96
+ content: trimmed
97
+ },
98
+ {
99
+ role: "assistant",
100
+ content: "",
101
+ status: "reading"
102
+ }
103
+ ]);
104
+ setInput("");
105
+ setIsLoading(true);
106
+ const controller = new AbortController();
107
+ submitAbortRef.current = controller;
108
+ try {
109
+ const currentPage = options.currentPage || (typeof window !== "undefined" ? window.location.pathname : "/");
110
+ const response = await fetch(customEndpoint, {
111
+ method: "POST",
112
+ headers: { "Content-Type": "application/json" },
113
+ body: JSON.stringify({
114
+ question: trimmed,
115
+ currentPage
116
+ }),
117
+ signal: controller.signal
118
+ });
119
+ if (!response.ok) throw new Error(`Server returned ${response.status}`);
120
+ if (!response.body) throw new Error("No streaming body from endpoint");
121
+ const reader = response.body.getReader();
122
+ const decoder = new TextDecoder();
123
+ let lineBuffer = "";
124
+ let firstTextSeen = false;
125
+ while (true) {
126
+ const { value, done } = await reader.read();
127
+ if (done) break;
128
+ lineBuffer += decoder.decode(value, { stream: true });
129
+ const lines = lineBuffer.split("\n");
130
+ lineBuffer = lines.pop() || "";
131
+ for (const line of lines) {
132
+ const cleaned = line.trim();
133
+ if (!cleaned.startsWith("data:")) continue;
134
+ const dataStr = cleaned.slice(5).trim();
135
+ if (!dataStr || dataStr === "[DONE]") continue;
136
+ let parsed;
137
+ try {
138
+ parsed = JSON.parse(dataStr);
139
+ } catch {
140
+ continue;
141
+ }
142
+ if (parsed.context) setMessages((prev) => {
143
+ const next = [...prev];
144
+ const last = next[next.length - 1];
145
+ if (last && last.role === "assistant") {
146
+ last.contextChip = parsed.context;
147
+ startTimeRef.current = Date.now();
148
+ }
149
+ return next;
150
+ });
151
+ else if (parsed.usage) usageRef.current = parsed.usage;
152
+ else if (typeof parsed.text === "string") {
153
+ if (!firstTextSeen) {
154
+ firstTextSeen = true;
155
+ setMessages((prev) => {
156
+ const next = [...prev];
157
+ const last = next[next.length - 1];
158
+ if (last && last.role === "assistant") last.status = "streaming";
159
+ return next;
160
+ });
161
+ }
162
+ pendingTextRef.current.value += parsed.text;
163
+ scheduleFlush();
164
+ } else if (parsed.error) throw new Error(parsed.error);
165
+ }
166
+ }
167
+ flushPendingText();
168
+ setMessages((prev) => {
169
+ const next = [...prev];
170
+ const last = next[next.length - 1];
171
+ if (last && last.role === "assistant") {
172
+ last.status = last.content ? "done" : "error";
173
+ if (last.status === "error" && !last.errorMessage) last.errorMessage = "No response received.";
174
+ if (usageRef.current && devMode) last.usage = {
175
+ ...usageRef.current,
176
+ elapsedMs: startTimeRef.current ? Date.now() - startTimeRef.current : usageRef.current.elapsedMs
177
+ };
178
+ }
179
+ return next;
180
+ });
181
+ } catch (error) {
182
+ if (error instanceof DOMException && error.name === "AbortError") finalizeAssistantTerminal({
183
+ kind: "cancel",
184
+ message: "Request cancelled."
185
+ });
186
+ else {
187
+ finalizeAssistantTerminal({
188
+ kind: "error",
189
+ message: error instanceof Error ? error.message : "Unknown error"
190
+ });
191
+ console.error("[Ask AI] failed:", error);
192
+ }
193
+ } finally {
194
+ if (pendingRafRef.current !== null) {
195
+ cancelAnimationFrame(pendingRafRef.current);
196
+ pendingRafRef.current = null;
197
+ }
198
+ if (submitAbortRef.current === controller) submitAbortRef.current = null;
199
+ setIsLoading(false);
200
+ }
201
+ }, [
202
+ customEndpoint,
203
+ options.currentPage,
204
+ isLoading,
205
+ scheduleFlush,
206
+ finalizeAssistantTerminal
207
+ ]),
208
+ stopStreaming,
209
+ clearChat,
210
+ isOpen,
211
+ setIsOpen,
212
+ devMode
213
+ };
214
+ }
215
+ //#endregion
216
+ //#region src/client/render-markdown.tsx
217
+ function MarkdownRenderer({ content, className, parseIncompleteMarkdown = true }) {
218
+ if (!content) return null;
219
+ return /* @__PURE__ */ jsx("div", {
220
+ className,
221
+ children: /* @__PURE__ */ jsx(Streamdown, {
222
+ parseIncompleteMarkdown,
223
+ children: content
224
+ })
225
+ });
226
+ }
227
+ //#endregion
228
+ //#region src/client/components/ask-ai-bubble.tsx
229
+ function AskAiBubble() {
230
+ const { messages, input, setInput, isLoading, submitQuestion, stopStreaming, clearChat, isOpen, setIsOpen, devMode } = useAskAi();
231
+ const messagesEndRef = useRef(null);
232
+ useEffect(() => {
233
+ if (isOpen) messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
234
+ }, [messages, isOpen]);
235
+ const handleSubmit = (e) => {
236
+ e.preventDefault();
237
+ if (!input.trim()) return;
238
+ submitQuestion(input);
239
+ };
240
+ return /* @__PURE__ */ jsxs("div", {
241
+ className: "fixed bottom-6 right-6 z-50 flex flex-col items-end",
242
+ children: [isOpen && /* @__PURE__ */ jsxs("div", {
243
+ className: "mb-4 w-[380px] max-w-[calc(100vw-2rem)] h-[min(520px,calc(100vh-8rem))] bg-main/90 backdrop-blur-md border border-subtle rounded-2xl shadow-xl flex flex-col overflow-hidden animate-in fade-in slide-in-from-bottom-5 duration-200",
244
+ children: [
245
+ /* @__PURE__ */ jsxs("div", {
246
+ className: "px-4 py-3 border-b border-subtle flex items-center justify-between bg-surface/50",
247
+ children: [/* @__PURE__ */ jsxs("div", {
248
+ className: "flex items-center gap-2",
249
+ children: [/* @__PURE__ */ jsx("svg", {
250
+ xmlns: "http://www.w3.org/2000/svg",
251
+ width: "16",
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
+ })]
307
+ }),
308
+ /* @__PURE__ */ jsxs("div", {
309
+ className: "flex-1 overflow-y-auto p-4 flex flex-col gap-4",
310
+ children: [
311
+ messages.length === 0 && /* @__PURE__ */ jsxs("div", {
312
+ className: "flex-1 flex flex-col items-center justify-center text-center p-6",
313
+ children: [
314
+ /* @__PURE__ */ jsx("div", {
315
+ className: "w-12 h-12 rounded-full bg-primary-500/10 flex items-center justify-center text-primary-500 mb-3",
316
+ children: /* @__PURE__ */ jsx("svg", {
317
+ xmlns: "http://www.w3.org/2000/svg",
318
+ width: "24",
319
+ height: "24",
320
+ viewBox: "0 0 24 24",
321
+ fill: "none",
322
+ stroke: "currentColor",
323
+ strokeWidth: "2",
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)),
444
+ /* @__PURE__ */ jsx("div", { ref: messagesEndRef })
445
+ ]
446
+ }),
447
+ /* @__PURE__ */ jsxs("form", {
448
+ onSubmit: handleSubmit,
449
+ className: "p-3 border-t border-subtle bg-surface/30 flex gap-2",
450
+ children: [/* @__PURE__ */ jsx("input", {
451
+ type: "text",
452
+ value: input,
453
+ onChange: (e) => setInput(e.target.value),
454
+ placeholder: "Ask about this page…",
455
+ className: "flex-1 bg-surface border border-subtle rounded-xl px-3 py-1.5 text-sm outline-none text-body focus-visible:border-primary-500 transition-colors",
456
+ disabled: isLoading
457
+ }), isLoading ? /* @__PURE__ */ jsx("button", {
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
+ })]
495
+ })
496
+ ]
497
+ }), /* @__PURE__ */ jsx("button", {
498
+ onClick: () => setIsOpen((prev) => !prev),
499
+ className: "w-12 h-12 bg-primary-500 hover:bg-primary-600 active:scale-95 text-white rounded-full flex items-center justify-center shadow-lg transition-all cursor-pointer select-none",
500
+ title: "Ask AI assistant",
501
+ "aria-label": "Ask AI assistant",
502
+ children: isOpen ? /* @__PURE__ */ jsxs("svg", {
503
+ xmlns: "http://www.w3.org/2000/svg",
504
+ width: "20",
505
+ height: "20",
506
+ viewBox: "0 0 24 24",
507
+ fill: "none",
508
+ stroke: "currentColor",
509
+ strokeWidth: "2.5",
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
+ })
525
+ })]
526
+ });
527
+ }
528
+ //#endregion
529
+ //#region src/client/components/ask-ai-dialog.tsx
530
+ function AskAiDialog() {
531
+ const { messages, input, setInput, isLoading, submitQuestion, stopStreaming, clearChat, isOpen, setIsOpen, devMode } = useAskAi();
532
+ const messagesEndRef = useRef(null);
533
+ const inputRef = useRef(null);
534
+ useEffect(() => {
535
+ if (isOpen) {
536
+ window.dispatchEvent(new CustomEvent("boltdocs:ask-ai:open"));
537
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
538
+ inputRef.current?.focus();
539
+ } else window.dispatchEvent(new CustomEvent("boltdocs:ask-ai:close"));
540
+ }, [isOpen]);
541
+ const handleSubmit = (e) => {
542
+ e.preventDefault();
543
+ if (!input.trim() || isLoading) return;
544
+ submitQuestion(input);
545
+ };
546
+ if (!isOpen) return null;
547
+ return /* @__PURE__ */ jsxs("div", {
548
+ className: "hidden xl:flex flex-col shrink-0 w-[320px] border-l border-subtle bg-main overflow-hidden",
549
+ children: [
550
+ /* @__PURE__ */ jsxs("div", {
551
+ className: "px-4 py-3 border-b border-subtle flex items-center justify-between bg-surface/50 shrink-0",
552
+ children: [/* @__PURE__ */ jsxs("div", {
553
+ className: "flex items-center gap-2",
554
+ children: [/* @__PURE__ */ jsx("svg", {
555
+ xmlns: "http://www.w3.org/2000/svg",
556
+ width: "14",
557
+ height: "14",
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
+ })]
612
+ }),
613
+ /* @__PURE__ */ jsxs("div", {
614
+ className: "flex-1 overflow-y-auto p-4 flex flex-col gap-3 min-h-0",
615
+ children: [
616
+ messages.length === 0 && /* @__PURE__ */ jsxs("div", {
617
+ className: "flex-1 flex flex-col items-center justify-center text-center p-2",
618
+ children: [/* @__PURE__ */ jsx("div", {
619
+ className: "w-10 h-10 rounded-xl bg-primary-500/10 flex items-center justify-center text-primary-500 mb-3",
620
+ children: /* @__PURE__ */ jsx("svg", {
621
+ xmlns: "http://www.w3.org/2000/svg",
622
+ width: "20",
623
+ height: "20",
624
+ viewBox: "0 0 24 24",
625
+ fill: "none",
626
+ stroke: "currentColor",
627
+ strokeWidth: "2",
628
+ strokeLinecap: "round",
629
+ strokeLinejoin: "round",
630
+ 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" })
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)),
716
+ /* @__PURE__ */ jsx("div", { ref: messagesEndRef })
717
+ ]
718
+ }),
719
+ /* @__PURE__ */ jsxs("form", {
720
+ onSubmit: handleSubmit,
721
+ className: "p-3 border-t border-subtle bg-surface/30 flex gap-2 shrink-0",
722
+ children: [/* @__PURE__ */ jsx("input", {
723
+ ref: inputRef,
724
+ type: "text",
725
+ value: input,
726
+ onChange: (e) => setInput(e.target.value),
727
+ placeholder: "Ask about this page…",
728
+ className: "flex-1 bg-surface border border-subtle rounded-lg px-2.5 py-1.5 text-xs outline-none text-body focus-within:border-primary-500 transition-colors min-w-0",
729
+ disabled: isLoading
730
+ }), isLoading ? /* @__PURE__ */ jsx("button", {
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
+ })]
768
+ })
769
+ ]
770
+ });
771
+ }
772
+ //#endregion
773
+ //#region src/client/index.ts
774
+ var client_default = null;
775
+ //#endregion
776
+ export { AskAiBubble, AskAiDialog, MarkdownRenderer, client_default as default, useAskAi };