radiant-docs 0.1.41 → 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 +1 -0
- package/template/src/components/Header.astro +150 -16
- 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.tsx +221 -8
- package/template/src/components/chat/AssistantEmbedPanel.tsx +1090 -104
- 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 +2 -2
- 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 +2 -4
- package/template/src/lib/assistant-chrome-defaults.ts +12 -0
- package/template/src/lib/assistant-embed-script.ts +209 -18
- package/template/src/lib/validation.ts +325 -75
- package/template/src/styles/global.css +81 -4
- package/template/src/components/chat/AskAiWidget.tsx +0 -2011
|
@@ -28,8 +28,10 @@ import remarkParse from "remark-parse";
|
|
|
28
28
|
import remarkGfm from "remark-gfm";
|
|
29
29
|
import remarkRehype from "remark-rehype";
|
|
30
30
|
import rehypeStringify from "rehype-stringify";
|
|
31
|
+
import { getDocsBasePath, withBasePath } from "../../lib/base-path";
|
|
31
32
|
|
|
32
33
|
type AssistantLinkTarget = "current" | "blank";
|
|
34
|
+
export type AssistantPanelSize = "default" | "expanded";
|
|
33
35
|
|
|
34
36
|
type HastNode = {
|
|
35
37
|
type?: string;
|
|
@@ -56,7 +58,11 @@ type AssistantEmbedPanelProps = {
|
|
|
56
58
|
linkTarget?: AssistantLinkTarget;
|
|
57
59
|
allowApiPathQueryOverride?: boolean;
|
|
58
60
|
openSignal?: number;
|
|
61
|
+
panelSize?: AssistantPanelSize;
|
|
62
|
+
onRequestOpen?: () => void;
|
|
59
63
|
onRequestClose?: () => void;
|
|
64
|
+
onRequestPanelSizeToggle?: (size: AssistantPanelSize) => void;
|
|
65
|
+
onCurrentLinkNavigate?: (href: string, sourceElement?: Element) => void;
|
|
60
66
|
};
|
|
61
67
|
|
|
62
68
|
type AssistantColorByMode = {
|
|
@@ -77,8 +83,42 @@ type AssistantStreamEvent = {
|
|
|
77
83
|
|
|
78
84
|
type ChatInputKeyEvent = JSX.TargetedKeyboardEvent<HTMLTextAreaElement>;
|
|
79
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
|
+
};
|
|
80
112
|
|
|
81
|
-
const
|
|
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;
|
|
82
122
|
const MARKDOWN_HTML_CACHE_LIMIT = 300;
|
|
83
123
|
const markdownHtmlCache = new Map<string, string>();
|
|
84
124
|
const PRISM_LANGUAGE_ALIAS: Record<string, string> = {
|
|
@@ -113,6 +153,8 @@ const PRISM_LANGUAGE_LABEL: Record<string, string> = {
|
|
|
113
153
|
mdx: "MDX",
|
|
114
154
|
};
|
|
115
155
|
|
|
156
|
+
const EXTERNAL_PROTOCOL_REGEX = /^[a-zA-Z][a-zA-Z\d+\-.]*:/;
|
|
157
|
+
|
|
116
158
|
function normalizeRelTokens(value: unknown): string[] {
|
|
117
159
|
if (Array.isArray(value)) {
|
|
118
160
|
return value
|
|
@@ -138,6 +180,154 @@ function visitHastNodes(
|
|
|
138
180
|
}
|
|
139
181
|
}
|
|
140
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
|
+
|
|
141
331
|
const rehypeOpenLinksInNewTab: Plugin<[], HastNode> = () => {
|
|
142
332
|
return (tree) => {
|
|
143
333
|
visitHastNodes(tree, (node) => {
|
|
@@ -166,41 +356,212 @@ function createMessageId(): string {
|
|
|
166
356
|
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
167
357
|
}
|
|
168
358
|
|
|
169
|
-
function
|
|
170
|
-
if (
|
|
359
|
+
function normalizePersistedMessages(rawMessages: unknown): ChatMessage[] {
|
|
360
|
+
if (!Array.isArray(rawMessages)) {
|
|
171
361
|
return [];
|
|
172
362
|
}
|
|
173
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
|
+
|
|
174
441
|
try {
|
|
175
|
-
const
|
|
176
|
-
if (
|
|
177
|
-
return
|
|
442
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
443
|
+
if (Array.isArray(parsed)) {
|
|
444
|
+
return {
|
|
445
|
+
...createEmptyPersistedPanelState(),
|
|
446
|
+
messages: normalizePersistedMessages(parsed),
|
|
447
|
+
};
|
|
178
448
|
}
|
|
179
449
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
+
}
|
|
183
556
|
}
|
|
184
557
|
|
|
185
|
-
|
|
186
|
-
.
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
const candidate = message as Partial<ChatMessage>;
|
|
191
|
-
return (
|
|
192
|
-
(candidate.role === "user" || candidate.role === "assistant") &&
|
|
193
|
-
typeof candidate.id === "string" &&
|
|
194
|
-
typeof candidate.content === "string"
|
|
195
|
-
);
|
|
196
|
-
})
|
|
197
|
-
.map((message) => ({
|
|
198
|
-
id: message.id,
|
|
199
|
-
role: message.role,
|
|
200
|
-
content: message.content,
|
|
201
|
-
}));
|
|
558
|
+
window.history.replaceState(
|
|
559
|
+
window.history.state,
|
|
560
|
+
"",
|
|
561
|
+
`${url.pathname}${url.search}${url.hash}`,
|
|
562
|
+
);
|
|
202
563
|
} catch {
|
|
203
|
-
|
|
564
|
+
// Leave the URL alone if parsing fails.
|
|
204
565
|
}
|
|
205
566
|
}
|
|
206
567
|
|
|
@@ -458,7 +819,8 @@ function renderMarkdownToHtml(
|
|
|
458
819
|
const processor = unified()
|
|
459
820
|
.use(remarkParse)
|
|
460
821
|
.use(remarkGfm)
|
|
461
|
-
.use(remarkRehype, { allowDangerousHtml: false })
|
|
822
|
+
.use(remarkRehype, { allowDangerousHtml: false })
|
|
823
|
+
.use(rehypeRebaseInternalLinks);
|
|
462
824
|
|
|
463
825
|
if (linkTarget === "blank") {
|
|
464
826
|
processor.use(rehypeOpenLinksInNewTab);
|
|
@@ -504,12 +866,15 @@ function extractErrorMessage(rawBody: string): string {
|
|
|
504
866
|
return rawBody.trim();
|
|
505
867
|
}
|
|
506
868
|
|
|
507
|
-
function postParentMessage(
|
|
869
|
+
function postParentMessage(
|
|
870
|
+
type: string,
|
|
871
|
+
payload: Record<string, unknown> = {},
|
|
872
|
+
) {
|
|
508
873
|
if (typeof window === "undefined") {
|
|
509
874
|
return;
|
|
510
875
|
}
|
|
511
876
|
|
|
512
|
-
window.parent.postMessage({ type }, "*");
|
|
877
|
+
window.parent.postMessage({ type, ...payload }, "*");
|
|
513
878
|
}
|
|
514
879
|
|
|
515
880
|
function getApiPath(
|
|
@@ -578,20 +943,50 @@ export default function AssistantEmbedPanel({
|
|
|
578
943
|
linkTarget = "current",
|
|
579
944
|
allowApiPathQueryOverride = true,
|
|
580
945
|
openSignal = 0,
|
|
946
|
+
panelSize,
|
|
947
|
+
onRequestOpen,
|
|
581
948
|
onRequestClose,
|
|
949
|
+
onRequestPanelSizeToggle,
|
|
950
|
+
onCurrentLinkNavigate,
|
|
582
951
|
}: AssistantEmbedPanelProps) {
|
|
952
|
+
const [initialPanelState] = useState<PersistedPanelState>(() =>
|
|
953
|
+
canSendChatRequest
|
|
954
|
+
? readPersistedPanelState()
|
|
955
|
+
: createEmptyPersistedPanelState(),
|
|
956
|
+
);
|
|
583
957
|
const [messages, setMessages] = useState<ChatMessage[]>(
|
|
584
|
-
|
|
958
|
+
initialPanelState.messages,
|
|
585
959
|
);
|
|
586
960
|
const [input, setInput] = useState("");
|
|
587
|
-
const [isBusy, setIsBusy] = useState(
|
|
588
|
-
const [isAwaitingFirstToken, setIsAwaitingFirstToken] = useState(
|
|
961
|
+
const [isBusy, setIsBusy] = useState(initialPanelState.inFlight);
|
|
962
|
+
const [isAwaitingFirstToken, setIsAwaitingFirstToken] = useState(
|
|
963
|
+
initialPanelState.isAwaitingFirstToken,
|
|
964
|
+
);
|
|
589
965
|
const [errorMessage, setErrorMessage] = useState("");
|
|
590
966
|
const [showUnavailableState, setShowUnavailableState] =
|
|
591
967
|
useState(!isChatAvailable);
|
|
592
968
|
const [emptyStateAnimationKey, setEmptyStateAnimationKey] = useState(0);
|
|
969
|
+
const [localPanelSize, setLocalPanelSize] = useState<AssistantPanelSize>(
|
|
970
|
+
panelSize ?? initialPanelState.panelSize,
|
|
971
|
+
);
|
|
972
|
+
const [isShellFullscreen, setIsShellFullscreen] = useState(false);
|
|
593
973
|
const activeRequestAbortRef = useRef<AbortController | null>(null);
|
|
594
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);
|
|
595
990
|
const inputRef = useRef<HTMLTextAreaElement | null>(null);
|
|
596
991
|
const resolvedApiPathRef = useRef(
|
|
597
992
|
getApiPath(apiPath, allowApiPathQueryOverride),
|
|
@@ -608,33 +1003,500 @@ export default function AssistantEmbedPanel({
|
|
|
608
1003
|
launcherIconColors?.light ?? launcherIconColor ?? "#ffffff";
|
|
609
1004
|
const launcherIconColorDark =
|
|
610
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
|
+
};
|
|
611
1267
|
|
|
612
1268
|
useEffect(() => {
|
|
613
1269
|
return () => {
|
|
614
1270
|
activeRequestAbortRef.current?.abort();
|
|
615
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
|
+
}
|
|
616
1282
|
};
|
|
617
1283
|
}, []);
|
|
618
1284
|
|
|
1285
|
+
useEffect(() => {
|
|
1286
|
+
messagesRef.current = messages;
|
|
1287
|
+
if (skipNextMessagesPersistRef.current) {
|
|
1288
|
+
skipNextMessagesPersistRef.current = false;
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
persistPanelState(messages);
|
|
1292
|
+
}, [messages]);
|
|
1293
|
+
|
|
619
1294
|
useEffect(() => {
|
|
620
1295
|
if (typeof window === "undefined") {
|
|
621
1296
|
return;
|
|
622
1297
|
}
|
|
623
1298
|
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
} catch {
|
|
627
|
-
// Ignore storage failures in private mode or constrained embeds.
|
|
1299
|
+
if (!isBusy || !activeRequestAbortRef.current) {
|
|
1300
|
+
return;
|
|
628
1301
|
}
|
|
629
|
-
|
|
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]);
|
|
630
1315
|
|
|
631
1316
|
useEffect(() => {
|
|
632
|
-
|
|
633
|
-
if (!viewport) {
|
|
1317
|
+
if (typeof window === "undefined") {
|
|
634
1318
|
return;
|
|
635
1319
|
}
|
|
636
|
-
|
|
637
|
-
|
|
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
|
+
}, []);
|
|
638
1500
|
|
|
639
1501
|
const resizeChatInputTextarea = () => {
|
|
640
1502
|
const textarea = inputRef.current;
|
|
@@ -669,21 +1531,50 @@ export default function AssistantEmbedPanel({
|
|
|
669
1531
|
resizeChatInputTextarea();
|
|
670
1532
|
}, [input]);
|
|
671
1533
|
|
|
1534
|
+
useEffect(() => {
|
|
1535
|
+
if (panelSize) {
|
|
1536
|
+
setSharedPanelSize(panelSize, false);
|
|
1537
|
+
}
|
|
1538
|
+
}, [panelSize]);
|
|
1539
|
+
|
|
1540
|
+
useEffect(() => {
|
|
1541
|
+
notifyPanelSizeChange(resolvedPanelSize);
|
|
1542
|
+
}, [resolvedPanelSize]);
|
|
1543
|
+
|
|
672
1544
|
useEffect(() => {
|
|
673
1545
|
if (typeof window === "undefined") {
|
|
674
1546
|
return;
|
|
675
1547
|
}
|
|
676
1548
|
|
|
677
1549
|
const handlePanelMessage = (event: MessageEvent) => {
|
|
678
|
-
if (
|
|
679
|
-
!event.data ||
|
|
680
|
-
typeof event.data !== "object" ||
|
|
681
|
-
event.data.type !== "assistant-embed:panel-opened"
|
|
682
|
-
) {
|
|
1550
|
+
if (!event.data || typeof event.data !== "object") {
|
|
683
1551
|
return;
|
|
684
1552
|
}
|
|
685
1553
|
|
|
686
|
-
|
|
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
|
+
}
|
|
687
1578
|
};
|
|
688
1579
|
|
|
689
1580
|
window.addEventListener("message", handlePanelMessage);
|
|
@@ -694,15 +1585,17 @@ export default function AssistantEmbedPanel({
|
|
|
694
1585
|
|
|
695
1586
|
useEffect(() => {
|
|
696
1587
|
setEmptyStateAnimationKey((previous) => previous + 1);
|
|
1588
|
+
queueSavedScrollRestore();
|
|
697
1589
|
}, [openSignal]);
|
|
698
1590
|
|
|
699
1591
|
const handleStartNewChat = () => {
|
|
700
1592
|
activeRequestAbortRef.current?.abort();
|
|
701
1593
|
activeRequestAbortRef.current = null;
|
|
1594
|
+
setSharedBusy(false);
|
|
1595
|
+
setSharedAwaitingFirstToken(false);
|
|
1596
|
+
resetThreadScrollPosition([]);
|
|
702
1597
|
setMessages([]);
|
|
703
1598
|
setInput("");
|
|
704
|
-
setIsBusy(false);
|
|
705
|
-
setIsAwaitingFirstToken(false);
|
|
706
1599
|
setErrorMessage("");
|
|
707
1600
|
setShowUnavailableState(false);
|
|
708
1601
|
};
|
|
@@ -716,13 +1609,20 @@ export default function AssistantEmbedPanel({
|
|
|
716
1609
|
postParentMessage("assistant-embed:close");
|
|
717
1610
|
};
|
|
718
1611
|
|
|
1612
|
+
const handlePanelSizeToggle = () => {
|
|
1613
|
+
const nextPanelSize =
|
|
1614
|
+
resolvedPanelSize === "expanded" ? "default" : "expanded";
|
|
1615
|
+
setSharedPanelSize(nextPanelSize);
|
|
1616
|
+
};
|
|
1617
|
+
|
|
719
1618
|
const handleUnavailableBack = () => {
|
|
720
1619
|
activeRequestAbortRef.current?.abort();
|
|
721
1620
|
activeRequestAbortRef.current = null;
|
|
1621
|
+
setSharedBusy(false);
|
|
1622
|
+
setSharedAwaitingFirstToken(false);
|
|
1623
|
+
resetThreadScrollPosition([]);
|
|
722
1624
|
setMessages([]);
|
|
723
1625
|
setInput("");
|
|
724
|
-
setIsBusy(false);
|
|
725
|
-
setIsAwaitingFirstToken(false);
|
|
726
1626
|
setErrorMessage("");
|
|
727
1627
|
setShowUnavailableState(false);
|
|
728
1628
|
};
|
|
@@ -739,8 +1639,8 @@ export default function AssistantEmbedPanel({
|
|
|
739
1639
|
}
|
|
740
1640
|
|
|
741
1641
|
setErrorMessage("");
|
|
742
|
-
|
|
743
|
-
|
|
1642
|
+
setSharedBusy(true);
|
|
1643
|
+
setSharedAwaitingFirstToken(true);
|
|
744
1644
|
|
|
745
1645
|
const userMessage: ChatMessage = {
|
|
746
1646
|
id: createMessageId(),
|
|
@@ -752,6 +1652,7 @@ export default function AssistantEmbedPanel({
|
|
|
752
1652
|
|
|
753
1653
|
setInput("");
|
|
754
1654
|
setMessages(nextConversation);
|
|
1655
|
+
queueThreadScrollToBottom(nextConversation);
|
|
755
1656
|
|
|
756
1657
|
activeRequestAbortRef.current?.abort();
|
|
757
1658
|
const abortController = new AbortController();
|
|
@@ -842,7 +1743,7 @@ export default function AssistantEmbedPanel({
|
|
|
842
1743
|
) {
|
|
843
1744
|
if (!hasReceivedFirstTextDelta) {
|
|
844
1745
|
hasReceivedFirstTextDelta = true;
|
|
845
|
-
|
|
1746
|
+
setSharedAwaitingFirstToken(false);
|
|
846
1747
|
}
|
|
847
1748
|
|
|
848
1749
|
setMessages((previous) => {
|
|
@@ -885,8 +1786,9 @@ export default function AssistantEmbedPanel({
|
|
|
885
1786
|
if (activeRequestAbortRef.current === abortController) {
|
|
886
1787
|
activeRequestAbortRef.current = null;
|
|
887
1788
|
}
|
|
888
|
-
|
|
889
|
-
|
|
1789
|
+
setSharedBusy(false);
|
|
1790
|
+
setSharedAwaitingFirstToken(false);
|
|
1791
|
+
persistPanelState();
|
|
890
1792
|
}
|
|
891
1793
|
};
|
|
892
1794
|
|
|
@@ -915,39 +1817,73 @@ export default function AssistantEmbedPanel({
|
|
|
915
1817
|
".ask-ai-copy-code-button",
|
|
916
1818
|
) as HTMLButtonElement | null;
|
|
917
1819
|
|
|
918
|
-
if (
|
|
919
|
-
|
|
920
|
-
|
|
1820
|
+
if (copyButton) {
|
|
1821
|
+
event.preventDefault();
|
|
1822
|
+
event.stopPropagation();
|
|
921
1823
|
|
|
922
|
-
|
|
923
|
-
|
|
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
|
+
}
|
|
924
1833
|
|
|
925
|
-
|
|
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
|
+
})();
|
|
926
1850
|
return;
|
|
927
1851
|
}
|
|
928
1852
|
|
|
929
|
-
const
|
|
930
|
-
|
|
931
|
-
|
|
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
|
+
}
|
|
932
1868
|
return;
|
|
933
1869
|
}
|
|
934
1870
|
|
|
935
|
-
const
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
} else {
|
|
943
|
-
delete copyButton.dataset.copied;
|
|
944
|
-
}
|
|
1871
|
+
const navigationRequest =
|
|
1872
|
+
linkTarget === "current" && onCurrentLinkNavigate
|
|
1873
|
+
? getSameOriginNavigationRequest(event)
|
|
1874
|
+
: null;
|
|
1875
|
+
if (!navigationRequest) {
|
|
1876
|
+
return;
|
|
1877
|
+
}
|
|
945
1878
|
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
1879
|
+
event.preventDefault();
|
|
1880
|
+
event.stopPropagation();
|
|
1881
|
+
snapshotThreadScrollPosition();
|
|
1882
|
+
lockScrollPersistence();
|
|
1883
|
+
onCurrentLinkNavigate(
|
|
1884
|
+
navigationRequest.href,
|
|
1885
|
+
navigationRequest.sourceElement,
|
|
1886
|
+
);
|
|
951
1887
|
};
|
|
952
1888
|
|
|
953
1889
|
const handleThreadViewportWheel = (event: ChatViewportWheelEvent) => {
|
|
@@ -993,6 +1929,26 @@ export default function AssistantEmbedPanel({
|
|
|
993
1929
|
}
|
|
994
1930
|
};
|
|
995
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
|
+
|
|
996
1952
|
const panelClassName = [
|
|
997
1953
|
"relative flex min-h-0 flex-col overflow-hidden text-neutral-900 shadow-2xl dark:text-neutral-50",
|
|
998
1954
|
panelSurface === "inline"
|
|
@@ -1002,8 +1958,8 @@ export default function AssistantEmbedPanel({
|
|
|
1002
1958
|
|
|
1003
1959
|
return (
|
|
1004
1960
|
<div className={panelClassName}>
|
|
1005
|
-
<header className="flex items-center justify-between gap-
|
|
1006
|
-
<div className="flex min-w-0 items-center gap-2
|
|
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">
|
|
1007
1963
|
<AssistantPanelIcon
|
|
1008
1964
|
color={launcherIconColor}
|
|
1009
1965
|
imageSrc={launcherIconImageSrc}
|
|
@@ -1017,32 +1973,61 @@ export default function AssistantEmbedPanel({
|
|
|
1017
1973
|
Assistant
|
|
1018
1974
|
</p>
|
|
1019
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}
|
|
1020
1992
|
</div>
|
|
1021
|
-
|
|
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}
|
|
1022
2021
|
<button
|
|
1023
2022
|
type="button"
|
|
1024
|
-
className="
|
|
1025
|
-
onClick={
|
|
1026
|
-
aria-label="
|
|
1027
|
-
title="
|
|
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"
|
|
1028
2027
|
>
|
|
1029
|
-
<Icon
|
|
1030
|
-
icon="lucide:trash-2"
|
|
1031
|
-
className="size-3.5 -ml-px"
|
|
1032
|
-
aria-hidden="true"
|
|
1033
|
-
/>
|
|
1034
|
-
Clear
|
|
2028
|
+
<Icon icon="lucide:x" className="size-5" aria-hidden="true" />
|
|
1035
2029
|
</button>
|
|
1036
|
-
|
|
1037
|
-
<button
|
|
1038
|
-
type="button"
|
|
1039
|
-
className="inline-flex items-center justify-center gap-1 rounded-md size-9 text-[13px] text-neutral-500 hover:bg-neutral-900/5 dark:text-neutral-300 dark:hover:bg-white/10 transition cursor-pointer"
|
|
1040
|
-
onClick={handleRequestClose}
|
|
1041
|
-
aria-label="Close chat"
|
|
1042
|
-
title="Close"
|
|
1043
|
-
>
|
|
1044
|
-
<Icon icon="lucide:x" className="size-5" aria-hidden="true" />
|
|
1045
|
-
</button>
|
|
2030
|
+
</div>
|
|
1046
2031
|
</header>
|
|
1047
2032
|
|
|
1048
2033
|
{!showUnavailableState ? (
|
|
@@ -1050,6 +2035,7 @@ export default function AssistantEmbedPanel({
|
|
|
1050
2035
|
<div
|
|
1051
2036
|
ref={scrollViewportRef}
|
|
1052
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}
|
|
1053
2039
|
onWheel={handleThreadViewportWheel}
|
|
1054
2040
|
>
|
|
1055
2041
|
{messages.length === 0 ? (
|