AssistantSidebar
Overview
A chat sidebar show on the right side of the screen. Useful for co-pilot use cases.
Getting Started
Add assistant-sidebar
npx shadcn@latest add @assistant-ui/assistant-sidebarMain Component
npm install @assistant-ui/reactyarn add @assistant-ui/reactpnpm add @assistant-ui/reactbun add @assistant-ui/reactxpm add @assistant-ui/reactimport { ResizableHandle, ResizablePanel, ResizablePanelGroup,} from "@/components/ui/resizable";import type { FC, PropsWithChildren } from "react";import { Thread } from "@/components/assistant-ui/thread";export const AssistantSidebar: FC<PropsWithChildren> = ({ children }) => { return ( <ResizablePanelGroup direction="horizontal"> <ResizablePanel>{children}</ResizablePanel> <ResizableHandle /> <ResizablePanel> <Thread /> </ResizablePanel> </ResizablePanelGroup> );};assistant-ui dependencies
npm install @assistant-ui/react @assistant-ui/react-markdown remark-gfm zustandyarn add @assistant-ui/react @assistant-ui/react-markdown remark-gfm zustandpnpm add @assistant-ui/react @assistant-ui/react-markdown remark-gfm zustandbun add @assistant-ui/react @assistant-ui/react-markdown remark-gfm zustandxpm add @assistant-ui/react @assistant-ui/react-markdown remark-gfm zustandimport { ArrowDownIcon, ArrowUpIcon, CheckIcon, ChevronLeftIcon, ChevronRightIcon, CopyIcon, PencilIcon, RefreshCwIcon, Square,} from "lucide-react";import { ActionBarPrimitive, AssistantIf, BranchPickerPrimitive, ComposerPrimitive, ErrorPrimitive, MessagePrimitive, ThreadPrimitive,} from "@assistant-ui/react";import type { FC } from "react";import { Button } from "@/components/ui/button";import { MarkdownText } from "@/components/assistant-ui/markdown-text";import { ToolFallback } from "@/components/assistant-ui/tool-fallback";import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";import { ComposerAddAttachment, ComposerAttachments, UserMessageAttachments,} from "@/components/assistant-ui/attachment";import { cn } from "@/lib/utils";export const Thread: FC = () => { return ( <ThreadPrimitive.Root className="aui-root aui-thread-root @container flex h-full flex-col bg-background" style={{ ["--thread-max-width" as string]: "44rem", }} > <ThreadPrimitive.Viewport turnAnchor="top" className="aui-thread-viewport relative flex flex-1 flex-col overflow-x-auto overflow-y-scroll scroll-smooth px-4 pt-4" > <AssistantIf condition={({ thread }) => thread.isEmpty}> <ThreadWelcome /> </AssistantIf> <ThreadPrimitive.Messages components={{ UserMessage, EditComposer, AssistantMessage, }} /> <ThreadPrimitive.ViewportFooter className="aui-thread-viewport-footer sticky bottom-0 mx-auto mt-4 flex w-full max-w-(--thread-max-width) flex-col gap-4 overflow-visible rounded-t-3xl bg-background pb-4 md:pb-6"> <ThreadScrollToBottom /> <Composer /> </ThreadPrimitive.ViewportFooter> </ThreadPrimitive.Viewport> </ThreadPrimitive.Root> );};const ThreadScrollToBottom: FC = () => { return ( <ThreadPrimitive.ScrollToBottom asChild> <TooltipIconButton tooltip="Scroll to bottom" variant="outline" className="aui-thread-scroll-to-bottom -top-12 absolute z-10 self-center rounded-full p-4 disabled:invisible dark:bg-background dark:hover:bg-accent" > <ArrowDownIcon /> </TooltipIconButton> </ThreadPrimitive.ScrollToBottom> );};const ThreadWelcome: FC = () => { return ( <div className="aui-thread-welcome-root mx-auto my-auto flex w-full max-w-(--thread-max-width) grow flex-col"> <div className="aui-thread-welcome-center flex w-full grow flex-col items-center justify-center"> <div className="aui-thread-welcome-message flex size-full flex-col justify-center px-8"> <div className="aui-thread-welcome-message-inner fade-in slide-in-from-bottom-2 animate-in font-semibold text-2xl duration-300 ease-out"> Hello there! </div> <div className="aui-thread-welcome-message-inner fade-in slide-in-from-bottom-2 animate-in text-2xl text-muted-foreground/65 delay-100 duration-300 ease-out"> How can I help you today? </div> </div> </div> <ThreadSuggestions /> </div> );};const ThreadSuggestions: FC = () => { return ( <div className="aui-thread-welcome-suggestions grid w-full @md:grid-cols-2 gap-2 pb-4"> {[ { title: "What's the weather", label: "in San Francisco?", action: "What's the weather in San Francisco?", }, { title: "Explain React hooks", label: "like useState and useEffect", action: "Explain React hooks like useState and useEffect", }, { title: "Write a SQL query", label: "to find top customers", action: "Write a SQL query to find top customers", }, { title: "Create a meal plan", label: "for healthy weight loss", action: "Create a meal plan for healthy weight loss", }, ].map((suggestedAction, index) => ( <div key={`suggested-action-${suggestedAction.title}-${index}`} className="aui-thread-welcome-suggestion-display fade-in slide-in-from-bottom-4 @md:nth-[n+3]:block nth-[n+3]:hidden animate-in fill-mode-both duration-300 ease-out" style={{ animationDelay: `${index * 50}ms` }} > <ThreadPrimitive.Suggestion prompt={suggestedAction.action} send asChild > <Button variant="ghost" className="aui-thread-welcome-suggestion h-auto w-full flex-1 @md:flex-col flex-wrap items-start justify-start gap-1 rounded-3xl border px-5 py-4 text-left text-sm dark:hover:bg-accent/60" aria-label={suggestedAction.action} > <span className="aui-thread-welcome-suggestion-text-1 font-medium"> {suggestedAction.title} </span> <span className="aui-thread-welcome-suggestion-text-2 text-muted-foreground"> {suggestedAction.label} </span> </Button> </ThreadPrimitive.Suggestion> </div> ))} </div> );};const Composer: FC = () => { return ( <ComposerPrimitive.Root className="aui-composer-root relative flex w-full flex-col"> <ComposerPrimitive.AttachmentDropzone className="aui-composer-attachment-dropzone flex w-full flex-col rounded-3xl border border-input bg-background px-1 pt-2 shadow-xs outline-none transition-[color,box-shadow] has-[textarea:focus-visible]:border-ring has-[textarea:focus-visible]:ring-[3px] has-[textarea:focus-visible]:ring-ring/50 data-[dragging=true]:border-ring data-[dragging=true]:border-dashed data-[dragging=true]:bg-accent/50 dark:bg-background"> <ComposerAttachments /> <ComposerPrimitive.Input placeholder="Send a message..." className="aui-composer-input mb-1 max-h-32 min-h-16 w-full resize-none bg-transparent px-3.5 pt-1.5 pb-3 text-base outline-none placeholder:text-muted-foreground focus-visible:ring-0" rows={1} autoFocus aria-label="Message input" /> <ComposerAction /> </ComposerPrimitive.AttachmentDropzone> </ComposerPrimitive.Root> );};const ComposerAction: FC = () => { return ( <div className="aui-composer-action-wrapper relative mx-1 mt-2 mb-2 flex items-center justify-between"> <ComposerAddAttachment /> <AssistantIf condition={({ thread }) => !thread.isRunning}> <ComposerPrimitive.Send asChild> <TooltipIconButton tooltip="Send message" side="bottom" type="submit" variant="default" size="icon" className="aui-composer-send size-[34px] rounded-full p-1" aria-label="Send message" > <ArrowUpIcon className="aui-composer-send-icon size-5" /> </TooltipIconButton> </ComposerPrimitive.Send> </AssistantIf> <AssistantIf condition={({ thread }) => thread.isRunning}> <ComposerPrimitive.Cancel asChild> <Button type="button" variant="default" size="icon" className="aui-composer-cancel size-[34px] rounded-full border border-muted-foreground/60 hover:bg-primary/75 dark:border-muted-foreground/90" aria-label="Stop generating" > <Square className="aui-composer-cancel-icon size-3.5 fill-white dark:fill-black" /> </Button> </ComposerPrimitive.Cancel> </AssistantIf> </div> );};const MessageError: FC = () => { return ( <MessagePrimitive.Error> <ErrorPrimitive.Root className="aui-message-error-root mt-2 rounded-md border border-destructive bg-destructive/10 p-3 text-destructive text-sm dark:bg-destructive/5 dark:text-red-200"> <ErrorPrimitive.Message className="aui-message-error-message line-clamp-2" /> </ErrorPrimitive.Root> </MessagePrimitive.Error> );};const AssistantMessage: FC = () => { return ( <MessagePrimitive.Root className="aui-assistant-message-root fade-in slide-in-from-bottom-1 relative mx-auto w-full max-w-(--thread-max-width) animate-in py-4 duration-150 ease-out" data-role="assistant" > <div className="aui-assistant-message-content wrap-break-word mx-2 text-foreground leading-7"> <MessagePrimitive.Parts components={{ Text: MarkdownText, tools: { Fallback: ToolFallback }, }} /> <MessageError /> </div> <div className="aui-assistant-message-footer mt-2 ml-2 flex"> <BranchPicker /> <AssistantActionBar /> </div> </MessagePrimitive.Root> );};const AssistantActionBar: FC = () => { return ( <ActionBarPrimitive.Root hideWhenRunning autohide="not-last" autohideFloat="single-branch" className="aui-assistant-action-bar-root -ml-1 col-start-3 row-start-2 flex gap-1 text-muted-foreground data-floating:absolute data-floating:rounded-md data-floating:border data-floating:bg-background data-floating:p-1 data-floating:shadow-sm" > <ActionBarPrimitive.Copy asChild> <TooltipIconButton tooltip="Copy"> <AssistantIf condition={({ message }) => message.isCopied}> <CheckIcon /> </AssistantIf> <AssistantIf condition={({ message }) => !message.isCopied}> <CopyIcon /> </AssistantIf> </TooltipIconButton> </ActionBarPrimitive.Copy> <ActionBarPrimitive.Reload asChild> <TooltipIconButton tooltip="Refresh"> <RefreshCwIcon /> </TooltipIconButton> </ActionBarPrimitive.Reload> </ActionBarPrimitive.Root> );};const UserMessage: FC = () => { return ( <MessagePrimitive.Root className="aui-user-message-root fade-in slide-in-from-bottom-1 mx-auto grid w-full max-w-(--thread-max-width) animate-in auto-rows-auto grid-cols-[minmax(72px,1fr)_auto] content-start gap-y-2 px-2 py-4 duration-150 ease-out [&:where(>*)]:col-start-2" data-role="user" > <UserMessageAttachments /> <div className="aui-user-message-content-wrapper relative col-start-2 min-w-0"> <div className="aui-user-message-content wrap-break-word rounded-3xl bg-muted px-5 py-2.5 text-foreground"> <MessagePrimitive.Parts /> </div> <div className="aui-user-action-bar-wrapper -translate-x-full -translate-y-1/2 absolute top-1/2 left-0 pr-2"> <UserActionBar /> </div> </div> <BranchPicker className="aui-user-branch-picker -mr-1 col-span-full col-start-1 row-start-3 justify-end" /> </MessagePrimitive.Root> );};const UserActionBar: FC = () => { return ( <ActionBarPrimitive.Root hideWhenRunning autohide="not-last" className="aui-user-action-bar-root flex flex-col items-end" > <ActionBarPrimitive.Edit asChild> <TooltipIconButton tooltip="Edit" className="aui-user-action-edit p-4"> <PencilIcon /> </TooltipIconButton> </ActionBarPrimitive.Edit> </ActionBarPrimitive.Root> );};const EditComposer: FC = () => { return ( <MessagePrimitive.Root className="aui-edit-composer-wrapper mx-auto flex w-full max-w-(--thread-max-width) flex-col gap-4 px-2"> <ComposerPrimitive.Root className="aui-edit-composer-root ml-auto flex w-full max-w-7/8 flex-col rounded-xl bg-muted"> <ComposerPrimitive.Input className="aui-edit-composer-input flex min-h-[60px] w-full resize-none bg-transparent p-4 text-foreground outline-none" autoFocus /> <div className="aui-edit-composer-footer mx-3 mb-3 flex items-center justify-center gap-2 self-end"> <ComposerPrimitive.Cancel asChild> <Button variant="ghost" size="sm" aria-label="Cancel edit"> Cancel </Button> </ComposerPrimitive.Cancel> <ComposerPrimitive.Send asChild> <Button size="sm" aria-label="Update message"> Update </Button> </ComposerPrimitive.Send> </div> </ComposerPrimitive.Root> </MessagePrimitive.Root> );};const BranchPicker: FC<BranchPickerPrimitive.Root.Props> = ({ className, ...rest}) => { return ( <BranchPickerPrimitive.Root hideWhenSingleBranch className={cn( "aui-branch-picker-root -ml-2 mr-2 inline-flex items-center text-muted-foreground text-xs", className, )} {...rest} > <BranchPickerPrimitive.Previous asChild> <TooltipIconButton tooltip="Previous"> <ChevronLeftIcon /> </TooltipIconButton> </BranchPickerPrimitive.Previous> <span className="aui-branch-picker-state font-medium"> <BranchPickerPrimitive.Number /> / <BranchPickerPrimitive.Count /> </span> <BranchPickerPrimitive.Next asChild> <TooltipIconButton tooltip="Next"> <ChevronRightIcon /> </TooltipIconButton> </BranchPickerPrimitive.Next> </BranchPickerPrimitive.Root> );};"use client";import { PropsWithChildren, useEffect, useState, type FC } from "react";import Image from "next/image";import { XIcon, PlusIcon, FileText } from "lucide-react";import { AttachmentPrimitive, ComposerPrimitive, MessagePrimitive, useAssistantState, useAssistantApi,} from "@assistant-ui/react";import { useShallow } from "zustand/shallow";import { Tooltip, TooltipContent, TooltipTrigger,} from "@/components/ui/tooltip";import { Dialog, DialogTitle, DialogContent, DialogTrigger,} from "@/components/ui/dialog";import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";import { cn } from "@/lib/utils";const useFileSrc = (file: File | undefined) => { const [src, setSrc] = useState<string | undefined>(undefined); useEffect(() => { if (!file) { setSrc(undefined); return; } const objectUrl = URL.createObjectURL(file); setSrc(objectUrl); return () => { URL.revokeObjectURL(objectUrl); }; }, [file]); return src;};const useAttachmentSrc = () => { const { file, src } = useAssistantState( useShallow(({ attachment }): { file?: File; src?: string } => { if (attachment.type !== "image") return {}; if (attachment.file) return { file: attachment.file }; const src = attachment.content?.filter((c) => c.type === "image")[0] ?.image; if (!src) return {}; return { src }; }), ); return useFileSrc(file) ?? src;};type AttachmentPreviewProps = { src: string;};const AttachmentPreview: FC<AttachmentPreviewProps> = ({ src }) => { const [isLoaded, setIsLoaded] = useState(false); return ( <Image src={src} alt="Image Preview" width={1} height={1} className={ isLoaded ? "aui-attachment-preview-image-loaded block h-auto max-h-[80vh] w-auto max-w-full object-contain" : "aui-attachment-preview-image-loading hidden" } onLoadingComplete={() => setIsLoaded(true)} priority={false} /> );};const AttachmentPreviewDialog: FC<PropsWithChildren> = ({ children }) => { const src = useAttachmentSrc(); if (!src) return children; return ( <Dialog> <DialogTrigger className="aui-attachment-preview-trigger cursor-pointer transition-colors hover:bg-accent/50" asChild > {children} </DialogTrigger> <DialogContent className="aui-attachment-preview-dialog-content p-2 sm:max-w-3xl [&>button]:rounded-full [&>button]:bg-foreground/60 [&>button]:p-1 [&>button]:opacity-100 [&>button]:ring-0! [&_svg]:text-background [&>button]:hover:[&_svg]:text-destructive"> <DialogTitle className="aui-sr-only sr-only"> Image Attachment Preview </DialogTitle> <div className="aui-attachment-preview relative mx-auto flex max-h-[80dvh] w-full items-center justify-center overflow-hidden bg-background"> <AttachmentPreview src={src} /> </div> </DialogContent> </Dialog> );};const AttachmentThumb: FC = () => { const isImage = useAssistantState( ({ attachment }) => attachment.type === "image", ); const src = useAttachmentSrc(); return ( <Avatar className="aui-attachment-tile-avatar h-full w-full rounded-none"> <AvatarImage src={src} alt="Attachment preview" className="aui-attachment-tile-image object-cover" /> <AvatarFallback delayMs={isImage ? 200 : 0}> <FileText className="aui-attachment-tile-fallback-icon size-8 text-muted-foreground" /> </AvatarFallback> </Avatar> );};const AttachmentUI: FC = () => { const api = useAssistantApi(); const isComposer = api.attachment.source === "composer"; const isImage = useAssistantState( ({ attachment }) => attachment.type === "image", ); const typeLabel = useAssistantState(({ attachment }) => { const type = attachment.type; switch (type) { case "image": return "Image"; case "document": return "Document"; case "file": return "File"; default: const _exhaustiveCheck: never = type; throw new Error(`Unknown attachment type: ${_exhaustiveCheck}`); } }); return ( <Tooltip> <AttachmentPrimitive.Root className={cn( "aui-attachment-root relative", isImage && "aui-attachment-root-composer only:[&>#attachment-tile]:size-24", )} > <AttachmentPreviewDialog> <TooltipTrigger asChild> <div className={cn( "aui-attachment-tile size-14 cursor-pointer overflow-hidden rounded-[14px] border bg-muted transition-opacity hover:opacity-75", isComposer && "aui-attachment-tile-composer border-foreground/20", )} role="button" id="attachment-tile" aria-label={`${typeLabel} attachment`} > <AttachmentThumb /> </div> </TooltipTrigger> </AttachmentPreviewDialog> {isComposer && <AttachmentRemove />} </AttachmentPrimitive.Root> <TooltipContent side="top"> <AttachmentPrimitive.Name /> </TooltipContent> </Tooltip> );};const AttachmentRemove: FC = () => { return ( <AttachmentPrimitive.Remove asChild> <TooltipIconButton tooltip="Remove file" className="aui-attachment-tile-remove absolute top-1.5 right-1.5 size-3.5 rounded-full bg-white text-muted-foreground opacity-100 shadow-sm hover:bg-white! [&_svg]:text-black hover:[&_svg]:text-destructive" side="top" > <XIcon className="aui-attachment-remove-icon size-3 dark:stroke-[2.5px]" /> </TooltipIconButton> </AttachmentPrimitive.Remove> );};export const UserMessageAttachments: FC = () => { return ( <div className="aui-user-message-attachments-end col-span-full col-start-1 row-start-1 flex w-full flex-row justify-end gap-2"> <MessagePrimitive.Attachments components={{ Attachment: AttachmentUI }} /> </div> );};export const ComposerAttachments: FC = () => { return ( <div className="aui-composer-attachments mb-2 flex w-full flex-row items-center gap-2 overflow-x-auto px-1.5 pt-0.5 pb-1 empty:hidden"> <ComposerPrimitive.Attachments components={{ Attachment: AttachmentUI }} /> </div> );};export const ComposerAddAttachment: FC = () => { return ( <ComposerPrimitive.AddAttachment asChild> <TooltipIconButton tooltip="Add Attachment" side="bottom" variant="ghost" size="icon" className="aui-composer-add-attachment size-[34px] rounded-full p-1 font-semibold text-xs hover:bg-muted-foreground/15 dark:border-muted-foreground/15 dark:hover:bg-muted-foreground/30" aria-label="Add Attachment" > <PlusIcon className="aui-attachment-add-icon size-5 stroke-[1.5px]" /> </TooltipIconButton> </ComposerPrimitive.AddAttachment> );};"use client";import { ComponentPropsWithRef, forwardRef } from "react";import { Slottable } from "@radix-ui/react-slot";import { Tooltip, TooltipContent, TooltipTrigger,} from "@/components/ui/tooltip";import { Button } from "@/components/ui/button";import { cn } from "@/lib/utils";export type TooltipIconButtonProps = ComponentPropsWithRef<typeof Button> & { tooltip: string; side?: "top" | "bottom" | "left" | "right";};export const TooltipIconButton = forwardRef< HTMLButtonElement, TooltipIconButtonProps>(({ children, tooltip, side = "bottom", className, ...rest }, ref) => { return ( <Tooltip> <TooltipTrigger asChild> <Button variant="ghost" size="icon" {...rest} className={cn("aui-button-icon size-6 p-1", className)} ref={ref} > <Slottable>{children}</Slottable> <span className="aui-sr-only sr-only">{tooltip}</span> </Button> </TooltipTrigger> <TooltipContent side={side}>{tooltip}</TooltipContent> </Tooltip> );});TooltipIconButton.displayName = "TooltipIconButton";"use client";import "@assistant-ui/react-markdown/styles/dot.css";import { type CodeHeaderProps, MarkdownTextPrimitive, unstable_memoizeMarkdownComponents as memoizeMarkdownComponents, useIsMarkdownCodeBlock,} from "@assistant-ui/react-markdown";import remarkGfm from "remark-gfm";import { type FC, memo, useState } from "react";import { CheckIcon, CopyIcon } from "lucide-react";import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";import { cn } from "@/lib/utils";const MarkdownTextImpl = () => { return ( <MarkdownTextPrimitive remarkPlugins={[remarkGfm]} className="aui-md" components={defaultComponents} /> );};export const MarkdownText = memo(MarkdownTextImpl);const CodeHeader: FC<CodeHeaderProps> = ({ language, code }) => { const { isCopied, copyToClipboard } = useCopyToClipboard(); const onCopy = () => { if (!code || isCopied) return; copyToClipboard(code); }; return ( <div className="aui-code-header-root mt-4 flex items-center justify-between gap-4 rounded-t-lg bg-muted-foreground/15 px-4 py-2 font-semibold text-foreground text-sm dark:bg-muted-foreground/20"> <span className="aui-code-header-language lowercase [&>span]:text-xs"> {language} </span> <TooltipIconButton tooltip="Copy" onClick={onCopy}> {!isCopied && <CopyIcon />} {isCopied && <CheckIcon />} </TooltipIconButton> </div> );};const useCopyToClipboard = ({ copiedDuration = 3000,}: { copiedDuration?: number;} = {}) => { const [isCopied, setIsCopied] = useState<boolean>(false); const copyToClipboard = (value: string) => { if (!value) return; navigator.clipboard.writeText(value).then(() => { setIsCopied(true); setTimeout(() => setIsCopied(false), copiedDuration); }); }; return { isCopied, copyToClipboard };};const defaultComponents = memoizeMarkdownComponents({ h1: ({ className, ...props }) => ( <h1 className={cn( "aui-md-h1 mb-8 scroll-m-20 font-extrabold text-4xl tracking-tight last:mb-0", className, )} {...props} /> ), h2: ({ className, ...props }) => ( <h2 className={cn( "aui-md-h2 mt-8 mb-4 scroll-m-20 font-semibold text-3xl tracking-tight first:mt-0 last:mb-0", className, )} {...props} /> ), h3: ({ className, ...props }) => ( <h3 className={cn( "aui-md-h3 mt-6 mb-4 scroll-m-20 font-semibold text-2xl tracking-tight first:mt-0 last:mb-0", className, )} {...props} /> ), h4: ({ className, ...props }) => ( <h4 className={cn( "aui-md-h4 mt-6 mb-4 scroll-m-20 font-semibold text-xl tracking-tight first:mt-0 last:mb-0", className, )} {...props} /> ), h5: ({ className, ...props }) => ( <h5 className={cn( "aui-md-h5 my-4 font-semibold text-lg first:mt-0 last:mb-0", className, )} {...props} /> ), h6: ({ className, ...props }) => ( <h6 className={cn( "aui-md-h6 my-4 font-semibold first:mt-0 last:mb-0", className, )} {...props} /> ), p: ({ className, ...props }) => ( <p className={cn( "aui-md-p mt-5 mb-5 leading-7 first:mt-0 last:mb-0", className, )} {...props} /> ), a: ({ className, ...props }) => ( <a className={cn( "aui-md-a font-medium text-primary underline underline-offset-4", className, )} {...props} /> ), blockquote: ({ className, ...props }) => ( <blockquote className={cn("aui-md-blockquote border-l-2 pl-6 italic", className)} {...props} /> ), ul: ({ className, ...props }) => ( <ul className={cn("aui-md-ul my-5 ml-6 list-disc [&>li]:mt-2", className)} {...props} /> ), ol: ({ className, ...props }) => ( <ol className={cn("aui-md-ol my-5 ml-6 list-decimal [&>li]:mt-2", className)} {...props} /> ), hr: ({ className, ...props }) => ( <hr className={cn("aui-md-hr my-5 border-b", className)} {...props} /> ), table: ({ className, ...props }) => ( <table className={cn( "aui-md-table my-5 w-full border-separate border-spacing-0 overflow-y-auto", className, )} {...props} /> ), th: ({ className, ...props }) => ( <th className={cn( "aui-md-th bg-muted px-4 py-2 text-left font-bold first:rounded-tl-lg last:rounded-tr-lg [[align=center]]:text-center [[align=right]]:text-right", className, )} {...props} /> ), td: ({ className, ...props }) => ( <td className={cn( "aui-md-td border-b border-l px-4 py-2 text-left last:border-r [[align=center]]:text-center [[align=right]]:text-right", className, )} {...props} /> ), tr: ({ className, ...props }) => ( <tr className={cn( "aui-md-tr m-0 border-b p-0 first:border-t [&:last-child>td:first-child]:rounded-bl-lg [&:last-child>td:last-child]:rounded-br-lg", className, )} {...props} /> ), sup: ({ className, ...props }) => ( <sup className={cn("aui-md-sup [&>a]:text-xs [&>a]:no-underline", className)} {...props} /> ), pre: ({ className, ...props }) => ( <pre className={cn( "aui-md-pre overflow-x-auto rounded-t-none! rounded-b-lg bg-black p-4 text-white", className, )} {...props} /> ), code: function Code({ className, ...props }) { const isCodeBlock = useIsMarkdownCodeBlock(); return ( <code className={cn( !isCodeBlock && "aui-md-inline-code rounded border bg-muted font-semibold", className, )} {...props} /> ); }, CodeHeader,});import type { ToolCallMessagePartComponent } from "@assistant-ui/react";import { CheckIcon, ChevronDownIcon, ChevronUpIcon, XCircleIcon,} from "lucide-react";import { useState } from "react";import { Button } from "@/components/ui/button";import { cn } from "@/lib/utils";export const ToolFallback: ToolCallMessagePartComponent = ({ toolName, argsText, result, status,}) => { const [isCollapsed, setIsCollapsed] = useState(true); const isCancelled = status?.type === "incomplete" && status.reason === "cancelled"; const cancelledReason = isCancelled && status.error ? typeof status.error === "string" ? status.error : JSON.stringify(status.error) : null; return ( <div className={cn( "aui-tool-fallback-root mb-4 flex w-full flex-col gap-3 rounded-lg border py-3", isCancelled && "border-muted-foreground/30 bg-muted/30", )} > <div className="aui-tool-fallback-header flex items-center gap-2 px-4"> {isCancelled ? ( <XCircleIcon className="aui-tool-fallback-icon size-4 text-muted-foreground" /> ) : ( <CheckIcon className="aui-tool-fallback-icon size-4" /> )} <p className={cn( "aui-tool-fallback-title grow", isCancelled && "text-muted-foreground line-through", )} > {isCancelled ? "Cancelled tool: " : "Used tool: "} <b>{toolName}</b> </p> <Button onClick={() => setIsCollapsed(!isCollapsed)}> {isCollapsed ? <ChevronUpIcon /> : <ChevronDownIcon />} </Button> </div> {!isCollapsed && ( <div className="aui-tool-fallback-content flex flex-col gap-2 border-t pt-2"> {cancelledReason && ( <div className="aui-tool-fallback-cancelled-root px-4"> <p className="aui-tool-fallback-cancelled-header font-semibold text-muted-foreground"> Cancelled reason: </p> <p className="aui-tool-fallback-cancelled-reason text-muted-foreground"> {cancelledReason} </p> </div> )} <div className={cn( "aui-tool-fallback-args-root px-4", isCancelled && "opacity-60", )} > <pre className="aui-tool-fallback-args-value whitespace-pre-wrap"> {argsText} </pre> </div> {!isCancelled && result !== undefined && ( <div className="aui-tool-fallback-result-root border-t border-dashed px-4 pt-2"> <p className="aui-tool-fallback-result-header font-semibold"> Result: </p> <pre className="aui-tool-fallback-result-content whitespace-pre-wrap"> {typeof result === "string" ? result : JSON.stringify(result, null, 2)} </pre> </div> )} </div> )} </div> );};shadcn/ui dependencies
npm install @radix-ui/react-avatar @radix-ui/react-dialog @radix-ui/react-slot @radix-ui/react-tooltip react-resizable-panelsyarn add @radix-ui/react-avatar @radix-ui/react-dialog @radix-ui/react-slot @radix-ui/react-tooltip react-resizable-panelspnpm add @radix-ui/react-avatar @radix-ui/react-dialog @radix-ui/react-slot @radix-ui/react-tooltip react-resizable-panelsbun add @radix-ui/react-avatar @radix-ui/react-dialog @radix-ui/react-slot @radix-ui/react-tooltip react-resizable-panelsxpm add @radix-ui/react-avatar @radix-ui/react-dialog @radix-ui/react-slot @radix-ui/react-tooltip react-resizable-panelsimport * as React from "react";import { Slot } from "@radix-ui/react-slot";import { cva, type VariantProps } from "class-variance-authority";import { cn } from "@/lib/utils";const buttonVariants = cva( "inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md font-medium text-sm no-underline outline-none transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0", { variants: { variant: { default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", destructive: "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40", outline: "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50", secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", link: "text-primary underline-offset-4 hover:underline", }, size: { default: "h-9 px-4 py-2 has-[>svg]:px-3", sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5", lg: "h-10 rounded-md px-6 has-[>svg]:px-4", icon: "size-9", }, }, defaultVariants: { variant: "default", size: "default", }, },);function Button({ className, variant, size, asChild = false, ...props}: React.ComponentProps<"button"> & VariantProps<typeof buttonVariants> & { asChild?: boolean; }) { const Comp = asChild ? Slot : "button"; return ( <Comp data-slot="button" className={cn(buttonVariants({ variant, size, className }))} {...props} /> );}export { Button, buttonVariants };"use client";import * as React from "react";import * as DialogPrimitive from "@radix-ui/react-dialog";import { XIcon } from "lucide-react";import { cn } from "@/lib/utils";function Dialog({ ...props}: React.ComponentProps<typeof DialogPrimitive.Root>) { return <DialogPrimitive.Root data-slot="dialog" {...props} />;}function DialogTrigger({ ...props}: React.ComponentProps<typeof DialogPrimitive.Trigger>) { return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;}function DialogPortal({ ...props}: React.ComponentProps<typeof DialogPrimitive.Portal>) { return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;}function DialogClose({ ...props}: React.ComponentProps<typeof DialogPrimitive.Close>) { return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;}function DialogOverlay({ className, ...props}: React.ComponentProps<typeof DialogPrimitive.Overlay>) { return ( <DialogPrimitive.Overlay data-slot="dialog-overlay" className={cn( "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80 data-[state=closed]:animate-out data-[state=open]:animate-in", className, )} {...props} /> );}function DialogContent({ className, children, ...props}: React.ComponentProps<typeof DialogPrimitive.Content>) { return ( <DialogPortal data-slot="dialog-portal"> <DialogOverlay /> <DialogPrimitive.Content data-slot="dialog-content" className={cn( "data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg duration-200 data-[state=closed]:animate-out data-[state=open]:animate-in sm:max-w-lg", className, )} {...props} > {children} <DialogPrimitive.Close className="absolute top-4 right-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0"> <XIcon /> <span className="sr-only">Close</span> </DialogPrimitive.Close> </DialogPrimitive.Content> </DialogPortal> );}function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="dialog-header" className={cn("flex flex-col gap-2 text-center sm:text-left", className)} {...props} /> );}function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="dialog-footer" className={cn( "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className, )} {...props} /> );}function DialogTitle({ className, ...props}: React.ComponentProps<typeof DialogPrimitive.Title>) { return ( <DialogPrimitive.Title data-slot="dialog-title" className={cn("font-semibold text-lg leading-none", className)} {...props} /> );}function DialogDescription({ className, ...props}: React.ComponentProps<typeof DialogPrimitive.Description>) { return ( <DialogPrimitive.Description data-slot="dialog-description" className={cn("text-muted-foreground text-sm", className)} {...props} /> );}export { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger,};"use client";import * as React from "react";import * as TooltipPrimitive from "@radix-ui/react-tooltip";import { cn } from "@/lib/utils";function TooltipProvider({ delayDuration = 0, ...props}: React.ComponentProps<typeof TooltipPrimitive.Provider>) { return ( <TooltipPrimitive.Provider data-slot="tooltip-provider" delayDuration={delayDuration} {...props} /> );}function Tooltip({ ...props}: React.ComponentProps<typeof TooltipPrimitive.Root>) { return ( <TooltipProvider> <TooltipPrimitive.Root data-slot="tooltip" {...props} /> </TooltipProvider> );}function TooltipTrigger({ ...props}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) { return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;}function TooltipContent({ className, sideOffset = 0, children, ...props}: React.ComponentProps<typeof TooltipPrimitive.Content>) { return ( <TooltipPrimitive.Portal> <TooltipPrimitive.Content data-slot="tooltip-content" sideOffset={sideOffset} className={cn( "fade-in-0 zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) animate-in text-balance rounded-md bg-foreground px-3 py-1.5 text-background text-xs data-[state=closed]:animate-out", className, )} {...props} > {children} <TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground" /> </TooltipPrimitive.Content> </TooltipPrimitive.Portal> );}export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };"use client";import * as React from "react";import * as AvatarPrimitive from "@radix-ui/react-avatar";import { cn } from "@/lib/utils";function Avatar({ className, ...props}: React.ComponentProps<typeof AvatarPrimitive.Root>) { return ( <AvatarPrimitive.Root data-slot="avatar" className={cn( "relative flex size-8 shrink-0 overflow-hidden rounded-full", className, )} {...props} /> );}function AvatarImage({ className, ...props}: React.ComponentProps<typeof AvatarPrimitive.Image>) { return ( <AvatarPrimitive.Image data-slot="avatar-image" className={cn("aspect-square size-full", className)} {...props} /> );}function AvatarFallback({ className, ...props}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) { return ( <AvatarPrimitive.Fallback data-slot="avatar-fallback" className={cn( "flex size-full items-center justify-center rounded-full bg-muted", className, )} {...props} /> );}export { Avatar, AvatarImage, AvatarFallback };"use client";import * as React from "react";import { GripVerticalIcon } from "lucide-react";import * as ResizablePrimitive from "react-resizable-panels";import { cn } from "@/lib/utils";function ResizablePanelGroup({ className, ...props}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) { return ( <ResizablePrimitive.PanelGroup data-slot="resizable-panel-group" className={cn( "flex h-full w-full data-[panel-group-direction=vertical]:flex-col", className, )} {...props} /> );}function ResizablePanel({ ...props}: React.ComponentProps<typeof ResizablePrimitive.Panel>) { return <ResizablePrimitive.Panel data-slot="resizable-panel" {...props} />;}function ResizableHandle({ withHandle, className, ...props}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & { withHandle?: boolean;}) { return ( <ResizablePrimitive.PanelResizeHandle data-slot="resizable-handle" className={cn( "after:-translate-x-1/2 data-[panel-group-direction=vertical]:after:-translate-y-1/2 relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90", className, )} {...props} > {withHandle && ( <div className="z-10 flex h-4 w-3 items-center justify-center rounded-xs border bg-border"> <GripVerticalIcon className="size-2.5" /> </div> )} </ResizablePrimitive.PanelResizeHandle> );}export { ResizablePanelGroup, ResizablePanel, ResizableHandle };This adds /components/assistant-ui/assistant-sidebar.tsx to your project, which you can adjust as needed.
Use in your application
import { AssistantSidebar } from "@/components/assistant-ui/assistant-sidebar";
export default function Home() {
return (
<div className="h-full">
<AssistantSidebar>{/* your app */}</AssistantSidebar>
</div>
);
}