radiant-docs 0.1.40 → 0.1.41

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,1693 @@
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 "prism-themes/themes/prism-one-light.css";
26
+ import { type Plugin, unified } from "unified";
27
+ import remarkParse from "remark-parse";
28
+ import remarkGfm from "remark-gfm";
29
+ import remarkRehype from "remark-rehype";
30
+ import rehypeStringify from "rehype-stringify";
31
+
32
+ type AssistantLinkTarget = "current" | "blank";
33
+
34
+ type HastNode = {
35
+ type?: string;
36
+ tagName?: string;
37
+ properties?: Record<string, unknown>;
38
+ children?: HastNode[];
39
+ };
40
+
41
+ type AssistantEmbedPanelProps = {
42
+ apiPath: string;
43
+ docsTitle: string;
44
+ isChatAvailable: boolean;
45
+ canSendChatRequest: boolean;
46
+ launcherThemeColor?: string;
47
+ launcherThemeColors?: AssistantColorByMode;
48
+ launcherIconColor?: string;
49
+ launcherIconColors?: AssistantColorByMode;
50
+ launcherIconImageSrc?: string;
51
+ emptyStateHeading?: string;
52
+ emptyStateQuestions?: string[];
53
+ unavailableMessage?: string;
54
+ devProxyToken?: string;
55
+ panelSurface?: "iframe" | "inline";
56
+ linkTarget?: AssistantLinkTarget;
57
+ allowApiPathQueryOverride?: boolean;
58
+ openSignal?: number;
59
+ onRequestClose?: () => void;
60
+ };
61
+
62
+ type AssistantColorByMode = {
63
+ light: string;
64
+ dark: string;
65
+ };
66
+
67
+ type ChatMessage = {
68
+ id: string;
69
+ role: "user" | "assistant";
70
+ content: string;
71
+ };
72
+
73
+ type AssistantStreamEvent = {
74
+ type?: string;
75
+ delta?: string;
76
+ };
77
+
78
+ type ChatInputKeyEvent = JSX.TargetedKeyboardEvent<HTMLTextAreaElement>;
79
+ type ChatViewportWheelEvent = JSX.TargetedWheelEvent<HTMLDivElement>;
80
+
81
+ const STORAGE_KEY = "docs:assistant-embed-panel:v1";
82
+ const MARKDOWN_HTML_CACHE_LIMIT = 300;
83
+ const markdownHtmlCache = new Map<string, string>();
84
+ const PRISM_LANGUAGE_ALIAS: Record<string, string> = {
85
+ js: "javascript",
86
+ ts: "typescript",
87
+ yml: "yaml",
88
+ sh: "bash",
89
+ shell: "bash",
90
+ shellscript: "bash",
91
+ md: "markdown",
92
+ mdx: "markdown",
93
+ };
94
+
95
+ const PRISM_LANGUAGE_LABEL: Record<string, string> = {
96
+ javascript: "JavaScript",
97
+ typescript: "TypeScript",
98
+ jsx: "JSX",
99
+ tsx: "TSX",
100
+ json: "JSON",
101
+ markdown: "Markdown",
102
+ bash: "Bash",
103
+ shell: "Shell",
104
+ python: "Python",
105
+ yaml: "YAML",
106
+ sql: "SQL",
107
+ rust: "Rust",
108
+ go: "Go",
109
+ java: "Java",
110
+ css: "CSS",
111
+ html: "HTML",
112
+ diff: "Diff",
113
+ mdx: "MDX",
114
+ };
115
+
116
+ function normalizeRelTokens(value: unknown): string[] {
117
+ if (Array.isArray(value)) {
118
+ return value
119
+ .flatMap((entry) => (typeof entry === "string" ? entry.split(/\s+/) : []))
120
+ .filter(Boolean);
121
+ }
122
+
123
+ if (typeof value === "string") {
124
+ return value.split(/\s+/).filter(Boolean);
125
+ }
126
+
127
+ return [];
128
+ }
129
+
130
+ function visitHastNodes(
131
+ node: HastNode,
132
+ visitor: (node: HastNode) => void,
133
+ ): void {
134
+ visitor(node);
135
+ if (!Array.isArray(node.children)) return;
136
+ for (const child of node.children) {
137
+ visitHastNodes(child, visitor);
138
+ }
139
+ }
140
+
141
+ const rehypeOpenLinksInNewTab: Plugin<[], HastNode> = () => {
142
+ return (tree) => {
143
+ visitHastNodes(tree, (node) => {
144
+ if (node.type !== "element" || node.tagName !== "a") return;
145
+
146
+ const props = (node.properties ??= {});
147
+ props.target = "_blank";
148
+
149
+ const relTokens = normalizeRelTokens(props.rel);
150
+ if (!relTokens.includes("noopener")) relTokens.push("noopener");
151
+ if (!relTokens.includes("noreferrer")) relTokens.push("noreferrer");
152
+ props.rel = relTokens.join(" ");
153
+ });
154
+ };
155
+ };
156
+
157
+ function clamp(value: number, min: number, max: number) {
158
+ return Math.min(Math.max(value, min), max);
159
+ }
160
+
161
+ function createMessageId(): string {
162
+ if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
163
+ return crypto.randomUUID();
164
+ }
165
+
166
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
167
+ }
168
+
169
+ function readPersistedMessages(): ChatMessage[] {
170
+ if (typeof window === "undefined") {
171
+ return [];
172
+ }
173
+
174
+ try {
175
+ const raw = window.localStorage.getItem(STORAGE_KEY);
176
+ if (!raw) {
177
+ return [];
178
+ }
179
+
180
+ const parsed = JSON.parse(raw) as { messages?: unknown };
181
+ if (!Array.isArray(parsed.messages)) {
182
+ return [];
183
+ }
184
+
185
+ return parsed.messages
186
+ .filter((message): message is ChatMessage => {
187
+ if (!message || typeof message !== "object") {
188
+ return false;
189
+ }
190
+ const candidate = message as Partial<ChatMessage>;
191
+ return (
192
+ (candidate.role === "user" || candidate.role === "assistant") &&
193
+ typeof candidate.id === "string" &&
194
+ typeof candidate.content === "string"
195
+ );
196
+ })
197
+ .map((message) => ({
198
+ id: message.id,
199
+ role: message.role,
200
+ content: message.content,
201
+ }));
202
+ } catch {
203
+ return [];
204
+ }
205
+ }
206
+
207
+ function escapeHtml(value: string): string {
208
+ return value
209
+ .replaceAll("&", "&amp;")
210
+ .replaceAll("<", "&lt;")
211
+ .replaceAll(">", "&gt;")
212
+ .replaceAll('"', "&quot;")
213
+ .replaceAll("'", "&#39;");
214
+ }
215
+
216
+ function resolvePrismLanguage(rawLanguage: string): string {
217
+ const normalized = rawLanguage.trim().toLowerCase();
218
+ return PRISM_LANGUAGE_ALIAS[normalized] ?? normalized;
219
+ }
220
+
221
+ function resolvePrismLanguageLabel(language: string): string {
222
+ const normalized = language.trim().toLowerCase();
223
+ if (!normalized) {
224
+ return "";
225
+ }
226
+
227
+ return (
228
+ PRISM_LANGUAGE_LABEL[normalized] ??
229
+ normalized
230
+ .replace(/[-_]+/g, " ")
231
+ .replace(/\b\w/g, (char) => char.toUpperCase())
232
+ );
233
+ }
234
+
235
+ function resolvePrismGrammar(language: string) {
236
+ return (
237
+ Prism.languages[language] ??
238
+ Prism.languages[PRISM_LANGUAGE_ALIAS[language] ?? ""]
239
+ );
240
+ }
241
+
242
+ function ensureCodeBlockCopyButton(preElement: HTMLPreElement): void {
243
+ const existingButton = Array.from(preElement.children).find(
244
+ (child) =>
245
+ child instanceof HTMLButtonElement &&
246
+ child.classList.contains("ask-ai-copy-code-button"),
247
+ );
248
+
249
+ if (existingButton instanceof HTMLButtonElement) {
250
+ return;
251
+ }
252
+
253
+ const copyButton = document.createElement("button");
254
+ copyButton.type = "button";
255
+ copyButton.className = "ask-ai-copy-code-button";
256
+ copyButton.setAttribute("data-copy-code", "true");
257
+ copyButton.setAttribute("aria-label", "Copy code to clipboard");
258
+ copyButton.setAttribute("title", "Copy code");
259
+
260
+ const copyIcon = document.createElement("span");
261
+ copyIcon.className = "ask-ai-copy-icon";
262
+ copyIcon.setAttribute("aria-hidden", "true");
263
+
264
+ const checkIcon = document.createElement("span");
265
+ checkIcon.className = "ask-ai-copy-check-icon";
266
+ checkIcon.setAttribute("aria-hidden", "true");
267
+
268
+ copyButton.append(copyIcon, checkIcon);
269
+ preElement.append(copyButton);
270
+ }
271
+
272
+ async function copyToClipboard(text: string): Promise<boolean> {
273
+ if (
274
+ typeof navigator !== "undefined" &&
275
+ navigator.clipboard &&
276
+ typeof navigator.clipboard.writeText === "function"
277
+ ) {
278
+ try {
279
+ await navigator.clipboard.writeText(text);
280
+ return true;
281
+ } catch {
282
+ // Fall through to legacy copy fallback.
283
+ }
284
+ }
285
+
286
+ if (typeof document === "undefined") {
287
+ return false;
288
+ }
289
+
290
+ try {
291
+ const textarea = document.createElement("textarea");
292
+ textarea.value = text;
293
+ textarea.setAttribute("readonly", "true");
294
+ textarea.style.position = "fixed";
295
+ textarea.style.top = "-9999px";
296
+ textarea.style.opacity = "0";
297
+ document.body.append(textarea);
298
+ textarea.select();
299
+ const didCopy = document.execCommand("copy");
300
+ textarea.remove();
301
+ return didCopy;
302
+ } catch {
303
+ return false;
304
+ }
305
+ }
306
+
307
+ function unwrapEscapedFenceCodeBlock(rawCode: string): {
308
+ language: string;
309
+ code: string;
310
+ } | null {
311
+ const normalizedCode = rawCode.replaceAll("\r\n", "\n").trim();
312
+ if (!normalizedCode) {
313
+ return null;
314
+ }
315
+
316
+ const lines = normalizedCode.split("\n");
317
+ if (lines.length < 2) {
318
+ return null;
319
+ }
320
+
321
+ const openingLine = lines[0]?.trim() ?? "";
322
+ const openingMatch = openingLine.match(/^(`{3,})([^\s`]*)?(?:\s+[\s\S]*)?$/);
323
+ if (!openingMatch) {
324
+ return null;
325
+ }
326
+
327
+ const fenceToken = openingMatch[1];
328
+ const closingLine = lines.at(-1)?.trim() ?? "";
329
+ if (closingLine !== fenceToken) {
330
+ return null;
331
+ }
332
+
333
+ const languageToken = (openingMatch[2] ?? "").trim();
334
+ if (!languageToken) {
335
+ return null;
336
+ }
337
+
338
+ return {
339
+ language: resolvePrismLanguage(languageToken),
340
+ code: lines.slice(1, -1).join("\n"),
341
+ };
342
+ }
343
+
344
+ function highlightCodeBlocksInHtml(html: string): string {
345
+ if (typeof document === "undefined" || !html.trim()) {
346
+ return html;
347
+ }
348
+
349
+ try {
350
+ const container = document.createElement("div");
351
+ container.innerHTML = html;
352
+
353
+ const codeNodes = container.querySelectorAll("pre > code");
354
+ codeNodes.forEach((codeNode) => {
355
+ const getLanguageClassName = (element: Element | null) =>
356
+ Array.from(element?.classList ?? []).find(
357
+ (className) =>
358
+ className.startsWith("language-") || className.startsWith("lang-"),
359
+ );
360
+
361
+ const setLanguageClass = (language: string) => {
362
+ Array.from(codeNode.classList).forEach((className) => {
363
+ if (
364
+ className.startsWith("language-") ||
365
+ className.startsWith("lang-")
366
+ ) {
367
+ codeNode.classList.remove(className);
368
+ }
369
+ });
370
+ codeNode.classList.add(`language-${language}`);
371
+
372
+ const preParent = codeNode.parentElement;
373
+ if (preParent instanceof HTMLPreElement) {
374
+ Array.from(preParent.classList).forEach((className) => {
375
+ if (
376
+ className.startsWith("language-") ||
377
+ className.startsWith("lang-")
378
+ ) {
379
+ preParent.classList.remove(className);
380
+ }
381
+ });
382
+ preParent.classList.add(`language-${language}`);
383
+ const languageLabel = resolvePrismLanguageLabel(language);
384
+ if (languageLabel) {
385
+ preParent.setAttribute("data-language", languageLabel);
386
+ } else {
387
+ preParent.removeAttribute("data-language");
388
+ }
389
+
390
+ ensureCodeBlockCopyButton(preParent);
391
+ }
392
+ };
393
+
394
+ const codeLanguageClassName = getLanguageClassName(codeNode);
395
+ const preLanguageClassName = getLanguageClassName(codeNode.parentElement);
396
+ let rawLanguage =
397
+ codeLanguageClassName?.replace(/^(language-|lang-)/, "") ??
398
+ preLanguageClassName?.replace(/^(language-|lang-)/, "") ??
399
+ "";
400
+
401
+ let rawCode = codeNode.textContent ?? "";
402
+ if (!rawLanguage) {
403
+ const unwrappedFence = unwrapEscapedFenceCodeBlock(rawCode);
404
+ if (unwrappedFence) {
405
+ rawLanguage = unwrappedFence.language || rawLanguage;
406
+ rawCode = unwrappedFence.code;
407
+ }
408
+ }
409
+
410
+ if (!rawLanguage) {
411
+ return;
412
+ }
413
+
414
+ const resolvedLanguage = resolvePrismLanguage(rawLanguage);
415
+ const grammar =
416
+ resolvePrismGrammar(resolvedLanguage) ??
417
+ resolvePrismGrammar(rawLanguage);
418
+ if (!grammar) {
419
+ return;
420
+ }
421
+
422
+ setLanguageClass(resolvedLanguage);
423
+ try {
424
+ const highlighted = Prism.highlight(rawCode, grammar, resolvedLanguage);
425
+ codeNode.innerHTML = highlighted;
426
+ } catch (error) {
427
+ console.error("Assistant embed code highlighting failed", {
428
+ language: resolvedLanguage,
429
+ error,
430
+ });
431
+ }
432
+ });
433
+
434
+ return container.innerHTML;
435
+ } catch (error) {
436
+ console.error(
437
+ "Assistant embed markdown highlighting pipeline failed",
438
+ error,
439
+ );
440
+ return html;
441
+ }
442
+ }
443
+
444
+ function renderMarkdownToHtml(
445
+ markdown: string,
446
+ linkTarget: AssistantLinkTarget,
447
+ ): string {
448
+ const normalizedMarkdown = markdown.replaceAll("\r\n", "\n");
449
+ const cacheKey = `${linkTarget}\0${normalizedMarkdown}`;
450
+ const cached = markdownHtmlCache.get(cacheKey);
451
+ if (cached !== undefined) {
452
+ return cached;
453
+ }
454
+
455
+ let html = "";
456
+
457
+ try {
458
+ const processor = unified()
459
+ .use(remarkParse)
460
+ .use(remarkGfm)
461
+ .use(remarkRehype, { allowDangerousHtml: false });
462
+
463
+ if (linkTarget === "blank") {
464
+ processor.use(rehypeOpenLinksInNewTab);
465
+ }
466
+
467
+ html = String(
468
+ processor.use(rehypeStringify).processSync(normalizedMarkdown),
469
+ );
470
+ } catch {
471
+ html = escapeHtml(normalizedMarkdown).replaceAll("\n", "<br/>");
472
+ }
473
+
474
+ html = highlightCodeBlocksInHtml(html);
475
+
476
+ markdownHtmlCache.set(cacheKey, html);
477
+ if (markdownHtmlCache.size > MARKDOWN_HTML_CACHE_LIMIT) {
478
+ const oldestKey = markdownHtmlCache.keys().next().value;
479
+ if (oldestKey) {
480
+ markdownHtmlCache.delete(oldestKey);
481
+ }
482
+ }
483
+
484
+ return html;
485
+ }
486
+
487
+ function extractErrorMessage(rawBody: string): string {
488
+ if (!rawBody.trim()) {
489
+ return "";
490
+ }
491
+
492
+ try {
493
+ const parsed = JSON.parse(rawBody) as { error?: unknown; detail?: unknown };
494
+ if (typeof parsed.error === "string" && parsed.error.trim()) {
495
+ if (typeof parsed.detail === "string" && parsed.detail.trim()) {
496
+ return `${parsed.error}: ${parsed.detail}`;
497
+ }
498
+ return parsed.error;
499
+ }
500
+ } catch {
501
+ // Fall back to the plain response body.
502
+ }
503
+
504
+ return rawBody.trim();
505
+ }
506
+
507
+ function postParentMessage(type: string) {
508
+ if (typeof window === "undefined") {
509
+ return;
510
+ }
511
+
512
+ window.parent.postMessage({ type }, "*");
513
+ }
514
+
515
+ function getApiPath(
516
+ apiPath: string,
517
+ allowApiPathQueryOverride: boolean,
518
+ ): string {
519
+ if (typeof window === "undefined") {
520
+ return apiPath;
521
+ }
522
+
523
+ if (allowApiPathQueryOverride) {
524
+ try {
525
+ const configuredApiUrl = new URLSearchParams(window.location.search).get(
526
+ "apiUrl",
527
+ );
528
+ if (configuredApiUrl && configuredApiUrl.trim().length > 0) {
529
+ return configuredApiUrl;
530
+ }
531
+ } catch {
532
+ // Fall back to the server-rendered value.
533
+ }
534
+ }
535
+
536
+ return apiPath;
537
+ }
538
+
539
+ export function AssistantPanelIcon({
540
+ color,
541
+ imageSrc,
542
+ className,
543
+ style,
544
+ }: {
545
+ color?: string;
546
+ imageSrc?: string;
547
+ className: string;
548
+ style?: JSX.CSSProperties;
549
+ }) {
550
+ if (!imageSrc) return null;
551
+
552
+ return (
553
+ <span
554
+ className={className}
555
+ style={color ? { ...style, color } : style}
556
+ aria-hidden="true"
557
+ >
558
+ <img src={imageSrc} alt="" />
559
+ </span>
560
+ );
561
+ }
562
+
563
+ export default function AssistantEmbedPanel({
564
+ apiPath,
565
+ docsTitle,
566
+ isChatAvailable,
567
+ canSendChatRequest,
568
+ launcherThemeColor,
569
+ launcherThemeColors,
570
+ launcherIconColor,
571
+ launcherIconColors,
572
+ launcherIconImageSrc,
573
+ emptyStateHeading = "How can I help?",
574
+ emptyStateQuestions = [],
575
+ unavailableMessage = "Chat assistant is not available.",
576
+ devProxyToken,
577
+ panelSurface = "iframe",
578
+ linkTarget = "current",
579
+ allowApiPathQueryOverride = true,
580
+ openSignal = 0,
581
+ onRequestClose,
582
+ }: AssistantEmbedPanelProps) {
583
+ const [messages, setMessages] = useState<ChatMessage[]>(
584
+ canSendChatRequest ? readPersistedMessages : [],
585
+ );
586
+ const [input, setInput] = useState("");
587
+ const [isBusy, setIsBusy] = useState(false);
588
+ const [isAwaitingFirstToken, setIsAwaitingFirstToken] = useState(false);
589
+ const [errorMessage, setErrorMessage] = useState("");
590
+ const [showUnavailableState, setShowUnavailableState] =
591
+ useState(!isChatAvailable);
592
+ const [emptyStateAnimationKey, setEmptyStateAnimationKey] = useState(0);
593
+ const activeRequestAbortRef = useRef<AbortController | null>(null);
594
+ const scrollViewportRef = useRef<HTMLDivElement | null>(null);
595
+ const inputRef = useRef<HTMLTextAreaElement | null>(null);
596
+ const resolvedApiPathRef = useRef(
597
+ getApiPath(apiPath, allowApiPathQueryOverride),
598
+ );
599
+ const visibleEmptyStateQuestions = emptyStateQuestions
600
+ .map((question) => question.trim())
601
+ .filter((question) => question.length > 0)
602
+ .slice(0, 3);
603
+ const launcherThemeColorLight =
604
+ launcherThemeColors?.light ?? launcherThemeColor ?? "#171717";
605
+ const launcherThemeColorDark =
606
+ launcherThemeColors?.dark ?? launcherThemeColor ?? "#171717";
607
+ const launcherIconColorLight =
608
+ launcherIconColors?.light ?? launcherIconColor ?? "#ffffff";
609
+ const launcherIconColorDark =
610
+ launcherIconColors?.dark ?? launcherIconColor ?? "#ffffff";
611
+
612
+ useEffect(() => {
613
+ return () => {
614
+ activeRequestAbortRef.current?.abort();
615
+ activeRequestAbortRef.current = null;
616
+ };
617
+ }, []);
618
+
619
+ useEffect(() => {
620
+ if (typeof window === "undefined") {
621
+ return;
622
+ }
623
+
624
+ try {
625
+ window.localStorage.setItem(STORAGE_KEY, JSON.stringify({ messages }));
626
+ } catch {
627
+ // Ignore storage failures in private mode or constrained embeds.
628
+ }
629
+ }, [messages]);
630
+
631
+ useEffect(() => {
632
+ const viewport = scrollViewportRef.current;
633
+ if (!viewport) {
634
+ return;
635
+ }
636
+ viewport.scrollTop = viewport.scrollHeight;
637
+ }, [messages, isAwaitingFirstToken]);
638
+
639
+ const resizeChatInputTextarea = () => {
640
+ const textarea = inputRef.current;
641
+ if (!textarea || typeof window === "undefined") {
642
+ return;
643
+ }
644
+
645
+ textarea.style.height = "auto";
646
+
647
+ const computedStyle = window.getComputedStyle(textarea);
648
+ const lineHeightRaw = Number.parseFloat(computedStyle.lineHeight);
649
+ const paddingTopRaw = Number.parseFloat(computedStyle.paddingTop);
650
+ const paddingBottomRaw = Number.parseFloat(computedStyle.paddingBottom);
651
+
652
+ const lineHeight = Number.isFinite(lineHeightRaw) ? lineHeightRaw : 20;
653
+ const paddingTop = Number.isFinite(paddingTopRaw) ? paddingTopRaw : 0;
654
+ const paddingBottom = Number.isFinite(paddingBottomRaw)
655
+ ? paddingBottomRaw
656
+ : 0;
657
+
658
+ const minHeight = lineHeight + paddingTop + paddingBottom;
659
+ const measuredScrollHeight = Math.max(textarea.scrollHeight, minHeight);
660
+ const maxHeight = lineHeight * 11.2 + paddingTop + paddingBottom;
661
+ const nextHeight = Math.min(measuredScrollHeight, maxHeight);
662
+
663
+ textarea.style.height = `${nextHeight}px`;
664
+ textarea.style.overflowY =
665
+ measuredScrollHeight > maxHeight ? "auto" : "hidden";
666
+ };
667
+
668
+ useEffect(() => {
669
+ resizeChatInputTextarea();
670
+ }, [input]);
671
+
672
+ useEffect(() => {
673
+ if (typeof window === "undefined") {
674
+ return;
675
+ }
676
+
677
+ const handlePanelMessage = (event: MessageEvent) => {
678
+ if (
679
+ !event.data ||
680
+ typeof event.data !== "object" ||
681
+ event.data.type !== "assistant-embed:panel-opened"
682
+ ) {
683
+ return;
684
+ }
685
+
686
+ setEmptyStateAnimationKey((previous) => previous + 1);
687
+ };
688
+
689
+ window.addEventListener("message", handlePanelMessage);
690
+ return () => {
691
+ window.removeEventListener("message", handlePanelMessage);
692
+ };
693
+ }, []);
694
+
695
+ useEffect(() => {
696
+ setEmptyStateAnimationKey((previous) => previous + 1);
697
+ }, [openSignal]);
698
+
699
+ const handleStartNewChat = () => {
700
+ activeRequestAbortRef.current?.abort();
701
+ activeRequestAbortRef.current = null;
702
+ setMessages([]);
703
+ setInput("");
704
+ setIsBusy(false);
705
+ setIsAwaitingFirstToken(false);
706
+ setErrorMessage("");
707
+ setShowUnavailableState(false);
708
+ };
709
+
710
+ const handleRequestClose = () => {
711
+ if (onRequestClose) {
712
+ onRequestClose();
713
+ return;
714
+ }
715
+
716
+ postParentMessage("assistant-embed:close");
717
+ };
718
+
719
+ const handleUnavailableBack = () => {
720
+ activeRequestAbortRef.current?.abort();
721
+ activeRequestAbortRef.current = null;
722
+ setMessages([]);
723
+ setInput("");
724
+ setIsBusy(false);
725
+ setIsAwaitingFirstToken(false);
726
+ setErrorMessage("");
727
+ setShowUnavailableState(false);
728
+ };
729
+
730
+ const submitPrompt = async (rawPrompt: string) => {
731
+ const prompt = rawPrompt.trim();
732
+ if (!prompt || isBusy || showUnavailableState) {
733
+ return;
734
+ }
735
+
736
+ if (!isChatAvailable || !canSendChatRequest) {
737
+ setShowUnavailableState(true);
738
+ return;
739
+ }
740
+
741
+ setErrorMessage("");
742
+ setIsBusy(true);
743
+ setIsAwaitingFirstToken(true);
744
+
745
+ const userMessage: ChatMessage = {
746
+ id: createMessageId(),
747
+ role: "user",
748
+ content: prompt,
749
+ };
750
+ const assistantId = createMessageId();
751
+ const nextConversation = [...messages, userMessage];
752
+
753
+ setInput("");
754
+ setMessages(nextConversation);
755
+
756
+ activeRequestAbortRef.current?.abort();
757
+ const abortController = new AbortController();
758
+ activeRequestAbortRef.current = abortController;
759
+
760
+ try {
761
+ const requestHeaders: Record<string, string> = {
762
+ "Content-Type": "application/json",
763
+ };
764
+
765
+ if (devProxyToken && devProxyToken.trim().length > 0) {
766
+ requestHeaders["x-ask-ai-dev-token"] = devProxyToken;
767
+ }
768
+
769
+ const response = await fetch(resolvedApiPathRef.current, {
770
+ method: "POST",
771
+ headers: requestHeaders,
772
+ body: JSON.stringify({
773
+ messages: nextConversation.map((message) => ({
774
+ role: message.role,
775
+ content: message.content,
776
+ })),
777
+ }),
778
+ signal: abortController.signal,
779
+ });
780
+
781
+ if (!response.ok) {
782
+ const rawBody = await response.text();
783
+ const detail = extractErrorMessage(rawBody);
784
+ throw new Error(
785
+ detail || `Request failed with status ${response.status}`,
786
+ );
787
+ }
788
+
789
+ if (!response.body) {
790
+ throw new Error("Response body is empty");
791
+ }
792
+
793
+ const reader = response.body.getReader();
794
+ const decoder = new TextDecoder();
795
+ let buffer = "";
796
+ let streamDone = false;
797
+ let hasReceivedFirstTextDelta = false;
798
+
799
+ while (!streamDone) {
800
+ const { done, value } = await reader.read();
801
+ if (done) {
802
+ break;
803
+ }
804
+
805
+ buffer += decoder.decode(value, { stream: true });
806
+
807
+ while (true) {
808
+ const eventBreakIndex = buffer.indexOf("\n\n");
809
+ if (eventBreakIndex === -1) {
810
+ break;
811
+ }
812
+
813
+ const rawEvent = buffer.slice(0, eventBreakIndex);
814
+ buffer = buffer.slice(eventBreakIndex + 2);
815
+
816
+ const dataPayload = rawEvent
817
+ .split("\n")
818
+ .filter((line) => line.startsWith("data:"))
819
+ .map((line) => line.slice(5).trimStart())
820
+ .join("\n");
821
+
822
+ if (!dataPayload) {
823
+ continue;
824
+ }
825
+
826
+ if (dataPayload === "[DONE]") {
827
+ streamDone = true;
828
+ break;
829
+ }
830
+
831
+ let parsed: AssistantStreamEvent;
832
+ try {
833
+ parsed = JSON.parse(dataPayload) as AssistantStreamEvent;
834
+ } catch {
835
+ continue;
836
+ }
837
+
838
+ if (
839
+ parsed.type === "text-delta" &&
840
+ typeof parsed.delta === "string" &&
841
+ parsed.delta.length > 0
842
+ ) {
843
+ if (!hasReceivedFirstTextDelta) {
844
+ hasReceivedFirstTextDelta = true;
845
+ setIsAwaitingFirstToken(false);
846
+ }
847
+
848
+ setMessages((previous) => {
849
+ const existingAssistantMessage = previous.find(
850
+ (message) => message.id === assistantId,
851
+ );
852
+
853
+ if (!existingAssistantMessage) {
854
+ return [
855
+ ...previous,
856
+ {
857
+ id: assistantId,
858
+ role: "assistant",
859
+ content: parsed.delta ?? "",
860
+ },
861
+ ];
862
+ }
863
+
864
+ return previous.map((message) =>
865
+ message.id === assistantId
866
+ ? {
867
+ ...message,
868
+ content: `${message.content}${parsed.delta ?? ""}`,
869
+ }
870
+ : message,
871
+ );
872
+ });
873
+ }
874
+ }
875
+ }
876
+ } catch (error) {
877
+ if (abortController.signal.aborted) {
878
+ return;
879
+ }
880
+
881
+ const resolvedError =
882
+ error instanceof Error ? error.message : String(error);
883
+ setErrorMessage(resolvedError || "Failed to get response.");
884
+ } finally {
885
+ if (activeRequestAbortRef.current === abortController) {
886
+ activeRequestAbortRef.current = null;
887
+ }
888
+ setIsBusy(false);
889
+ setIsAwaitingFirstToken(false);
890
+ }
891
+ };
892
+
893
+ const handleSubmit = async (event: Event) => {
894
+ event.preventDefault();
895
+ await submitPrompt(input);
896
+ };
897
+
898
+ const handleChatInputKeyDown = (event: ChatInputKeyEvent) => {
899
+ if (event.key !== "Enter" || event.shiftKey || event.isComposing) {
900
+ return;
901
+ }
902
+
903
+ event.preventDefault();
904
+ const form = event.currentTarget.form;
905
+ if (form) {
906
+ form.requestSubmit();
907
+ }
908
+ };
909
+
910
+ const handleRenderedMarkdownClick = (
911
+ event: JSX.TargetedMouseEvent<HTMLDivElement>,
912
+ ) => {
913
+ const targetElement = event.target instanceof Element ? event.target : null;
914
+ const copyButton = targetElement?.closest(
915
+ ".ask-ai-copy-code-button",
916
+ ) as HTMLButtonElement | null;
917
+
918
+ if (!copyButton) {
919
+ return;
920
+ }
921
+
922
+ event.preventDefault();
923
+ event.stopPropagation();
924
+
925
+ if (copyButton.dataset.copying === "true") {
926
+ return;
927
+ }
928
+
929
+ const preElement = copyButton.closest("pre");
930
+ const codeElement = preElement?.querySelector("code");
931
+ if (!codeElement) {
932
+ return;
933
+ }
934
+
935
+ const codeText = codeElement.textContent ?? "";
936
+ copyButton.dataset.copying = "true";
937
+
938
+ void (async () => {
939
+ const didCopy = await copyToClipboard(codeText);
940
+ if (didCopy) {
941
+ copyButton.dataset.copied = "true";
942
+ } else {
943
+ delete copyButton.dataset.copied;
944
+ }
945
+
946
+ window.setTimeout(() => {
947
+ delete copyButton.dataset.copied;
948
+ delete copyButton.dataset.copying;
949
+ }, 1200);
950
+ })();
951
+ };
952
+
953
+ const handleThreadViewportWheel = (event: ChatViewportWheelEvent) => {
954
+ const eventTargetElement =
955
+ event.target instanceof Element ? event.target : null;
956
+ const codeBlockPre = eventTargetElement?.closest(
957
+ ".ask-ai-markdown pre",
958
+ ) as HTMLPreElement | null;
959
+ const codeBlockScroller =
960
+ (eventTargetElement?.closest(
961
+ ".ask-ai-markdown pre > code",
962
+ ) as HTMLElement | null) ??
963
+ (codeBlockPre?.querySelector(":scope > code") as HTMLElement | null);
964
+
965
+ if (!codeBlockScroller) {
966
+ return;
967
+ }
968
+
969
+ const horizontalIntentDelta =
970
+ Math.abs(event.deltaX) > 0.01
971
+ ? event.deltaX
972
+ : event.shiftKey
973
+ ? event.deltaY
974
+ : 0;
975
+
976
+ if (Math.abs(horizontalIntentDelta) <= 0.01) {
977
+ return;
978
+ }
979
+
980
+ event.preventDefault();
981
+
982
+ const maxScrollLeft = Math.max(
983
+ 0,
984
+ codeBlockScroller.scrollWidth - codeBlockScroller.clientWidth,
985
+ );
986
+
987
+ if (maxScrollLeft > 0) {
988
+ codeBlockScroller.scrollLeft = clamp(
989
+ codeBlockScroller.scrollLeft + horizontalIntentDelta,
990
+ 0,
991
+ maxScrollLeft,
992
+ );
993
+ }
994
+ };
995
+
996
+ const panelClassName = [
997
+ "relative flex min-h-0 flex-col overflow-hidden text-neutral-900 shadow-2xl dark:text-neutral-50",
998
+ panelSurface === "inline"
999
+ ? "h-full rounded-2xl"
1000
+ : "h-dvh sm:h-screen sm:rounded-2xl",
1001
+ ].join(" ");
1002
+
1003
+ return (
1004
+ <div className={panelClassName}>
1005
+ <header className="flex items-center justify-between gap-3 px-2 pt-2 pb-1">
1006
+ <div className="flex min-w-0 items-center gap-2 ">
1007
+ <AssistantPanelIcon
1008
+ color={launcherIconColor}
1009
+ imageSrc={launcherIconImageSrc}
1010
+ className="assistant-embed-header-icon inline-flex size-9 shrink-0 items-center justify-center rounded-md dark:border-[0.5px] border-neutral-900/7 bg-radial from-neutral-700/5 dark:from-white/7 to-neutral-900/7 dark:to-white/5 to-60% text-neutral-900 dark:border-white/7 dark:text-neutral-50 [&_img]:block [&_img]:size-5.5 [&_img]:object-contain [&_svg]:static [&_svg]:block [&_svg]:size-5 [&_svg]:transform-none [&_svg]:opacity-100"
1011
+ />
1012
+ <div className="min-w-0 space-y-px">
1013
+ <p className="truncate text-sm font-medium leading-3.5 text-neutral-900 dark:text-neutral-50">
1014
+ {docsTitle}
1015
+ </p>
1016
+ <p className="text-xs leading-3 text-neutral-500/80 dark:text-neutral-400">
1017
+ Assistant
1018
+ </p>
1019
+ </div>
1020
+ </div>
1021
+ {messages.length > 0 ? (
1022
+ <button
1023
+ type="button"
1024
+ className="ml-auto inline-flex items-center gap-1.5 rounded-md border border-neutral-900/8 px-2 py-1 text-[12px] text-neutral-500 transition hover:bg-neutral-900/4 disabled:cursor-not-allowed disabled:opacity-50 dark:border-white/10 dark:bg-white/5 dark:text-neutral-300 dark:hover:bg-white/10 cursor-pointer"
1025
+ onClick={handleStartNewChat}
1026
+ aria-label="Clear chat"
1027
+ title="Clear chat"
1028
+ >
1029
+ <Icon
1030
+ icon="lucide:trash-2"
1031
+ className="size-3.5 -ml-px"
1032
+ aria-hidden="true"
1033
+ />
1034
+ Clear
1035
+ </button>
1036
+ ) : null}
1037
+ <button
1038
+ type="button"
1039
+ className="inline-flex items-center justify-center gap-1 rounded-md size-9 text-[13px] text-neutral-500 hover:bg-neutral-900/5 dark:text-neutral-300 dark:hover:bg-white/10 transition cursor-pointer"
1040
+ onClick={handleRequestClose}
1041
+ aria-label="Close chat"
1042
+ title="Close"
1043
+ >
1044
+ <Icon icon="lucide:x" className="size-5" aria-hidden="true" />
1045
+ </button>
1046
+ </header>
1047
+
1048
+ {!showUnavailableState ? (
1049
+ <>
1050
+ <div
1051
+ ref={scrollViewportRef}
1052
+ className={`mask-t-from-[calc(100%-1rem)] flex-1 overflow-y-auto overscroll-contain [scrollbar-width:none] [&::-webkit-scrollbar]:hidden px-4 py-4 ${messages.length > 0 ? "pb-60" : ""} mb-10 space-y-6`}
1053
+ onWheel={handleThreadViewportWheel}
1054
+ >
1055
+ {messages.length === 0 ? (
1056
+ <div
1057
+ key={emptyStateAnimationKey}
1058
+ className="flex min-h-full flex-col items-center justify-center px-1. pb-28. pt-8. text-center"
1059
+ >
1060
+ <AssistantPanelIcon
1061
+ color={launcherIconColor}
1062
+ imageSrc={launcherIconImageSrc}
1063
+ className="assistant-empty-state-item assistant-embed-header-icon inline-flex size-16 shrink-0 items-center justify-center rounded-xl border-[0.5px] border-neutral-900/7 bg-white/90 text-neutral-900 shadow-[0_.5px_1px_rgba(0,0,0,0.15),0_5px_12px_-4px_rgba(0,0,0,0.08)] dark:border-white/10 dark:bg-white/5 dark:text-neutral-50 dark:shadow-none [&_img]:block [&_img]:size-10 [&_img]:object-contain [&_svg]:static [&_svg]:block [&_svg]:size-9 [&_svg]:transform-none [&_svg]:opacity-100"
1064
+ style={
1065
+ {
1066
+ "--assistant-empty-state-delay": "200ms",
1067
+ } as JSX.CSSProperties
1068
+ }
1069
+ />
1070
+ <p
1071
+ className="assistant-empty-state-item mt-3 text-3xl font-[450] leading-9 text-neutral-900 dark:text-neutral-50"
1072
+ style={
1073
+ {
1074
+ "--assistant-empty-state-delay": "300ms",
1075
+ } as JSX.CSSProperties
1076
+ }
1077
+ >
1078
+ {emptyStateHeading}
1079
+ </p>
1080
+ {visibleEmptyStateQuestions.length > 0 ? (
1081
+ <div className="mt-10 flex w-full flex-col items-center gap-2">
1082
+ {visibleEmptyStateQuestions.map((question, index) => (
1083
+ <button
1084
+ key={question}
1085
+ type="button"
1086
+ className="assistant-empty-state-item w-fit rounded-full border-neutral-900/8 bg-white/90 px-3.5 py-2 text-left text-[13px] leading-5 text-neutral-700 shadow-[0px_1px_3px_0px_rgba(0,0,0,0.04),0px_0px_0px_1px_rgba(0,0,0,0.06)_inset,0px_-1px_0px_0px_rgba(0,0,0,0.06)_inset] transition hover:bg-white dark:bg-white/4 dark:text-neutral-200 dark:hover:bg-white/8 cursor-pointer dark:shadow-[0_1px_3px_0_rgba(0,0,0,0.04),inset_0_1px_0_0_rgba(255,255,255,0.04),inset_0_0_0_1px_rgba(0,0,0,0.06),inset_0_-1px_0_0_rgba(0,0,0,0.06),inset_0_0_0_1px_rgba(196,196,196,0.07)]"
1087
+ style={
1088
+ {
1089
+ "--assistant-empty-state-delay": `${400 + index * 100}ms`,
1090
+ } as JSX.CSSProperties
1091
+ }
1092
+ disabled={isBusy}
1093
+ onClick={() => {
1094
+ void submitPrompt(question);
1095
+ }}
1096
+ >
1097
+ {question}
1098
+ </button>
1099
+ ))}
1100
+ </div>
1101
+ ) : null}
1102
+ </div>
1103
+ ) : null}
1104
+
1105
+ {messages.map((message) => {
1106
+ if (
1107
+ message.role === "assistant" &&
1108
+ message.content.length === 0
1109
+ ) {
1110
+ return null;
1111
+ }
1112
+ const isUser = message.role === "user";
1113
+ return (
1114
+ <div
1115
+ key={message.id}
1116
+ className={isUser ? "text-right" : "text-left"}
1117
+ >
1118
+ <div
1119
+ className={[
1120
+ "ask-ai-markdown prose-rules text-[15px]! 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!",
1121
+ isUser
1122
+ ? "inline-block ml-2 px-3 py-1.5 rounded-2xl rounded-br-sm bg-neutral-900/5 text-neutral-700/85 dark:bg-neutral-800 dark:text-neutral-100 *:text-left"
1123
+ : "block w-full bg-transparent text-neutral-900 dark:text-neutral-100",
1124
+ ].join(" ")}
1125
+ onClick={handleRenderedMarkdownClick}
1126
+ dangerouslySetInnerHTML={{
1127
+ __html: renderMarkdownToHtml(message.content, linkTarget),
1128
+ }}
1129
+ />
1130
+ </div>
1131
+ );
1132
+ })}
1133
+
1134
+ {isBusy && isAwaitingFirstToken ? (
1135
+ <div className="text-left">
1136
+ <span
1137
+ 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%]"
1138
+ style={{
1139
+ animation: "ask-ai-thinking-shimmer 3.4s linear infinite",
1140
+ }}
1141
+ >
1142
+ Thinking
1143
+ </span>
1144
+ </div>
1145
+ ) : null}
1146
+
1147
+ {errorMessage ? (
1148
+ <p className="text-sm text-red-600 dark:text-red-300">
1149
+ {errorMessage}
1150
+ </p>
1151
+ ) : null}
1152
+ </div>
1153
+
1154
+ <form
1155
+ className="absolute z-10 bottom-0 inset-x-0 px-3 pb-3"
1156
+ onSubmit={(event) => {
1157
+ void handleSubmit(event);
1158
+ }}
1159
+ >
1160
+ <div className="flex gap-2 rounded-[24px] bg-white dark:bg-(--rd-code-surface) shadow-[0px_1px_3px_0px_rgba(0,0,0,0.04),0px_0px_0px_1px_rgba(0,0,0,0.06)_inset,0px_-1px_0px_0px_rgba(0,0,0,0.06)_inset] dark:shadow-[0_1px_3px_0_rgba(0,0,0,0.04),inset_0_1px_0_0_rgba(255,255,255,0.04),inset_0_0_0_1px_rgba(0,0,0,0.06),inset_0_-1px_0_0_rgba(0,0,0,0.06),inset_0_0_0_1px_rgba(196,196,196,0.07)]">
1161
+ <textarea
1162
+ ref={inputRef}
1163
+ value={input}
1164
+ onInput={(event) => {
1165
+ setInput((event.target as HTMLTextAreaElement).value);
1166
+ }}
1167
+ onKeyDown={handleChatInputKeyDown}
1168
+ placeholder="Ask a question..."
1169
+ className="text-sm my-auto min-w-0 flex-1 bg-transparent pl-4 py-2.5 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"
1170
+ disabled={isBusy}
1171
+ rows={1}
1172
+ />
1173
+ <div className="p-1 pl-0 mt-auto">
1174
+ <button
1175
+ type="submit"
1176
+ className="assistant-embed-send-button flex items-center justify-center rounded-full size-9 disabled:opacity-90 hover:opacity-95 cursor-pointer"
1177
+ style={
1178
+ {
1179
+ "--assistant-embed-send-theme-light":
1180
+ launcherThemeColorLight,
1181
+ "--assistant-embed-send-theme-dark":
1182
+ launcherThemeColorDark,
1183
+ "--assistant-embed-send-foreground-light":
1184
+ launcherIconColorLight,
1185
+ "--assistant-embed-send-foreground-dark":
1186
+ launcherIconColorDark,
1187
+ } as JSX.CSSProperties
1188
+ }
1189
+ disabled={isBusy || input.trim().length === 0}
1190
+ aria-label="Send message"
1191
+ >
1192
+ <Icon
1193
+ icon="lucide:arrow-up"
1194
+ className="size-4.5 **:stroke-[2.2]"
1195
+ aria-hidden="true"
1196
+ />
1197
+ </button>
1198
+ </div>
1199
+ </div>
1200
+ </form>
1201
+ </>
1202
+ ) : (
1203
+ <div className="flex flex-1 items-center justify-center px-5 text-center">
1204
+ <div className="flex max-w-64 flex-col items-center gap-3">
1205
+ <AssistantPanelIcon
1206
+ color={launcherIconColor}
1207
+ imageSrc={launcherIconImageSrc}
1208
+ className="assistant-embed-unavailable-icon inline-flex size-11 items-center justify-center rounded-full border border-neutral-900/7 bg-neutral-100 text-neutral-900 dark:border-white/10 dark:bg-white/5 dark:text-neutral-50 [&_img]:block [&_img]:size-5 [&_img]:object-contain [&_svg]:static [&_svg]:block [&_svg]:size-5 [&_svg]:transform-none [&_svg]:opacity-100"
1209
+ />
1210
+ <p className="text-sm text-neutral-500 dark:text-neutral-300">
1211
+ {unavailableMessage}
1212
+ </p>
1213
+ <button
1214
+ type="button"
1215
+ className="mt-1 inline-flex items-center gap-1.5 rounded-md border border-neutral-900/8 bg-white px-2.5 py-1.5 text-[13px] font-medium text-neutral-700 shadow-xs transition hover:bg-neutral-50 dark:border-white/10 dark:bg-white/5 dark:text-neutral-200 dark:hover:bg-white/10 cursor-pointer"
1216
+ onClick={handleUnavailableBack}
1217
+ >
1218
+ <Icon
1219
+ icon="lucide:arrow-left"
1220
+ className="size-3.5"
1221
+ aria-hidden="true"
1222
+ />
1223
+ Back
1224
+ </button>
1225
+ </div>
1226
+ </div>
1227
+ )}
1228
+
1229
+ <style>{`
1230
+ @keyframes assistant-empty-state-enter {
1231
+ 0% {
1232
+ opacity: 0;
1233
+ filter: blur(8px);
1234
+ transform: translateY(12px);
1235
+ }
1236
+ 100% {
1237
+ opacity: 1;
1238
+ filter: blur(0);
1239
+ transform: translateY(0);
1240
+ }
1241
+ }
1242
+
1243
+ .assistant-empty-state-item {
1244
+ animation: assistant-empty-state-enter 840ms cubic-bezier(0.22, 1, 0.36, 1) both;
1245
+ animation-delay: var(--assistant-empty-state-delay, 0ms);
1246
+ will-change: opacity, filter, transform;
1247
+ }
1248
+
1249
+ @keyframes ask-ai-thinking-shimmer {
1250
+ 0% { background-position: 200% 0; }
1251
+ 100% { background-position: -200% 0; }
1252
+ }
1253
+
1254
+ .prose-rules pre[class*="language-"],
1255
+ .prose-rules code[class*="language-"] {
1256
+ font-family: var(--font-mono);
1257
+ font-size: 13px;
1258
+ line-height: inherit;
1259
+ }
1260
+
1261
+ .ask-ai-markdown pre {
1262
+ position: relative;
1263
+ width: 100%;
1264
+ max-width: 100%;
1265
+ min-width: 0;
1266
+ box-sizing: border-box;
1267
+ padding-top: 2.25rem;
1268
+ background: var(--color-neutral-50) !important;
1269
+ overflow-x: hidden;
1270
+ overflow-y: hidden;
1271
+ white-space: pre;
1272
+ }
1273
+
1274
+ .dark .ask-ai-markdown pre {
1275
+ background: var(--rd-code-surface) !important;
1276
+ border-color: var(--color-neutral-800) !important;
1277
+ }
1278
+
1279
+ .ask-ai-markdown pre[data-language]::before {
1280
+ content: "";
1281
+ position: absolute;
1282
+ top: 0.52rem;
1283
+ left: 0.75rem;
1284
+ width: 16px;
1285
+ height: 16px;
1286
+ background-color: var(--color-neutral-500);
1287
+ -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");
1288
+ 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");
1289
+ -webkit-mask-repeat: no-repeat;
1290
+ mask-repeat: no-repeat;
1291
+ -webkit-mask-position: center;
1292
+ mask-position: center;
1293
+ -webkit-mask-size: contain;
1294
+ mask-size: contain;
1295
+ pointer-events: none;
1296
+ user-select: none;
1297
+ }
1298
+
1299
+ .dark .ask-ai-markdown pre[data-language]::before {
1300
+ background-color: var(--color-neutral-400);
1301
+ }
1302
+
1303
+ .ask-ai-markdown pre[data-language]::after {
1304
+ content: attr(data-language);
1305
+ position: absolute;
1306
+ top: 0.6rem;
1307
+ left: 2.2rem;
1308
+ font-family: var(--font-sans);
1309
+ font-size: 14px;
1310
+ line-height: 1;
1311
+ letter-spacing: 0.01em;
1312
+ color: var(--color-neutral-500);
1313
+ pointer-events: none;
1314
+ user-select: none;
1315
+ }
1316
+
1317
+ .dark .ask-ai-markdown pre[data-language]::after {
1318
+ color: var(--color-neutral-400);
1319
+ }
1320
+
1321
+ .ask-ai-markdown .ask-ai-copy-code-button {
1322
+ position: absolute;
1323
+ top: 0.3rem;
1324
+ right: 0.46rem;
1325
+ z-index: 2;
1326
+ appearance: none;
1327
+ display: inline-flex;
1328
+ align-items: center;
1329
+ justify-content: center;
1330
+ width: 1.75rem;
1331
+ height: 1.75rem;
1332
+ border: 1px solid
1333
+ color-mix(in oklab, var(--color-neutral-200) 80%, transparent);
1334
+ background: color-mix(in oklab, #fff 80%, transparent);
1335
+ backdrop-filter: blur(4px);
1336
+ color: color-mix(
1337
+ in oklab,
1338
+ var(--color-neutral-500) 80%,
1339
+ transparent
1340
+ );
1341
+ border-radius: 0.375rem;
1342
+ outline: none;
1343
+ box-shadow: none;
1344
+ cursor: pointer;
1345
+ transition: background-color 150ms ease, color 150ms ease,
1346
+ border-color 150ms ease, transform 120ms ease;
1347
+ }
1348
+
1349
+ .dark .ask-ai-markdown .ask-ai-copy-code-button {
1350
+ border-color: color-mix(
1351
+ in oklab,
1352
+ var(--color-neutral-700) 50%,
1353
+ transparent
1354
+ );
1355
+ background: var(--rd-code-surface);
1356
+ color: var(--color-neutral-400);
1357
+ }
1358
+
1359
+ .ask-ai-markdown .ask-ai-copy-code-button:hover {
1360
+ background: var(--color-neutral-50);
1361
+ color: var(--color-neutral-600);
1362
+ }
1363
+
1364
+ .dark .ask-ai-markdown .ask-ai-copy-code-button:hover {
1365
+ background: var(--color-neutral-800);
1366
+ color: var(--color-neutral-200);
1367
+ }
1368
+
1369
+ .ask-ai-markdown .ask-ai-copy-code-button:active {
1370
+ transform: translateY(0.5px);
1371
+ }
1372
+
1373
+ .ask-ai-markdown .ask-ai-copy-code-button:focus-visible,
1374
+ .dark .ask-ai-markdown .ask-ai-copy-code-button:focus-visible {
1375
+ outline: none;
1376
+ box-shadow: none;
1377
+ }
1378
+
1379
+ .ask-ai-markdown .ask-ai-copy-icon,
1380
+ .ask-ai-markdown .ask-ai-copy-check-icon {
1381
+ position: absolute;
1382
+ width: 14px;
1383
+ height: 14px;
1384
+ transition: transform 250ms cubic-bezier(0.22, 1, 0.36, 1),
1385
+ opacity 250ms cubic-bezier(0.22, 1, 0.36, 1);
1386
+ will-change: transform, opacity;
1387
+ }
1388
+
1389
+ .ask-ai-markdown .ask-ai-copy-icon {
1390
+ opacity: 1;
1391
+ transform: scale(1) rotate(0deg);
1392
+ background-color: currentColor;
1393
+ -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");
1394
+ 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");
1395
+ -webkit-mask-repeat: no-repeat;
1396
+ mask-repeat: no-repeat;
1397
+ -webkit-mask-position: center;
1398
+ mask-position: center;
1399
+ -webkit-mask-size: contain;
1400
+ mask-size: contain;
1401
+ }
1402
+
1403
+ .ask-ai-markdown .ask-ai-copy-check-icon {
1404
+ opacity: 0;
1405
+ transform: scale(0.25) rotate(6deg);
1406
+ background-color: color-mix(
1407
+ in oklab,
1408
+ var(--color-green-700) 80%,
1409
+ transparent
1410
+ );
1411
+ -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");
1412
+ 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");
1413
+ -webkit-mask-repeat: no-repeat;
1414
+ mask-repeat: no-repeat;
1415
+ -webkit-mask-position: center;
1416
+ mask-position: center;
1417
+ -webkit-mask-size: contain;
1418
+ mask-size: contain;
1419
+ }
1420
+
1421
+ .dark .ask-ai-markdown .ask-ai-copy-check-icon {
1422
+ background-color: color-mix(
1423
+ in oklab,
1424
+ var(--color-green-400) 90%,
1425
+ transparent
1426
+ );
1427
+ }
1428
+
1429
+ .ask-ai-markdown .ask-ai-copy-code-button[data-copied="true"] .ask-ai-copy-icon {
1430
+ opacity: 0;
1431
+ transform: scale(0.5) rotate(-6deg);
1432
+ }
1433
+
1434
+ .ask-ai-markdown .ask-ai-copy-code-button[data-copied="true"] .ask-ai-copy-check-icon {
1435
+ opacity: 1;
1436
+ transform: scale(1.1) rotate(0deg);
1437
+ }
1438
+
1439
+ .ask-ai-markdown pre > code {
1440
+ display: block;
1441
+ width: 100%;
1442
+ min-width: 100%;
1443
+ background: transparent !important;
1444
+ white-space: inherit;
1445
+ word-break: normal;
1446
+ overflow-wrap: normal;
1447
+ overflow-x: auto;
1448
+ overflow-y: hidden;
1449
+ -webkit-overflow-scrolling: touch;
1450
+ scrollbar-width: none;
1451
+ -ms-overflow-style: none;
1452
+ }
1453
+
1454
+ .dark .ask-ai-markdown pre > code {
1455
+ scrollbar-width: none;
1456
+ }
1457
+
1458
+ .ask-ai-markdown pre[class*="language-"] {
1459
+ background: var(--color-neutral-50) !important;
1460
+ }
1461
+
1462
+ .ask-ai-markdown pre[class*="language-"] > code[class*="language-"] {
1463
+ background: transparent !important;
1464
+ }
1465
+
1466
+ .ask-ai-markdown :not(pre) > code[class*="language-"] {
1467
+ background: var(--color-neutral-100);
1468
+ }
1469
+
1470
+ .dark .ask-ai-markdown pre[class*="language-"] {
1471
+ background: var(--rd-code-surface) !important;
1472
+ border-color: var(--color-neutral-800) !important;
1473
+ }
1474
+
1475
+ .dark .ask-ai-markdown pre[class*="language-"] > code[class*="language-"] {
1476
+ background: transparent !important;
1477
+ }
1478
+
1479
+ .dark .ask-ai-markdown pre[class*="language-"],
1480
+ .dark .ask-ai-markdown code[class*="language-"] {
1481
+ color: var(--color-neutral-100);
1482
+ }
1483
+
1484
+ .dark .ask-ai-markdown :not(pre) > code[class*="language-"] {
1485
+ background: var(--color-neutral-800);
1486
+ }
1487
+
1488
+ .dark .ask-ai-markdown .token.comment,
1489
+ .dark .ask-ai-markdown .token.prolog,
1490
+ .dark .ask-ai-markdown .token.doctype,
1491
+ .dark .ask-ai-markdown .token.cdata {
1492
+ color: #7f848e;
1493
+ }
1494
+
1495
+ .dark .ask-ai-markdown .token.punctuation {
1496
+ color: #abb2bf;
1497
+ }
1498
+
1499
+ .dark .ask-ai-markdown .token.property,
1500
+ .dark .ask-ai-markdown .token.tag,
1501
+ .dark .ask-ai-markdown .token.boolean,
1502
+ .dark .ask-ai-markdown .token.number,
1503
+ .dark .ask-ai-markdown .token.constant,
1504
+ .dark .ask-ai-markdown .token.symbol,
1505
+ .dark .ask-ai-markdown .token.deleted {
1506
+ color: #d19a66;
1507
+ }
1508
+
1509
+ .dark .ask-ai-markdown .token.selector,
1510
+ .dark .ask-ai-markdown .token.attr-name,
1511
+ .dark .ask-ai-markdown .token.string,
1512
+ .dark .ask-ai-markdown .token.char,
1513
+ .dark .ask-ai-markdown .token.builtin,
1514
+ .dark .ask-ai-markdown .token.inserted {
1515
+ color: #98c379;
1516
+ }
1517
+
1518
+ .dark .ask-ai-markdown .token.operator,
1519
+ .dark .ask-ai-markdown .token.entity,
1520
+ .dark .ask-ai-markdown .token.url,
1521
+ .dark .ask-ai-markdown .language-css .token.string,
1522
+ .dark .ask-ai-markdown .style .token.string {
1523
+ color: #56b6c2;
1524
+ }
1525
+
1526
+ .dark .ask-ai-markdown .token.atrule,
1527
+ .dark .ask-ai-markdown .token.attr-value,
1528
+ .dark .ask-ai-markdown .token.keyword {
1529
+ color: #c678dd;
1530
+ }
1531
+
1532
+ .dark .ask-ai-markdown .token.function,
1533
+ .dark .ask-ai-markdown .token.class-name {
1534
+ color: #e5c07b;
1535
+ }
1536
+
1537
+ .dark .ask-ai-markdown .token.regex,
1538
+ .dark .ask-ai-markdown .token.important,
1539
+ .dark .ask-ai-markdown .token.variable {
1540
+ color: #e06c75;
1541
+ }
1542
+
1543
+ .dark .ask-ai-markdown .token.attr-value > .token.punctuation.attr-equals,
1544
+ .dark
1545
+ .ask-ai-markdown
1546
+ .token.special-attr
1547
+ > .token.attr-value
1548
+ > .token.value.css {
1549
+ color: #abb2bf;
1550
+ }
1551
+
1552
+ .dark .ask-ai-markdown .language-css .token.selector {
1553
+ color: #d19a66;
1554
+ }
1555
+
1556
+ .dark .ask-ai-markdown .language-css .token.property {
1557
+ color: #abb2bf;
1558
+ }
1559
+
1560
+ .dark .ask-ai-markdown .language-css .token.function,
1561
+ .dark .ask-ai-markdown .language-css .token.url > .token.function {
1562
+ color: #56b6c2;
1563
+ }
1564
+
1565
+ .dark .ask-ai-markdown .language-css .token.url > .token.string.url {
1566
+ color: #98c379;
1567
+ }
1568
+
1569
+ .dark .ask-ai-markdown .language-css .token.important,
1570
+ .dark .ask-ai-markdown .language-css .token.atrule .token.rule {
1571
+ color: #c678dd;
1572
+ }
1573
+
1574
+ .dark .ask-ai-markdown .language-javascript .token.operator {
1575
+ color: #c678dd;
1576
+ }
1577
+
1578
+ .dark
1579
+ .ask-ai-markdown
1580
+ .language-javascript
1581
+ .token.template-string
1582
+ > .token.interpolation
1583
+ > .token.interpolation-punctuation.punctuation {
1584
+ color: #e06c75;
1585
+ }
1586
+
1587
+ .dark .ask-ai-markdown .language-json .token.operator {
1588
+ color: #abb2bf;
1589
+ }
1590
+
1591
+ .dark .ask-ai-markdown .language-json .token.null.keyword {
1592
+ color: #d19a66;
1593
+ }
1594
+
1595
+ .dark .ask-ai-markdown .language-markdown .token.url,
1596
+ .dark .ask-ai-markdown .language-markdown .token.url > .token.operator,
1597
+ .dark
1598
+ .ask-ai-markdown
1599
+ .language-markdown
1600
+ .token.url-reference.url
1601
+ > .token.string {
1602
+ color: #abb2bf;
1603
+ }
1604
+
1605
+ .dark .ask-ai-markdown .language-markdown .token.url > .token.content {
1606
+ color: #56b6c2;
1607
+ }
1608
+
1609
+ .dark .ask-ai-markdown .language-markdown .token.url > .token.url,
1610
+ .dark .ask-ai-markdown .language-markdown .token.url-reference.url {
1611
+ color: #98c379;
1612
+ }
1613
+
1614
+ .dark .ask-ai-markdown .language-markdown .token.blockquote.punctuation,
1615
+ .dark .ask-ai-markdown .language-markdown .token.hr.punctuation {
1616
+ color: #7f848e;
1617
+ }
1618
+
1619
+ .dark .ask-ai-markdown .language-markdown .token.code-snippet {
1620
+ color: #98c379;
1621
+ }
1622
+
1623
+ .dark .ask-ai-markdown .language-markdown .token.bold .token.content {
1624
+ color: #e5c07b;
1625
+ }
1626
+
1627
+ .dark .ask-ai-markdown .language-markdown .token.italic .token.content {
1628
+ color: #c678dd;
1629
+ }
1630
+
1631
+ .dark .ask-ai-markdown .language-markdown .token.strike .token.content,
1632
+ .dark
1633
+ .ask-ai-markdown
1634
+ .language-markdown
1635
+ .token.strike
1636
+ .token.punctuation,
1637
+ .dark .ask-ai-markdown .language-markdown .token.list.punctuation,
1638
+ .dark
1639
+ .ask-ai-markdown
1640
+ .language-markdown
1641
+ .token.title.important
1642
+ > .token.punctuation {
1643
+ color: #e06c75;
1644
+ }
1645
+
1646
+ .ask-ai-markdown pre > code::-webkit-scrollbar {
1647
+ width: 0;
1648
+ height: 0;
1649
+ display: none;
1650
+ }
1651
+
1652
+ .assistant-embed-send-button {
1653
+ --assistant-embed-send-theme: var(
1654
+ --assistant-embed-send-theme-light
1655
+ );
1656
+ --assistant-embed-send-foreground: var(
1657
+ --assistant-embed-send-foreground-light
1658
+ );
1659
+ color: var(--assistant-embed-send-foreground);
1660
+ background: linear-gradient(
1661
+ to bottom,
1662
+ color-mix(
1663
+ in oklab,
1664
+ var(--assistant-embed-send-theme) 88%,
1665
+ white
1666
+ ),
1667
+ color-mix(
1668
+ in oklab,
1669
+ var(--assistant-embed-send-theme) 90%,
1670
+ black
1671
+ )
1672
+ );
1673
+ }
1674
+
1675
+ .dark .assistant-embed-send-button,
1676
+ [data-theme="dark"] .assistant-embed-send-button {
1677
+ --assistant-embed-send-theme: var(
1678
+ --assistant-embed-send-theme-dark
1679
+ );
1680
+ --assistant-embed-send-foreground: var(
1681
+ --assistant-embed-send-foreground-dark
1682
+ );
1683
+ }
1684
+
1685
+ @media (prefers-reduced-motion: reduce) {
1686
+ .assistant-empty-state-item {
1687
+ animation: none;
1688
+ }
1689
+ }
1690
+ `}</style>
1691
+ </div>
1692
+ );
1693
+ }