@dbx-tools/ui-mastra 0.6.218 → 0.6.219
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/README.md +114 -21
- package/index.ts +10 -9
- package/package.json +8 -8
- package/src/react/bubbles.tsx +4 -1
- package/src/react/chat-approvals.ts +9 -4
- package/src/react/chat-composer.tsx +80 -73
- package/src/react/chat-feedback.ts +2 -2
- package/src/react/chat-history.ts +79 -10
- package/src/react/chat-stream.ts +29 -59
- package/src/react/chat-thread-layout.tsx +17 -1
- package/src/react/chat-view.tsx +10 -0
- package/src/react/export-menu.tsx +8 -1
- package/src/react/index.ts +18 -2
- package/src/react/mastra-assistant.tsx +509 -0
- package/src/react/mastra-chat.tsx +120 -72
- package/src/react/thread-sidebar.tsx +33 -43
- package/src/react/thread-tabs.tsx +141 -117
- package/src/react/tool-pill.tsx +24 -1
- package/src/react/types.ts +32 -22
- package/src/support/mastra-client.ts +203 -171
- package/src/support/request-context.ts +30 -0
- package/src/support/thread-sessions.ts +16 -38
- package/src/support/tool-events.ts +41 -0
- package/src/react/chat-stream-reducer.ts +0 -243
- package/src/support/mastra-stream.ts +0 -59
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Button,
|
|
3
|
+
Tooltip,
|
|
4
|
+
TooltipContent,
|
|
5
|
+
TooltipProvider,
|
|
6
|
+
TooltipTrigger,
|
|
7
|
+
cn,
|
|
8
|
+
} from "@dbx-tools/ui-appkit/react";
|
|
9
|
+
import { BotIcon, MessageCircleIcon, XIcon } from "lucide-react";
|
|
10
|
+
import {
|
|
11
|
+
createContext,
|
|
12
|
+
type CSSProperties,
|
|
13
|
+
type PointerEvent as ReactPointerEvent,
|
|
14
|
+
type ReactNode,
|
|
15
|
+
useCallback,
|
|
16
|
+
useContext,
|
|
17
|
+
useEffect,
|
|
18
|
+
useMemo,
|
|
19
|
+
useRef,
|
|
20
|
+
useState,
|
|
21
|
+
} from "react";
|
|
22
|
+
import { ChatView } from "./chat-view.tsx";
|
|
23
|
+
import { useMastraChat, type UseMastraChatOptions } from "./mastra-chat.tsx";
|
|
24
|
+
import { ThreadActions } from "./thread-tabs.tsx";
|
|
25
|
+
import type { MastraRequestContextInput } from "../support/request-context.ts";
|
|
26
|
+
|
|
27
|
+
/** Outer assistant placement behavior. */
|
|
28
|
+
export type MastraAssistantMode = "dock" | "overlay";
|
|
29
|
+
/** Edge from which the assistant opens. */
|
|
30
|
+
export type MastraAssistantSide = "top" | "right" | "bottom" | "left";
|
|
31
|
+
/** Viewport corner used by the built-in launcher. */
|
|
32
|
+
export type MastraAssistantLauncherPosition =
|
|
33
|
+
"top-left" | "top-right" | "bottom-left" | "bottom-right";
|
|
34
|
+
|
|
35
|
+
/** Optional panel resizing and persistence. Sizes are CSS pixels. */
|
|
36
|
+
export interface MastraAssistantResizeOptions {
|
|
37
|
+
defaultSize?: number;
|
|
38
|
+
minSize?: number;
|
|
39
|
+
maxSize?: number;
|
|
40
|
+
keyboardStep?: number;
|
|
41
|
+
storageKey?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Built-in floating launcher configuration. */
|
|
45
|
+
export interface MastraAssistantLauncherOptions {
|
|
46
|
+
position?: MastraAssistantLauncherPosition;
|
|
47
|
+
icon?: ReactNode;
|
|
48
|
+
label?: string;
|
|
49
|
+
className?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Controller exposed to descendants through {@link useMastraAssistant}. */
|
|
53
|
+
export interface MastraAssistantController<TValues extends Record<string, unknown>> {
|
|
54
|
+
open: (requestContext?: MastraRequestContextInput<TValues>) => void;
|
|
55
|
+
close: () => void;
|
|
56
|
+
toggle: () => void;
|
|
57
|
+
setRequestContext: (requestContext?: MastraRequestContextInput<TValues>) => void;
|
|
58
|
+
isOpen: boolean;
|
|
59
|
+
requestContext: MastraRequestContextInput<TValues> | undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Props for the persistent Mastra assistant application shell. */
|
|
63
|
+
export interface MastraAssistantProps<TValues extends Record<string, unknown>> extends Omit<
|
|
64
|
+
React.HTMLAttributes<HTMLDivElement>,
|
|
65
|
+
"title"
|
|
66
|
+
> {
|
|
67
|
+
children: ReactNode;
|
|
68
|
+
chat?: Omit<UseMastraChatOptions<TValues>, "requestContext">;
|
|
69
|
+
requestContext?: MastraRequestContextInput<TValues>;
|
|
70
|
+
mode?: MastraAssistantMode;
|
|
71
|
+
side?: MastraAssistantSide;
|
|
72
|
+
open?: boolean;
|
|
73
|
+
defaultOpen?: boolean;
|
|
74
|
+
onOpenChange?: (open: boolean) => void;
|
|
75
|
+
available?: boolean;
|
|
76
|
+
resizable?: boolean | MastraAssistantResizeOptions;
|
|
77
|
+
mobileBreakpoint?: number;
|
|
78
|
+
launcher?: false | MastraAssistantLauncherOptions;
|
|
79
|
+
title?: ReactNode;
|
|
80
|
+
description?: ReactNode;
|
|
81
|
+
icon?: ReactNode;
|
|
82
|
+
header?: boolean;
|
|
83
|
+
closeIcon?: ReactNode;
|
|
84
|
+
newConversationIcon?: ReactNode;
|
|
85
|
+
historyIcon?: ReactNode;
|
|
86
|
+
contentClassName?: string;
|
|
87
|
+
panelClassName?: string;
|
|
88
|
+
headerClassName?: string;
|
|
89
|
+
chatClassName?: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const AssistantContext = createContext<MastraAssistantController<Record<string, unknown>> | null>(
|
|
93
|
+
null,
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
/** Access the nearest persistent assistant from any descendant button or page. */
|
|
97
|
+
export function useMastraAssistant<
|
|
98
|
+
TValues extends Record<string, unknown> = Record<string, unknown>,
|
|
99
|
+
>(): MastraAssistantController<TValues> {
|
|
100
|
+
const context = useContext(AssistantContext);
|
|
101
|
+
if (!context) {
|
|
102
|
+
throw new Error("useMastraAssistant must be used within MastraAssistant");
|
|
103
|
+
}
|
|
104
|
+
return context as MastraAssistantController<TValues>;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const launcherPositionClasses: Record<MastraAssistantLauncherPosition, string> = {
|
|
108
|
+
"top-left": "left-6 top-6",
|
|
109
|
+
"top-right": "right-6 top-6",
|
|
110
|
+
"bottom-left": "bottom-6 left-6",
|
|
111
|
+
"bottom-right": "bottom-6 right-6",
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const sideIsVertical = (side: MastraAssistantSide): boolean => side === "top" || side === "bottom";
|
|
115
|
+
|
|
116
|
+
const clampPanelSize = (
|
|
117
|
+
size: number,
|
|
118
|
+
available: number,
|
|
119
|
+
options: MastraAssistantResizeOptions,
|
|
120
|
+
vertical: boolean,
|
|
121
|
+
): number => {
|
|
122
|
+
const { minimum, maximum } = panelSizeBounds(available, options, vertical);
|
|
123
|
+
return Math.min(maximum, Math.max(minimum, size));
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const panelSizeBounds = (
|
|
127
|
+
available: number,
|
|
128
|
+
options: MastraAssistantResizeOptions,
|
|
129
|
+
vertical: boolean,
|
|
130
|
+
): { minimum: number; maximum: number } => {
|
|
131
|
+
const minimum = options.minSize ?? (vertical ? 240 : 320);
|
|
132
|
+
const maximum = Math.min(options.maxSize ?? available * 0.7, Math.max(minimum, available - 48));
|
|
133
|
+
return { minimum, maximum };
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const readStoredSize = (key: string | undefined): number | undefined => {
|
|
137
|
+
if (!key || typeof window === "undefined") return undefined;
|
|
138
|
+
try {
|
|
139
|
+
const value = Number(window.localStorage.getItem(key));
|
|
140
|
+
return Number.isFinite(value) && value > 0 ? value : undefined;
|
|
141
|
+
} catch {
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const panelPositionClasses: Record<MastraAssistantSide, string> = {
|
|
147
|
+
top: "inset-x-0 top-0 border-b",
|
|
148
|
+
right: "inset-y-0 right-0 border-l",
|
|
149
|
+
bottom: "inset-x-0 bottom-0 border-t",
|
|
150
|
+
left: "inset-y-0 left-0 border-r",
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const panelClosedClasses: Record<MastraAssistantSide, string> = {
|
|
154
|
+
top: "-translate-y-full",
|
|
155
|
+
right: "translate-x-full",
|
|
156
|
+
bottom: "translate-y-full",
|
|
157
|
+
left: "-translate-x-full",
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const resizeHandleClasses: Record<MastraAssistantSide, string> = {
|
|
161
|
+
top: "-bottom-1 inset-x-0 h-2 cursor-row-resize",
|
|
162
|
+
right: "-left-1 inset-y-0 w-2 cursor-col-resize",
|
|
163
|
+
bottom: "-top-1 inset-x-0 h-2 cursor-row-resize",
|
|
164
|
+
left: "-right-1 inset-y-0 w-2 cursor-col-resize",
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
type ResizeState = {
|
|
168
|
+
pointer: number;
|
|
169
|
+
size: number;
|
|
170
|
+
} | null;
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Persistent chat shell. Keep this above the route outlet so page navigation
|
|
174
|
+
* replaces application content without remounting the chat driver.
|
|
175
|
+
*/
|
|
176
|
+
export function MastraAssistant<TValues extends Record<string, unknown> = Record<string, unknown>>({
|
|
177
|
+
children,
|
|
178
|
+
chat = {},
|
|
179
|
+
requestContext: requestContextProp,
|
|
180
|
+
mode = "overlay",
|
|
181
|
+
side = "right",
|
|
182
|
+
open: openProp,
|
|
183
|
+
defaultOpen = false,
|
|
184
|
+
onOpenChange,
|
|
185
|
+
available = true,
|
|
186
|
+
resizable = true,
|
|
187
|
+
mobileBreakpoint = 640,
|
|
188
|
+
launcher = {},
|
|
189
|
+
title = "Assistant",
|
|
190
|
+
description,
|
|
191
|
+
icon,
|
|
192
|
+
header = true,
|
|
193
|
+
closeIcon,
|
|
194
|
+
newConversationIcon,
|
|
195
|
+
historyIcon,
|
|
196
|
+
className,
|
|
197
|
+
contentClassName,
|
|
198
|
+
panelClassName,
|
|
199
|
+
headerClassName,
|
|
200
|
+
chatClassName,
|
|
201
|
+
...rootProps
|
|
202
|
+
}: MastraAssistantProps<TValues>) {
|
|
203
|
+
const rootRef = useRef<HTMLDivElement>(null);
|
|
204
|
+
const [internalOpen, setInternalOpen] = useState(defaultOpen);
|
|
205
|
+
const isOpen = available && (openProp ?? internalOpen);
|
|
206
|
+
const [launchContext, setLaunchContext] = useState<
|
|
207
|
+
MastraRequestContextInput<TValues> | undefined
|
|
208
|
+
>();
|
|
209
|
+
const requestContext = launchContext ?? requestContextProp;
|
|
210
|
+
const resizeOptions = useMemo(() => (resizable === true ? {} : resizable || {}), [resizable]);
|
|
211
|
+
const vertical = sideIsVertical(side);
|
|
212
|
+
const defaultSize = resizeOptions.defaultSize ?? (vertical ? 360 : 432);
|
|
213
|
+
const [panelSize, setPanelSize] = useState(
|
|
214
|
+
() => readStoredSize(resizeOptions.storageKey) ?? defaultSize,
|
|
215
|
+
);
|
|
216
|
+
const [availableSize, setAvailableSize] = useState(0);
|
|
217
|
+
const [isNarrow, setIsNarrow] = useState(false);
|
|
218
|
+
const resizeState = useRef<ResizeState>(null);
|
|
219
|
+
|
|
220
|
+
const setOpen = useCallback(
|
|
221
|
+
(next: boolean) => {
|
|
222
|
+
if (openProp === undefined) setInternalOpen(next);
|
|
223
|
+
onOpenChange?.(next);
|
|
224
|
+
},
|
|
225
|
+
[onOpenChange, openProp],
|
|
226
|
+
);
|
|
227
|
+
const open = useCallback(
|
|
228
|
+
(nextContext?: MastraRequestContextInput<TValues>) => {
|
|
229
|
+
if (nextContext !== undefined) setLaunchContext(nextContext);
|
|
230
|
+
setOpen(true);
|
|
231
|
+
},
|
|
232
|
+
[setOpen],
|
|
233
|
+
);
|
|
234
|
+
const close = useCallback(() => setOpen(false), [setOpen]);
|
|
235
|
+
const toggle = useCallback(() => setOpen(!isOpen), [isOpen, setOpen]);
|
|
236
|
+
|
|
237
|
+
const controller = useMemo<MastraAssistantController<TValues>>(
|
|
238
|
+
() => ({
|
|
239
|
+
open,
|
|
240
|
+
close,
|
|
241
|
+
toggle,
|
|
242
|
+
setRequestContext: setLaunchContext,
|
|
243
|
+
isOpen,
|
|
244
|
+
requestContext,
|
|
245
|
+
}),
|
|
246
|
+
[close, isOpen, open, requestContext, toggle],
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
const chatView = useMastraChat<TValues>({
|
|
250
|
+
...chat,
|
|
251
|
+
threadPlacement: chat.threadPlacement ?? "top",
|
|
252
|
+
requestContext,
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
useEffect(() => {
|
|
256
|
+
const root = rootRef.current;
|
|
257
|
+
if (!root || typeof ResizeObserver === "undefined") return;
|
|
258
|
+
const observer = new ResizeObserver((entries) => {
|
|
259
|
+
const bounds = entries[0]?.contentRect;
|
|
260
|
+
const width = bounds?.width ?? root.clientWidth;
|
|
261
|
+
const height = bounds?.height ?? root.clientHeight;
|
|
262
|
+
if (width <= 0 || height <= 0) return;
|
|
263
|
+
setAvailableSize(vertical ? height : width);
|
|
264
|
+
setIsNarrow(width < mobileBreakpoint);
|
|
265
|
+
});
|
|
266
|
+
observer.observe(root);
|
|
267
|
+
return () => observer.disconnect();
|
|
268
|
+
}, [mobileBreakpoint, vertical]);
|
|
269
|
+
|
|
270
|
+
useEffect(() => {
|
|
271
|
+
if (!resizeOptions.storageKey || typeof window === "undefined") return;
|
|
272
|
+
try {
|
|
273
|
+
window.localStorage.setItem(resizeOptions.storageKey, String(panelSize));
|
|
274
|
+
} catch {
|
|
275
|
+
// Size persistence is best-effort; the panel remains usable without storage.
|
|
276
|
+
}
|
|
277
|
+
}, [panelSize, resizeOptions.storageKey]);
|
|
278
|
+
|
|
279
|
+
useEffect(() => {
|
|
280
|
+
const move = (event: PointerEvent) => {
|
|
281
|
+
const current = resizeState.current;
|
|
282
|
+
if (!current || availableSize <= 0) return;
|
|
283
|
+
const pointer = vertical ? event.clientY : event.clientX;
|
|
284
|
+
const direction = side === "right" || side === "bottom" ? -1 : 1;
|
|
285
|
+
setPanelSize(
|
|
286
|
+
clampPanelSize(
|
|
287
|
+
current.size + (pointer - current.pointer) * direction,
|
|
288
|
+
availableSize,
|
|
289
|
+
resizeOptions,
|
|
290
|
+
vertical,
|
|
291
|
+
),
|
|
292
|
+
);
|
|
293
|
+
};
|
|
294
|
+
const finish = () => {
|
|
295
|
+
resizeState.current = null;
|
|
296
|
+
document.body.style.removeProperty("user-select");
|
|
297
|
+
document.body.style.removeProperty("cursor");
|
|
298
|
+
};
|
|
299
|
+
window.addEventListener("pointermove", move);
|
|
300
|
+
window.addEventListener("pointerup", finish);
|
|
301
|
+
window.addEventListener("pointercancel", finish);
|
|
302
|
+
return () => {
|
|
303
|
+
window.removeEventListener("pointermove", move);
|
|
304
|
+
window.removeEventListener("pointerup", finish);
|
|
305
|
+
window.removeEventListener("pointercancel", finish);
|
|
306
|
+
finish();
|
|
307
|
+
};
|
|
308
|
+
}, [availableSize, resizeOptions, side, vertical]);
|
|
309
|
+
|
|
310
|
+
const beginResize = (event: ReactPointerEvent<HTMLDivElement>) => {
|
|
311
|
+
if (!resizable || isNarrow) return;
|
|
312
|
+
event.preventDefault();
|
|
313
|
+
event.currentTarget.focus();
|
|
314
|
+
resizeState.current = {
|
|
315
|
+
pointer: vertical ? event.clientY : event.clientX,
|
|
316
|
+
size: panelSize,
|
|
317
|
+
};
|
|
318
|
+
document.body.style.userSelect = "none";
|
|
319
|
+
document.body.style.cursor = vertical ? "row-resize" : "col-resize";
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const resizeByKeyboard = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
323
|
+
if (!resizable || isNarrow || availableSize <= 0) return;
|
|
324
|
+
const step = resizeOptions.keyboardStep ?? 24;
|
|
325
|
+
const decrease =
|
|
326
|
+
(side === "right" && event.key === "ArrowRight") ||
|
|
327
|
+
(side === "left" && event.key === "ArrowLeft") ||
|
|
328
|
+
(side === "top" && event.key === "ArrowUp") ||
|
|
329
|
+
(side === "bottom" && event.key === "ArrowDown");
|
|
330
|
+
const increase =
|
|
331
|
+
(side === "right" && event.key === "ArrowLeft") ||
|
|
332
|
+
(side === "left" && event.key === "ArrowRight") ||
|
|
333
|
+
(side === "top" && event.key === "ArrowDown") ||
|
|
334
|
+
(side === "bottom" && event.key === "ArrowUp");
|
|
335
|
+
if (!decrease && !increase) return;
|
|
336
|
+
event.preventDefault();
|
|
337
|
+
setPanelSize((current) =>
|
|
338
|
+
clampPanelSize(current + (increase ? step : -step), availableSize, resizeOptions, vertical),
|
|
339
|
+
);
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
const effectiveMode: MastraAssistantMode = isNarrow ? "overlay" : mode;
|
|
343
|
+
const effectiveSize =
|
|
344
|
+
availableSize > 0
|
|
345
|
+
? clampPanelSize(panelSize, availableSize, resizeOptions, vertical)
|
|
346
|
+
: panelSize;
|
|
347
|
+
const sizeBounds =
|
|
348
|
+
availableSize > 0
|
|
349
|
+
? panelSizeBounds(availableSize, resizeOptions, vertical)
|
|
350
|
+
: {
|
|
351
|
+
minimum: resizeOptions.minSize ?? (vertical ? 240 : 320),
|
|
352
|
+
maximum: resizeOptions.maxSize ?? panelSize,
|
|
353
|
+
};
|
|
354
|
+
const panelStyle: CSSProperties = isNarrow
|
|
355
|
+
? {}
|
|
356
|
+
: vertical
|
|
357
|
+
? { height: effectiveSize }
|
|
358
|
+
: { width: effectiveSize };
|
|
359
|
+
|
|
360
|
+
const resizeHandle =
|
|
361
|
+
resizable && !isNarrow ? (
|
|
362
|
+
<div
|
|
363
|
+
role="separator"
|
|
364
|
+
tabIndex={0}
|
|
365
|
+
aria-label="Resize assistant"
|
|
366
|
+
aria-orientation={vertical ? "horizontal" : "vertical"}
|
|
367
|
+
aria-valuemin={Math.round(sizeBounds.minimum)}
|
|
368
|
+
aria-valuemax={Math.round(sizeBounds.maximum)}
|
|
369
|
+
aria-valuenow={Math.round(effectiveSize)}
|
|
370
|
+
onPointerDown={beginResize}
|
|
371
|
+
onKeyDown={resizeByKeyboard}
|
|
372
|
+
className={cn(
|
|
373
|
+
"absolute z-20 touch-none outline-none",
|
|
374
|
+
"after:absolute after:bg-transparent after:transition-colors hover:after:bg-primary/30 focus-visible:after:bg-primary/40",
|
|
375
|
+
vertical
|
|
376
|
+
? "after:inset-x-0 after:top-1/2 after:h-px"
|
|
377
|
+
: "after:inset-y-0 after:left-1/2 after:w-px",
|
|
378
|
+
resizeHandleClasses[side],
|
|
379
|
+
)}
|
|
380
|
+
/>
|
|
381
|
+
) : null;
|
|
382
|
+
|
|
383
|
+
const closeAction = (
|
|
384
|
+
<Tooltip>
|
|
385
|
+
<TooltipTrigger asChild>
|
|
386
|
+
<Button type="button" variant="ghost" size="icon" className="size-7" onClick={close}>
|
|
387
|
+
{closeIcon ?? <XIcon className="size-4" />}
|
|
388
|
+
<span className="sr-only">Close assistant</span>
|
|
389
|
+
</Button>
|
|
390
|
+
</TooltipTrigger>
|
|
391
|
+
<TooltipContent>Close assistant</TooltipContent>
|
|
392
|
+
</Tooltip>
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
const headerActions =
|
|
396
|
+
chatView.threads && chatView.onSelectThread ? (
|
|
397
|
+
<ThreadActions
|
|
398
|
+
threads={chatView.threads}
|
|
399
|
+
activeThreadId={chatView.activeThreadId}
|
|
400
|
+
streamingThreadIds={chatView.streamingThreadIds}
|
|
401
|
+
isLoading={chatView.isLoadingThreads}
|
|
402
|
+
onSelect={chatView.onSelectThread}
|
|
403
|
+
onNew={chatView.onNewThread}
|
|
404
|
+
onDelete={chatView.onDeleteThread}
|
|
405
|
+
onRename={chatView.onRenameThread}
|
|
406
|
+
onCancel={chatView.onCancelThread}
|
|
407
|
+
newIcon={newConversationIcon}
|
|
408
|
+
historyIcon={historyIcon}
|
|
409
|
+
actions={closeAction}
|
|
410
|
+
/>
|
|
411
|
+
) : (
|
|
412
|
+
closeAction
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
const panel = (
|
|
416
|
+
<aside
|
|
417
|
+
aria-label={typeof title === "string" ? title : "Assistant"}
|
|
418
|
+
aria-hidden={!isOpen}
|
|
419
|
+
inert={!isOpen ? true : undefined}
|
|
420
|
+
style={panelStyle}
|
|
421
|
+
className={cn(
|
|
422
|
+
"relative z-40 flex min-h-0 min-w-0 flex-col bg-background text-foreground shadow-xl",
|
|
423
|
+
"transition-[transform,visibility] duration-200 ease-out",
|
|
424
|
+
(effectiveMode === "overlay" || !isOpen) && "fixed",
|
|
425
|
+
(effectiveMode === "overlay" || !isOpen) && !isNarrow && panelPositionClasses[side],
|
|
426
|
+
isNarrow && "fixed inset-0",
|
|
427
|
+
effectiveMode === "dock" && vertical && "w-full",
|
|
428
|
+
effectiveMode === "dock" && !vertical && "h-full",
|
|
429
|
+
!isOpen && panelClosedClasses[side],
|
|
430
|
+
!isOpen && "invisible pointer-events-none",
|
|
431
|
+
panelClassName,
|
|
432
|
+
)}
|
|
433
|
+
>
|
|
434
|
+
<TooltipProvider delayDuration={200}>
|
|
435
|
+
{resizeHandle}
|
|
436
|
+
{header ? (
|
|
437
|
+
<div
|
|
438
|
+
className={cn(
|
|
439
|
+
"flex min-h-14 shrink-0 items-center gap-3 border-b border-border px-4 py-2",
|
|
440
|
+
headerClassName,
|
|
441
|
+
)}
|
|
442
|
+
>
|
|
443
|
+
<span className="flex size-8 shrink-0 items-center justify-center rounded-lg bg-muted">
|
|
444
|
+
{icon ?? <BotIcon className="size-4" />}
|
|
445
|
+
</span>
|
|
446
|
+
<div className="min-w-0 flex-1">
|
|
447
|
+
<div className="truncate text-sm font-semibold">{title}</div>
|
|
448
|
+
{description ? (
|
|
449
|
+
<div className="truncate text-xs text-muted-foreground">{description}</div>
|
|
450
|
+
) : null}
|
|
451
|
+
</div>
|
|
452
|
+
<div className="flex shrink-0 items-center gap-1">{headerActions}</div>
|
|
453
|
+
</div>
|
|
454
|
+
) : null}
|
|
455
|
+
<ChatView
|
|
456
|
+
{...chatView}
|
|
457
|
+
showThreadBarActions={!header}
|
|
458
|
+
threadBarActions={!header ? closeAction : undefined}
|
|
459
|
+
threadNewIcon={newConversationIcon}
|
|
460
|
+
threadHistoryIcon={historyIcon}
|
|
461
|
+
className={cn("min-h-0 flex-1", chatClassName)}
|
|
462
|
+
/>
|
|
463
|
+
{!header && chatView.threadPlacement !== "top" ? (
|
|
464
|
+
<div className="absolute right-3 top-3 z-30">{closeAction}</div>
|
|
465
|
+
) : null}
|
|
466
|
+
</TooltipProvider>
|
|
467
|
+
</aside>
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
const launcherOptions = launcher === false ? null : launcher;
|
|
471
|
+
const launchButton =
|
|
472
|
+
available && !isOpen && launcherOptions ? (
|
|
473
|
+
<Button
|
|
474
|
+
type="button"
|
|
475
|
+
size="icon"
|
|
476
|
+
aria-label={launcherOptions.label ?? "Open assistant"}
|
|
477
|
+
title={launcherOptions.label ?? "Open assistant"}
|
|
478
|
+
onClick={() => open()}
|
|
479
|
+
className={cn(
|
|
480
|
+
"fixed z-30 size-14 rounded-full shadow-lg",
|
|
481
|
+
launcherPositionClasses[launcherOptions.position ?? "bottom-right"],
|
|
482
|
+
launcherOptions.className,
|
|
483
|
+
)}
|
|
484
|
+
>
|
|
485
|
+
{launcherOptions.icon ?? <MessageCircleIcon className="size-6" />}
|
|
486
|
+
</Button>
|
|
487
|
+
) : null;
|
|
488
|
+
|
|
489
|
+
const content = <div className={cn("min-h-0 min-w-0 flex-1", contentClassName)}>{children}</div>;
|
|
490
|
+
const rootDirection = vertical ? "flex-col" : "flex-row";
|
|
491
|
+
const panelFirst = side === "top" || side === "left";
|
|
492
|
+
|
|
493
|
+
return (
|
|
494
|
+
<AssistantContext.Provider
|
|
495
|
+
value={controller as MastraAssistantController<Record<string, unknown>>}
|
|
496
|
+
>
|
|
497
|
+
<div
|
|
498
|
+
ref={rootRef}
|
|
499
|
+
className={cn("relative flex h-full min-h-0 w-full min-w-0", rootDirection, className)}
|
|
500
|
+
{...rootProps}
|
|
501
|
+
>
|
|
502
|
+
{panelFirst ? panel : null}
|
|
503
|
+
{content}
|
|
504
|
+
{!panelFirst ? panel : null}
|
|
505
|
+
{launchButton}
|
|
506
|
+
</div>
|
|
507
|
+
</AssistantContext.Provider>
|
|
508
|
+
);
|
|
509
|
+
}
|