@dbx-tools/ui-mastra 0.6.218 → 0.6.220

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