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.
- package/package.json +1 -1
- package/template/astro.config.mjs +42 -40
- package/template/package-lock.json +7 -0
- package/template/package.json +3 -2
- package/template/public/favicon.svg +16 -8
- package/template/scripts/remove-assistant-for-non-pro.mjs +28 -0
- package/template/src/components/Header.astro +151 -17
- package/template/src/components/MdxPage.astro +76 -22
- package/template/src/components/PagePagination.astro +44 -8
- package/template/src/components/Sidebar.astro +10 -1
- package/template/src/components/TableOfContents.astro +159 -53
- package/template/src/components/chat/AssistantDocsWidget.astro +16 -0
- package/template/src/components/chat/AssistantDocsWidget.tsx +615 -0
- package/template/src/components/chat/AssistantEmbedPanel.tsx +2679 -0
- package/template/src/components/chat/AssistantEmbedPanelPage.astro +95 -0
- package/template/src/components/user/Accordion.astro +2 -2
- package/template/src/components/user/AccordionGroup.astro +1 -1
- package/template/src/components/user/Callout.astro +10 -4
- package/template/src/components/user/Card.astro +488 -0
- package/template/src/components/user/CardGradient.astro +964 -0
- package/template/src/components/user/CodeBlock.astro +1 -1
- package/template/src/components/user/CodeGroup.astro +1 -1
- package/template/src/components/user/Column.astro +25 -0
- package/template/src/components/user/Columns.astro +200 -0
- package/template/src/components/user/ComponentPreviewBlock.astro +1 -1
- package/template/src/components/user/Image.astro +1 -1
- package/template/src/components/user/Step.astro +1 -1
- package/template/src/components/user/Steps.astro +1 -1
- package/template/src/components/user/Tab.astro +1 -3
- package/template/src/components/user/Tabs.astro +2 -2
- package/template/src/layouts/Layout.astro +13 -156
- package/template/src/lib/assistant-chrome-defaults.ts +86 -0
- package/template/src/lib/assistant-chrome.ts +39 -0
- package/template/src/lib/assistant-embed-script.ts +1088 -0
- package/template/src/lib/assistant-panel-config.ts +80 -0
- package/template/src/lib/favicon.ts +31 -0
- package/template/src/lib/theme-css.ts +176 -0
- package/template/src/lib/validation.ts +668 -41
- package/template/src/pages/-/assistant/embed.js.ts +15 -0
- package/template/src/pages/-/assistant/panel.astro +5 -0
- package/template/src/pages/404.astro +4 -4
- package/template/src/styles/global.css +81 -4
- 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("&", "&")
|
|
571
|
+
.replaceAll("<", "<")
|
|
572
|
+
.replaceAll(">", ">")
|
|
573
|
+
.replaceAll('"', """)
|
|
574
|
+
.replaceAll("'", "'");
|
|
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
|
+
}
|