radiant-docs 0.1.40 → 0.1.42

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.
Files changed (43) hide show
  1. package/package.json +1 -1
  2. package/template/astro.config.mjs +42 -40
  3. package/template/package-lock.json +7 -0
  4. package/template/package.json +3 -2
  5. package/template/public/favicon.svg +16 -8
  6. package/template/scripts/remove-assistant-for-non-pro.mjs +28 -0
  7. package/template/src/components/Header.astro +151 -17
  8. package/template/src/components/MdxPage.astro +76 -22
  9. package/template/src/components/PagePagination.astro +44 -8
  10. package/template/src/components/Sidebar.astro +10 -1
  11. package/template/src/components/TableOfContents.astro +159 -53
  12. package/template/src/components/chat/AssistantDocsWidget.astro +16 -0
  13. package/template/src/components/chat/AssistantDocsWidget.tsx +615 -0
  14. package/template/src/components/chat/AssistantEmbedPanel.tsx +2679 -0
  15. package/template/src/components/chat/AssistantEmbedPanelPage.astro +95 -0
  16. package/template/src/components/user/Accordion.astro +2 -2
  17. package/template/src/components/user/AccordionGroup.astro +1 -1
  18. package/template/src/components/user/Callout.astro +10 -4
  19. package/template/src/components/user/Card.astro +488 -0
  20. package/template/src/components/user/CardGradient.astro +964 -0
  21. package/template/src/components/user/CodeBlock.astro +1 -1
  22. package/template/src/components/user/CodeGroup.astro +1 -1
  23. package/template/src/components/user/Column.astro +25 -0
  24. package/template/src/components/user/Columns.astro +200 -0
  25. package/template/src/components/user/ComponentPreviewBlock.astro +1 -1
  26. package/template/src/components/user/Image.astro +1 -1
  27. package/template/src/components/user/Step.astro +1 -1
  28. package/template/src/components/user/Steps.astro +1 -1
  29. package/template/src/components/user/Tab.astro +1 -3
  30. package/template/src/components/user/Tabs.astro +2 -2
  31. package/template/src/layouts/Layout.astro +13 -156
  32. package/template/src/lib/assistant-chrome-defaults.ts +86 -0
  33. package/template/src/lib/assistant-chrome.ts +39 -0
  34. package/template/src/lib/assistant-embed-script.ts +1088 -0
  35. package/template/src/lib/assistant-panel-config.ts +80 -0
  36. package/template/src/lib/favicon.ts +31 -0
  37. package/template/src/lib/theme-css.ts +176 -0
  38. package/template/src/lib/validation.ts +668 -41
  39. package/template/src/pages/-/assistant/embed.js.ts +15 -0
  40. package/template/src/pages/-/assistant/panel.astro +5 -0
  41. package/template/src/pages/404.astro +4 -4
  42. package/template/src/styles/global.css +81 -4
  43. package/template/src/components/chat/AskAiWidget.tsx +0 -2011
