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
@@ -0,0 +1,2679 @@
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
+ import { getDocsBasePath, withBasePath } from "../../lib/base-path";
32
+
33
+ type AssistantLinkTarget = "current" | "blank";
34
+ export type AssistantPanelSize = "default" | "expanded";
35
+
36
+ type HastNode = {
37
+ type?: string;
38
+ tagName?: string;
39
+ properties?: Record<string, unknown>;
40
+ children?: HastNode[];
41
+ };
42
+
43
+ type AssistantEmbedPanelProps = {
44
+ apiPath: string;
45
+ docsTitle: string;
46
+ isChatAvailable: boolean;
47
+ canSendChatRequest: boolean;
48
+ launcherThemeColor?: string;
49
+ launcherThemeColors?: AssistantColorByMode;
50
+ launcherIconColor?: string;
51
+ launcherIconColors?: AssistantColorByMode;
52
+ launcherIconImageSrc?: string;
53
+ emptyStateHeading?: string;
54
+ emptyStateQuestions?: string[];
55
+ unavailableMessage?: string;
56
+ devProxyToken?: string;
57
+ panelSurface?: "iframe" | "inline";
58
+ linkTarget?: AssistantLinkTarget;
59
+ allowApiPathQueryOverride?: boolean;
60
+ openSignal?: number;
61
+ panelSize?: AssistantPanelSize;
62
+ onRequestOpen?: () => void;
63
+ onRequestClose?: () => void;
64
+ onRequestPanelSizeToggle?: (size: AssistantPanelSize) => void;
65
+ onCurrentLinkNavigate?: (href: string, sourceElement?: Element) => void;
66
+ };
67
+
68
+ type AssistantColorByMode = {
69
+ light: string;
70
+ dark: string;
71
+ };
72
+
73
+ type ChatMessage = {
74
+ id: string;
75
+ role: "user" | "assistant";
76
+ content: string;
77
+ };
78
+
79
+ type AssistantStreamEvent = {
80
+ type?: string;
81
+ delta?: string;
82
+ };
83
+
84
+ type ChatInputKeyEvent = JSX.TargetedKeyboardEvent<HTMLTextAreaElement>;
85
+ type ChatViewportWheelEvent = JSX.TargetedWheelEvent<HTMLDivElement>;
86
+ type ChatViewportScrollEvent = JSX.TargetedEvent<HTMLDivElement, Event>;
87
+
88
+ type PersistedPanelState = {
89
+ messages: ChatMessage[];
90
+ scrollTop: number;
91
+ inFlight: boolean;
92
+ isAwaitingFirstToken: boolean;
93
+ inFlightUpdatedAt: number;
94
+ panelSize: AssistantPanelSize;
95
+ };
96
+
97
+ type PersistPanelStateOptions = {
98
+ allowBusyListenerWrite?: boolean;
99
+ refreshInFlightTimestamp?: boolean;
100
+ };
101
+
102
+ type CurrentLinkNavigationRequest = {
103
+ href: string;
104
+ sourceElement: HTMLAnchorElement;
105
+ };
106
+
107
+ type PendingAssistantHandoff = {
108
+ state: PersistedPanelState;
109
+ targetWindow: Window;
110
+ timeoutId: number;
111
+ };
112
+
113
+ export const ASSISTANT_PANEL_STORAGE_KEY = "docs:assistant-embed-panel:v1";
114
+ const HANDOFF_QUERY_PARAM = "assistantHandoff";
115
+ const OPEN_QUERY_PARAM = "assistant";
116
+ const OPEN_QUERY_VALUE = "open";
117
+ const HANDOFF_READY_TYPE = "assistant-handoff:ready";
118
+ const HANDOFF_STATE_TYPE = "assistant-handoff:state";
119
+ const HANDOFF_ACK_TYPE = "assistant-handoff:ack";
120
+ const IN_FLIGHT_STALE_MS = 10000;
121
+ const IN_FLIGHT_HEARTBEAT_MS = 3000;
122
+ const MARKDOWN_HTML_CACHE_LIMIT = 300;
123
+ const markdownHtmlCache = new Map<string, string>();
124
+ const PRISM_LANGUAGE_ALIAS: Record<string, string> = {
125
+ js: "javascript",
126
+ ts: "typescript",
127
+ yml: "yaml",
128
+ sh: "bash",
129
+ shell: "bash",
130
+ shellscript: "bash",
131
+ md: "markdown",
132
+ mdx: "markdown",
133
+ };
134
+
135
+ const PRISM_LANGUAGE_LABEL: Record<string, string> = {
136
+ javascript: "JavaScript",
137
+ typescript: "TypeScript",
138
+ jsx: "JSX",
139
+ tsx: "TSX",
140
+ json: "JSON",
141
+ markdown: "Markdown",
142
+ bash: "Bash",
143
+ shell: "Shell",
144
+ python: "Python",
145
+ yaml: "YAML",
146
+ sql: "SQL",
147
+ rust: "Rust",
148
+ go: "Go",
149
+ java: "Java",
150
+ css: "CSS",
151
+ html: "HTML",
152
+ diff: "Diff",
153
+ mdx: "MDX",
154
+ };
155
+
156
+ const EXTERNAL_PROTOCOL_REGEX = /^[a-zA-Z][a-zA-Z\d+\-.]*:/;
157
+
158
+ function normalizeRelTokens(value: unknown): string[] {
159
+ if (Array.isArray(value)) {
160
+ return value
161
+ .flatMap((entry) => (typeof entry === "string" ? entry.split(/\s+/) : []))
162
+ .filter(Boolean);
163
+ }
164
+
165
+ if (typeof value === "string") {
166
+ return value.split(/\s+/).filter(Boolean);
167
+ }
168
+
169
+ return [];
170
+ }
171
+
172
+ function visitHastNodes(
173
+ node: HastNode,
174
+ visitor: (node: HastNode) => void,
175
+ ): void {
176
+ visitor(node);
177
+ if (!Array.isArray(node.children)) return;
178
+ for (const child of node.children) {
179
+ visitHastNodes(child, visitor);
180
+ }
181
+ }
182
+
183
+ function isDocumentLocalHref(href: string): boolean {
184
+ return (
185
+ href.startsWith("#") ||
186
+ href.startsWith("?") ||
187
+ href.startsWith("//") ||
188
+ href.startsWith("./") ||
189
+ href.startsWith("../")
190
+ );
191
+ }
192
+
193
+ function rebaseAssistantLinkHref(href: string): string {
194
+ const value = href.trim();
195
+ if (!value || isDocumentLocalHref(value)) {
196
+ return href;
197
+ }
198
+
199
+ if (!EXTERNAL_PROTOCOL_REGEX.test(value)) {
200
+ return withBasePath(value);
201
+ }
202
+
203
+ if (
204
+ typeof window === "undefined" ||
205
+ !/^https?:/i.test(value) ||
206
+ !getDocsBasePath()
207
+ ) {
208
+ return href;
209
+ }
210
+
211
+ try {
212
+ const parsed = new URL(value);
213
+ if (parsed.origin !== window.location.origin) {
214
+ return href;
215
+ }
216
+
217
+ const rebasedHref = withBasePath(
218
+ `${parsed.pathname}${parsed.search}${parsed.hash}`,
219
+ );
220
+ return new URL(rebasedHref, parsed.origin).toString();
221
+ } catch {
222
+ return href;
223
+ }
224
+ }
225
+
226
+ function getSameOriginNavigationRequest(
227
+ event: JSX.TargetedMouseEvent<HTMLDivElement>,
228
+ ): CurrentLinkNavigationRequest | null {
229
+ if (
230
+ event.defaultPrevented ||
231
+ event.button !== 0 ||
232
+ event.metaKey ||
233
+ event.ctrlKey ||
234
+ event.shiftKey ||
235
+ event.altKey
236
+ ) {
237
+ return null;
238
+ }
239
+
240
+ const targetElement = event.target instanceof Element ? event.target : null;
241
+ const anchor = targetElement?.closest("a[href]") as HTMLAnchorElement | null;
242
+ if (!anchor || !event.currentTarget.contains(anchor)) {
243
+ return null;
244
+ }
245
+
246
+ const target = anchor.getAttribute("target")?.trim().toLowerCase();
247
+ if ((target && target !== "_self") || anchor.hasAttribute("download")) {
248
+ return null;
249
+ }
250
+
251
+ const rawHref = anchor.getAttribute("href")?.trim();
252
+ if (!rawHref || rawHref.startsWith("#") || rawHref.startsWith("?")) {
253
+ return null;
254
+ }
255
+
256
+ try {
257
+ const url = new URL(anchor.href, window.location.href);
258
+ if (url.origin !== window.location.origin) {
259
+ return null;
260
+ }
261
+
262
+ if (
263
+ url.pathname === window.location.pathname &&
264
+ url.search === window.location.search &&
265
+ url.hash
266
+ ) {
267
+ return null;
268
+ }
269
+
270
+ return { href: url.href, sourceElement: anchor };
271
+ } catch {
272
+ return null;
273
+ }
274
+ }
275
+
276
+ function getSameOriginBlankNavigationRequest(
277
+ event: JSX.TargetedMouseEvent<HTMLDivElement>,
278
+ ): CurrentLinkNavigationRequest | null {
279
+ if (
280
+ event.defaultPrevented ||
281
+ event.button !== 0 ||
282
+ event.metaKey ||
283
+ event.ctrlKey ||
284
+ event.shiftKey ||
285
+ event.altKey
286
+ ) {
287
+ return null;
288
+ }
289
+
290
+ const targetElement = event.target instanceof Element ? event.target : null;
291
+ const anchor = targetElement?.closest("a[href]") as HTMLAnchorElement | null;
292
+ if (!anchor || !event.currentTarget.contains(anchor)) {
293
+ return null;
294
+ }
295
+
296
+ const target = anchor.getAttribute("target")?.trim().toLowerCase();
297
+ if ((target && target !== "_blank") || anchor.hasAttribute("download")) {
298
+ return null;
299
+ }
300
+
301
+ const rawHref = anchor.getAttribute("href")?.trim();
302
+ if (!rawHref || rawHref.startsWith("#") || rawHref.startsWith("?")) {
303
+ return null;
304
+ }
305
+
306
+ try {
307
+ const url = new URL(anchor.href, window.location.href);
308
+ if (url.origin !== window.location.origin) {
309
+ return null;
310
+ }
311
+
312
+ return { href: url.href, sourceElement: anchor };
313
+ } catch {
314
+ return null;
315
+ }
316
+ }
317
+
318
+ const rehypeRebaseInternalLinks: Plugin<[], HastNode> = () => {
319
+ return (tree) => {
320
+ visitHastNodes(tree, (node) => {
321
+ if (node.type !== "element" || node.tagName !== "a") return;
322
+
323
+ const props = node.properties;
324
+ if (!props || typeof props.href !== "string") return;
325
+
326
+ props.href = rebaseAssistantLinkHref(props.href);
327
+ });
328
+ };
329
+ };
330
+
331
+ const rehypeOpenLinksInNewTab: Plugin<[], HastNode> = () => {
332
+ return (tree) => {
333
+ visitHastNodes(tree, (node) => {
334
+ if (node.type !== "element" || node.tagName !== "a") return;
335
+
336
+ const props = (node.properties ??= {});
337
+ props.target = "_blank";
338
+
339
+ const relTokens = normalizeRelTokens(props.rel);
340
+ if (!relTokens.includes("noopener")) relTokens.push("noopener");
341
+ if (!relTokens.includes("noreferrer")) relTokens.push("noreferrer");
342
+ props.rel = relTokens.join(" ");
343
+ });
344
+ };
345
+ };
346
+
347
+ function clamp(value: number, min: number, max: number) {
348
+ return Math.min(Math.max(value, min), max);
349
+ }
350
+
351
+ function createMessageId(): string {
352
+ if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
353
+ return crypto.randomUUID();
354
+ }
355
+
356
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
357
+ }
358
+
359
+ function normalizePersistedMessages(rawMessages: unknown): ChatMessage[] {
360
+ if (!Array.isArray(rawMessages)) {
361
+ return [];
362
+ }
363
+
364
+ return rawMessages
365
+ .filter((message): message is ChatMessage => {
366
+ if (!message || typeof message !== "object") {
367
+ return false;
368
+ }
369
+ const candidate = message as Partial<ChatMessage>;
370
+ return (
371
+ (candidate.role === "user" || candidate.role === "assistant") &&
372
+ typeof candidate.id === "string" &&
373
+ typeof candidate.content === "string"
374
+ );
375
+ })
376
+ .map((message) => ({
377
+ id: message.id,
378
+ role: message.role,
379
+ content: message.content,
380
+ }));
381
+ }
382
+
383
+ function createEmptyPersistedPanelState(): PersistedPanelState {
384
+ return {
385
+ messages: [],
386
+ scrollTop: 0,
387
+ inFlight: false,
388
+ isAwaitingFirstToken: false,
389
+ inFlightUpdatedAt: 0,
390
+ panelSize: "default",
391
+ };
392
+ }
393
+
394
+ function normalizePanelSize(value: unknown): AssistantPanelSize {
395
+ return value === "expanded" ? "expanded" : "default";
396
+ }
397
+
398
+ function normalizePersistedPanelState(rawState: unknown): PersistedPanelState {
399
+ if (!rawState || typeof rawState !== "object") {
400
+ return createEmptyPersistedPanelState();
401
+ }
402
+
403
+ const state = rawState as {
404
+ messages?: unknown;
405
+ scrollTop?: unknown;
406
+ inFlight?: unknown;
407
+ isAwaitingFirstToken?: unknown;
408
+ inFlightUpdatedAt?: unknown;
409
+ panelSize?: unknown;
410
+ };
411
+ const inFlightUpdatedAt =
412
+ typeof state.inFlightUpdatedAt === "number" &&
413
+ Number.isFinite(state.inFlightUpdatedAt)
414
+ ? Math.max(0, state.inFlightUpdatedAt)
415
+ : 0;
416
+ const isInFlightFresh =
417
+ state.inFlight === true &&
418
+ inFlightUpdatedAt > 0 &&
419
+ Date.now() - inFlightUpdatedAt <= IN_FLIGHT_STALE_MS;
420
+ const scrollTop =
421
+ typeof state.scrollTop === "number" && Number.isFinite(state.scrollTop)
422
+ ? Math.max(0, state.scrollTop)
423
+ : 0;
424
+
425
+ return {
426
+ messages: normalizePersistedMessages(state.messages),
427
+ scrollTop,
428
+ inFlight: isInFlightFresh,
429
+ isAwaitingFirstToken:
430
+ isInFlightFresh && state.isAwaitingFirstToken === true,
431
+ inFlightUpdatedAt: isInFlightFresh ? inFlightUpdatedAt : 0,
432
+ panelSize: normalizePanelSize(state.panelSize),
433
+ };
434
+ }
435
+
436
+ function parsePersistedPanelState(raw: string | null): PersistedPanelState {
437
+ if (!raw) {
438
+ return createEmptyPersistedPanelState();
439
+ }
440
+
441
+ try {
442
+ const parsed = JSON.parse(raw) as unknown;
443
+ if (Array.isArray(parsed)) {
444
+ return {
445
+ ...createEmptyPersistedPanelState(),
446
+ messages: normalizePersistedMessages(parsed),
447
+ };
448
+ }
449
+
450
+ return normalizePersistedPanelState(parsed);
451
+ } catch {
452
+ return createEmptyPersistedPanelState();
453
+ }
454
+ }
455
+
456
+ function readPersistedPanelState(): PersistedPanelState {
457
+ if (typeof window === "undefined") {
458
+ return createEmptyPersistedPanelState();
459
+ }
460
+
461
+ try {
462
+ return parsePersistedPanelState(
463
+ window.localStorage.getItem(ASSISTANT_PANEL_STORAGE_KEY),
464
+ );
465
+ } catch {
466
+ return createEmptyPersistedPanelState();
467
+ }
468
+ }
469
+
470
+ function writePersistedPanelState(state: PersistedPanelState) {
471
+ if (typeof window === "undefined") {
472
+ return;
473
+ }
474
+
475
+ try {
476
+ window.localStorage.setItem(
477
+ ASSISTANT_PANEL_STORAGE_KEY,
478
+ JSON.stringify({
479
+ messages: state.messages,
480
+ scrollTop:
481
+ Number.isFinite(state.scrollTop) && state.scrollTop > 0
482
+ ? state.scrollTop
483
+ : 0,
484
+ inFlight: state.inFlight,
485
+ isAwaitingFirstToken: state.inFlight && state.isAwaitingFirstToken,
486
+ inFlightUpdatedAt: state.inFlight ? state.inFlightUpdatedAt : 0,
487
+ panelSize: normalizePanelSize(state.panelSize),
488
+ }),
489
+ );
490
+ } catch {
491
+ // Ignore storage failures in private mode or constrained embeds.
492
+ }
493
+ }
494
+
495
+ function buildHandoffUrl(href: string, handoffId: string): string {
496
+ const url = new URL(href, window.location.href);
497
+ url.searchParams.set(HANDOFF_QUERY_PARAM, handoffId);
498
+ url.searchParams.set(OPEN_QUERY_PARAM, OPEN_QUERY_VALUE);
499
+ return url.toString();
500
+ }
501
+
502
+ function readAssistantUrlSignal(): {
503
+ handoffId: string;
504
+ shouldOpen: boolean;
505
+ } {
506
+ if (typeof window === "undefined") {
507
+ return { handoffId: "", shouldOpen: false };
508
+ }
509
+
510
+ try {
511
+ const url = new URL(window.location.href);
512
+ const searchHandoffId = url.searchParams.get(HANDOFF_QUERY_PARAM) ?? "";
513
+ const searchShouldOpen =
514
+ url.searchParams.get(OPEN_QUERY_PARAM) === OPEN_QUERY_VALUE;
515
+ const hashParams = new URLSearchParams(
516
+ url.hash.startsWith("#") ? url.hash.slice(1) : url.hash,
517
+ );
518
+ const hashHandoffId = hashParams.get(HANDOFF_QUERY_PARAM) ?? "";
519
+ const hashShouldOpen =
520
+ hashParams.get(OPEN_QUERY_PARAM) === OPEN_QUERY_VALUE;
521
+
522
+ return {
523
+ handoffId: searchHandoffId || hashHandoffId,
524
+ shouldOpen:
525
+ Boolean(searchHandoffId || hashHandoffId) ||
526
+ searchShouldOpen ||
527
+ hashShouldOpen,
528
+ };
529
+ } catch {
530
+ return { handoffId: "", shouldOpen: false };
531
+ }
532
+ }
533
+
534
+ function clearAssistantUrlSignal() {
535
+ if (typeof window === "undefined") {
536
+ return;
537
+ }
538
+
539
+ try {
540
+ const url = new URL(window.location.href);
541
+ url.searchParams.delete(HANDOFF_QUERY_PARAM);
542
+ url.searchParams.delete(OPEN_QUERY_PARAM);
543
+
544
+ const hashValue = url.hash.startsWith("#") ? url.hash.slice(1) : url.hash;
545
+ if (hashValue) {
546
+ const hashParams = new URLSearchParams(hashValue);
547
+ if (
548
+ hashParams.has(HANDOFF_QUERY_PARAM) ||
549
+ hashParams.has(OPEN_QUERY_PARAM)
550
+ ) {
551
+ hashParams.delete(HANDOFF_QUERY_PARAM);
552
+ hashParams.delete(OPEN_QUERY_PARAM);
553
+ const nextHash = hashParams.toString();
554
+ url.hash = nextHash ? `#${nextHash}` : "";
555
+ }
556
+ }
557
+
558
+ window.history.replaceState(
559
+ window.history.state,
560
+ "",
561
+ `${url.pathname}${url.search}${url.hash}`,
562
+ );
563
+ } catch {
564
+ // Leave the URL alone if parsing fails.
565
+ }
566
+ }
567
+
568
+ function escapeHtml(value: string): string {
569
+ return value
570
+ .replaceAll("&", "&amp;")
571
+ .replaceAll("<", "&lt;")
572
+ .replaceAll(">", "&gt;")
573
+ .replaceAll('"', "&quot;")
574
+ .replaceAll("'", "&#39;");
575
+ }
576
+
577
+ function resolvePrismLanguage(rawLanguage: string): string {
578
+ const normalized = rawLanguage.trim().toLowerCase();
579
+ return PRISM_LANGUAGE_ALIAS[normalized] ?? normalized;
580
+ }
581
+
582
+ function resolvePrismLanguageLabel(language: string): string {
583
+ const normalized = language.trim().toLowerCase();
584
+ if (!normalized) {
585
+ return "";
586
+ }
587
+
588
+ return (
589
+ PRISM_LANGUAGE_LABEL[normalized] ??
590
+ normalized
591
+ .replace(/[-_]+/g, " ")
592
+ .replace(/\b\w/g, (char) => char.toUpperCase())
593
+ );
594
+ }
595
+
596
+ function resolvePrismGrammar(language: string) {
597
+ return (
598
+ Prism.languages[language] ??
599
+ Prism.languages[PRISM_LANGUAGE_ALIAS[language] ?? ""]
600
+ );
601
+ }
602
+
603
+ function ensureCodeBlockCopyButton(preElement: HTMLPreElement): void {
604
+ const existingButton = Array.from(preElement.children).find(
605
+ (child) =>
606
+ child instanceof HTMLButtonElement &&
607
+ child.classList.contains("ask-ai-copy-code-button"),
608
+ );
609
+
610
+ if (existingButton instanceof HTMLButtonElement) {
611
+ return;
612
+ }
613
+
614
+ const copyButton = document.createElement("button");
615
+ copyButton.type = "button";
616
+ copyButton.className = "ask-ai-copy-code-button";
617
+ copyButton.setAttribute("data-copy-code", "true");
618
+ copyButton.setAttribute("aria-label", "Copy code to clipboard");
619
+ copyButton.setAttribute("title", "Copy code");
620
+
621
+ const copyIcon = document.createElement("span");
622
+ copyIcon.className = "ask-ai-copy-icon";
623
+ copyIcon.setAttribute("aria-hidden", "true");
624
+
625
+ const checkIcon = document.createElement("span");
626
+ checkIcon.className = "ask-ai-copy-check-icon";
627
+ checkIcon.setAttribute("aria-hidden", "true");
628
+
629
+ copyButton.append(copyIcon, checkIcon);
630
+ preElement.append(copyButton);
631
+ }
632
+
633
+ async function copyToClipboard(text: string): Promise<boolean> {
634
+ if (
635
+ typeof navigator !== "undefined" &&
636
+ navigator.clipboard &&
637
+ typeof navigator.clipboard.writeText === "function"
638
+ ) {
639
+ try {
640
+ await navigator.clipboard.writeText(text);
641
+ return true;
642
+ } catch {
643
+ // Fall through to legacy copy fallback.
644
+ }
645
+ }
646
+
647
+ if (typeof document === "undefined") {
648
+ return false;
649
+ }
650
+
651
+ try {
652
+ const textarea = document.createElement("textarea");
653
+ textarea.value = text;
654
+ textarea.setAttribute("readonly", "true");
655
+ textarea.style.position = "fixed";
656
+ textarea.style.top = "-9999px";
657
+ textarea.style.opacity = "0";
658
+ document.body.append(textarea);
659
+ textarea.select();
660
+ const didCopy = document.execCommand("copy");
661
+ textarea.remove();
662
+ return didCopy;
663
+ } catch {
664
+ return false;
665
+ }
666
+ }
667
+
668
+ function unwrapEscapedFenceCodeBlock(rawCode: string): {
669
+ language: string;
670
+ code: string;
671
+ } | null {
672
+ const normalizedCode = rawCode.replaceAll("\r\n", "\n").trim();
673
+ if (!normalizedCode) {
674
+ return null;
675
+ }
676
+
677
+ const lines = normalizedCode.split("\n");
678
+ if (lines.length < 2) {
679
+ return null;
680
+ }
681
+
682
+ const openingLine = lines[0]?.trim() ?? "";
683
+ const openingMatch = openingLine.match(/^(`{3,})([^\s`]*)?(?:\s+[\s\S]*)?$/);
684
+ if (!openingMatch) {
685
+ return null;
686
+ }
687
+
688
+ const fenceToken = openingMatch[1];
689
+ const closingLine = lines.at(-1)?.trim() ?? "";
690
+ if (closingLine !== fenceToken) {
691
+ return null;
692
+ }
693
+
694
+ const languageToken = (openingMatch[2] ?? "").trim();
695
+ if (!languageToken) {
696
+ return null;
697
+ }
698
+
699
+ return {
700
+ language: resolvePrismLanguage(languageToken),
701
+ code: lines.slice(1, -1).join("\n"),
702
+ };
703
+ }
704
+
705
+ function highlightCodeBlocksInHtml(html: string): string {
706
+ if (typeof document === "undefined" || !html.trim()) {
707
+ return html;
708
+ }
709
+
710
+ try {
711
+ const container = document.createElement("div");
712
+ container.innerHTML = html;
713
+
714
+ const codeNodes = container.querySelectorAll("pre > code");
715
+ codeNodes.forEach((codeNode) => {
716
+ const getLanguageClassName = (element: Element | null) =>
717
+ Array.from(element?.classList ?? []).find(
718
+ (className) =>
719
+ className.startsWith("language-") || className.startsWith("lang-"),
720
+ );
721
+
722
+ const setLanguageClass = (language: string) => {
723
+ Array.from(codeNode.classList).forEach((className) => {
724
+ if (
725
+ className.startsWith("language-") ||
726
+ className.startsWith("lang-")
727
+ ) {
728
+ codeNode.classList.remove(className);
729
+ }
730
+ });
731
+ codeNode.classList.add(`language-${language}`);
732
+
733
+ const preParent = codeNode.parentElement;
734
+ if (preParent instanceof HTMLPreElement) {
735
+ Array.from(preParent.classList).forEach((className) => {
736
+ if (
737
+ className.startsWith("language-") ||
738
+ className.startsWith("lang-")
739
+ ) {
740
+ preParent.classList.remove(className);
741
+ }
742
+ });
743
+ preParent.classList.add(`language-${language}`);
744
+ const languageLabel = resolvePrismLanguageLabel(language);
745
+ if (languageLabel) {
746
+ preParent.setAttribute("data-language", languageLabel);
747
+ } else {
748
+ preParent.removeAttribute("data-language");
749
+ }
750
+
751
+ ensureCodeBlockCopyButton(preParent);
752
+ }
753
+ };
754
+
755
+ const codeLanguageClassName = getLanguageClassName(codeNode);
756
+ const preLanguageClassName = getLanguageClassName(codeNode.parentElement);
757
+ let rawLanguage =
758
+ codeLanguageClassName?.replace(/^(language-|lang-)/, "") ??
759
+ preLanguageClassName?.replace(/^(language-|lang-)/, "") ??
760
+ "";
761
+
762
+ let rawCode = codeNode.textContent ?? "";
763
+ if (!rawLanguage) {
764
+ const unwrappedFence = unwrapEscapedFenceCodeBlock(rawCode);
765
+ if (unwrappedFence) {
766
+ rawLanguage = unwrappedFence.language || rawLanguage;
767
+ rawCode = unwrappedFence.code;
768
+ }
769
+ }
770
+
771
+ if (!rawLanguage) {
772
+ return;
773
+ }
774
+
775
+ const resolvedLanguage = resolvePrismLanguage(rawLanguage);
776
+ const grammar =
777
+ resolvePrismGrammar(resolvedLanguage) ??
778
+ resolvePrismGrammar(rawLanguage);
779
+ if (!grammar) {
780
+ return;
781
+ }
782
+
783
+ setLanguageClass(resolvedLanguage);
784
+ try {
785
+ const highlighted = Prism.highlight(rawCode, grammar, resolvedLanguage);
786
+ codeNode.innerHTML = highlighted;
787
+ } catch (error) {
788
+ console.error("Assistant embed code highlighting failed", {
789
+ language: resolvedLanguage,
790
+ error,
791
+ });
792
+ }
793
+ });
794
+
795
+ return container.innerHTML;
796
+ } catch (error) {
797
+ console.error(
798
+ "Assistant embed markdown highlighting pipeline failed",
799
+ error,
800
+ );
801
+ return html;
802
+ }
803
+ }
804
+
805
+ function renderMarkdownToHtml(
806
+ markdown: string,
807
+ linkTarget: AssistantLinkTarget,
808
+ ): string {
809
+ const normalizedMarkdown = markdown.replaceAll("\r\n", "\n");
810
+ const cacheKey = `${linkTarget}\0${normalizedMarkdown}`;
811
+ const cached = markdownHtmlCache.get(cacheKey);
812
+ if (cached !== undefined) {
813
+ return cached;
814
+ }
815
+
816
+ let html = "";
817
+
818
+ try {
819
+ const processor = unified()
820
+ .use(remarkParse)
821
+ .use(remarkGfm)
822
+ .use(remarkRehype, { allowDangerousHtml: false })
823
+ .use(rehypeRebaseInternalLinks);
824
+
825
+ if (linkTarget === "blank") {
826
+ processor.use(rehypeOpenLinksInNewTab);
827
+ }
828
+
829
+ html = String(
830
+ processor.use(rehypeStringify).processSync(normalizedMarkdown),
831
+ );
832
+ } catch {
833
+ html = escapeHtml(normalizedMarkdown).replaceAll("\n", "<br/>");
834
+ }
835
+
836
+ html = highlightCodeBlocksInHtml(html);
837
+
838
+ markdownHtmlCache.set(cacheKey, html);
839
+ if (markdownHtmlCache.size > MARKDOWN_HTML_CACHE_LIMIT) {
840
+ const oldestKey = markdownHtmlCache.keys().next().value;
841
+ if (oldestKey) {
842
+ markdownHtmlCache.delete(oldestKey);
843
+ }
844
+ }
845
+
846
+ return html;
847
+ }
848
+
849
+ function extractErrorMessage(rawBody: string): string {
850
+ if (!rawBody.trim()) {
851
+ return "";
852
+ }
853
+
854
+ try {
855
+ const parsed = JSON.parse(rawBody) as { error?: unknown; detail?: unknown };
856
+ if (typeof parsed.error === "string" && parsed.error.trim()) {
857
+ if (typeof parsed.detail === "string" && parsed.detail.trim()) {
858
+ return `${parsed.error}: ${parsed.detail}`;
859
+ }
860
+ return parsed.error;
861
+ }
862
+ } catch {
863
+ // Fall back to the plain response body.
864
+ }
865
+
866
+ return rawBody.trim();
867
+ }
868
+
869
+ function postParentMessage(
870
+ type: string,
871
+ payload: Record<string, unknown> = {},
872
+ ) {
873
+ if (typeof window === "undefined") {
874
+ return;
875
+ }
876
+
877
+ window.parent.postMessage({ type, ...payload }, "*");
878
+ }
879
+
880
+ function getApiPath(
881
+ apiPath: string,
882
+ allowApiPathQueryOverride: boolean,
883
+ ): string {
884
+ if (typeof window === "undefined") {
885
+ return apiPath;
886
+ }
887
+
888
+ if (allowApiPathQueryOverride) {
889
+ try {
890
+ const configuredApiUrl = new URLSearchParams(window.location.search).get(
891
+ "apiUrl",
892
+ );
893
+ if (configuredApiUrl && configuredApiUrl.trim().length > 0) {
894
+ return configuredApiUrl;
895
+ }
896
+ } catch {
897
+ // Fall back to the server-rendered value.
898
+ }
899
+ }
900
+
901
+ return apiPath;
902
+ }
903
+
904
+ export function AssistantPanelIcon({
905
+ color,
906
+ imageSrc,
907
+ className,
908
+ style,
909
+ }: {
910
+ color?: string;
911
+ imageSrc?: string;
912
+ className: string;
913
+ style?: JSX.CSSProperties;
914
+ }) {
915
+ if (!imageSrc) return null;
916
+
917
+ return (
918
+ <span
919
+ className={className}
920
+ style={color ? { ...style, color } : style}
921
+ aria-hidden="true"
922
+ >
923
+ <img src={imageSrc} alt="" />
924
+ </span>
925
+ );
926
+ }
927
+
928
+ export default function AssistantEmbedPanel({
929
+ apiPath,
930
+ docsTitle,
931
+ isChatAvailable,
932
+ canSendChatRequest,
933
+ launcherThemeColor,
934
+ launcherThemeColors,
935
+ launcherIconColor,
936
+ launcherIconColors,
937
+ launcherIconImageSrc,
938
+ emptyStateHeading = "How can I help?",
939
+ emptyStateQuestions = [],
940
+ unavailableMessage = "Chat assistant is not available.",
941
+ devProxyToken,
942
+ panelSurface = "iframe",
943
+ linkTarget = "current",
944
+ allowApiPathQueryOverride = true,
945
+ openSignal = 0,
946
+ panelSize,
947
+ onRequestOpen,
948
+ onRequestClose,
949
+ onRequestPanelSizeToggle,
950
+ onCurrentLinkNavigate,
951
+ }: AssistantEmbedPanelProps) {
952
+ const [initialPanelState] = useState<PersistedPanelState>(() =>
953
+ canSendChatRequest
954
+ ? readPersistedPanelState()
955
+ : createEmptyPersistedPanelState(),
956
+ );
957
+ const [messages, setMessages] = useState<ChatMessage[]>(
958
+ initialPanelState.messages,
959
+ );
960
+ const [input, setInput] = useState("");
961
+ const [isBusy, setIsBusy] = useState(initialPanelState.inFlight);
962
+ const [isAwaitingFirstToken, setIsAwaitingFirstToken] = useState(
963
+ initialPanelState.isAwaitingFirstToken,
964
+ );
965
+ const [errorMessage, setErrorMessage] = useState("");
966
+ const [showUnavailableState, setShowUnavailableState] =
967
+ useState(!isChatAvailable);
968
+ const [emptyStateAnimationKey, setEmptyStateAnimationKey] = useState(0);
969
+ const [localPanelSize, setLocalPanelSize] = useState<AssistantPanelSize>(
970
+ panelSize ?? initialPanelState.panelSize,
971
+ );
972
+ const [isShellFullscreen, setIsShellFullscreen] = useState(false);
973
+ const activeRequestAbortRef = useRef<AbortController | null>(null);
974
+ const scrollViewportRef = useRef<HTMLDivElement | null>(null);
975
+ const savedScrollTopRef = useRef(initialPanelState.scrollTop);
976
+ const scrollPersistenceFrameRef = useRef<number | null>(null);
977
+ const scrollPersistenceUnlockTimeoutRef = useRef<number | null>(null);
978
+ const isScrollPersistenceLockedRef = useRef(false);
979
+ const pendingHandoffsRef = useRef(new Map<string, PendingAssistantHandoff>());
980
+ const messagesRef = useRef(initialPanelState.messages);
981
+ const isBusyRef = useRef(initialPanelState.inFlight);
982
+ const isAwaitingFirstTokenRef = useRef(
983
+ initialPanelState.isAwaitingFirstToken,
984
+ );
985
+ const inFlightUpdatedAtRef = useRef(initialPanelState.inFlightUpdatedAt);
986
+ const panelSizeRef = useRef<AssistantPanelSize>(
987
+ panelSize ?? initialPanelState.panelSize,
988
+ );
989
+ const skipNextMessagesPersistRef = useRef(false);
990
+ const inputRef = useRef<HTMLTextAreaElement | null>(null);
991
+ const resolvedApiPathRef = useRef(
992
+ getApiPath(apiPath, allowApiPathQueryOverride),
993
+ );
994
+ const visibleEmptyStateQuestions = emptyStateQuestions
995
+ .map((question) => question.trim())
996
+ .filter((question) => question.length > 0)
997
+ .slice(0, 3);
998
+ const launcherThemeColorLight =
999
+ launcherThemeColors?.light ?? launcherThemeColor ?? "#171717";
1000
+ const launcherThemeColorDark =
1001
+ launcherThemeColors?.dark ?? launcherThemeColor ?? "#171717";
1002
+ const launcherIconColorLight =
1003
+ launcherIconColors?.light ?? launcherIconColor ?? "#ffffff";
1004
+ const launcherIconColorDark =
1005
+ launcherIconColors?.dark ?? launcherIconColor ?? "#ffffff";
1006
+ const resolvedPanelSize = panelSize ?? localPanelSize;
1007
+
1008
+ const persistPanelState = (
1009
+ nextMessages = messagesRef.current,
1010
+ scrollTop = savedScrollTopRef.current,
1011
+ options: PersistPanelStateOptions = {},
1012
+ ) => {
1013
+ const ownsActiveRequest = Boolean(activeRequestAbortRef.current);
1014
+ if (
1015
+ isBusyRef.current &&
1016
+ !ownsActiveRequest &&
1017
+ !options.allowBusyListenerWrite
1018
+ ) {
1019
+ return;
1020
+ }
1021
+
1022
+ if (!isBusyRef.current) {
1023
+ inFlightUpdatedAtRef.current = 0;
1024
+ } else if (
1025
+ ownsActiveRequest &&
1026
+ options.refreshInFlightTimestamp !== false
1027
+ ) {
1028
+ inFlightUpdatedAtRef.current = Date.now();
1029
+ }
1030
+
1031
+ writePersistedPanelState({
1032
+ messages: nextMessages,
1033
+ scrollTop,
1034
+ inFlight: isBusyRef.current,
1035
+ isAwaitingFirstToken:
1036
+ isBusyRef.current && isAwaitingFirstTokenRef.current,
1037
+ inFlightUpdatedAt: inFlightUpdatedAtRef.current,
1038
+ panelSize: panelSizeRef.current,
1039
+ });
1040
+ };
1041
+
1042
+ const cancelQueuedScrollPersistence = () => {
1043
+ if (
1044
+ typeof window !== "undefined" &&
1045
+ scrollPersistenceFrameRef.current !== null
1046
+ ) {
1047
+ window.cancelAnimationFrame(scrollPersistenceFrameRef.current);
1048
+ scrollPersistenceFrameRef.current = null;
1049
+ }
1050
+ };
1051
+
1052
+ const clearScrollPersistenceUnlockTimeout = () => {
1053
+ if (
1054
+ typeof window !== "undefined" &&
1055
+ scrollPersistenceUnlockTimeoutRef.current !== null
1056
+ ) {
1057
+ window.clearTimeout(scrollPersistenceUnlockTimeoutRef.current);
1058
+ scrollPersistenceUnlockTimeoutRef.current = null;
1059
+ }
1060
+ };
1061
+
1062
+ const unlockScrollPersistence = () => {
1063
+ clearScrollPersistenceUnlockTimeout();
1064
+ isScrollPersistenceLockedRef.current = false;
1065
+ };
1066
+
1067
+ const lockScrollPersistence = () => {
1068
+ if (typeof window === "undefined") {
1069
+ isScrollPersistenceLockedRef.current = true;
1070
+ return;
1071
+ }
1072
+
1073
+ clearScrollPersistenceUnlockTimeout();
1074
+ isScrollPersistenceLockedRef.current = true;
1075
+ scrollPersistenceUnlockTimeoutRef.current = window.setTimeout(() => {
1076
+ scrollPersistenceUnlockTimeoutRef.current = null;
1077
+ isScrollPersistenceLockedRef.current = false;
1078
+ }, 1200);
1079
+ };
1080
+
1081
+ const snapshotThreadScrollPosition = () => {
1082
+ const viewport = scrollViewportRef.current;
1083
+ if (!viewport) {
1084
+ return;
1085
+ }
1086
+
1087
+ savedScrollTopRef.current = viewport.scrollTop;
1088
+ cancelQueuedScrollPersistence();
1089
+ persistPanelState();
1090
+ };
1091
+
1092
+ const restoreSavedScrollPosition = (unlockAfterRestore = false) => {
1093
+ const viewport = scrollViewportRef.current;
1094
+ if (!viewport) {
1095
+ if (unlockAfterRestore) {
1096
+ unlockScrollPersistence();
1097
+ }
1098
+ return;
1099
+ }
1100
+
1101
+ const maxScrollTop = Math.max(
1102
+ 0,
1103
+ viewport.scrollHeight - viewport.clientHeight,
1104
+ );
1105
+ viewport.scrollTop = clamp(savedScrollTopRef.current, 0, maxScrollTop);
1106
+
1107
+ if (unlockAfterRestore && typeof window !== "undefined") {
1108
+ window.requestAnimationFrame(() => {
1109
+ unlockScrollPersistence();
1110
+ });
1111
+ } else if (unlockAfterRestore) {
1112
+ unlockScrollPersistence();
1113
+ }
1114
+ };
1115
+
1116
+ const queueSavedScrollRestore = (unlockAfterRestore = false) => {
1117
+ if (typeof window === "undefined") {
1118
+ restoreSavedScrollPosition(unlockAfterRestore);
1119
+ return;
1120
+ }
1121
+
1122
+ window.requestAnimationFrame(() => {
1123
+ window.requestAnimationFrame(() => {
1124
+ restoreSavedScrollPosition(unlockAfterRestore);
1125
+ });
1126
+ });
1127
+ };
1128
+
1129
+ const queueThreadScrollToBottom = (nextMessages: ChatMessage[]) => {
1130
+ if (typeof window === "undefined") {
1131
+ return;
1132
+ }
1133
+
1134
+ window.requestAnimationFrame(() => {
1135
+ const viewport = scrollViewportRef.current;
1136
+ if (!viewport) {
1137
+ return;
1138
+ }
1139
+
1140
+ viewport.scrollTop = viewport.scrollHeight;
1141
+ savedScrollTopRef.current = viewport.scrollTop;
1142
+ persistPanelState(nextMessages, savedScrollTopRef.current);
1143
+ });
1144
+ };
1145
+
1146
+ const resetThreadScrollPosition = (nextMessages: ChatMessage[]) => {
1147
+ savedScrollTopRef.current = 0;
1148
+ if (scrollViewportRef.current) {
1149
+ scrollViewportRef.current.scrollTop = 0;
1150
+ }
1151
+ persistPanelState(nextMessages, 0);
1152
+ };
1153
+
1154
+ const setSharedBusy = (nextIsBusy: boolean) => {
1155
+ isBusyRef.current = nextIsBusy;
1156
+ inFlightUpdatedAtRef.current = nextIsBusy ? Date.now() : 0;
1157
+ setIsBusy(nextIsBusy);
1158
+ };
1159
+
1160
+ const setSharedAwaitingFirstToken = (nextIsAwaiting: boolean) => {
1161
+ isAwaitingFirstTokenRef.current = nextIsAwaiting;
1162
+ setIsAwaitingFirstToken(nextIsAwaiting);
1163
+ };
1164
+
1165
+ const notifyPanelSizeChange = (nextPanelSize: AssistantPanelSize) => {
1166
+ if (onRequestPanelSizeToggle) {
1167
+ onRequestPanelSizeToggle(nextPanelSize);
1168
+ return;
1169
+ }
1170
+
1171
+ postParentMessage("assistant-embed:set-panel-size", {
1172
+ size: nextPanelSize,
1173
+ });
1174
+ };
1175
+
1176
+ const setSharedPanelSize = (
1177
+ nextPanelSize: AssistantPanelSize,
1178
+ shouldNotifyShell = true,
1179
+ ) => {
1180
+ panelSizeRef.current = nextPanelSize;
1181
+ setLocalPanelSize(nextPanelSize);
1182
+ persistPanelState(messagesRef.current, savedScrollTopRef.current, {
1183
+ allowBusyListenerWrite: true,
1184
+ refreshInFlightTimestamp: false,
1185
+ });
1186
+
1187
+ if (shouldNotifyShell) {
1188
+ notifyPanelSizeChange(nextPanelSize);
1189
+ }
1190
+ };
1191
+
1192
+ const applyPersistedPanelState = (nextState: PersistedPanelState) => {
1193
+ messagesRef.current = nextState.messages;
1194
+ savedScrollTopRef.current = nextState.scrollTop;
1195
+ isBusyRef.current = nextState.inFlight;
1196
+ isAwaitingFirstTokenRef.current = nextState.isAwaitingFirstToken;
1197
+ inFlightUpdatedAtRef.current = nextState.inFlightUpdatedAt;
1198
+ panelSizeRef.current = nextState.panelSize;
1199
+ skipNextMessagesPersistRef.current = true;
1200
+ setMessages(nextState.messages);
1201
+ setIsBusy(nextState.inFlight);
1202
+ setIsAwaitingFirstToken(nextState.isAwaitingFirstToken);
1203
+ setLocalPanelSize(nextState.panelSize);
1204
+ queueSavedScrollRestore();
1205
+ };
1206
+
1207
+ const requestPanelOpen = () => {
1208
+ if (onRequestOpen) {
1209
+ onRequestOpen();
1210
+ return;
1211
+ }
1212
+
1213
+ postParentMessage("assistant-embed:open");
1214
+ };
1215
+
1216
+ const clearPendingHandoff = (handoffId: string) => {
1217
+ const pending = pendingHandoffsRef.current.get(handoffId);
1218
+ if (!pending) {
1219
+ return;
1220
+ }
1221
+
1222
+ window.clearTimeout(pending.timeoutId);
1223
+ pendingHandoffsRef.current.delete(handoffId);
1224
+ };
1225
+
1226
+ const openBlankLinkWithHandoff = (
1227
+ navigationRequest: CurrentLinkNavigationRequest,
1228
+ ) => {
1229
+ if (typeof window === "undefined") {
1230
+ return false;
1231
+ }
1232
+
1233
+ snapshotThreadScrollPosition();
1234
+
1235
+ const handoffId = createMessageId();
1236
+ const targetWindow = window.open(
1237
+ buildHandoffUrl(navigationRequest.href, handoffId),
1238
+ "_blank",
1239
+ );
1240
+ if (!targetWindow) {
1241
+ return false;
1242
+ }
1243
+
1244
+ const timeoutId = window.setTimeout(() => {
1245
+ pendingHandoffsRef.current.delete(handoffId);
1246
+ }, 30000);
1247
+
1248
+ pendingHandoffsRef.current.set(handoffId, {
1249
+ state: {
1250
+ messages: messagesRef.current,
1251
+ scrollTop: savedScrollTopRef.current,
1252
+ inFlight: isBusyRef.current,
1253
+ isAwaitingFirstToken:
1254
+ isBusyRef.current && isAwaitingFirstTokenRef.current,
1255
+ inFlightUpdatedAt:
1256
+ isBusyRef.current && activeRequestAbortRef.current
1257
+ ? Date.now()
1258
+ : inFlightUpdatedAtRef.current,
1259
+ panelSize: panelSizeRef.current,
1260
+ },
1261
+ targetWindow,
1262
+ timeoutId,
1263
+ });
1264
+
1265
+ return true;
1266
+ };
1267
+
1268
+ useEffect(() => {
1269
+ return () => {
1270
+ activeRequestAbortRef.current?.abort();
1271
+ activeRequestAbortRef.current = null;
1272
+ if (
1273
+ typeof window !== "undefined" &&
1274
+ scrollPersistenceFrameRef.current !== null
1275
+ ) {
1276
+ window.cancelAnimationFrame(scrollPersistenceFrameRef.current);
1277
+ }
1278
+ clearScrollPersistenceUnlockTimeout();
1279
+ for (const handoffId of pendingHandoffsRef.current.keys()) {
1280
+ clearPendingHandoff(handoffId);
1281
+ }
1282
+ };
1283
+ }, []);
1284
+
1285
+ useEffect(() => {
1286
+ messagesRef.current = messages;
1287
+ if (skipNextMessagesPersistRef.current) {
1288
+ skipNextMessagesPersistRef.current = false;
1289
+ return;
1290
+ }
1291
+ persistPanelState(messages);
1292
+ }, [messages]);
1293
+
1294
+ useEffect(() => {
1295
+ if (typeof window === "undefined") {
1296
+ return;
1297
+ }
1298
+
1299
+ if (!isBusy || !activeRequestAbortRef.current) {
1300
+ return;
1301
+ }
1302
+
1303
+ const inFlightHeartbeat = window.setInterval(() => {
1304
+ if (!activeRequestAbortRef.current) {
1305
+ return;
1306
+ }
1307
+
1308
+ persistPanelState();
1309
+ }, IN_FLIGHT_HEARTBEAT_MS);
1310
+
1311
+ return () => {
1312
+ window.clearInterval(inFlightHeartbeat);
1313
+ };
1314
+ }, [isBusy]);
1315
+
1316
+ useEffect(() => {
1317
+ if (typeof window === "undefined") {
1318
+ return;
1319
+ }
1320
+
1321
+ const handleStorage = (event: StorageEvent) => {
1322
+ if (
1323
+ event.key !== ASSISTANT_PANEL_STORAGE_KEY ||
1324
+ event.newValue === null
1325
+ ) {
1326
+ return;
1327
+ }
1328
+
1329
+ if (activeRequestAbortRef.current) {
1330
+ return;
1331
+ }
1332
+
1333
+ applyPersistedPanelState(parsePersistedPanelState(event.newValue));
1334
+ };
1335
+
1336
+ window.addEventListener("storage", handleStorage);
1337
+ return () => {
1338
+ window.removeEventListener("storage", handleStorage);
1339
+ };
1340
+ }, []);
1341
+
1342
+ useEffect(() => {
1343
+ queueSavedScrollRestore();
1344
+
1345
+ if (typeof document === "undefined") {
1346
+ return;
1347
+ }
1348
+
1349
+ const persistCurrentScrollPosition = () => {
1350
+ if (isScrollPersistenceLockedRef.current) {
1351
+ return;
1352
+ }
1353
+
1354
+ const viewport = scrollViewportRef.current;
1355
+ if (!viewport) {
1356
+ return;
1357
+ }
1358
+
1359
+ savedScrollTopRef.current = viewport.scrollTop;
1360
+ persistPanelState();
1361
+ };
1362
+
1363
+ const handleAfterSwap = () => {
1364
+ queueSavedScrollRestore(isScrollPersistenceLockedRef.current);
1365
+ };
1366
+
1367
+ document.addEventListener(
1368
+ "astro:before-preparation",
1369
+ persistCurrentScrollPosition,
1370
+ );
1371
+ document.addEventListener("astro:after-swap", handleAfterSwap);
1372
+ return () => {
1373
+ document.removeEventListener(
1374
+ "astro:before-preparation",
1375
+ persistCurrentScrollPosition,
1376
+ );
1377
+ document.removeEventListener("astro:after-swap", handleAfterSwap);
1378
+ };
1379
+ }, []);
1380
+
1381
+ useEffect(() => {
1382
+ if (typeof window === "undefined") {
1383
+ return;
1384
+ }
1385
+
1386
+ const handleHandoffMessage = (event: MessageEvent) => {
1387
+ if (
1388
+ event.origin !== window.location.origin ||
1389
+ !event.data ||
1390
+ typeof event.data !== "object"
1391
+ ) {
1392
+ return;
1393
+ }
1394
+
1395
+ const data = event.data as {
1396
+ type?: unknown;
1397
+ handoffId?: unknown;
1398
+ };
1399
+ if (typeof data.handoffId !== "string") {
1400
+ return;
1401
+ }
1402
+
1403
+ const pending = pendingHandoffsRef.current.get(data.handoffId);
1404
+ if (!pending || event.source !== pending.targetWindow) {
1405
+ return;
1406
+ }
1407
+
1408
+ if (data.type === HANDOFF_READY_TYPE) {
1409
+ pending.targetWindow.postMessage(
1410
+ {
1411
+ type: HANDOFF_STATE_TYPE,
1412
+ handoffId: data.handoffId,
1413
+ state: pending.state,
1414
+ },
1415
+ window.location.origin,
1416
+ );
1417
+ return;
1418
+ }
1419
+
1420
+ if (data.type === HANDOFF_ACK_TYPE) {
1421
+ clearPendingHandoff(data.handoffId);
1422
+ }
1423
+ };
1424
+
1425
+ window.addEventListener("message", handleHandoffMessage);
1426
+ return () => {
1427
+ window.removeEventListener("message", handleHandoffMessage);
1428
+ };
1429
+ }, []);
1430
+
1431
+ useEffect(() => {
1432
+ if (typeof window === "undefined") {
1433
+ return;
1434
+ }
1435
+
1436
+ const { handoffId, shouldOpen } = readAssistantUrlSignal();
1437
+ if (!handoffId && !shouldOpen) {
1438
+ return;
1439
+ }
1440
+
1441
+ requestPanelOpen();
1442
+ clearAssistantUrlSignal();
1443
+
1444
+ if (!handoffId || !window.opener) {
1445
+ return;
1446
+ }
1447
+
1448
+ const handleIncomingHandoff = (event: MessageEvent) => {
1449
+ if (
1450
+ event.origin !== window.location.origin ||
1451
+ event.source !== window.opener ||
1452
+ !event.data ||
1453
+ typeof event.data !== "object"
1454
+ ) {
1455
+ return;
1456
+ }
1457
+
1458
+ const data = event.data as {
1459
+ type?: unknown;
1460
+ handoffId?: unknown;
1461
+ state?: unknown;
1462
+ };
1463
+ if (data.type !== HANDOFF_STATE_TYPE || data.handoffId !== handoffId) {
1464
+ return;
1465
+ }
1466
+
1467
+ const handoffState = normalizePersistedPanelState(data.state);
1468
+ const nextState = {
1469
+ ...handoffState,
1470
+ inFlight: isBusyRef.current,
1471
+ isAwaitingFirstToken:
1472
+ isBusyRef.current && isAwaitingFirstTokenRef.current,
1473
+ inFlightUpdatedAt: isBusyRef.current ? inFlightUpdatedAtRef.current : 0,
1474
+ };
1475
+ applyPersistedPanelState(nextState);
1476
+ writePersistedPanelState(nextState);
1477
+
1478
+ window.opener?.postMessage(
1479
+ { type: HANDOFF_ACK_TYPE, handoffId },
1480
+ window.location.origin,
1481
+ );
1482
+ window.removeEventListener("message", handleIncomingHandoff);
1483
+ };
1484
+
1485
+ window.addEventListener("message", handleIncomingHandoff);
1486
+ window.opener.postMessage(
1487
+ { type: HANDOFF_READY_TYPE, handoffId },
1488
+ window.location.origin,
1489
+ );
1490
+
1491
+ const handoffTimeout = window.setTimeout(() => {
1492
+ window.removeEventListener("message", handleIncomingHandoff);
1493
+ }, 5000);
1494
+
1495
+ return () => {
1496
+ window.clearTimeout(handoffTimeout);
1497
+ window.removeEventListener("message", handleIncomingHandoff);
1498
+ };
1499
+ }, []);
1500
+
1501
+ const resizeChatInputTextarea = () => {
1502
+ const textarea = inputRef.current;
1503
+ if (!textarea || typeof window === "undefined") {
1504
+ return;
1505
+ }
1506
+
1507
+ textarea.style.height = "auto";
1508
+
1509
+ const computedStyle = window.getComputedStyle(textarea);
1510
+ const lineHeightRaw = Number.parseFloat(computedStyle.lineHeight);
1511
+ const paddingTopRaw = Number.parseFloat(computedStyle.paddingTop);
1512
+ const paddingBottomRaw = Number.parseFloat(computedStyle.paddingBottom);
1513
+
1514
+ const lineHeight = Number.isFinite(lineHeightRaw) ? lineHeightRaw : 20;
1515
+ const paddingTop = Number.isFinite(paddingTopRaw) ? paddingTopRaw : 0;
1516
+ const paddingBottom = Number.isFinite(paddingBottomRaw)
1517
+ ? paddingBottomRaw
1518
+ : 0;
1519
+
1520
+ const minHeight = lineHeight + paddingTop + paddingBottom;
1521
+ const measuredScrollHeight = Math.max(textarea.scrollHeight, minHeight);
1522
+ const maxHeight = lineHeight * 11.2 + paddingTop + paddingBottom;
1523
+ const nextHeight = Math.min(measuredScrollHeight, maxHeight);
1524
+
1525
+ textarea.style.height = `${nextHeight}px`;
1526
+ textarea.style.overflowY =
1527
+ measuredScrollHeight > maxHeight ? "auto" : "hidden";
1528
+ };
1529
+
1530
+ useEffect(() => {
1531
+ resizeChatInputTextarea();
1532
+ }, [input]);
1533
+
1534
+ useEffect(() => {
1535
+ if (panelSize) {
1536
+ setSharedPanelSize(panelSize, false);
1537
+ }
1538
+ }, [panelSize]);
1539
+
1540
+ useEffect(() => {
1541
+ notifyPanelSizeChange(resolvedPanelSize);
1542
+ }, [resolvedPanelSize]);
1543
+
1544
+ useEffect(() => {
1545
+ if (typeof window === "undefined") {
1546
+ return;
1547
+ }
1548
+
1549
+ const handlePanelMessage = (event: MessageEvent) => {
1550
+ if (!event.data || typeof event.data !== "object") {
1551
+ return;
1552
+ }
1553
+
1554
+ const data = event.data as {
1555
+ type?: unknown;
1556
+ size?: unknown;
1557
+ isFullscreen?: unknown;
1558
+ };
1559
+
1560
+ if (data.type === "assistant-embed:panel-opened") {
1561
+ setEmptyStateAnimationKey((previous) => previous + 1);
1562
+ queueSavedScrollRestore();
1563
+ postParentMessage("assistant-embed:get-panel-layout");
1564
+ return;
1565
+ }
1566
+
1567
+ if (data.type === "assistant-embed:set-panel-layout") {
1568
+ setIsShellFullscreen(data.isFullscreen === true);
1569
+ return;
1570
+ }
1571
+
1572
+ if (
1573
+ data.type === "assistant-embed:set-panel-size" &&
1574
+ (data.size === "default" || data.size === "expanded")
1575
+ ) {
1576
+ setSharedPanelSize(data.size, true);
1577
+ }
1578
+ };
1579
+
1580
+ window.addEventListener("message", handlePanelMessage);
1581
+ return () => {
1582
+ window.removeEventListener("message", handlePanelMessage);
1583
+ };
1584
+ }, []);
1585
+
1586
+ useEffect(() => {
1587
+ setEmptyStateAnimationKey((previous) => previous + 1);
1588
+ queueSavedScrollRestore();
1589
+ }, [openSignal]);
1590
+
1591
+ const handleStartNewChat = () => {
1592
+ activeRequestAbortRef.current?.abort();
1593
+ activeRequestAbortRef.current = null;
1594
+ setSharedBusy(false);
1595
+ setSharedAwaitingFirstToken(false);
1596
+ resetThreadScrollPosition([]);
1597
+ setMessages([]);
1598
+ setInput("");
1599
+ setErrorMessage("");
1600
+ setShowUnavailableState(false);
1601
+ };
1602
+
1603
+ const handleRequestClose = () => {
1604
+ if (onRequestClose) {
1605
+ onRequestClose();
1606
+ return;
1607
+ }
1608
+
1609
+ postParentMessage("assistant-embed:close");
1610
+ };
1611
+
1612
+ const handlePanelSizeToggle = () => {
1613
+ const nextPanelSize =
1614
+ resolvedPanelSize === "expanded" ? "default" : "expanded";
1615
+ setSharedPanelSize(nextPanelSize);
1616
+ };
1617
+
1618
+ const handleUnavailableBack = () => {
1619
+ activeRequestAbortRef.current?.abort();
1620
+ activeRequestAbortRef.current = null;
1621
+ setSharedBusy(false);
1622
+ setSharedAwaitingFirstToken(false);
1623
+ resetThreadScrollPosition([]);
1624
+ setMessages([]);
1625
+ setInput("");
1626
+ setErrorMessage("");
1627
+ setShowUnavailableState(false);
1628
+ };
1629
+
1630
+ const submitPrompt = async (rawPrompt: string) => {
1631
+ const prompt = rawPrompt.trim();
1632
+ if (!prompt || isBusy || showUnavailableState) {
1633
+ return;
1634
+ }
1635
+
1636
+ if (!isChatAvailable || !canSendChatRequest) {
1637
+ setShowUnavailableState(true);
1638
+ return;
1639
+ }
1640
+
1641
+ setErrorMessage("");
1642
+ setSharedBusy(true);
1643
+ setSharedAwaitingFirstToken(true);
1644
+
1645
+ const userMessage: ChatMessage = {
1646
+ id: createMessageId(),
1647
+ role: "user",
1648
+ content: prompt,
1649
+ };
1650
+ const assistantId = createMessageId();
1651
+ const nextConversation = [...messages, userMessage];
1652
+
1653
+ setInput("");
1654
+ setMessages(nextConversation);
1655
+ queueThreadScrollToBottom(nextConversation);
1656
+
1657
+ activeRequestAbortRef.current?.abort();
1658
+ const abortController = new AbortController();
1659
+ activeRequestAbortRef.current = abortController;
1660
+
1661
+ try {
1662
+ const requestHeaders: Record<string, string> = {
1663
+ "Content-Type": "application/json",
1664
+ };
1665
+
1666
+ if (devProxyToken && devProxyToken.trim().length > 0) {
1667
+ requestHeaders["x-ask-ai-dev-token"] = devProxyToken;
1668
+ }
1669
+
1670
+ const response = await fetch(resolvedApiPathRef.current, {
1671
+ method: "POST",
1672
+ headers: requestHeaders,
1673
+ body: JSON.stringify({
1674
+ messages: nextConversation.map((message) => ({
1675
+ role: message.role,
1676
+ content: message.content,
1677
+ })),
1678
+ }),
1679
+ signal: abortController.signal,
1680
+ });
1681
+
1682
+ if (!response.ok) {
1683
+ const rawBody = await response.text();
1684
+ const detail = extractErrorMessage(rawBody);
1685
+ throw new Error(
1686
+ detail || `Request failed with status ${response.status}`,
1687
+ );
1688
+ }
1689
+
1690
+ if (!response.body) {
1691
+ throw new Error("Response body is empty");
1692
+ }
1693
+
1694
+ const reader = response.body.getReader();
1695
+ const decoder = new TextDecoder();
1696
+ let buffer = "";
1697
+ let streamDone = false;
1698
+ let hasReceivedFirstTextDelta = false;
1699
+
1700
+ while (!streamDone) {
1701
+ const { done, value } = await reader.read();
1702
+ if (done) {
1703
+ break;
1704
+ }
1705
+
1706
+ buffer += decoder.decode(value, { stream: true });
1707
+
1708
+ while (true) {
1709
+ const eventBreakIndex = buffer.indexOf("\n\n");
1710
+ if (eventBreakIndex === -1) {
1711
+ break;
1712
+ }
1713
+
1714
+ const rawEvent = buffer.slice(0, eventBreakIndex);
1715
+ buffer = buffer.slice(eventBreakIndex + 2);
1716
+
1717
+ const dataPayload = rawEvent
1718
+ .split("\n")
1719
+ .filter((line) => line.startsWith("data:"))
1720
+ .map((line) => line.slice(5).trimStart())
1721
+ .join("\n");
1722
+
1723
+ if (!dataPayload) {
1724
+ continue;
1725
+ }
1726
+
1727
+ if (dataPayload === "[DONE]") {
1728
+ streamDone = true;
1729
+ break;
1730
+ }
1731
+
1732
+ let parsed: AssistantStreamEvent;
1733
+ try {
1734
+ parsed = JSON.parse(dataPayload) as AssistantStreamEvent;
1735
+ } catch {
1736
+ continue;
1737
+ }
1738
+
1739
+ if (
1740
+ parsed.type === "text-delta" &&
1741
+ typeof parsed.delta === "string" &&
1742
+ parsed.delta.length > 0
1743
+ ) {
1744
+ if (!hasReceivedFirstTextDelta) {
1745
+ hasReceivedFirstTextDelta = true;
1746
+ setSharedAwaitingFirstToken(false);
1747
+ }
1748
+
1749
+ setMessages((previous) => {
1750
+ const existingAssistantMessage = previous.find(
1751
+ (message) => message.id === assistantId,
1752
+ );
1753
+
1754
+ if (!existingAssistantMessage) {
1755
+ return [
1756
+ ...previous,
1757
+ {
1758
+ id: assistantId,
1759
+ role: "assistant",
1760
+ content: parsed.delta ?? "",
1761
+ },
1762
+ ];
1763
+ }
1764
+
1765
+ return previous.map((message) =>
1766
+ message.id === assistantId
1767
+ ? {
1768
+ ...message,
1769
+ content: `${message.content}${parsed.delta ?? ""}`,
1770
+ }
1771
+ : message,
1772
+ );
1773
+ });
1774
+ }
1775
+ }
1776
+ }
1777
+ } catch (error) {
1778
+ if (abortController.signal.aborted) {
1779
+ return;
1780
+ }
1781
+
1782
+ const resolvedError =
1783
+ error instanceof Error ? error.message : String(error);
1784
+ setErrorMessage(resolvedError || "Failed to get response.");
1785
+ } finally {
1786
+ if (activeRequestAbortRef.current === abortController) {
1787
+ activeRequestAbortRef.current = null;
1788
+ }
1789
+ setSharedBusy(false);
1790
+ setSharedAwaitingFirstToken(false);
1791
+ persistPanelState();
1792
+ }
1793
+ };
1794
+
1795
+ const handleSubmit = async (event: Event) => {
1796
+ event.preventDefault();
1797
+ await submitPrompt(input);
1798
+ };
1799
+
1800
+ const handleChatInputKeyDown = (event: ChatInputKeyEvent) => {
1801
+ if (event.key !== "Enter" || event.shiftKey || event.isComposing) {
1802
+ return;
1803
+ }
1804
+
1805
+ event.preventDefault();
1806
+ const form = event.currentTarget.form;
1807
+ if (form) {
1808
+ form.requestSubmit();
1809
+ }
1810
+ };
1811
+
1812
+ const handleRenderedMarkdownClick = (
1813
+ event: JSX.TargetedMouseEvent<HTMLDivElement>,
1814
+ ) => {
1815
+ const targetElement = event.target instanceof Element ? event.target : null;
1816
+ const copyButton = targetElement?.closest(
1817
+ ".ask-ai-copy-code-button",
1818
+ ) as HTMLButtonElement | null;
1819
+
1820
+ if (copyButton) {
1821
+ event.preventDefault();
1822
+ event.stopPropagation();
1823
+
1824
+ if (copyButton.dataset.copying === "true") {
1825
+ return;
1826
+ }
1827
+
1828
+ const preElement = copyButton.closest("pre");
1829
+ const codeElement = preElement?.querySelector("code");
1830
+ if (!codeElement) {
1831
+ return;
1832
+ }
1833
+
1834
+ const codeText = codeElement.textContent ?? "";
1835
+ copyButton.dataset.copying = "true";
1836
+
1837
+ void (async () => {
1838
+ const didCopy = await copyToClipboard(codeText);
1839
+ if (didCopy) {
1840
+ copyButton.dataset.copied = "true";
1841
+ } else {
1842
+ delete copyButton.dataset.copied;
1843
+ }
1844
+
1845
+ window.setTimeout(() => {
1846
+ delete copyButton.dataset.copied;
1847
+ delete copyButton.dataset.copying;
1848
+ }, 1200);
1849
+ })();
1850
+ return;
1851
+ }
1852
+
1853
+ const blankNavigationRequest =
1854
+ linkTarget === "blank"
1855
+ ? getSameOriginBlankNavigationRequest(event)
1856
+ : null;
1857
+ if (blankNavigationRequest) {
1858
+ event.preventDefault();
1859
+ event.stopPropagation();
1860
+ const didOpen = openBlankLinkWithHandoff(blankNavigationRequest);
1861
+ if (!didOpen && typeof window !== "undefined") {
1862
+ window.open(
1863
+ blankNavigationRequest.href,
1864
+ "_blank",
1865
+ "noopener,noreferrer",
1866
+ );
1867
+ }
1868
+ return;
1869
+ }
1870
+
1871
+ const navigationRequest =
1872
+ linkTarget === "current" && onCurrentLinkNavigate
1873
+ ? getSameOriginNavigationRequest(event)
1874
+ : null;
1875
+ if (!navigationRequest) {
1876
+ return;
1877
+ }
1878
+
1879
+ event.preventDefault();
1880
+ event.stopPropagation();
1881
+ snapshotThreadScrollPosition();
1882
+ lockScrollPersistence();
1883
+ onCurrentLinkNavigate(
1884
+ navigationRequest.href,
1885
+ navigationRequest.sourceElement,
1886
+ );
1887
+ };
1888
+
1889
+ const handleThreadViewportWheel = (event: ChatViewportWheelEvent) => {
1890
+ const eventTargetElement =
1891
+ event.target instanceof Element ? event.target : null;
1892
+ const codeBlockPre = eventTargetElement?.closest(
1893
+ ".ask-ai-markdown pre",
1894
+ ) as HTMLPreElement | null;
1895
+ const codeBlockScroller =
1896
+ (eventTargetElement?.closest(
1897
+ ".ask-ai-markdown pre > code",
1898
+ ) as HTMLElement | null) ??
1899
+ (codeBlockPre?.querySelector(":scope > code") as HTMLElement | null);
1900
+
1901
+ if (!codeBlockScroller) {
1902
+ return;
1903
+ }
1904
+
1905
+ const horizontalIntentDelta =
1906
+ Math.abs(event.deltaX) > 0.01
1907
+ ? event.deltaX
1908
+ : event.shiftKey
1909
+ ? event.deltaY
1910
+ : 0;
1911
+
1912
+ if (Math.abs(horizontalIntentDelta) <= 0.01) {
1913
+ return;
1914
+ }
1915
+
1916
+ event.preventDefault();
1917
+
1918
+ const maxScrollLeft = Math.max(
1919
+ 0,
1920
+ codeBlockScroller.scrollWidth - codeBlockScroller.clientWidth,
1921
+ );
1922
+
1923
+ if (maxScrollLeft > 0) {
1924
+ codeBlockScroller.scrollLeft = clamp(
1925
+ codeBlockScroller.scrollLeft + horizontalIntentDelta,
1926
+ 0,
1927
+ maxScrollLeft,
1928
+ );
1929
+ }
1930
+ };
1931
+
1932
+ const handleThreadViewportScroll = (event: ChatViewportScrollEvent) => {
1933
+ if (isScrollPersistenceLockedRef.current) {
1934
+ return;
1935
+ }
1936
+
1937
+ savedScrollTopRef.current = event.currentTarget.scrollTop;
1938
+
1939
+ if (
1940
+ typeof window === "undefined" ||
1941
+ scrollPersistenceFrameRef.current !== null
1942
+ ) {
1943
+ return;
1944
+ }
1945
+
1946
+ scrollPersistenceFrameRef.current = window.requestAnimationFrame(() => {
1947
+ scrollPersistenceFrameRef.current = null;
1948
+ persistPanelState();
1949
+ });
1950
+ };
1951
+
1952
+ const panelClassName = [
1953
+ "relative flex min-h-0 flex-col overflow-hidden text-neutral-900 shadow-2xl dark:text-neutral-50",
1954
+ panelSurface === "inline"
1955
+ ? "h-full rounded-2xl"
1956
+ : "h-dvh sm:h-screen sm:rounded-2xl",
1957
+ ].join(" ");
1958
+
1959
+ return (
1960
+ <div className={panelClassName}>
1961
+ <header className="flex items-center justify-between gap-2 px-2 pt-2 pb-1">
1962
+ <div className="flex min-w-0 flex-1 items-center gap-2">
1963
+ <AssistantPanelIcon
1964
+ color={launcherIconColor}
1965
+ imageSrc={launcherIconImageSrc}
1966
+ 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"
1967
+ />
1968
+ <div className="min-w-0 space-y-px">
1969
+ <p className="truncate text-sm font-medium leading-3.5 text-neutral-900 dark:text-neutral-50">
1970
+ {docsTitle}
1971
+ </p>
1972
+ <p className="text-xs leading-3 text-neutral-500/80 dark:text-neutral-400">
1973
+ Assistant
1974
+ </p>
1975
+ </div>
1976
+ {messages.length > 0 ? (
1977
+ <button
1978
+ type="button"
1979
+ className="shrink-0 inline-flex items-center gap-1.5 rounded-md border border-neutral-900/8 px-2 py-1 ml-1.5 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"
1980
+ onClick={handleStartNewChat}
1981
+ aria-label="Clear chat"
1982
+ title="Clear chat"
1983
+ >
1984
+ <Icon
1985
+ icon="lucide:trash-2"
1986
+ className="size-3.5 -ml-px"
1987
+ aria-hidden="true"
1988
+ />
1989
+ Clear
1990
+ </button>
1991
+ ) : null}
1992
+ </div>
1993
+ <div className="flex shrink-0 items-center">
1994
+ {!isShellFullscreen ? (
1995
+ <button
1996
+ type="button"
1997
+ className="inline-flex items-center justify-center rounded-md size-9 rotate-90 text-[13px] text-neutral-500 hover:bg-neutral-900/5 dark:text-neutral-300 dark:hover:bg-white/10 transition cursor-pointer"
1998
+ onClick={handlePanelSizeToggle}
1999
+ aria-label={
2000
+ resolvedPanelSize === "expanded"
2001
+ ? "Use default panel size"
2002
+ : "Expand panel"
2003
+ }
2004
+ title={
2005
+ resolvedPanelSize === "expanded"
2006
+ ? "Default size"
2007
+ : "Expand panel"
2008
+ }
2009
+ >
2010
+ <Icon
2011
+ icon={
2012
+ resolvedPanelSize === "expanded"
2013
+ ? "lucide:minimize-2"
2014
+ : "lucide:maximize-2"
2015
+ }
2016
+ className="size-4"
2017
+ aria-hidden="true"
2018
+ />
2019
+ </button>
2020
+ ) : null}
2021
+ <button
2022
+ type="button"
2023
+ className="inline-flex items-center justify-center 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"
2024
+ onClick={handleRequestClose}
2025
+ aria-label="Close chat"
2026
+ title="Close"
2027
+ >
2028
+ <Icon icon="lucide:x" className="size-5" aria-hidden="true" />
2029
+ </button>
2030
+ </div>
2031
+ </header>
2032
+
2033
+ {!showUnavailableState ? (
2034
+ <>
2035
+ <div
2036
+ ref={scrollViewportRef}
2037
+ 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`}
2038
+ onScroll={handleThreadViewportScroll}
2039
+ onWheel={handleThreadViewportWheel}
2040
+ >
2041
+ {messages.length === 0 ? (
2042
+ <div
2043
+ key={emptyStateAnimationKey}
2044
+ className="flex min-h-full flex-col items-center justify-center px-1. pb-28. pt-8. text-center"
2045
+ >
2046
+ <AssistantPanelIcon
2047
+ color={launcherIconColor}
2048
+ imageSrc={launcherIconImageSrc}
2049
+ 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"
2050
+ style={
2051
+ {
2052
+ "--assistant-empty-state-delay": "200ms",
2053
+ } as JSX.CSSProperties
2054
+ }
2055
+ />
2056
+ <p
2057
+ className="assistant-empty-state-item mt-3 text-3xl font-[450] leading-9 text-neutral-900 dark:text-neutral-50"
2058
+ style={
2059
+ {
2060
+ "--assistant-empty-state-delay": "300ms",
2061
+ } as JSX.CSSProperties
2062
+ }
2063
+ >
2064
+ {emptyStateHeading}
2065
+ </p>
2066
+ {visibleEmptyStateQuestions.length > 0 ? (
2067
+ <div className="mt-10 flex w-full flex-col items-center gap-2">
2068
+ {visibleEmptyStateQuestions.map((question, index) => (
2069
+ <button
2070
+ key={question}
2071
+ type="button"
2072
+ 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)]"
2073
+ style={
2074
+ {
2075
+ "--assistant-empty-state-delay": `${400 + index * 100}ms`,
2076
+ } as JSX.CSSProperties
2077
+ }
2078
+ disabled={isBusy}
2079
+ onClick={() => {
2080
+ void submitPrompt(question);
2081
+ }}
2082
+ >
2083
+ {question}
2084
+ </button>
2085
+ ))}
2086
+ </div>
2087
+ ) : null}
2088
+ </div>
2089
+ ) : null}
2090
+
2091
+ {messages.map((message) => {
2092
+ if (
2093
+ message.role === "assistant" &&
2094
+ message.content.length === 0
2095
+ ) {
2096
+ return null;
2097
+ }
2098
+ const isUser = message.role === "user";
2099
+ return (
2100
+ <div
2101
+ key={message.id}
2102
+ className={isUser ? "text-right" : "text-left"}
2103
+ >
2104
+ <div
2105
+ className={[
2106
+ "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!",
2107
+ isUser
2108
+ ? "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"
2109
+ : "block w-full bg-transparent text-neutral-900 dark:text-neutral-100",
2110
+ ].join(" ")}
2111
+ onClick={handleRenderedMarkdownClick}
2112
+ dangerouslySetInnerHTML={{
2113
+ __html: renderMarkdownToHtml(message.content, linkTarget),
2114
+ }}
2115
+ />
2116
+ </div>
2117
+ );
2118
+ })}
2119
+
2120
+ {isBusy && isAwaitingFirstToken ? (
2121
+ <div className="text-left">
2122
+ <span
2123
+ 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%]"
2124
+ style={{
2125
+ animation: "ask-ai-thinking-shimmer 3.4s linear infinite",
2126
+ }}
2127
+ >
2128
+ Thinking
2129
+ </span>
2130
+ </div>
2131
+ ) : null}
2132
+
2133
+ {errorMessage ? (
2134
+ <p className="text-sm text-red-600 dark:text-red-300">
2135
+ {errorMessage}
2136
+ </p>
2137
+ ) : null}
2138
+ </div>
2139
+
2140
+ <form
2141
+ className="absolute z-10 bottom-0 inset-x-0 px-3 pb-3"
2142
+ onSubmit={(event) => {
2143
+ void handleSubmit(event);
2144
+ }}
2145
+ >
2146
+ <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)]">
2147
+ <textarea
2148
+ ref={inputRef}
2149
+ value={input}
2150
+ onInput={(event) => {
2151
+ setInput((event.target as HTMLTextAreaElement).value);
2152
+ }}
2153
+ onKeyDown={handleChatInputKeyDown}
2154
+ placeholder="Ask a question..."
2155
+ 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"
2156
+ disabled={isBusy}
2157
+ rows={1}
2158
+ />
2159
+ <div className="p-1 pl-0 mt-auto">
2160
+ <button
2161
+ type="submit"
2162
+ className="assistant-embed-send-button flex items-center justify-center rounded-full size-9 disabled:opacity-90 hover:opacity-95 cursor-pointer"
2163
+ style={
2164
+ {
2165
+ "--assistant-embed-send-theme-light":
2166
+ launcherThemeColorLight,
2167
+ "--assistant-embed-send-theme-dark":
2168
+ launcherThemeColorDark,
2169
+ "--assistant-embed-send-foreground-light":
2170
+ launcherIconColorLight,
2171
+ "--assistant-embed-send-foreground-dark":
2172
+ launcherIconColorDark,
2173
+ } as JSX.CSSProperties
2174
+ }
2175
+ disabled={isBusy || input.trim().length === 0}
2176
+ aria-label="Send message"
2177
+ >
2178
+ <Icon
2179
+ icon="lucide:arrow-up"
2180
+ className="size-4.5 **:stroke-[2.2]"
2181
+ aria-hidden="true"
2182
+ />
2183
+ </button>
2184
+ </div>
2185
+ </div>
2186
+ </form>
2187
+ </>
2188
+ ) : (
2189
+ <div className="flex flex-1 items-center justify-center px-5 text-center">
2190
+ <div className="flex max-w-64 flex-col items-center gap-3">
2191
+ <AssistantPanelIcon
2192
+ color={launcherIconColor}
2193
+ imageSrc={launcherIconImageSrc}
2194
+ 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"
2195
+ />
2196
+ <p className="text-sm text-neutral-500 dark:text-neutral-300">
2197
+ {unavailableMessage}
2198
+ </p>
2199
+ <button
2200
+ type="button"
2201
+ 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"
2202
+ onClick={handleUnavailableBack}
2203
+ >
2204
+ <Icon
2205
+ icon="lucide:arrow-left"
2206
+ className="size-3.5"
2207
+ aria-hidden="true"
2208
+ />
2209
+ Back
2210
+ </button>
2211
+ </div>
2212
+ </div>
2213
+ )}
2214
+
2215
+ <style>{`
2216
+ @keyframes assistant-empty-state-enter {
2217
+ 0% {
2218
+ opacity: 0;
2219
+ filter: blur(8px);
2220
+ transform: translateY(12px);
2221
+ }
2222
+ 100% {
2223
+ opacity: 1;
2224
+ filter: blur(0);
2225
+ transform: translateY(0);
2226
+ }
2227
+ }
2228
+
2229
+ .assistant-empty-state-item {
2230
+ animation: assistant-empty-state-enter 840ms cubic-bezier(0.22, 1, 0.36, 1) both;
2231
+ animation-delay: var(--assistant-empty-state-delay, 0ms);
2232
+ will-change: opacity, filter, transform;
2233
+ }
2234
+
2235
+ @keyframes ask-ai-thinking-shimmer {
2236
+ 0% { background-position: 200% 0; }
2237
+ 100% { background-position: -200% 0; }
2238
+ }
2239
+
2240
+ .prose-rules pre[class*="language-"],
2241
+ .prose-rules code[class*="language-"] {
2242
+ font-family: var(--font-mono);
2243
+ font-size: 13px;
2244
+ line-height: inherit;
2245
+ }
2246
+
2247
+ .ask-ai-markdown pre {
2248
+ position: relative;
2249
+ width: 100%;
2250
+ max-width: 100%;
2251
+ min-width: 0;
2252
+ box-sizing: border-box;
2253
+ padding-top: 2.25rem;
2254
+ background: var(--color-neutral-50) !important;
2255
+ overflow-x: hidden;
2256
+ overflow-y: hidden;
2257
+ white-space: pre;
2258
+ }
2259
+
2260
+ .dark .ask-ai-markdown pre {
2261
+ background: var(--rd-code-surface) !important;
2262
+ border-color: var(--color-neutral-800) !important;
2263
+ }
2264
+
2265
+ .ask-ai-markdown pre[data-language]::before {
2266
+ content: "";
2267
+ position: absolute;
2268
+ top: 0.52rem;
2269
+ left: 0.75rem;
2270
+ width: 16px;
2271
+ height: 16px;
2272
+ background-color: var(--color-neutral-500);
2273
+ -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");
2274
+ 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");
2275
+ -webkit-mask-repeat: no-repeat;
2276
+ mask-repeat: no-repeat;
2277
+ -webkit-mask-position: center;
2278
+ mask-position: center;
2279
+ -webkit-mask-size: contain;
2280
+ mask-size: contain;
2281
+ pointer-events: none;
2282
+ user-select: none;
2283
+ }
2284
+
2285
+ .dark .ask-ai-markdown pre[data-language]::before {
2286
+ background-color: var(--color-neutral-400);
2287
+ }
2288
+
2289
+ .ask-ai-markdown pre[data-language]::after {
2290
+ content: attr(data-language);
2291
+ position: absolute;
2292
+ top: 0.6rem;
2293
+ left: 2.2rem;
2294
+ font-family: var(--font-sans);
2295
+ font-size: 14px;
2296
+ line-height: 1;
2297
+ letter-spacing: 0.01em;
2298
+ color: var(--color-neutral-500);
2299
+ pointer-events: none;
2300
+ user-select: none;
2301
+ }
2302
+
2303
+ .dark .ask-ai-markdown pre[data-language]::after {
2304
+ color: var(--color-neutral-400);
2305
+ }
2306
+
2307
+ .ask-ai-markdown .ask-ai-copy-code-button {
2308
+ position: absolute;
2309
+ top: 0.3rem;
2310
+ right: 0.46rem;
2311
+ z-index: 2;
2312
+ appearance: none;
2313
+ display: inline-flex;
2314
+ align-items: center;
2315
+ justify-content: center;
2316
+ width: 1.75rem;
2317
+ height: 1.75rem;
2318
+ border: 1px solid
2319
+ color-mix(in oklab, var(--color-neutral-200) 80%, transparent);
2320
+ background: color-mix(in oklab, #fff 80%, transparent);
2321
+ backdrop-filter: blur(4px);
2322
+ color: color-mix(
2323
+ in oklab,
2324
+ var(--color-neutral-500) 80%,
2325
+ transparent
2326
+ );
2327
+ border-radius: 0.375rem;
2328
+ outline: none;
2329
+ box-shadow: none;
2330
+ cursor: pointer;
2331
+ transition: background-color 150ms ease, color 150ms ease,
2332
+ border-color 150ms ease, transform 120ms ease;
2333
+ }
2334
+
2335
+ .dark .ask-ai-markdown .ask-ai-copy-code-button {
2336
+ border-color: color-mix(
2337
+ in oklab,
2338
+ var(--color-neutral-700) 50%,
2339
+ transparent
2340
+ );
2341
+ background: var(--rd-code-surface);
2342
+ color: var(--color-neutral-400);
2343
+ }
2344
+
2345
+ .ask-ai-markdown .ask-ai-copy-code-button:hover {
2346
+ background: var(--color-neutral-50);
2347
+ color: var(--color-neutral-600);
2348
+ }
2349
+
2350
+ .dark .ask-ai-markdown .ask-ai-copy-code-button:hover {
2351
+ background: var(--color-neutral-800);
2352
+ color: var(--color-neutral-200);
2353
+ }
2354
+
2355
+ .ask-ai-markdown .ask-ai-copy-code-button:active {
2356
+ transform: translateY(0.5px);
2357
+ }
2358
+
2359
+ .ask-ai-markdown .ask-ai-copy-code-button:focus-visible,
2360
+ .dark .ask-ai-markdown .ask-ai-copy-code-button:focus-visible {
2361
+ outline: none;
2362
+ box-shadow: none;
2363
+ }
2364
+
2365
+ .ask-ai-markdown .ask-ai-copy-icon,
2366
+ .ask-ai-markdown .ask-ai-copy-check-icon {
2367
+ position: absolute;
2368
+ width: 14px;
2369
+ height: 14px;
2370
+ transition: transform 250ms cubic-bezier(0.22, 1, 0.36, 1),
2371
+ opacity 250ms cubic-bezier(0.22, 1, 0.36, 1);
2372
+ will-change: transform, opacity;
2373
+ }
2374
+
2375
+ .ask-ai-markdown .ask-ai-copy-icon {
2376
+ opacity: 1;
2377
+ transform: scale(1) rotate(0deg);
2378
+ background-color: currentColor;
2379
+ -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");
2380
+ 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");
2381
+ -webkit-mask-repeat: no-repeat;
2382
+ mask-repeat: no-repeat;
2383
+ -webkit-mask-position: center;
2384
+ mask-position: center;
2385
+ -webkit-mask-size: contain;
2386
+ mask-size: contain;
2387
+ }
2388
+
2389
+ .ask-ai-markdown .ask-ai-copy-check-icon {
2390
+ opacity: 0;
2391
+ transform: scale(0.25) rotate(6deg);
2392
+ background-color: color-mix(
2393
+ in oklab,
2394
+ var(--color-green-700) 80%,
2395
+ transparent
2396
+ );
2397
+ -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");
2398
+ 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");
2399
+ -webkit-mask-repeat: no-repeat;
2400
+ mask-repeat: no-repeat;
2401
+ -webkit-mask-position: center;
2402
+ mask-position: center;
2403
+ -webkit-mask-size: contain;
2404
+ mask-size: contain;
2405
+ }
2406
+
2407
+ .dark .ask-ai-markdown .ask-ai-copy-check-icon {
2408
+ background-color: color-mix(
2409
+ in oklab,
2410
+ var(--color-green-400) 90%,
2411
+ transparent
2412
+ );
2413
+ }
2414
+
2415
+ .ask-ai-markdown .ask-ai-copy-code-button[data-copied="true"] .ask-ai-copy-icon {
2416
+ opacity: 0;
2417
+ transform: scale(0.5) rotate(-6deg);
2418
+ }
2419
+
2420
+ .ask-ai-markdown .ask-ai-copy-code-button[data-copied="true"] .ask-ai-copy-check-icon {
2421
+ opacity: 1;
2422
+ transform: scale(1.1) rotate(0deg);
2423
+ }
2424
+
2425
+ .ask-ai-markdown pre > code {
2426
+ display: block;
2427
+ width: 100%;
2428
+ min-width: 100%;
2429
+ background: transparent !important;
2430
+ white-space: inherit;
2431
+ word-break: normal;
2432
+ overflow-wrap: normal;
2433
+ overflow-x: auto;
2434
+ overflow-y: hidden;
2435
+ -webkit-overflow-scrolling: touch;
2436
+ scrollbar-width: none;
2437
+ -ms-overflow-style: none;
2438
+ }
2439
+
2440
+ .dark .ask-ai-markdown pre > code {
2441
+ scrollbar-width: none;
2442
+ }
2443
+
2444
+ .ask-ai-markdown pre[class*="language-"] {
2445
+ background: var(--color-neutral-50) !important;
2446
+ }
2447
+
2448
+ .ask-ai-markdown pre[class*="language-"] > code[class*="language-"] {
2449
+ background: transparent !important;
2450
+ }
2451
+
2452
+ .ask-ai-markdown :not(pre) > code[class*="language-"] {
2453
+ background: var(--color-neutral-100);
2454
+ }
2455
+
2456
+ .dark .ask-ai-markdown pre[class*="language-"] {
2457
+ background: var(--rd-code-surface) !important;
2458
+ border-color: var(--color-neutral-800) !important;
2459
+ }
2460
+
2461
+ .dark .ask-ai-markdown pre[class*="language-"] > code[class*="language-"] {
2462
+ background: transparent !important;
2463
+ }
2464
+
2465
+ .dark .ask-ai-markdown pre[class*="language-"],
2466
+ .dark .ask-ai-markdown code[class*="language-"] {
2467
+ color: var(--color-neutral-100);
2468
+ }
2469
+
2470
+ .dark .ask-ai-markdown :not(pre) > code[class*="language-"] {
2471
+ background: var(--color-neutral-800);
2472
+ }
2473
+
2474
+ .dark .ask-ai-markdown .token.comment,
2475
+ .dark .ask-ai-markdown .token.prolog,
2476
+ .dark .ask-ai-markdown .token.doctype,
2477
+ .dark .ask-ai-markdown .token.cdata {
2478
+ color: #7f848e;
2479
+ }
2480
+
2481
+ .dark .ask-ai-markdown .token.punctuation {
2482
+ color: #abb2bf;
2483
+ }
2484
+
2485
+ .dark .ask-ai-markdown .token.property,
2486
+ .dark .ask-ai-markdown .token.tag,
2487
+ .dark .ask-ai-markdown .token.boolean,
2488
+ .dark .ask-ai-markdown .token.number,
2489
+ .dark .ask-ai-markdown .token.constant,
2490
+ .dark .ask-ai-markdown .token.symbol,
2491
+ .dark .ask-ai-markdown .token.deleted {
2492
+ color: #d19a66;
2493
+ }
2494
+
2495
+ .dark .ask-ai-markdown .token.selector,
2496
+ .dark .ask-ai-markdown .token.attr-name,
2497
+ .dark .ask-ai-markdown .token.string,
2498
+ .dark .ask-ai-markdown .token.char,
2499
+ .dark .ask-ai-markdown .token.builtin,
2500
+ .dark .ask-ai-markdown .token.inserted {
2501
+ color: #98c379;
2502
+ }
2503
+
2504
+ .dark .ask-ai-markdown .token.operator,
2505
+ .dark .ask-ai-markdown .token.entity,
2506
+ .dark .ask-ai-markdown .token.url,
2507
+ .dark .ask-ai-markdown .language-css .token.string,
2508
+ .dark .ask-ai-markdown .style .token.string {
2509
+ color: #56b6c2;
2510
+ }
2511
+
2512
+ .dark .ask-ai-markdown .token.atrule,
2513
+ .dark .ask-ai-markdown .token.attr-value,
2514
+ .dark .ask-ai-markdown .token.keyword {
2515
+ color: #c678dd;
2516
+ }
2517
+
2518
+ .dark .ask-ai-markdown .token.function,
2519
+ .dark .ask-ai-markdown .token.class-name {
2520
+ color: #e5c07b;
2521
+ }
2522
+
2523
+ .dark .ask-ai-markdown .token.regex,
2524
+ .dark .ask-ai-markdown .token.important,
2525
+ .dark .ask-ai-markdown .token.variable {
2526
+ color: #e06c75;
2527
+ }
2528
+
2529
+ .dark .ask-ai-markdown .token.attr-value > .token.punctuation.attr-equals,
2530
+ .dark
2531
+ .ask-ai-markdown
2532
+ .token.special-attr
2533
+ > .token.attr-value
2534
+ > .token.value.css {
2535
+ color: #abb2bf;
2536
+ }
2537
+
2538
+ .dark .ask-ai-markdown .language-css .token.selector {
2539
+ color: #d19a66;
2540
+ }
2541
+
2542
+ .dark .ask-ai-markdown .language-css .token.property {
2543
+ color: #abb2bf;
2544
+ }
2545
+
2546
+ .dark .ask-ai-markdown .language-css .token.function,
2547
+ .dark .ask-ai-markdown .language-css .token.url > .token.function {
2548
+ color: #56b6c2;
2549
+ }
2550
+
2551
+ .dark .ask-ai-markdown .language-css .token.url > .token.string.url {
2552
+ color: #98c379;
2553
+ }
2554
+
2555
+ .dark .ask-ai-markdown .language-css .token.important,
2556
+ .dark .ask-ai-markdown .language-css .token.atrule .token.rule {
2557
+ color: #c678dd;
2558
+ }
2559
+
2560
+ .dark .ask-ai-markdown .language-javascript .token.operator {
2561
+ color: #c678dd;
2562
+ }
2563
+
2564
+ .dark
2565
+ .ask-ai-markdown
2566
+ .language-javascript
2567
+ .token.template-string
2568
+ > .token.interpolation
2569
+ > .token.interpolation-punctuation.punctuation {
2570
+ color: #e06c75;
2571
+ }
2572
+
2573
+ .dark .ask-ai-markdown .language-json .token.operator {
2574
+ color: #abb2bf;
2575
+ }
2576
+
2577
+ .dark .ask-ai-markdown .language-json .token.null.keyword {
2578
+ color: #d19a66;
2579
+ }
2580
+
2581
+ .dark .ask-ai-markdown .language-markdown .token.url,
2582
+ .dark .ask-ai-markdown .language-markdown .token.url > .token.operator,
2583
+ .dark
2584
+ .ask-ai-markdown
2585
+ .language-markdown
2586
+ .token.url-reference.url
2587
+ > .token.string {
2588
+ color: #abb2bf;
2589
+ }
2590
+
2591
+ .dark .ask-ai-markdown .language-markdown .token.url > .token.content {
2592
+ color: #56b6c2;
2593
+ }
2594
+
2595
+ .dark .ask-ai-markdown .language-markdown .token.url > .token.url,
2596
+ .dark .ask-ai-markdown .language-markdown .token.url-reference.url {
2597
+ color: #98c379;
2598
+ }
2599
+
2600
+ .dark .ask-ai-markdown .language-markdown .token.blockquote.punctuation,
2601
+ .dark .ask-ai-markdown .language-markdown .token.hr.punctuation {
2602
+ color: #7f848e;
2603
+ }
2604
+
2605
+ .dark .ask-ai-markdown .language-markdown .token.code-snippet {
2606
+ color: #98c379;
2607
+ }
2608
+
2609
+ .dark .ask-ai-markdown .language-markdown .token.bold .token.content {
2610
+ color: #e5c07b;
2611
+ }
2612
+
2613
+ .dark .ask-ai-markdown .language-markdown .token.italic .token.content {
2614
+ color: #c678dd;
2615
+ }
2616
+
2617
+ .dark .ask-ai-markdown .language-markdown .token.strike .token.content,
2618
+ .dark
2619
+ .ask-ai-markdown
2620
+ .language-markdown
2621
+ .token.strike
2622
+ .token.punctuation,
2623
+ .dark .ask-ai-markdown .language-markdown .token.list.punctuation,
2624
+ .dark
2625
+ .ask-ai-markdown
2626
+ .language-markdown
2627
+ .token.title.important
2628
+ > .token.punctuation {
2629
+ color: #e06c75;
2630
+ }
2631
+
2632
+ .ask-ai-markdown pre > code::-webkit-scrollbar {
2633
+ width: 0;
2634
+ height: 0;
2635
+ display: none;
2636
+ }
2637
+
2638
+ .assistant-embed-send-button {
2639
+ --assistant-embed-send-theme: var(
2640
+ --assistant-embed-send-theme-light
2641
+ );
2642
+ --assistant-embed-send-foreground: var(
2643
+ --assistant-embed-send-foreground-light
2644
+ );
2645
+ color: var(--assistant-embed-send-foreground);
2646
+ background: linear-gradient(
2647
+ to bottom,
2648
+ color-mix(
2649
+ in oklab,
2650
+ var(--assistant-embed-send-theme) 88%,
2651
+ white
2652
+ ),
2653
+ color-mix(
2654
+ in oklab,
2655
+ var(--assistant-embed-send-theme) 90%,
2656
+ black
2657
+ )
2658
+ );
2659
+ }
2660
+
2661
+ .dark .assistant-embed-send-button,
2662
+ [data-theme="dark"] .assistant-embed-send-button {
2663
+ --assistant-embed-send-theme: var(
2664
+ --assistant-embed-send-theme-dark
2665
+ );
2666
+ --assistant-embed-send-foreground: var(
2667
+ --assistant-embed-send-foreground-dark
2668
+ );
2669
+ }
2670
+
2671
+ @media (prefers-reduced-motion: reduce) {
2672
+ .assistant-empty-state-item {
2673
+ animation: none;
2674
+ }
2675
+ }
2676
+ `}</style>
2677
+ </div>
2678
+ );
2679
+ }