logoassistant-ui

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-sidebar

Main Component

npm install @assistant-ui/react
yarn add @assistant-ui/react
pnpm add @assistant-ui/react
bun add @assistant-ui/react
xpm add @assistant-ui/react
import {  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 zustand
yarn add @assistant-ui/react @assistant-ui/react-markdown remark-gfm zustand
pnpm add @assistant-ui/react @assistant-ui/react-markdown remark-gfm zustand
bun add @assistant-ui/react @assistant-ui/react-markdown remark-gfm zustand
xpm add @assistant-ui/react @assistant-ui/react-markdown remark-gfm zustand
import {  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-panels
yarn add @radix-ui/react-avatar @radix-ui/react-dialog @radix-ui/react-slot @radix-ui/react-tooltip react-resizable-panels
pnpm add @radix-ui/react-avatar @radix-ui/react-dialog @radix-ui/react-slot @radix-ui/react-tooltip react-resizable-panels
bun add @radix-ui/react-avatar @radix-ui/react-dialog @radix-ui/react-slot @radix-ui/react-tooltip react-resizable-panels
xpm add @radix-ui/react-avatar @radix-ui/react-dialog @radix-ui/react-slot @radix-ui/react-tooltip react-resizable-panels
import * 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

/app/page.tsx
import { AssistantSidebar } from "@/components/assistant-ui/assistant-sidebar";

export default function Home() {
  return (
    <div className="h-full">
      <AssistantSidebar>{/* your app */}</AssistantSidebar>
    </div>
  );
}