@@ -1,2011 +0,0 @@
1
- import type { JSX } from "preact";
2
- import { useEffect, useRef, useState } from "preact/hooks";
3
- import { Icon } from "@iconify/react";
4
- import Prism from "prismjs";
5
- import "prismjs/components/prism-markup.js";
6
- import "prismjs/components/prism-clike.js";
7
- import "prismjs/components/prism-javascript.js";
8
- import "prismjs/components/prism-typescript.js";
9
- import "prismjs/components/prism-jsx.js";
10
- import "prismjs/components/prism-tsx.js";
11
- import "prismjs/components/prism-json.js";
12
- import "prismjs/components/prism-markdown.js";
13
- import "prismjs/components/prism-bash.js";
14
- import "prismjs/components/prism-python.js";
15
- import "prismjs/components/prism-yaml.js";
16
- import "prismjs/components/prism-sql.js";
17
- import "prismjs/components/prism-rust.js";
18
- import "prismjs/components/prism-go.js";
19
- import "prismjs/components/prism-java.js";
20
- import "prismjs/components/prism-markup-templating.js";
21
- import "prismjs/components/prism-php.js";
22
- import "prismjs/components/prism-ruby.js";
23
- import "prismjs/components/prism-css.js";
24
- import "prismjs/components/prism-diff.js";
25
- // import "prismjs/themes/prism.css";
26
- import "prism-themes/themes/prism-one-light.css";
27
- // import "@jongwooo/prism-theme-github/themes/prism-github-default-light.css";
28
- import { unified } from "unified";
29
- import remarkParse from "remark-parse";
30
- import remarkGfm from "remark-gfm";
31
- import remarkRehype from "remark-rehype";
32
- import rehypeStringify from "rehype-stringify";
33
- import { Drawer } from "vaul";
34
- import rehypeExternalLinks from "../../lib/mdx/rehype-external-links";
35
- import sparkleIcon from "../../assets/icons/sparkle.svg?url";
36
-
37
- type AskAiWidgetProps = {
38
- apiPath: string;
39
- isChatAvailable: boolean;
40
- unavailableMessage?: string;
41
- devProxyToken?: string;
42
- };
43
-
44
- type ChatMessage = {
45
- id: string;
46
- role: "user" | "assistant";
47
- content: string;
48
- };
49
-
50
- type AskAiStreamEvent = {
51
- type?: string;
52
- delta?: string;
53
- };
54
-
55
- type ResizeHandlePointerEvent = JSX.TargetedPointerEvent<HTMLDivElement>;
56
- type DesktopWidgetWheelEvent = JSX.TargetedWheelEvent<HTMLDivElement>;
57
- type ChatInputKeyEvent = JSX.TargetedKeyboardEvent<HTMLTextAreaElement>;
58
-
59
- type DesktopSize = {
60
- width: number;
61
- height: number;
62
- };
63
-
64
- type InlineStyleSnapshot = {
65
- transform: string;
66
- transformOrigin: string;
67
- transitionProperty: string;
68
- transitionDuration: string;
69
- transitionTimingFunction: string;
70
- overflow: string;
71
- };
72
-
73
- type PersistedWidgetState = {
74
- isOpen: boolean;
75
- messages: ChatMessage[];
76
- input: string;
77
- desktopWidth: number;
78
- desktopHeight: number;
79
- threadScrollTop: number;
80
- };
81
-
82
- const STORAGE_KEY = "radiant-docs:ask-ai-widget:v1";
83
- const DESKTOP_MIN_WIDTH_PX = 320;
84
- const DESKTOP_MAX_WIDTH_PX = 768 + 12 + 12;
85
- const DESKTOP_DEFAULT_WIDTH_PX = 448;
86
- const DESKTOP_MIN_HEIGHT_PX = 320;
87
- const DESKTOP_DEFAULT_HEIGHT_PX = 448;
88
- const DESKTOP_MAX_HEIGHT_OFFSET_PX = 20 + 4 + 8;
89
- const MOBILE_DRAWER_TRANSITION_MS = 500;
90
- const VAUL_SCALE_WINDOW_TOP_OFFSET_PX = 26;
91
- const VAUL_SCALE_TRANSLATE_TOP_OFFSET_PX = 14;
92
- const VAUL_SCALE_TRANSITION_EASE = "cubic-bezier(0.32, 0.72, 0, 1)";
93
- const MARKDOWN_HTML_CACHE_LIMIT = 300;
94
- const markdownHtmlCache = new Map<string, string>();
95
- const PRISM_LANGUAGE_ALIAS: Record<string, string> = {
96
- js: "javascript",
97
- ts: "typescript",
98
- yml: "yaml",
99
- sh: "bash",
100
- shell: "bash",
101
- shellscript: "bash",
102
- md: "markdown",
103
- mdx: "markdown",
104
- };
105
-
106
- const PRISM_LANGUAGE_LABEL: Record<string, string> = {
107
- javascript: "JavaScript",
108
- typescript: "TypeScript",
109
- jsx: "JSX",
110
- tsx: "TSX",
111
- json: "JSON",
112
- markdown: "Markdown",
113
- bash: "Bash",
114
- shell: "Shell",
115
- python: "Python",
116
- yaml: "YAML",
117
- sql: "SQL",
118
- rust: "Rust",
119
- go: "Go",
120
- java: "Java",
121
- css: "CSS",
122
- html: "HTML",
123
- diff: "Diff",
124
- mdx: "MDX",
125
- };
126
-
127
- function clamp(value: number, min: number, max: number) {
128
- return Math.min(Math.max(value, min), max);
129
- }
130
-
131
- function getViewportHeightPx() {
132
- if (typeof window === "undefined") {
133
- return DESKTOP_DEFAULT_HEIGHT_PX + DESKTOP_MAX_HEIGHT_OFFSET_PX;
134
- }
135
- return window.visualViewport?.height ?? window.innerHeight;
136
- }
137
-
138
- function getDesktopMaxHeightPx() {
139
- return Math.max(
140
- DESKTOP_MIN_HEIGHT_PX,
141
- getViewportHeightPx() - DESKTOP_MAX_HEIGHT_OFFSET_PX,
142
- );
143
- }
144
-
145
- function getDefaultPersistedState(): PersistedWidgetState {
146
- return {
147
- isOpen: false,
148
- messages: [],
149
- input: "",
150
- desktopWidth: DESKTOP_DEFAULT_WIDTH_PX,
151
- desktopHeight: getDesktopMaxHeightPx(),
152
- threadScrollTop: 0,
153
- };
154
- }
155
-
156
- function readPersistedState(): PersistedWidgetState {
157
- if (typeof window === "undefined") {
158
- return getDefaultPersistedState();
159
- }
160
-
161
- try {
162
- const raw = window.localStorage.getItem(STORAGE_KEY);
163
- if (!raw) {
164
- return getDefaultPersistedState();
165
- }
166
-
167
- const parsed = JSON.parse(raw) as Partial<PersistedWidgetState>;
168
- const parsedMessages = Array.isArray(parsed.messages)
169
- ? parsed.messages
170
- .filter((message) => {
171
- if (!message || typeof message !== "object") {
172
- return false;
173
- }
174
- const candidate = message as Partial<ChatMessage>;
175
- return (
176
- (candidate.role === "user" || candidate.role === "assistant") &&
177
- typeof candidate.id === "string" &&
178
- typeof candidate.content === "string"
179
- );
180
- })
181
- .map((message) => {
182
- const candidate = message as ChatMessage;
183
- return {
184
- id: candidate.id,
185
- role: candidate.role,
186
- content: candidate.content,
187
- };
188
- })
189
- : [];
190
-
191
- return {
192
- isOpen: parsed.isOpen === true,
193
- messages: parsedMessages,
194
- input: typeof parsed.input === "string" ? parsed.input : "",
195
- desktopWidth:
196
- typeof parsed.desktopWidth === "number" &&
197
- Number.isFinite(parsed.desktopWidth)
198
- ? parsed.desktopWidth
199
- : DESKTOP_DEFAULT_WIDTH_PX,
200
- desktopHeight:
201
- typeof parsed.desktopHeight === "number" &&
202
- Number.isFinite(parsed.desktopHeight)
203
- ? parsed.desktopHeight
204
- : getDesktopMaxHeightPx(),
205
- threadScrollTop:
206
- typeof parsed.threadScrollTop === "number" &&
207
- Number.isFinite(parsed.threadScrollTop) &&
208
- parsed.threadScrollTop >= 0
209
- ? parsed.threadScrollTop
210
- : 0,
211
- };
212
- } catch {
213
- return getDefaultPersistedState();
214
- }
215
- }
216
-
217
- function escapeHtml(value: string): string {
218
- return value
219
- .replaceAll("&", "&amp;")
220
- .replaceAll("<", "&lt;")
221
- .replaceAll(">", "&gt;")
222
- .replaceAll('"', "&quot;")
223
- .replaceAll("'", "&#39;");
224
- }
225
-
226
- function resolvePrismLanguage(rawLanguage: string): string {
227
- const normalized = rawLanguage.trim().toLowerCase();
228
- return PRISM_LANGUAGE_ALIAS[normalized] ?? normalized;
229
- }
230
-
231
- function resolvePrismLanguageLabel(language: string): string {
232
- const normalized = language.trim().toLowerCase();
233
- if (!normalized) {
234
- return "";
235
- }
236
-
237
- return (
238
- PRISM_LANGUAGE_LABEL[normalized] ??
239
- normalized
240
- .replace(/[-_]+/g, " ")
241
- .replace(/\b\w/g, (char) => char.toUpperCase())
242
- );
243
- }
244
-
245
- function resolvePrismGrammar(language: string) {
246
- return (
247
- Prism.languages[language] ??
248
- Prism.languages[PRISM_LANGUAGE_ALIAS[language] ?? ""]
249
- );
250
- }
251
-
252
- function ensureCodeBlockCopyButton(preElement: HTMLPreElement): void {
253
- const existingButton = Array.from(preElement.children).find(
254
- (child) =>
255
- child instanceof HTMLButtonElement &&
256
- child.classList.contains("ask-ai-copy-code-button"),
257
- );
258
-
259
- if (existingButton instanceof HTMLButtonElement) {
260
- return;
261
- }
262
-
263
- const copyButton = document.createElement("button");
264
- copyButton.type = "button";
265
- copyButton.className = "ask-ai-copy-code-button";
266
- copyButton.setAttribute("data-copy-code", "true");
267
- copyButton.setAttribute("aria-label", "Copy code to clipboard");
268
- copyButton.setAttribute("title", "Copy code");
269
-
270
- const copyIcon = document.createElement("span");
271
- copyIcon.className = "ask-ai-copy-icon";
272
- copyIcon.setAttribute("aria-hidden", "true");
273
-
274
- const checkIcon = document.createElement("span");
275
- checkIcon.className = "ask-ai-copy-check-icon";
276
- checkIcon.setAttribute("aria-hidden", "true");
277
-
278
- copyButton.append(copyIcon, checkIcon);
279
- preElement.append(copyButton);
280
- }
281
-
282
- async function copyToClipboard(text: string): Promise<boolean> {
283
- if (
284
- typeof navigator !== "undefined" &&
285
- navigator.clipboard &&
286
- typeof navigator.clipboard.writeText === "function"
287
- ) {
288
- try {
289
- await navigator.clipboard.writeText(text);
290
- return true;
291
- } catch {
292
- // Fall through to legacy copy fallback.
293
- }
294
- }
295
-
296
- if (typeof document === "undefined") {
297
- return false;
298
- }
299
-
300
- try {
301
- const textarea = document.createElement("textarea");
302
- textarea.value = text;
303
- textarea.setAttribute("readonly", "true");
304
- textarea.style.position = "fixed";
305
- textarea.style.top = "-9999px";
306
- textarea.style.opacity = "0";
307
- document.body.append(textarea);
308
- textarea.select();
309
- const didCopy = document.execCommand("copy");
310
- textarea.remove();
311
- return didCopy;
312
- } catch {
313
- return false;
314
- }
315
- }
316
-
317
- function unwrapEscapedFenceCodeBlock(rawCode: string): {
318
- language: string;
319
- code: string;
320
- } | null {
321
- const normalizedCode = rawCode.replaceAll("\r\n", "\n").trim();
322
- if (!normalizedCode) {
323
- return null;
324
- }
325
-
326
- const lines = normalizedCode.split("\n");
327
- if (lines.length < 2) {
328
- return null;
329
- }
330
-
331
- const openingLine = lines[0]?.trim() ?? "";
332
- const openingMatch = openingLine.match(/^(`{3,})([^\s`]*)?(?:\s+[\s\S]*)?$/);
333
- if (!openingMatch) {
334
- return null;
335
- }
336
-
337
- const fenceToken = openingMatch[1];
338
- const closingLine = lines.at(-1)?.trim() ?? "";
339
- if (closingLine !== fenceToken) {
340
- return null;
341
- }
342
-
343
- const languageToken = (openingMatch[2] ?? "").trim();
344
- if (!languageToken) {
345
- return null;
346
- }
347
-
348
- return {
349
- language: resolvePrismLanguage(languageToken),
350
- code: lines.slice(1, -1).join("\n"),
351
- };
352
- }
353
-
354
- function highlightCodeBlocksInHtml(html: string): string {
355
- if (typeof document === "undefined" || !html.trim()) {
356
- return html;
357
- }
358
-
359
- try {
360
- const container = document.createElement("div");
361
- container.innerHTML = html;
362
-
363
- const codeNodes = container.querySelectorAll("pre > code");
364
- codeNodes.forEach((codeNode) => {
365
- const getLanguageClassName = (element: Element | null) =>
366
- Array.from(element?.classList ?? []).find(
367
- (className) =>
368
- className.startsWith("language-") || className.startsWith("lang-"),
369
- );
370
-
371
- const setLanguageClass = (language: string) => {
372
- Array.from(codeNode.classList).forEach((className) => {
373
- if (
374
- className.startsWith("language-") ||
375
- className.startsWith("lang-")
376
- ) {
377
- codeNode.classList.remove(className);
378
- }
379
- });
380
- codeNode.classList.add(`language-${language}`);
381
-
382
- const preParent = codeNode.parentElement;
383
- if (preParent instanceof HTMLPreElement) {
384
- Array.from(preParent.classList).forEach((className) => {
385
- if (
386
- className.startsWith("language-") ||
387
- className.startsWith("lang-")
388
- ) {
389
- preParent.classList.remove(className);
390
- }
391
- });
392
- preParent.classList.add(`language-${language}`);
393
- const languageLabel = resolvePrismLanguageLabel(language);
394
- if (languageLabel) {
395
- preParent.setAttribute("data-language", languageLabel);
396
- } else {
397
- preParent.removeAttribute("data-language");
398
- }
399
-
400
- ensureCodeBlockCopyButton(preParent);
401
- }
402
- };
403
-
404
- const codeLanguageClassName = getLanguageClassName(codeNode);
405
- const preLanguageClassName = getLanguageClassName(codeNode.parentElement);
406
- let rawLanguage =
407
- codeLanguageClassName?.replace(/^(language-|lang-)/, "") ??
408
- preLanguageClassName?.replace(/^(language-|lang-)/, "") ??
409
- "";
410
-
411
- let rawCode = codeNode.textContent ?? "";
412
- if (!rawLanguage) {
413
- const unwrappedFence = unwrapEscapedFenceCodeBlock(rawCode);
414
- if (unwrappedFence) {
415
- rawLanguage = unwrappedFence.language || rawLanguage;
416
- rawCode = unwrappedFence.code;
417
- }
418
- }
419
-
420
- if (!rawLanguage) {
421
- return;
422
- }
423
-
424
- const resolvedLanguage = resolvePrismLanguage(rawLanguage);
425
-
426
- const grammar =
427
- resolvePrismGrammar(resolvedLanguage) ??
428
- resolvePrismGrammar(rawLanguage);
429
- if (!grammar) {
430
- return;
431
- }
432
-
433
- setLanguageClass(resolvedLanguage);
434
- try {
435
- const highlighted = Prism.highlight(rawCode, grammar, resolvedLanguage);
436
- codeNode.innerHTML = highlighted;
437
- } catch (error) {
438
- console.error("Ask AI code highlighting failed", {
439
- language: resolvedLanguage,
440
- error,
441
- });
442
- }
443
- });
444
-
445
- return container.innerHTML;
446
- } catch (error) {
447
- console.error("Ask AI markdown highlighting pipeline failed", error);
448
- return html;
449
- }
450
- }
451
-
452
- function renderMarkdownToHtml(markdown: string): string {
453
- const normalizedMarkdown = markdown.replaceAll("\r\n", "\n");
454
- const cached = markdownHtmlCache.get(normalizedMarkdown);
455
- if (cached !== undefined) {
456
- return cached;
457
- }
458
-
459
- let html = "";
460
-
461
- try {
462
- html = String(
463
- unified()
464
- .use(remarkParse)
465
- .use(remarkGfm)
466
- .use(remarkRehype, { allowDangerousHtml: false })
467
- .use(rehypeExternalLinks)
468
- .use(rehypeStringify)
469
- .processSync(normalizedMarkdown),
470
- );
471
- } catch {
472
- html = escapeHtml(normalizedMarkdown).replaceAll("\n", "<br/>");
473
- }
474
-
475
- html = highlightCodeBlocksInHtml(html);
476
-
477
- markdownHtmlCache.set(normalizedMarkdown, html);
478
- if (markdownHtmlCache.size > MARKDOWN_HTML_CACHE_LIMIT) {
479
- const oldestKey = markdownHtmlCache.keys().next().value;
480
- if (oldestKey) {
481
- markdownHtmlCache.delete(oldestKey);
482
- }
483
- }
484
-
485
- return html;
486
- }
487
-
488
- function createMessageId() {
489
- if (
490
- typeof crypto !== "undefined" &&
491
- typeof crypto.randomUUID === "function"
492
- ) {
493
- return crypto.randomUUID();
494
- }
495
- return `msg_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
496
- }
497
-
498
- function extractErrorMessage(rawBody: string) {
499
- if (!rawBody.trim()) {
500
- return "";
501
- }
502
-
503
- try {
504
- const parsed = JSON.parse(rawBody) as { error?: unknown; detail?: unknown };
505
- if (typeof parsed.error === "string" && parsed.error.trim()) {
506
- if (typeof parsed.detail === "string" && parsed.detail.trim()) {
507
- return `${parsed.error}: ${parsed.detail}`;
508
- }
509
- return parsed.error;
510
- }
511
- } catch {
512
- // Ignore JSON parse errors and fall back to raw text.
513
- }
514
-
515
- return rawBody.trim();
516
- }
517
-
518
- export default function AskAiWidget({
519
- apiPath,
520
- isChatAvailable,
521
- unavailableMessage = "This feature is available on production sites for Pro tier organizations.",
522
- devProxyToken,
523
- }: AskAiWidgetProps) {
524
- const [initialPersistedState] = useState(readPersistedState);
525
- const [isOpen, setIsOpen] = useState(initialPersistedState.isOpen);
526
- const [isMobile, setIsMobile] = useState(false);
527
- const [desktopSize, setDesktopSize] = useState<DesktopSize>({
528
- width: clamp(
529
- initialPersistedState.desktopWidth,
530
- DESKTOP_MIN_WIDTH_PX,
531
- DESKTOP_MAX_WIDTH_PX,
532
- ),
533
- height: initialPersistedState.desktopHeight,
534
- });
535
- const [messages, setMessages] = useState<ChatMessage[]>(
536
- initialPersistedState.messages,
537
- );
538
- const [input, setInput] = useState(initialPersistedState.input);
539
- const [threadScrollTop, setThreadScrollTop] = useState(
540
- initialPersistedState.threadScrollTop,
541
- );
542
- const [isBusy, setIsBusy] = useState(false);
543
- const [isAwaitingFirstToken, setIsAwaitingFirstToken] = useState(false);
544
- const [errorMessage, setErrorMessage] = useState("");
545
- const activeRequestAbortRef = useRef<AbortController | null>(null);
546
- const resizeModeRef = useRef<"left" | "top" | "corner" | null>(null);
547
- const resizeStartPointerRef = useRef<{ x: number; y: number } | null>(null);
548
- const resizeStartSizeRef = useRef<DesktopSize | null>(null);
549
- const desktopScrollViewportRef = useRef<HTMLDivElement | null>(null);
550
- const chatInputTextareaRef = useRef<HTMLTextAreaElement | null>(null);
551
- const bodyBackgroundBeforeMobileDrawerRef = useRef<string | null>(null);
552
- const bodyBackgroundResetTimeoutRef = useRef<number | null>(null);
553
- const chromeScaleOriginalStylesRef = useRef<
554
- Map<HTMLElement, InlineStyleSnapshot>
555
- >(new Map());
556
- const chromeScaleResetTimeoutRef = useRef<number | null>(null);
557
- const threadScrollTopRef = useRef(initialPersistedState.threadScrollTop);
558
- const hasRestoredThreadScrollRef = useRef(false);
559
-
560
- const readCurrentThreadScrollTop = () => {
561
- const viewport = desktopScrollViewportRef.current;
562
- if (!viewport) {
563
- return threadScrollTopRef.current;
564
- }
565
-
566
- const maxScrollTop = Math.max(
567
- 0,
568
- viewport.scrollHeight - viewport.clientHeight,
569
- );
570
- return clamp(viewport.scrollTop, 0, maxScrollTop);
571
- };
572
-
573
- const captureThreadScrollTop = () => {
574
- const nextThreadScrollTop = readCurrentThreadScrollTop();
575
- threadScrollTopRef.current = nextThreadScrollTop;
576
- setThreadScrollTop((previous) =>
577
- Math.abs(previous - nextThreadScrollTop) < 0.5
578
- ? previous
579
- : nextThreadScrollTop,
580
- );
581
- };
582
-
583
- const handleOpenChange = (nextOpen: boolean) => {
584
- if (!nextOpen) {
585
- captureThreadScrollTop();
586
- hasRestoredThreadScrollRef.current = false;
587
- } else {
588
- hasRestoredThreadScrollRef.current = false;
589
- }
590
- setIsOpen(nextOpen);
591
- };
592
-
593
- const handleThreadViewportScroll = () => {
594
- threadScrollTopRef.current = readCurrentThreadScrollTop();
595
- };
596
-
597
- useEffect(() => {
598
- return () => {
599
- activeRequestAbortRef.current?.abort();
600
- activeRequestAbortRef.current = null;
601
- };
602
- }, []);
603
-
604
- useEffect(() => {
605
- const mediaQuery = window.matchMedia("(max-width: 1023px)");
606
- const update = () => {
607
- setIsMobile(mediaQuery.matches);
608
- };
609
-
610
- update();
611
-
612
- if (typeof mediaQuery.addEventListener === "function") {
613
- mediaQuery.addEventListener("change", update);
614
- return () => mediaQuery.removeEventListener("change", update);
615
- }
616
-
617
- mediaQuery.addListener(update);
618
- return () => mediaQuery.removeListener(update);
619
- }, []);
620
-
621
- useEffect(() => {
622
- const clampDesktopSizeToViewport = () => {
623
- setDesktopSize((previous) => {
624
- const maxHeight = getDesktopMaxHeightPx();
625
- const nextWidth = clamp(
626
- previous.width,
627
- DESKTOP_MIN_WIDTH_PX,
628
- DESKTOP_MAX_WIDTH_PX,
629
- );
630
- const nextHeight = clamp(
631
- previous.height,
632
- DESKTOP_MIN_HEIGHT_PX,
633
- maxHeight,
634
- );
635
-
636
- if (nextWidth === previous.width && nextHeight === previous.height) {
637
- return previous;
638
- }
639
-
640
- return {
641
- width: nextWidth,
642
- height: nextHeight,
643
- };
644
- });
645
- };
646
-
647
- clampDesktopSizeToViewport();
648
- window.addEventListener("resize", clampDesktopSizeToViewport);
649
- window.visualViewport?.addEventListener(
650
- "resize",
651
- clampDesktopSizeToViewport,
652
- );
653
- return () => {
654
- window.removeEventListener("resize", clampDesktopSizeToViewport);
655
- window.visualViewport?.removeEventListener(
656
- "resize",
657
- clampDesktopSizeToViewport,
658
- );
659
- };
660
- }, []);
661
-
662
- useEffect(() => {
663
- const openFromHeader = () => {
664
- handleOpenChange(true);
665
- };
666
-
667
- window.addEventListener("ask-ai:open", openFromHeader);
668
- return () => {
669
- window.removeEventListener("ask-ai:open", openFromHeader);
670
- };
671
- }, []);
672
-
673
- useEffect(() => {
674
- if (typeof window === "undefined" || !isOpen) {
675
- return;
676
- }
677
-
678
- if (hasRestoredThreadScrollRef.current) {
679
- return;
680
- }
681
-
682
- const viewport = desktopScrollViewportRef.current;
683
- if (!viewport) {
684
- return;
685
- }
686
-
687
- const restoreFrame = window.requestAnimationFrame(() => {
688
- const maxScrollTop = Math.max(
689
- 0,
690
- viewport.scrollHeight - viewport.clientHeight,
691
- );
692
- const nextScrollTop = clamp(threadScrollTopRef.current, 0, maxScrollTop);
693
- viewport.scrollTop = nextScrollTop;
694
- threadScrollTopRef.current = nextScrollTop;
695
- hasRestoredThreadScrollRef.current = true;
696
- });
697
-
698
- return () => {
699
- window.cancelAnimationFrame(restoreFrame);
700
- };
701
- }, [isOpen, isMobile, messages.length]);
702
-
703
- useEffect(() => {
704
- if (typeof document === "undefined" || typeof window === "undefined") {
705
- return;
706
- }
707
-
708
- if (chromeScaleResetTimeoutRef.current !== null) {
709
- window.clearTimeout(chromeScaleResetTimeoutRef.current);
710
- chromeScaleResetTimeoutRef.current = null;
711
- }
712
-
713
- const chromeElements = Array.from(
714
- document.querySelectorAll<HTMLElement>("[data-vaul-scale-chrome]"),
715
- );
716
- const sharedChromePivotX = window.innerWidth / 2;
717
- const sharedChromePivotY = 0;
718
- const resolveSharedChromeTransformOrigin = (element: HTMLElement) => {
719
- const rect = element.getBoundingClientRect();
720
- return `${sharedChromePivotX - rect.left}px ${sharedChromePivotY - rect.top}px`;
721
- };
722
-
723
- const hasConnectedSnapshotElement = Array.from(
724
- chromeScaleOriginalStylesRef.current.keys(),
725
- ).some((element) => element.isConnected);
726
-
727
- if (isMobile && isOpen) {
728
- if (
729
- chromeScaleOriginalStylesRef.current.size === 0 ||
730
- !hasConnectedSnapshotElement
731
- ) {
732
- chromeScaleOriginalStylesRef.current = new Map(
733
- chromeElements.map((element) => [
734
- element,
735
- {
736
- transform: element.style.transform,
737
- transformOrigin: element.style.transformOrigin,
738
- transitionProperty: element.style.transitionProperty,
739
- transitionDuration: element.style.transitionDuration,
740
- transitionTimingFunction: element.style.transitionTimingFunction,
741
- overflow: element.style.overflow,
742
- },
743
- ]),
744
- );
745
- }
746
-
747
- const wrapperScale =
748
- (window.innerWidth - VAUL_SCALE_WINDOW_TOP_OFFSET_PX) /
749
- window.innerWidth;
750
- const nextTransform = `scale(${wrapperScale}) translate3d(0, calc(env(safe-area-inset-top) + ${VAUL_SCALE_TRANSLATE_TOP_OFFSET_PX}px), 0)`;
751
-
752
- for (const element of chromeElements) {
753
- element.style.transformOrigin =
754
- resolveSharedChromeTransformOrigin(element);
755
- element.style.transitionProperty = "transform";
756
- element.style.transitionDuration = `${MOBILE_DRAWER_TRANSITION_MS}ms`;
757
- element.style.transitionTimingFunction = VAUL_SCALE_TRANSITION_EASE;
758
- element.style.transform = nextTransform;
759
- element.style.overflow = "hidden";
760
- }
761
- return;
762
- }
763
-
764
- if (chromeScaleOriginalStylesRef.current.size === 0) {
765
- return;
766
- }
767
-
768
- const snapshots = chromeScaleOriginalStylesRef.current;
769
- for (const [element, snapshot] of snapshots) {
770
- if (!element.isConnected) {
771
- continue;
772
- }
773
-
774
- element.style.transitionProperty = "transform";
775
- element.style.transitionDuration = `${MOBILE_DRAWER_TRANSITION_MS}ms`;
776
- element.style.transitionTimingFunction = VAUL_SCALE_TRANSITION_EASE;
777
- element.style.transform = snapshot.transform;
778
- element.style.overflow = snapshot.overflow;
779
- }
780
-
781
- chromeScaleResetTimeoutRef.current = window.setTimeout(() => {
782
- for (const [element, snapshot] of snapshots) {
783
- if (!element.isConnected) {
784
- continue;
785
- }
786
- element.style.transform = snapshot.transform;
787
- element.style.transformOrigin = snapshot.transformOrigin;
788
- element.style.transitionProperty = snapshot.transitionProperty;
789
- element.style.transitionDuration = snapshot.transitionDuration;
790
- element.style.transitionTimingFunction =
791
- snapshot.transitionTimingFunction;
792
- element.style.overflow = snapshot.overflow;
793
- }
794
- chromeScaleOriginalStylesRef.current = new Map();
795
- chromeScaleResetTimeoutRef.current = null;
796
- }, MOBILE_DRAWER_TRANSITION_MS);
797
- }, [isMobile, isOpen]);
798
-
799
- useEffect(() => {
800
- if (typeof document === "undefined") {
801
- return;
802
- }
803
-
804
- if (typeof window === "undefined") {
805
- return;
806
- }
807
-
808
- if (bodyBackgroundResetTimeoutRef.current !== null) {
809
- window.clearTimeout(bodyBackgroundResetTimeoutRef.current);
810
- bodyBackgroundResetTimeoutRef.current = null;
811
- }
812
-
813
- if (isMobile && isOpen) {
814
- if (bodyBackgroundBeforeMobileDrawerRef.current === null) {
815
- bodyBackgroundBeforeMobileDrawerRef.current =
816
- document.body.style.backgroundColor;
817
- }
818
- document.body.style.backgroundColor = "var(--color-neutral-100)";
819
- return;
820
- }
821
-
822
- if (bodyBackgroundBeforeMobileDrawerRef.current === null) {
823
- return;
824
- }
825
-
826
- const previousBodyBackgroundColor =
827
- bodyBackgroundBeforeMobileDrawerRef.current;
828
- bodyBackgroundResetTimeoutRef.current = window.setTimeout(() => {
829
- document.body.style.backgroundColor = previousBodyBackgroundColor;
830
- bodyBackgroundBeforeMobileDrawerRef.current = null;
831
- bodyBackgroundResetTimeoutRef.current = null;
832
- }, MOBILE_DRAWER_TRANSITION_MS);
833
- }, [isMobile, isOpen]);
834
-
835
- useEffect(() => {
836
- return () => {
837
- if (
838
- typeof window !== "undefined" &&
839
- bodyBackgroundResetTimeoutRef.current !== null
840
- ) {
841
- window.clearTimeout(bodyBackgroundResetTimeoutRef.current);
842
- bodyBackgroundResetTimeoutRef.current = null;
843
- }
844
-
845
- if (
846
- typeof window !== "undefined" &&
847
- chromeScaleResetTimeoutRef.current !== null
848
- ) {
849
- window.clearTimeout(chromeScaleResetTimeoutRef.current);
850
- chromeScaleResetTimeoutRef.current = null;
851
- }
852
-
853
- if (
854
- typeof document !== "undefined" &&
855
- bodyBackgroundBeforeMobileDrawerRef.current !== null
856
- ) {
857
- document.body.style.backgroundColor =
858
- bodyBackgroundBeforeMobileDrawerRef.current;
859
- bodyBackgroundBeforeMobileDrawerRef.current = null;
860
- }
861
-
862
- for (const [element, snapshot] of chromeScaleOriginalStylesRef.current) {
863
- if (!element.isConnected) {
864
- continue;
865
- }
866
- element.style.transform = snapshot.transform;
867
- element.style.transformOrigin = snapshot.transformOrigin;
868
- element.style.transitionProperty = snapshot.transitionProperty;
869
- element.style.transitionDuration = snapshot.transitionDuration;
870
- element.style.transitionTimingFunction =
871
- snapshot.transitionTimingFunction;
872
- element.style.overflow = snapshot.overflow;
873
- }
874
- chromeScaleOriginalStylesRef.current = new Map();
875
- };
876
- }, []);
877
-
878
- useEffect(() => {
879
- if (typeof window === "undefined") {
880
- return;
881
- }
882
-
883
- const timeoutId = window.setTimeout(() => {
884
- try {
885
- const persistedState: PersistedWidgetState = {
886
- isOpen,
887
- messages,
888
- input,
889
- desktopWidth: desktopSize.width,
890
- desktopHeight: desktopSize.height,
891
- threadScrollTop,
892
- };
893
- window.localStorage.setItem(
894
- STORAGE_KEY,
895
- JSON.stringify(persistedState),
896
- );
897
- } catch {
898
- // Ignore storage write errors (quota/private mode).
899
- }
900
- }, 120);
901
-
902
- return () => {
903
- window.clearTimeout(timeoutId);
904
- };
905
- }, [
906
- isOpen,
907
- messages,
908
- input,
909
- desktopSize.width,
910
- desktopSize.height,
911
- threadScrollTop,
912
- ]);
913
-
914
- useEffect(() => {
915
- const finishResize = () => {
916
- if (resizeModeRef.current) {
917
- resizeModeRef.current = null;
918
- resizeStartPointerRef.current = null;
919
- resizeStartSizeRef.current = null;
920
- document.body.style.userSelect = "";
921
- document.body.style.cursor = "";
922
- }
923
- };
924
-
925
- const handlePointerMove = (event: PointerEvent) => {
926
- if (
927
- !resizeModeRef.current ||
928
- !resizeStartPointerRef.current ||
929
- !resizeStartSizeRef.current
930
- ) {
931
- return;
932
- }
933
-
934
- const deltaX = event.clientX - resizeStartPointerRef.current.x;
935
- const deltaY = event.clientY - resizeStartPointerRef.current.y;
936
- const mode = resizeModeRef.current;
937
- const maxHeight = getDesktopMaxHeightPx();
938
-
939
- let nextWidth = resizeStartSizeRef.current.width;
940
- let nextHeight = resizeStartSizeRef.current.height;
941
-
942
- if (mode === "left" || mode === "corner") {
943
- nextWidth = clamp(
944
- resizeStartSizeRef.current.width - deltaX,
945
- DESKTOP_MIN_WIDTH_PX,
946
- DESKTOP_MAX_WIDTH_PX,
947
- );
948
- }
949
-
950
- if (mode === "top" || mode === "corner") {
951
- nextHeight = clamp(
952
- resizeStartSizeRef.current.height - deltaY,
953
- DESKTOP_MIN_HEIGHT_PX,
954
- maxHeight,
955
- );
956
- }
957
-
958
- setDesktopSize((previous) => {
959
- if (previous.width === nextWidth && previous.height === nextHeight) {
960
- return previous;
961
- }
962
-
963
- return {
964
- width: nextWidth,
965
- height: nextHeight,
966
- };
967
- });
968
- };
969
-
970
- window.addEventListener("pointermove", handlePointerMove);
971
- window.addEventListener("pointerup", finishResize);
972
- window.addEventListener("pointercancel", finishResize);
973
- return () => {
974
- window.removeEventListener("pointermove", handlePointerMove);
975
- window.removeEventListener("pointerup", finishResize);
976
- window.removeEventListener("pointercancel", finishResize);
977
- };
978
- }, []);
979
-
980
- const beginDesktopResize =
981
- (mode: "left" | "top" | "corner") => (event: ResizeHandlePointerEvent) => {
982
- if (isMobile) {
983
- return;
984
- }
985
-
986
- event.preventDefault();
987
- event.stopPropagation();
988
-
989
- resizeModeRef.current = mode;
990
- resizeStartPointerRef.current = {
991
- x: event.clientX,
992
- y: event.clientY,
993
- };
994
- resizeStartSizeRef.current = {
995
- width: desktopSize.width,
996
- height: desktopSize.height,
997
- };
998
-
999
- document.body.style.userSelect = "none";
1000
- document.body.style.cursor =
1001
- mode === "left"
1002
- ? "ew-resize"
1003
- : mode === "top"
1004
- ? "ns-resize"
1005
- : "nwse-resize";
1006
- };
1007
-
1008
- const handleDesktopWheelCapture = (event: DesktopWidgetWheelEvent) => {
1009
- if (isMobile || !isOpen) {
1010
- return;
1011
- }
1012
-
1013
- const inputTextarea = chatInputTextareaRef.current;
1014
- let remainingDeltaY = event.deltaY;
1015
- const eventTargetElement =
1016
- event.target instanceof Element ? event.target : null;
1017
-
1018
- if (
1019
- inputTextarea &&
1020
- event.target instanceof Node &&
1021
- inputTextarea.contains(event.target)
1022
- ) {
1023
- const inputMaxScrollTop =
1024
- inputTextarea.scrollHeight - inputTextarea.clientHeight;
1025
-
1026
- event.preventDefault();
1027
-
1028
- if (inputMaxScrollTop > 0) {
1029
- const previousInputScrollTop = inputTextarea.scrollTop;
1030
- const nextInputScrollTop = clamp(
1031
- previousInputScrollTop + remainingDeltaY,
1032
- 0,
1033
- inputMaxScrollTop,
1034
- );
1035
-
1036
- inputTextarea.scrollTop = nextInputScrollTop;
1037
- remainingDeltaY -= nextInputScrollTop - previousInputScrollTop;
1038
-
1039
- if (Math.abs(remainingDeltaY) < 0.5) {
1040
- return;
1041
- }
1042
- }
1043
- }
1044
-
1045
- const codeBlockPre = eventTargetElement?.closest(
1046
- ".ask-ai-markdown pre",
1047
- ) as HTMLPreElement | null;
1048
- const codeBlockScroller =
1049
- (eventTargetElement?.closest(
1050
- ".ask-ai-markdown pre > code",
1051
- ) as HTMLElement | null) ??
1052
- (codeBlockPre?.querySelector(":scope > code") as HTMLElement | null);
1053
-
1054
- if (codeBlockScroller) {
1055
- const maxScrollLeft = Math.max(
1056
- 0,
1057
- codeBlockScroller.scrollWidth - codeBlockScroller.clientWidth,
1058
- );
1059
- const horizontalIntentDelta =
1060
- Math.abs(event.deltaX) > 0.01
1061
- ? event.deltaX
1062
- : event.shiftKey
1063
- ? event.deltaY
1064
- : 0;
1065
-
1066
- if (Math.abs(horizontalIntentDelta) > 0.01) {
1067
- event.preventDefault();
1068
-
1069
- if (maxScrollLeft > 0) {
1070
- codeBlockScroller.scrollLeft = clamp(
1071
- codeBlockScroller.scrollLeft + horizontalIntentDelta,
1072
- 0,
1073
- maxScrollLeft,
1074
- );
1075
- }
1076
-
1077
- // Shift+wheel maps to horizontal intent; don't also move chat vertically.
1078
- if (event.shiftKey && Math.abs(event.deltaX) <= 0.01) {
1079
- return;
1080
- }
1081
- }
1082
- }
1083
-
1084
- const scrollViewport = desktopScrollViewportRef.current;
1085
- if (!scrollViewport) {
1086
- event.preventDefault();
1087
- return;
1088
- }
1089
-
1090
- event.preventDefault();
1091
-
1092
- const maxScrollTop =
1093
- scrollViewport.scrollHeight - scrollViewport.clientHeight;
1094
- if (maxScrollTop <= 0) {
1095
- return;
1096
- }
1097
-
1098
- const nextDeltaY =
1099
- Math.abs(remainingDeltaY) < 0.5 ? event.deltaY : remainingDeltaY;
1100
-
1101
- scrollViewport.scrollTop = clamp(
1102
- scrollViewport.scrollTop + nextDeltaY,
1103
- 0,
1104
- maxScrollTop,
1105
- );
1106
- };
1107
-
1108
- const resizeChatInputTextarea = () => {
1109
- const textarea = chatInputTextareaRef.current;
1110
- if (!textarea || typeof window === "undefined") {
1111
- return;
1112
- }
1113
-
1114
- textarea.style.height = "auto";
1115
-
1116
- const computedStyle = window.getComputedStyle(textarea);
1117
- const lineHeightRaw = Number.parseFloat(computedStyle.lineHeight);
1118
- const paddingTopRaw = Number.parseFloat(computedStyle.paddingTop);
1119
- const paddingBottomRaw = Number.parseFloat(computedStyle.paddingBottom);
1120
-
1121
- const lineHeight = Number.isFinite(lineHeightRaw) ? lineHeightRaw : 20;
1122
- const paddingTop = Number.isFinite(paddingTopRaw) ? paddingTopRaw : 0;
1123
- const paddingBottom = Number.isFinite(paddingBottomRaw)
1124
- ? paddingBottomRaw
1125
- : 0;
1126
-
1127
- const maxHeight = lineHeight * 11.2 + paddingTop + paddingBottom;
1128
- const nextHeight = Math.min(textarea.scrollHeight, maxHeight);
1129
-
1130
- textarea.style.height = `${nextHeight}px`;
1131
- textarea.style.overflowY =
1132
- textarea.scrollHeight > maxHeight ? "auto" : "hidden";
1133
- };
1134
-
1135
- useEffect(() => {
1136
- resizeChatInputTextarea();
1137
- }, [input, isOpen, isMobile, desktopSize.width]);
1138
-
1139
- const handleSubmit = async (event: Event) => {
1140
- event.preventDefault();
1141
- const prompt = input.trim();
1142
- if (!prompt || isBusy) {
1143
- return;
1144
- }
1145
-
1146
- setErrorMessage("");
1147
- setIsBusy(true);
1148
- setIsAwaitingFirstToken(true);
1149
-
1150
- const userMessage: ChatMessage = {
1151
- id: createMessageId(),
1152
- role: "user",
1153
- content: prompt,
1154
- };
1155
- const assistantId = createMessageId();
1156
- const nextConversation = [...messages, userMessage];
1157
-
1158
- setInput("");
1159
- setMessages(nextConversation);
1160
-
1161
- activeRequestAbortRef.current?.abort();
1162
- const abortController = new AbortController();
1163
- activeRequestAbortRef.current = abortController;
1164
-
1165
- try {
1166
- const requestHeaders: Record<string, string> = {
1167
- "Content-Type": "application/json",
1168
- };
1169
-
1170
- if (devProxyToken && devProxyToken.trim().length > 0) {
1171
- requestHeaders["x-ask-ai-dev-token"] = devProxyToken;
1172
- }
1173
-
1174
- const response = await fetch(apiPath, {
1175
- method: "POST",
1176
- headers: requestHeaders,
1177
- body: JSON.stringify({
1178
- messages: nextConversation.map((message) => ({
1179
- role: message.role,
1180
- content: message.content,
1181
- })),
1182
- }),
1183
- signal: abortController.signal,
1184
- });
1185
-
1186
- if (!response.ok) {
1187
- const rawBody = await response.text();
1188
- const detail = extractErrorMessage(rawBody);
1189
- throw new Error(
1190
- detail || `Request failed with status ${response.status}`,
1191
- );
1192
- }
1193
-
1194
- if (!response.body) {
1195
- throw new Error("Response body is empty");
1196
- }
1197
-
1198
- const reader = response.body.getReader();
1199
- const decoder = new TextDecoder();
1200
- let buffer = "";
1201
- let streamDone = false;
1202
- let hasReceivedFirstTextDelta = false;
1203
-
1204
- while (!streamDone) {
1205
- const { done, value } = await reader.read();
1206
- if (done) {
1207
- break;
1208
- }
1209
-
1210
- buffer += decoder.decode(value, { stream: true });
1211
-
1212
- while (true) {
1213
- const eventBreakIndex = buffer.indexOf("\n\n");
1214
- if (eventBreakIndex === -1) {
1215
- break;
1216
- }
1217
-
1218
- const rawEvent = buffer.slice(0, eventBreakIndex);
1219
- buffer = buffer.slice(eventBreakIndex + 2);
1220
-
1221
- const dataPayload = rawEvent
1222
- .split("\n")
1223
- .filter((line) => line.startsWith("data:"))
1224
- .map((line) => line.slice(5).trimStart())
1225
- .join("\n");
1226
-
1227
- if (!dataPayload) {
1228
- continue;
1229
- }
1230
-
1231
- if (dataPayload === "[DONE]") {
1232
- streamDone = true;
1233
- break;
1234
- }
1235
-
1236
- let parsed: AskAiStreamEvent;
1237
- try {
1238
- parsed = JSON.parse(dataPayload) as AskAiStreamEvent;
1239
- } catch {
1240
- continue;
1241
- }
1242
-
1243
- if (
1244
- parsed.type === "text-delta" &&
1245
- typeof parsed.delta === "string" &&
1246
- parsed.delta.length > 0
1247
- ) {
1248
- const deltaText = parsed.delta;
1249
-
1250
- if (!hasReceivedFirstTextDelta) {
1251
- hasReceivedFirstTextDelta = true;
1252
- setIsAwaitingFirstToken(false);
1253
- }
1254
-
1255
- setMessages((previous) => {
1256
- const existingAssistantMessage = previous.find(
1257
- (message) => message.id === assistantId,
1258
- );
1259
-
1260
- if (!existingAssistantMessage) {
1261
- return [
1262
- ...previous,
1263
- {
1264
- id: assistantId,
1265
- role: "assistant",
1266
- content: deltaText,
1267
- },
1268
- ];
1269
- }
1270
-
1271
- return previous.map((message) =>
1272
- message.id === assistantId
1273
- ? {
1274
- ...message,
1275
- content: `${message.content}${deltaText}`,
1276
- }
1277
- : message,
1278
- );
1279
- });
1280
- }
1281
- }
1282
- }
1283
- } catch (error) {
1284
- if (abortController.signal.aborted) {
1285
- return;
1286
- }
1287
-
1288
- const resolvedError =
1289
- error instanceof Error ? error.message : String(error);
1290
- setErrorMessage(resolvedError || "Failed to get response.");
1291
- } finally {
1292
- if (activeRequestAbortRef.current === abortController) {
1293
- activeRequestAbortRef.current = null;
1294
- }
1295
- setIsBusy(false);
1296
- setIsAwaitingFirstToken(false);
1297
- }
1298
- };
1299
-
1300
- const handleStartNewChat = () => {
1301
- activeRequestAbortRef.current?.abort();
1302
- activeRequestAbortRef.current = null;
1303
- setIsBusy(false);
1304
- setIsAwaitingFirstToken(false);
1305
- setErrorMessage("");
1306
- setInput("");
1307
- setMessages([]);
1308
- setThreadScrollTop(0);
1309
- threadScrollTopRef.current = 0;
1310
- const viewport = desktopScrollViewportRef.current;
1311
- if (viewport) {
1312
- viewport.scrollTop = 0;
1313
- }
1314
- };
1315
-
1316
- const handleChatInputKeyDown = (event: ChatInputKeyEvent) => {
1317
- if (event.key !== "Enter" || event.shiftKey || event.isComposing) {
1318
- return;
1319
- }
1320
-
1321
- event.preventDefault();
1322
- const form = event.currentTarget.form;
1323
- if (form) {
1324
- form.requestSubmit();
1325
- }
1326
- };
1327
-
1328
- const handleRenderedMarkdownClick = (
1329
- event: JSX.TargetedMouseEvent<HTMLDivElement>,
1330
- ) => {
1331
- const targetElement = event.target instanceof Element ? event.target : null;
1332
- const copyButton = targetElement?.closest(
1333
- ".ask-ai-copy-code-button",
1334
- ) as HTMLButtonElement | null;
1335
-
1336
- if (!copyButton) {
1337
- return;
1338
- }
1339
-
1340
- event.preventDefault();
1341
- event.stopPropagation();
1342
-
1343
- if (copyButton.dataset.copying === "true") {
1344
- return;
1345
- }
1346
-
1347
- const preElement = copyButton.closest("pre");
1348
- const codeElement = preElement?.querySelector("code");
1349
- if (!codeElement) {
1350
- return;
1351
- }
1352
-
1353
- const codeText = codeElement.textContent ?? "";
1354
- copyButton.dataset.copying = "true";
1355
-
1356
- void (async () => {
1357
- const didCopy = await copyToClipboard(codeText);
1358
- if (didCopy) {
1359
- copyButton.dataset.copied = "true";
1360
- } else {
1361
- delete copyButton.dataset.copied;
1362
- }
1363
-
1364
- window.setTimeout(() => {
1365
- delete copyButton.dataset.copied;
1366
- delete copyButton.dataset.copying;
1367
- }, 1200);
1368
- })();
1369
- };
1370
-
1371
- const renderPanelContent = (mobile: boolean) => (
1372
- <>
1373
- <div
1374
- className={`flex items-center justify-between border-b border-border px-4 ${mobile ? "py-3" : "py-2"}`}
1375
- >
1376
- <button
1377
- type="button"
1378
- className="inline-flex items-center gap-1.5 rounded-md -ml-2 px-2 py-1 text-[13px] text-neutral-600 hover:bg-neutral-100 dark:text-neutral-300 dark:hover:bg-white/10 transition cursor-pointer"
1379
- onClick={handleStartNewChat}
1380
- disabled={isBusy}
1381
- >
1382
- <Icon
1383
- icon="lucide:messages-square"
1384
- className="size-3.5 -ml-px"
1385
- aria-hidden="true"
1386
- />
1387
- New chat
1388
- </button>
1389
- <button
1390
- type="button"
1391
- className="inline-flex items-center justify-center gap-1 rounded-full -mr-2 -my-px size-8 text-[13px] text-neutral-500 hover:bg-neutral-100 dark:text-neutral-300 dark:hover:bg-white/10 transition cursor-pointer"
1392
- onClick={() => handleOpenChange(false)}
1393
- >
1394
- <Icon icon="lucide:x" className="size-4.5" aria-hidden="true" />
1395
- </button>
1396
- </div>
1397
-
1398
- {isChatAvailable ? (
1399
- <>
1400
- <div
1401
- className={
1402
- mobile
1403
- ? `flex-1 overflow-y-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden px-4 py-3 ${messages.length > 0 && "pb-60"} mb-10 space-y-6`
1404
- : `flex-1 overflow-y-auto overscroll-contain [scrollbar-width:none] [&::-webkit-scrollbar]:hidden px-4 py-3 ${messages.length > 0 && "pb-60"} mb-10 space-y-6`
1405
- }
1406
- ref={desktopScrollViewportRef}
1407
- onScroll={handleThreadViewportScroll}
1408
- >
1409
- {messages.length === 0 ? (
1410
- <div class="flex items-center justify-center h-full">
1411
- <p className="text-3xl font-[450]">How can I help?</p>
1412
- </div>
1413
- ) : null}
1414
-
1415
- {messages.map((message) => {
1416
- if (
1417
- message.role === "assistant" &&
1418
- message.content.length === 0
1419
- ) {
1420
- return null;
1421
- }
1422
- const isUser = message.role === "user";
1423
- return (
1424
- <div
1425
- key={message.id}
1426
- className={isUser ? "text-right" : "text-left"}
1427
- >
1428
- <div
1429
- className={[
1430
- "ask-ai-markdown prose-rules max-w-full min-w-0 wrap-break-word prose-code:text-neutral-700 dark:prose-code:text-neutral-200 prose-pre:shadow-xs prose-pre:text-neutral-700! dark:prose-pre:text-neutral-100! prose-pre:border prose-pre:border-neutral-200 dark:prose-pre:border-neutral-800 prose-pre:rounded-xl!",
1431
- isUser
1432
- ? "inline-block px-3 py-2 rounded-3xl rounded-br-sm bg-neutral-100 text-neutral-700 dark:bg-neutral-800 dark:text-neutral-100 *:text-left"
1433
- : "block w-full bg-transparent text-neutral-900 dark:text-neutral-100",
1434
- ].join(" ")}
1435
- onClick={handleRenderedMarkdownClick}
1436
- dangerouslySetInnerHTML={{
1437
- __html: renderMarkdownToHtml(message.content),
1438
- }}
1439
- ></div>
1440
- </div>
1441
- );
1442
- })}
1443
-
1444
- {isBusy && isAwaitingFirstToken ? (
1445
- <div className="text-left">
1446
- <span
1447
- className="inline-block text-sm text-transparent bg-clip-text bg-[linear-gradient(110deg,var(--color-neutral-400)_50%,var(--color-neutral-300)_60%,var(--color-neutral-400)_70%)] dark:bg-[linear-gradient(110deg,#a3a3a3_30%,#ffffff_45%,#a3a3a3_60%)] bg-[length:200%_100%]"
1448
- style={{
1449
- animation: "ask-ai-thinking-shimmer 3.4s linear infinite",
1450
- }}
1451
- >
1452
- Thinking
1453
- </span>
1454
- </div>
1455
- ) : null}
1456
-
1457
- {errorMessage ? (
1458
- <p className="text-sm text-red-600 dark:text-red-300">
1459
- {errorMessage}
1460
- </p>
1461
- ) : null}
1462
- </div>
1463
-
1464
- <form
1465
- className={
1466
- mobile
1467
- ? "absolute z-10 bottom-0 inset-x-0 px-3 pb-3"
1468
- : "absolute z-10 bottom-0 inset-x-0 px-3 pb-3"
1469
- }
1470
- onSubmit={(event) => {
1471
- void handleSubmit(event);
1472
- }}
1473
- >
1474
- <div className="bg-white dark:bg-(--rd-code-surface) flex gap-2 border border-neutral-200 dark:border-neutral-800 rounded-[24px] shadow-xs">
1475
- <textarea
1476
- ref={chatInputTextareaRef}
1477
- value={input}
1478
- onInput={(event) => {
1479
- setInput((event.target as HTMLTextAreaElement).value);
1480
- }}
1481
- onKeyDown={handleChatInputKeyDown}
1482
- placeholder="Ask a question..."
1483
- className="my-auto min-w-0 flex-1 bg-transparent pl-4 py-3 text-neutral-900 placeholder:text-neutral-400 focus:outline-none dark:text-white dark:placeholder:text-neutral-400 leading-5 resize-none [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
1484
- disabled={isBusy}
1485
- rows={1}
1486
- />
1487
- <div class="p-1 pl-0 mt-auto">
1488
- <button
1489
- type="submit"
1490
- className="flex items-center justify-center rounded-full size-10 bg-linear-to-b from-[var(--color-theme-top)] to-[var(--color-theme-bottom)] text-white disabled:opacity-90 hover:opacity-95 cursor-pointer"
1491
- disabled={isBusy || input.trim().length === 0}
1492
- >
1493
- <Icon
1494
- icon="lucide:arrow-up"
1495
- className="size-5 text-white"
1496
- aria-hidden="true"
1497
- />
1498
- </button>
1499
- </div>
1500
- </div>
1501
- </form>
1502
- </>
1503
- ) : (
1504
- <div className="flex-1 px-4 py-4 flex items-center justify-center">
1505
- <p className="text-sm text-neutral-600 dark:text-neutral-300 flex justify-center gap-1.5">
1506
- <Icon
1507
- icon="lucide:circle-alert"
1508
- className="size-5 shrink-0 self-start justify-self-start mt-0.5 block"
1509
- />
1510
- {unavailableMessage}
1511
- </p>
1512
- </div>
1513
- )}
1514
- </>
1515
- );
1516
-
1517
- const desktopMaxHeight = getDesktopMaxHeightPx();
1518
- const desktopPanelWidth = clamp(
1519
- desktopSize.width,
1520
- DESKTOP_MIN_WIDTH_PX,
1521
- DESKTOP_MAX_WIDTH_PX,
1522
- );
1523
- const desktopPanelHeight = clamp(
1524
- desktopSize.height,
1525
- DESKTOP_MIN_HEIGHT_PX,
1526
- desktopMaxHeight,
1527
- );
1528
-
1529
- return (
1530
- <>
1531
- <style>{`
1532
- @keyframes ask-ai-thinking-shimmer {
1533
- 0% { background-position: 200% 0; }
1534
- 100% { background-position: -200% 0; }
1535
- }
1536
-
1537
- .prose-rules pre[class*="language-"],
1538
- .prose-rules code[class*="language-"] {
1539
- font-family: var(--font-mono);
1540
- font-size: 13px;
1541
- line-height: inherit;
1542
- }
1543
-
1544
- .ask-ai-markdown pre {
1545
- position: relative;
1546
- width: 100%;
1547
- max-width: 100%;
1548
- min-width: 0;
1549
- box-sizing: border-box;
1550
- padding-top: 2.25rem;
1551
- background: var(--color-neutral-50) !important;
1552
- overflow-x: hidden;
1553
- overflow-y: hidden;
1554
- white-space: pre;
1555
- }
1556
-
1557
- .dark .ask-ai-markdown pre {
1558
- background: var(--rd-code-surface) !important;
1559
- border-color: var(--color-neutral-800) !important;
1560
- }
1561
-
1562
- .ask-ai-markdown pre[data-language]::before {
1563
- content: "";
1564
- position: absolute;
1565
- top: 0.52rem;
1566
- left: 0.75rem;
1567
- width: 16px;
1568
- height: 16px;
1569
- background-color: var(--color-neutral-500);
1570
- -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='black' d='m9.4 16.6-4.6-4.6 4.6-4.6L8 6l-6 6 6 6zM14.6 16.6 19.2 12l-4.6-4.6L16 6l6 6-6 6z'/%3E%3C/svg%3E");
1571
- mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='black' d='m9.4 16.6-4.6-4.6 4.6-4.6L8 6l-6 6 6 6zM14.6 16.6 19.2 12l-4.6-4.6L16 6l6 6-6 6z'/%3E%3C/svg%3E");
1572
- -webkit-mask-repeat: no-repeat;
1573
- mask-repeat: no-repeat;
1574
- -webkit-mask-position: center;
1575
- mask-position: center;
1576
- -webkit-mask-size: contain;
1577
- mask-size: contain;
1578
- pointer-events: none;
1579
- user-select: none;
1580
- }
1581
-
1582
- .dark .ask-ai-markdown pre[data-language]::before {
1583
- background-color: var(--color-neutral-400);
1584
- }
1585
-
1586
- .ask-ai-markdown pre[data-language]::after {
1587
- content: attr(data-language);
1588
- position: absolute;
1589
- top: 0.6rem;
1590
- left: 2.2rem;
1591
- font-family: var(--font-sans);
1592
- font-size: 14px;
1593
- line-height: 1;
1594
- letter-spacing: 0.01em;
1595
- color: var(--color-neutral-500);
1596
- pointer-events: none;
1597
- user-select: none;
1598
- }
1599
-
1600
- .dark .ask-ai-markdown pre[data-language]::after {
1601
- color: var(--color-neutral-400);
1602
- }
1603
-
1604
- .ask-ai-markdown .ask-ai-copy-code-button {
1605
- position: absolute;
1606
- top: 0.3rem;
1607
- right: 0.46rem;
1608
- z-index: 2;
1609
- appearance: none;
1610
- display: inline-flex;
1611
- align-items: center;
1612
- justify-content: center;
1613
- width: 1.75rem;
1614
- height: 1.75rem;
1615
- border: 1px solid
1616
- color-mix(in oklab, var(--color-neutral-200) 80%, transparent);
1617
- background: color-mix(
1618
- in oklab,
1619
- #fff 80%,
1620
- transparent
1621
- );
1622
- backdrop-filter: blur(4px);
1623
- color: color-mix(
1624
- in oklab,
1625
- var(--color-neutral-500) 80%,
1626
- transparent
1627
- );
1628
- border-radius: 0.375rem;
1629
- outline: none;
1630
- box-shadow: none;
1631
- cursor: pointer;
1632
- transition: background-color 150ms ease, color 150ms ease,
1633
- border-color 150ms ease, transform 120ms ease;
1634
- }
1635
-
1636
- .dark .ask-ai-markdown .ask-ai-copy-code-button {
1637
- border-color: color-mix(
1638
- in oklab,
1639
- var(--color-neutral-700) 50%,
1640
- transparent
1641
- );
1642
- background: var(--rd-code-surface);
1643
- color: var(--color-neutral-400);
1644
- }
1645
-
1646
- .ask-ai-markdown .ask-ai-copy-code-button:hover {
1647
- background: var(--color-neutral-50);
1648
- color: var(--color-neutral-600);
1649
- }
1650
-
1651
- .dark .ask-ai-markdown .ask-ai-copy-code-button:hover {
1652
- background: var(--color-neutral-800);
1653
- color: var(--color-neutral-200);
1654
- }
1655
-
1656
- .ask-ai-markdown .ask-ai-copy-code-button:active {
1657
- transform: translateY(0.5px);
1658
- }
1659
-
1660
- .ask-ai-markdown .ask-ai-copy-code-button:focus-visible {
1661
- outline: none;
1662
- box-shadow: none;
1663
- }
1664
-
1665
- .dark .ask-ai-markdown .ask-ai-copy-code-button:focus-visible {
1666
- outline: none;
1667
- box-shadow: none;
1668
- }
1669
-
1670
- .ask-ai-markdown .ask-ai-copy-icon,
1671
- .ask-ai-markdown .ask-ai-copy-check-icon {
1672
- position: absolute;
1673
- width: 14px;
1674
- height: 14px;
1675
- transition: transform 250ms cubic-bezier(0.22, 1, 0.36, 1),
1676
- opacity 250ms cubic-bezier(0.22, 1, 0.36, 1);
1677
- will-change: transform, opacity;
1678
- }
1679
-
1680
- .ask-ai-markdown .ask-ai-copy-icon {
1681
- opacity: 1;
1682
- transform: scale(1) rotate(0deg);
1683
- background-color: currentColor;
1684
- -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='black' stroke-width='2'%3E%3Crect x='9' y='9' width='13' height='13' rx='2'/%3E%3Cpath d='M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1'/%3E%3C/svg%3E");
1685
- mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='black' stroke-width='2'%3E%3Crect x='9' y='9' width='13' height='13' rx='2'/%3E%3Cpath d='M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1'/%3E%3C/svg%3E");
1686
- -webkit-mask-repeat: no-repeat;
1687
- mask-repeat: no-repeat;
1688
- -webkit-mask-position: center;
1689
- mask-position: center;
1690
- -webkit-mask-size: contain;
1691
- mask-size: contain;
1692
- }
1693
-
1694
- .ask-ai-markdown .ask-ai-copy-check-icon {
1695
- opacity: 0;
1696
- transform: scale(0.25) rotate(6deg);
1697
- background-color: color-mix(
1698
- in oklab,
1699
- var(--color-green-700) 80%,
1700
- transparent
1701
- );
1702
- -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='black' stroke-width='2'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M20 6L9 17l-5-5'/%3E%3C/svg%3E");
1703
- mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='black' stroke-width='2'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M20 6L9 17l-5-5'/%3E%3C/svg%3E");
1704
- -webkit-mask-repeat: no-repeat;
1705
- mask-repeat: no-repeat;
1706
- -webkit-mask-position: center;
1707
- mask-position: center;
1708
- -webkit-mask-size: contain;
1709
- mask-size: contain;
1710
- }
1711
-
1712
- .dark .ask-ai-markdown .ask-ai-copy-check-icon {
1713
- background-color: color-mix(
1714
- in oklab,
1715
- var(--color-green-400) 90%,
1716
- transparent
1717
- );
1718
- }
1719
-
1720
- .ask-ai-markdown .ask-ai-copy-code-button[data-copied="true"] .ask-ai-copy-icon {
1721
- opacity: 0;
1722
- transform: scale(0.5) rotate(-6deg);
1723
- }
1724
-
1725
- .ask-ai-markdown .ask-ai-copy-code-button[data-copied="true"] .ask-ai-copy-check-icon {
1726
- opacity: 1;
1727
- transform: scale(1.1) rotate(0deg);
1728
- }
1729
-
1730
- .ask-ai-markdown pre > code {
1731
- display: block;
1732
- width: 100%;
1733
- min-width: 100%;
1734
- background: transparent !important;
1735
- white-space: inherit;
1736
- word-break: normal;
1737
- overflow-wrap: normal;
1738
- overflow-x: auto;
1739
- overflow-y: hidden;
1740
- -webkit-overflow-scrolling: touch;
1741
- scrollbar-width: none;
1742
- -ms-overflow-style: none;
1743
- }
1744
-
1745
- .dark .ask-ai-markdown pre > code {
1746
- scrollbar-width: none;
1747
- }
1748
-
1749
- .ask-ai-markdown pre[class*="language-"] {
1750
- background: var(--color-neutral-50) !important;
1751
- }
1752
-
1753
- .ask-ai-markdown pre[class*="language-"] > code[class*="language-"] {
1754
- background: transparent !important;
1755
- }
1756
-
1757
- .ask-ai-markdown :not(pre) > code[class*="language-"] {
1758
- background: var(--color-neutral-100);
1759
- }
1760
-
1761
- .dark .ask-ai-markdown pre[class*="language-"] {
1762
- background: var(--rd-code-surface) !important;
1763
- border-color: var(--color-neutral-800) !important;
1764
- }
1765
-
1766
- .dark .ask-ai-markdown pre[class*="language-"] > code[class*="language-"] {
1767
- background: transparent !important;
1768
- }
1769
-
1770
- .dark .ask-ai-markdown pre[class*="language-"],
1771
- .dark .ask-ai-markdown code[class*="language-"] {
1772
- color: var(--color-neutral-100);
1773
- }
1774
-
1775
- .dark .ask-ai-markdown :not(pre) > code[class*="language-"] {
1776
- background: var(--color-neutral-800);
1777
- }
1778
-
1779
- .dark .ask-ai-markdown .token.comment,
1780
- .dark .ask-ai-markdown .token.prolog,
1781
- .dark .ask-ai-markdown .token.doctype,
1782
- .dark .ask-ai-markdown .token.cdata {
1783
- color: #7f848e;
1784
- }
1785
-
1786
- .dark .ask-ai-markdown .token.punctuation {
1787
- color: #abb2bf;
1788
- }
1789
-
1790
- .dark .ask-ai-markdown .token.property,
1791
- .dark .ask-ai-markdown .token.tag,
1792
- .dark .ask-ai-markdown .token.boolean,
1793
- .dark .ask-ai-markdown .token.number,
1794
- .dark .ask-ai-markdown .token.constant,
1795
- .dark .ask-ai-markdown .token.symbol,
1796
- .dark .ask-ai-markdown .token.deleted {
1797
- color: #d19a66;
1798
- }
1799
-
1800
- .dark .ask-ai-markdown .token.selector,
1801
- .dark .ask-ai-markdown .token.attr-name,
1802
- .dark .ask-ai-markdown .token.string,
1803
- .dark .ask-ai-markdown .token.char,
1804
- .dark .ask-ai-markdown .token.builtin,
1805
- .dark .ask-ai-markdown .token.inserted {
1806
- color: #98c379;
1807
- }
1808
-
1809
- .dark .ask-ai-markdown .token.operator,
1810
- .dark .ask-ai-markdown .token.entity,
1811
- .dark .ask-ai-markdown .token.url,
1812
- .dark .ask-ai-markdown .language-css .token.string,
1813
- .dark .ask-ai-markdown .style .token.string {
1814
- color: #56b6c2;
1815
- }
1816
-
1817
- .dark .ask-ai-markdown .token.atrule,
1818
- .dark .ask-ai-markdown .token.attr-value,
1819
- .dark .ask-ai-markdown .token.keyword {
1820
- color: #c678dd;
1821
- }
1822
-
1823
- .dark .ask-ai-markdown .token.function,
1824
- .dark .ask-ai-markdown .token.class-name {
1825
- color: #e5c07b;
1826
- }
1827
-
1828
- .dark .ask-ai-markdown .token.regex,
1829
- .dark .ask-ai-markdown .token.important,
1830
- .dark .ask-ai-markdown .token.variable {
1831
- color: #e06c75;
1832
- }
1833
-
1834
- .dark .ask-ai-markdown .token.attr-value > .token.punctuation.attr-equals,
1835
- .dark
1836
- .ask-ai-markdown
1837
- .token.special-attr
1838
- > .token.attr-value
1839
- > .token.value.css {
1840
- color: #abb2bf;
1841
- }
1842
-
1843
- .dark .ask-ai-markdown .language-css .token.selector {
1844
- color: #d19a66;
1845
- }
1846
-
1847
- .dark .ask-ai-markdown .language-css .token.property {
1848
- color: #abb2bf;
1849
- }
1850
-
1851
- .dark .ask-ai-markdown .language-css .token.function,
1852
- .dark .ask-ai-markdown .language-css .token.url > .token.function {
1853
- color: #56b6c2;
1854
- }
1855
-
1856
- .dark .ask-ai-markdown .language-css .token.url > .token.string.url {
1857
- color: #98c379;
1858
- }
1859
-
1860
- .dark .ask-ai-markdown .language-css .token.important,
1861
- .dark .ask-ai-markdown .language-css .token.atrule .token.rule {
1862
- color: #c678dd;
1863
- }
1864
-
1865
- .dark .ask-ai-markdown .language-javascript .token.operator {
1866
- color: #c678dd;
1867
- }
1868
-
1869
- .dark
1870
- .ask-ai-markdown
1871
- .language-javascript
1872
- .token.template-string
1873
- > .token.interpolation
1874
- > .token.interpolation-punctuation.punctuation {
1875
- color: #e06c75;
1876
- }
1877
-
1878
- .dark .ask-ai-markdown .language-json .token.operator {
1879
- color: #abb2bf;
1880
- }
1881
-
1882
- .dark .ask-ai-markdown .language-json .token.null.keyword {
1883
- color: #d19a66;
1884
- }
1885
-
1886
- .dark .ask-ai-markdown .language-markdown .token.url,
1887
- .dark .ask-ai-markdown .language-markdown .token.url > .token.operator,
1888
- .dark
1889
- .ask-ai-markdown
1890
- .language-markdown
1891
- .token.url-reference.url
1892
- > .token.string {
1893
- color: #abb2bf;
1894
- }
1895
-
1896
- .dark .ask-ai-markdown .language-markdown .token.url > .token.content {
1897
- color: #56b6c2;
1898
- }
1899
-
1900
- .dark .ask-ai-markdown .language-markdown .token.url > .token.url,
1901
- .dark .ask-ai-markdown .language-markdown .token.url-reference.url {
1902
- color: #98c379;
1903
- }
1904
-
1905
- .dark .ask-ai-markdown .language-markdown .token.blockquote.punctuation,
1906
- .dark .ask-ai-markdown .language-markdown .token.hr.punctuation {
1907
- color: #7f848e;
1908
- }
1909
-
1910
- .dark .ask-ai-markdown .language-markdown .token.code-snippet {
1911
- color: #98c379;
1912
- }
1913
-
1914
- .dark .ask-ai-markdown .language-markdown .token.bold .token.content {
1915
- color: #e5c07b;
1916
- }
1917
-
1918
- .dark .ask-ai-markdown .language-markdown .token.italic .token.content {
1919
- color: #c678dd;
1920
- }
1921
-
1922
- .dark .ask-ai-markdown .language-markdown .token.strike .token.content,
1923
- .dark
1924
- .ask-ai-markdown
1925
- .language-markdown
1926
- .token.strike
1927
- .token.punctuation,
1928
- .dark .ask-ai-markdown .language-markdown .token.list.punctuation,
1929
- .dark
1930
- .ask-ai-markdown
1931
- .language-markdown
1932
- .token.title.important
1933
- > .token.punctuation {
1934
- color: #e06c75;
1935
- }
1936
-
1937
- .ask-ai-markdown pre > code::-webkit-scrollbar {
1938
- width: 0;
1939
- height: 0;
1940
- display: none;
1941
- }
1942
- `}</style>
1943
- {!isOpen ? (
1944
- <div className="fixed bottom-5 right-5 size-12 z-40 bg-background rounded-full hover:scale-105 transition duration-300 ease-in-out">
1945
- <button
1946
- type="button"
1947
- className="w-full h-full inline-flex items-center justify-center gap-2 rounded-full bg-linear-to-b from-[var(--color-theme-top)] to-[var(--color-theme-bottom)] text-[var(--color-theme-foreground)] shadow-xl dark:shadow-white/5 hover:opacity-95 cursor-pointer"
1948
- onClick={() => handleOpenChange(true)}
1949
- data-pagefind-ignore
1950
- >
1951
- <img
1952
- src={sparkleIcon}
1953
- alt=""
1954
- aria-hidden="true"
1955
- className="size-4"
1956
- style={{ filter: "var(--color-theme-icon-filter)" }}
1957
- />
1958
- </button>
1959
- </div>
1960
- ) : null}
1961
-
1962
- {isMobile ? (
1963
- <Drawer.Root
1964
- open={isOpen}
1965
- onOpenChange={handleOpenChange}
1966
- disablePreventScroll={true}
1967
- repositionInputs={false}
1968
- >
1969
- <Drawer.Portal>
1970
- <Drawer.Overlay className="fixed inset-0 z-50 bg-black/40" />
1971
- <Drawer.Content
1972
- className="fixed inset-x-0 bottom-0 z-50 flex h-[85dvh] max-h-[85dvh] flex-col rounded-t-2xl border border-border bg-background shadow-xl will-change-transform"
1973
- data-pagefind-ignore
1974
- >
1975
- <div className="absolute left-1/2 -translate-x-1/2 mt-2 h-1.5 w-12 rounded-full bg-neutral-300 dark:bg-neutral-700" />
1976
- {renderPanelContent(true)}
1977
- </Drawer.Content>
1978
- </Drawer.Portal>
1979
- </Drawer.Root>
1980
- ) : isOpen ? (
1981
- <div
1982
- className="fixed bottom-4 right-4 z-50 dark:bg-neutral-800 rounded-2xl"
1983
- data-pagefind-ignore
1984
- onWheelCapture={handleDesktopWheelCapture}
1985
- >
1986
- <div
1987
- className="relative flex flex-col overflow-hidden rounded-2xl border border-border bg-background dark:bg-neutral-900/80 shadow-[0_6px_20px_rgba(0,0,0,0.2)] shadow-neutral-300. shadow-black/15"
1988
- style={{
1989
- width: `${desktopPanelWidth}px`,
1990
- height: `${desktopPanelHeight}px`,
1991
- }}
1992
- >
1993
- <div
1994
- className="absolute left-0 top-3 bottom-3 z-20 w-2 cursor-ew-resize touch-none"
1995
- onPointerDown={beginDesktopResize("left")}
1996
- />
1997
- <div
1998
- className="absolute left-3 right-3 top-0 z-20 h-2 cursor-ns-resize touch-none"
1999
- onPointerDown={beginDesktopResize("top")}
2000
- />
2001
- <div
2002
- className="absolute left-0 top-0 z-30 h-3 w-3 cursor-nwse-resize touch-none"
2003
- onPointerDown={beginDesktopResize("corner")}
2004
- />
2005
- {renderPanelContent(false)}
2006
- </div>
2007
- </div>
2008
- ) : null}
2009
- </>
2010
- );
2011
- }