Get assistant-ui running in 5 minutes with npm and your first chat component.
Quick Start
The fastest way to get started with assistant-ui.

Initialize assistant-ui
Create a new project:
npx assistant-ui@latest createOr choose a template:
# Assistant Cloud - with persistence and thread management
npx assistant-ui@latest create -t cloud
# LangGraph
npx assistant-ui@latest create -t langgraph
# MCP support
npx assistant-ui@latest create -t mcpAdd to an existing project:
npx assistant-ui@latest initAdd API key
Create a .env file with your API key:
OPENAI_API_KEY="sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"Start the app
npm run devManual Setup
If you prefer not to use the CLI, you can install components manually.
Add assistant-ui
npx shadcn@latest add https://r.assistant-ui.com/thread.json https://r.assistant-ui.com/thread-list.jsonPrerequisites
- Setup React
- Setup shadcn/ui
Follow the manual installation guide to configure:
- Tailwind CSS
- TypeScript path aliases
- tw-animate-css
- The cn helper
- components.json (for CLI usage)
Main Component
npm install @assistant-ui/reactimport { ComposerAddAttachment, ComposerAttachments, UserMessageAttachments,} from "@/components/assistant-ui/attachment";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 { Button } from "@/components/ui/button";import { cn } from "@/lib/utils";import { ActionBarMorePrimitive, ActionBarPrimitive, AuiIf, BranchPickerPrimitive, ComposerPrimitive, ErrorPrimitive, MessagePrimitive, ThreadPrimitive,} from "@assistant-ui/react";import { ArrowDownIcon, ArrowUpIcon, CheckIcon, ChevronLeftIcon, ChevronRightIcon, CopyIcon, DownloadIcon, MoreHorizontalIcon, PencilIcon, RefreshCwIcon, SquareIcon,} from "lucide-react";import type { FC } from "react";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" > <AuiIf condition={({ thread }) => thread.isEmpty}> <ThreadWelcome /> </AuiIf> <ThreadPrimitive.Messages components={{ UserMessage, EditComposer, AssistantMessage, }} /> <ThreadPrimitive.ViewportFooter className="aui-thread-viewport-footer sticky bottom-0 mx-auto mt-auto flex w-full max-w-(--thread-max-width) flex-col gap-4 overflow-visible rounded-t-3xl 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 absolute -top-12 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-4"> <h1 className="aui-thread-welcome-message-inner fade-in slide-in-from-bottom-1 animate-in font-semibold text-2xl duration-200"> Hello there! </h1> <p className="aui-thread-welcome-message-inner fade-in slide-in-from-bottom-1 animate-in text-muted-foreground text-xl delay-75 duration-200"> How can I help you today? </p> </div> </div> <ThreadSuggestions /> </div> );};const SUGGESTIONS = [ { title: "What's the weather", label: "in San Francisco?", prompt: "What's the weather in San Francisco?", }, { title: "Explain React hooks", label: "like useState and useEffect", prompt: "Explain React hooks like useState and useEffect", },] as const;const ThreadSuggestions: FC = () => { return ( <div className="aui-thread-welcome-suggestions grid w-full @md:grid-cols-2 gap-2 pb-4"> {SUGGESTIONS.map((suggestion, index) => ( <div key={suggestion.prompt} className="aui-thread-welcome-suggestion-display fade-in slide-in-from-bottom-2 @md:nth-[n+3]:block nth-[n+3]:hidden animate-in fill-mode-both duration-200" style={{ animationDelay: `${100 + index * 50}ms` }} > <ThreadPrimitive.Suggestion prompt={suggestion.prompt} send asChild> <Button variant="ghost" className="aui-thread-welcome-suggestion h-auto w-full @md:flex-col flex-wrap items-start justify-start gap-1 rounded-2xl border px-4 py-3 text-left text-sm transition-colors hover:bg-muted" aria-label={suggestion.prompt} > <span className="aui-thread-welcome-suggestion-text-1 font-medium"> {suggestion.title} </span> <span className="aui-thread-welcome-suggestion-text-2 text-muted-foreground"> {suggestion.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-2xl border border-input px-1 pt-2 outline-none transition-shadow has-[textarea:focus-visible]:border-ring has-[textarea:focus-visible]:ring-2 has-[textarea:focus-visible]:ring-ring/20 data-[dragging=true]:border-ring data-[dragging=true]:border-dashed data-[dragging=true]:bg-accent/50"> <ComposerAttachments /> <ComposerPrimitive.Input placeholder="Send a message..." className="aui-composer-input mb-1 max-h-32 min-h-14 w-full resize-none bg-transparent px-4 pt-2 pb-3 text-sm 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-2 mb-2 flex items-center justify-between"> <ComposerAddAttachment /> <AuiIf condition={({ thread }) => !thread.isRunning}> <ComposerPrimitive.Send asChild> <TooltipIconButton tooltip="Send message" side="bottom" type="submit" variant="default" size="icon" className="aui-composer-send size-8 rounded-full" aria-label="Send message" > <ArrowUpIcon className="aui-composer-send-icon size-4" /> </TooltipIconButton> </ComposerPrimitive.Send> </AuiIf> <AuiIf condition={({ thread }) => thread.isRunning}> <ComposerPrimitive.Cancel asChild> <Button type="button" variant="default" size="icon" className="aui-composer-cancel size-8 rounded-full" aria-label="Stop generating" > <SquareIcon className="aui-composer-cancel-icon size-3 fill-current" /> </Button> </ComposerPrimitive.Cancel> </AuiIf> </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-3 duration-150" data-role="assistant" > <div className="aui-assistant-message-content wrap-break-word px-2 text-foreground leading-relaxed"> <MessagePrimitive.Parts components={{ Text: MarkdownText, tools: { Fallback: ToolFallback }, }} /> <MessageError /> </div> <div className="aui-assistant-message-footer mt-1 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 col-start-3 row-start-2 -ml-1 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"> <AuiIf condition={({ message }) => message.isCopied}> <CheckIcon /> </AuiIf> <AuiIf condition={({ message }) => !message.isCopied}> <CopyIcon /> </AuiIf> </TooltipIconButton> </ActionBarPrimitive.Copy> <ActionBarPrimitive.Reload asChild> <TooltipIconButton tooltip="Refresh"> <RefreshCwIcon /> </TooltipIconButton> </ActionBarPrimitive.Reload> <ActionBarMorePrimitive.Root> <ActionBarMorePrimitive.Trigger asChild> <TooltipIconButton tooltip="More" className="data-[state=open]:bg-accent" > <MoreHorizontalIcon /> </TooltipIconButton> </ActionBarMorePrimitive.Trigger> <ActionBarMorePrimitive.Content side="bottom" align="start" className="aui-action-bar-more-content z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md" > <ActionBarPrimitive.ExportMarkdown asChild> <ActionBarMorePrimitive.Item className="aui-action-bar-more-item flex cursor-pointer select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground"> <DownloadIcon className="size-4" /> Export as Markdown </ActionBarMorePrimitive.Item> </ActionBarPrimitive.ExportMarkdown> </ActionBarMorePrimitive.Content> </ActionBarMorePrimitive.Root> </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-3 duration-150 [&: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-2xl bg-muted px-4 py-2.5 text-foreground"> <MessagePrimitive.Parts /> </div> <div className="aui-user-action-bar-wrapper absolute top-1/2 left-0 -translate-x-full -translate-y-1/2 pr-2"> <UserActionBar /> </div> </div> <BranchPicker className="aui-user-branch-picker col-span-full col-start-1 row-start-3 -mr-1 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 px-2 py-3"> <ComposerPrimitive.Root className="aui-edit-composer-root ml-auto flex w-full max-w-[85%] flex-col rounded-2xl bg-muted"> <ComposerPrimitive.Input className="aui-edit-composer-input min-h-14 w-full resize-none bg-transparent p-4 text-foreground text-sm outline-none" autoFocus /> <div className="aui-edit-composer-footer mx-3 mb-3 flex items-center gap-2 self-end"> <ComposerPrimitive.Cancel asChild> <Button variant="ghost" size="sm"> Cancel </Button> </ComposerPrimitive.Cancel> <ComposerPrimitive.Send asChild> <Button size="sm">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 mr-2 -ml-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> );};import { Button } from "@/components/ui/button";import { Skeleton } from "@/components/ui/skeleton";import { AuiIf, ThreadListItemMorePrimitive, ThreadListItemPrimitive, ThreadListPrimitive,} from "@assistant-ui/react";import { ArchiveIcon, MoreHorizontalIcon, PlusIcon } from "lucide-react";import type { FC } from "react";export const ThreadList: FC = () => { return ( <ThreadListPrimitive.Root className="aui-root aui-thread-list-root flex flex-col gap-1"> <ThreadListNew /> <AuiIf condition={({ threads }) => threads.isLoading}> <ThreadListSkeleton /> </AuiIf> <AuiIf condition={({ threads }) => !threads.isLoading}> <ThreadListPrimitive.Items components={{ ThreadListItem }} /> </AuiIf> </ThreadListPrimitive.Root> );};const ThreadListNew: FC = () => { return ( <ThreadListPrimitive.New asChild> <Button variant="outline" className="aui-thread-list-new h-9 justify-start gap-2 rounded-lg px-3 text-sm hover:bg-muted data-active:bg-muted" > <PlusIcon className="size-4" /> New Thread </Button> </ThreadListPrimitive.New> );};const ThreadListSkeleton: FC = () => { return ( <div className="flex flex-col gap-1"> {Array.from({ length: 5 }, (_, i) => ( <div key={i} role="status" aria-label="Loading threads" className="aui-thread-list-skeleton-wrapper flex h-9 items-center px-3" > <Skeleton className="aui-thread-list-skeleton h-4 w-full" /> </div> ))} </div> );};const ThreadListItem: FC = () => { return ( <ThreadListItemPrimitive.Root className="aui-thread-list-item group flex h-9 items-center gap-2 rounded-lg transition-colors hover:bg-muted focus-visible:bg-muted focus-visible:outline-none data-active:bg-muted"> <ThreadListItemPrimitive.Trigger className="aui-thread-list-item-trigger flex h-full min-w-0 flex-1 items-center truncate px-3 text-start text-sm"> <ThreadListItemPrimitive.Title fallback="New Chat" /> </ThreadListItemPrimitive.Trigger> <ThreadListItemMore /> </ThreadListItemPrimitive.Root> );};const ThreadListItemMore: FC = () => { return ( <ThreadListItemMorePrimitive.Root> <ThreadListItemMorePrimitive.Trigger asChild> <Button variant="ghost" size="icon" className="aui-thread-list-item-more mr-2 size-7 p-0 opacity-0 transition-opacity group-hover:opacity-100 data-[state=open]:bg-accent data-[state=open]:opacity-100 group-data-active:opacity-100" > <MoreHorizontalIcon className="size-4" /> <span className="sr-only">More options</span> </Button> </ThreadListItemMorePrimitive.Trigger> <ThreadListItemMorePrimitive.Content side="bottom" align="start" className="aui-thread-list-item-more-content z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md" > <ThreadListItemPrimitive.Archive asChild> <ThreadListItemMorePrimitive.Item className="aui-thread-list-item-more-item flex cursor-pointer select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground"> <ArchiveIcon className="size-4" /> Archive </ThreadListItemMorePrimitive.Item> </ThreadListItemPrimitive.Archive> </ThreadListItemMorePrimitive.Content> </ThreadListItemMorePrimitive.Root> );};assistant-ui dependencies
npm install @assistant-ui/react @assistant-ui/react-markdown remark-gfm zustand"use client";import { PropsWithChildren, useEffect, useState, type FC } from "react";import { XIcon, PlusIcon, FileText } from "lucide-react";import { AttachmentPrimitive, ComposerPrimitive, MessagePrimitive, useAuiState, useAui,} 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 } = useAuiState( 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 ( <img src={src} alt="Image Preview" className={cn( "block h-auto max-h-[80vh] w-auto max-w-full object-contain", isLoaded ? "aui-attachment-preview-image-loaded" : "aui-attachment-preview-image-loading invisible", )} onLoad={() => setIsLoaded(true)} /> );};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 = useAuiState(({ 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 aui = useAui(); const isComposer = aui.attachment.source === "composer"; const isImage = useAuiState(({ attachment }) => attachment.type === "image"); const typeLabel = useAuiState(({ 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-8.5 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-2.5 flex items-center justify-between rounded-t-lg border border-border/50 border-b-0 bg-muted/50 px-3 py-1.5 text-xs"> <span className="aui-code-header-language font-medium text-muted-foreground lowercase"> {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-2 scroll-m-20 font-semibold text-base first:mt-0 last:mb-0", className, )} {...props} /> ), h2: ({ className, ...props }) => ( <h2 className={cn( "aui-md-h2 mt-3 mb-1.5 scroll-m-20 font-semibold text-sm first:mt-0 last:mb-0", className, )} {...props} /> ), h3: ({ className, ...props }) => ( <h3 className={cn( "aui-md-h3 mt-2.5 mb-1 scroll-m-20 font-semibold text-sm first:mt-0 last:mb-0", className, )} {...props} /> ), h4: ({ className, ...props }) => ( <h4 className={cn( "aui-md-h4 mt-2 mb-1 scroll-m-20 font-medium text-sm first:mt-0 last:mb-0", className, )} {...props} /> ), h5: ({ className, ...props }) => ( <h5 className={cn( "aui-md-h5 mt-2 mb-1 font-medium text-sm first:mt-0 last:mb-0", className, )} {...props} /> ), h6: ({ className, ...props }) => ( <h6 className={cn( "aui-md-h6 mt-2 mb-1 font-medium text-sm first:mt-0 last:mb-0", className, )} {...props} /> ), p: ({ className, ...props }) => ( <p className={cn( "aui-md-p my-2.5 leading-normal first:mt-0 last:mb-0", className, )} {...props} /> ), a: ({ className, ...props }) => ( <a className={cn( "aui-md-a text-primary underline underline-offset-2 hover:text-primary/80", className, )} {...props} /> ), blockquote: ({ className, ...props }) => ( <blockquote className={cn( "aui-md-blockquote my-2.5 border-muted-foreground/30 border-l-2 pl-3 text-muted-foreground italic", className, )} {...props} /> ), ul: ({ className, ...props }) => ( <ul className={cn( "aui-md-ul my-2 ml-4 list-disc marker:text-muted-foreground [&>li]:mt-1", className, )} {...props} /> ), ol: ({ className, ...props }) => ( <ol className={cn( "aui-md-ol my-2 ml-4 list-decimal marker:text-muted-foreground [&>li]:mt-1", className, )} {...props} /> ), hr: ({ className, ...props }) => ( <hr className={cn("aui-md-hr my-2 border-muted-foreground/20", className)} {...props} /> ), table: ({ className, ...props }) => ( <table className={cn( "aui-md-table my-2 w-full border-separate border-spacing-0 overflow-y-auto", className, )} {...props} /> ), th: ({ className, ...props }) => ( <th className={cn( "aui-md-th bg-muted px-2 py-1 text-left font-medium 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-muted-foreground/20 border-b border-l px-2 py-1 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} /> ), li: ({ className, ...props }) => ( <li className={cn("aui-md-li leading-normal", 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 border border-border/50 border-t-0 bg-muted/30 p-3 text-xs leading-relaxed", className, )} {...props} /> ), code: function Code({ className, ...props }) { const isCodeBlock = useIsMarkdownCodeBlock(); return ( <code className={cn( !isCodeBlock && "aui-md-inline-code rounded-md border border-border/50 bg-muted/50 px-1.5 py-0.5 font-mono text-[0.85em]", className, )} {...props} /> ); }, CodeHeader,});"use client";import { memo, useCallback, useRef, useState } from "react";import { AlertCircleIcon, CheckIcon, ChevronDownIcon, LoaderIcon, XCircleIcon,} from "lucide-react";import { useScrollLock, type ToolCallMessagePartStatus, type ToolCallMessagePartComponent,} from "@assistant-ui/react";import { Collapsible, CollapsibleContent, CollapsibleTrigger,} from "@/components/ui/collapsible";import { cn } from "@/lib/utils";const ANIMATION_DURATION = 200;export type ToolFallbackRootProps = Omit< React.ComponentProps<typeof Collapsible>, "open" | "onOpenChange"> & { open?: boolean; onOpenChange?: (open: boolean) => void; defaultOpen?: boolean;};function ToolFallbackRoot({ className, open: controlledOpen, onOpenChange: controlledOnOpenChange, defaultOpen = false, children, ...props}: ToolFallbackRootProps) { const collapsibleRef = useRef<HTMLDivElement>(null); const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen); const lockScroll = useScrollLock(collapsibleRef, ANIMATION_DURATION); const isControlled = controlledOpen !== undefined; const isOpen = isControlled ? controlledOpen : uncontrolledOpen; const handleOpenChange = useCallback( (open: boolean) => { if (!open) { lockScroll(); } if (!isControlled) { setUncontrolledOpen(open); } controlledOnOpenChange?.(open); }, [lockScroll, isControlled, controlledOnOpenChange], ); return ( <Collapsible ref={collapsibleRef} data-slot="tool-fallback-root" open={isOpen} onOpenChange={handleOpenChange} className={cn( "aui-tool-fallback-root group/tool-fallback-root w-full rounded-lg border py-3", className, )} style={ { "--animation-duration": `${ANIMATION_DURATION}ms`, } as React.CSSProperties } {...props} > {children} </Collapsible> );}type ToolStatus = ToolCallMessagePartStatus["type"];const statusIconMap: Record<ToolStatus, React.ElementType> = { running: LoaderIcon, complete: CheckIcon, incomplete: XCircleIcon, "requires-action": AlertCircleIcon,};function ToolFallbackTrigger({ toolName, status, className, ...props}: React.ComponentProps<typeof CollapsibleTrigger> & { toolName: string; status?: ToolCallMessagePartStatus;}) { const statusType = status?.type ?? "complete"; const isRunning = statusType === "running"; const isCancelled = status?.type === "incomplete" && status.reason === "cancelled"; const Icon = statusIconMap[statusType]; const label = isCancelled ? "Cancelled tool" : "Used tool"; return ( <CollapsibleTrigger data-slot="tool-fallback-trigger" className={cn( "aui-tool-fallback-trigger group/trigger flex w-full items-center gap-2 px-4 text-sm transition-colors", className, )} {...props} > <Icon data-slot="tool-fallback-trigger-icon" className={cn( "aui-tool-fallback-trigger-icon size-4 shrink-0", isCancelled && "text-muted-foreground", isRunning && "animate-spin", )} /> <span data-slot="tool-fallback-trigger-label" className={cn( "aui-tool-fallback-trigger-label-wrapper relative inline-block grow text-left leading-none", isCancelled && "text-muted-foreground line-through", )} > <span> {label}: <b>{toolName}</b> </span> {isRunning && ( <span aria-hidden data-slot="tool-fallback-trigger-shimmer" className="aui-tool-fallback-trigger-shimmer shimmer pointer-events-none absolute inset-0 motion-reduce:animate-none" > {label}: <b>{toolName}</b> </span> )} </span> <ChevronDownIcon data-slot="tool-fallback-trigger-chevron" className={cn( "aui-tool-fallback-trigger-chevron size-4 shrink-0", "transition-transform duration-(--animation-duration) ease-out", "group-data-[state=closed]/trigger:-rotate-90", "group-data-[state=open]/trigger:rotate-0", )} /> </CollapsibleTrigger> );}function ToolFallbackContent({ className, children, ...props}: React.ComponentProps<typeof CollapsibleContent>) { return ( <CollapsibleContent data-slot="tool-fallback-content" className={cn( "aui-tool-fallback-content relative overflow-hidden text-sm outline-none", "group/collapsible-content ease-out", "data-[state=closed]:animate-collapsible-up", "data-[state=open]:animate-collapsible-down", "data-[state=closed]:fill-mode-forwards", "data-[state=closed]:pointer-events-none", "data-[state=open]:duration-(--animation-duration)", "data-[state=closed]:duration-(--animation-duration)", className, )} {...props} > <div className="mt-3 flex flex-col gap-2 border-t pt-2">{children}</div> </CollapsibleContent> );}function ToolFallbackArgs({ argsText, className, ...props}: React.ComponentProps<"div"> & { argsText?: string;}) { if (!argsText) return null; return ( <div data-slot="tool-fallback-args" className={cn("aui-tool-fallback-args px-4", className)} {...props} > <pre className="aui-tool-fallback-args-value whitespace-pre-wrap"> {argsText} </pre> </div> );}function ToolFallbackResult({ result, className, ...props}: React.ComponentProps<"div"> & { result?: unknown;}) { if (result === undefined) return null; return ( <div data-slot="tool-fallback-result" className={cn( "aui-tool-fallback-result border-t border-dashed px-4 pt-2", className, )} {...props} > <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> );}function ToolFallbackError({ status, className, ...props}: React.ComponentProps<"div"> & { status?: ToolCallMessagePartStatus;}) { if (status?.type !== "incomplete") return null; const error = status.error; const errorText = error ? typeof error === "string" ? error : JSON.stringify(error) : null; if (!errorText) return null; const isCancelled = status.reason === "cancelled"; const headerText = isCancelled ? "Cancelled reason:" : "Error:"; return ( <div data-slot="tool-fallback-error" className={cn("aui-tool-fallback-error px-4", className)} {...props} > <p className="aui-tool-fallback-error-header font-semibold text-muted-foreground"> {headerText} </p> <p className="aui-tool-fallback-error-reason text-muted-foreground"> {errorText} </p> </div> );}const ToolFallbackImpl: ToolCallMessagePartComponent = ({ toolName, argsText, result, status,}) => { const isCancelled = status?.type === "incomplete" && status.reason === "cancelled"; return ( <ToolFallbackRoot className={cn(isCancelled && "border-muted-foreground/30 bg-muted/30")} > <ToolFallbackTrigger toolName={toolName} status={status} /> <ToolFallbackContent> <ToolFallbackError status={status} /> <ToolFallbackArgs argsText={argsText} className={cn(isCancelled && "opacity-60")} /> {!isCancelled && <ToolFallbackResult result={result} />} </ToolFallbackContent> </ToolFallbackRoot> );};const ToolFallback = memo( ToolFallbackImpl,) as unknown as ToolCallMessagePartComponent & { Root: typeof ToolFallbackRoot; Trigger: typeof ToolFallbackTrigger; Content: typeof ToolFallbackContent; Args: typeof ToolFallbackArgs; Result: typeof ToolFallbackResult; Error: typeof ToolFallbackError;};ToolFallback.displayName = "ToolFallback";ToolFallback.Root = ToolFallbackRoot;ToolFallback.Trigger = ToolFallbackTrigger;ToolFallback.Content = ToolFallbackContent;ToolFallback.Args = ToolFallbackArgs;ToolFallback.Result = ToolFallbackResult;ToolFallback.Error = ToolFallbackError;export { ToolFallback, ToolFallbackRoot, ToolFallbackTrigger, ToolFallbackContent, ToolFallbackArgs, ToolFallbackResult, ToolFallbackError,};Setup Backend Endpoint
Install provider SDK:
npm install ai @assistant-ui/react-ai-sdk @ai-sdk/openainpm install ai @assistant-ui/react-ai-sdk @ai-sdk/anthropicnpm install ai @assistant-ui/react-ai-sdk @ai-sdk/azurenpm install ai @assistant-ui/react-ai-sdk @ai-sdk/amazon-bedrocknpm install ai @assistant-ui/react-ai-sdk @ai-sdk/googlenpm install ai @assistant-ui/react-ai-sdk @ai-sdk/google-vertexnpm install ai @assistant-ui/react-ai-sdk @ai-sdk/openainpm install ai @assistant-ui/react-ai-sdk @ai-sdk/openainpm install ai @assistant-ui/react-ai-sdk @ai-sdk/coherenpm install ai @assistant-ui/react-ai-sdk ollama-ai-provider-v2npm install ai @assistant-ui/react-ai-sdk chrome-aiAdd an API endpoint:
import { openai } from "@ai-sdk/openai";
import { convertToModelMessages, streamText } from "ai";
export const maxDuration = 30;
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: openai("gpt-4o-mini"),
messages: convertToModelMessages(messages),
});
return result.toUIMessageStreamResponse();
}import { anthropic } from "@ai-sdk/anthropic";
import { convertToModelMessages, streamText } from "ai";
export const maxDuration = 30;
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: anthropic("claude-3-5-sonnet-20240620"),
messages: convertToModelMessages(messages),
});
return result.toUIMessageStreamResponse();
}import { azure } from "@ai-sdk/azure";
import { convertToModelMessages, streamText } from "ai";
export const maxDuration = 30;
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: azure("your-deployment-name"),
messages: convertToModelMessages(messages),
});
return result.toUIMessageStreamResponse();
}import { bedrock } from "@ai-sdk/amazon-bedrock";
import { convertToModelMessages, streamText } from "ai";
export const maxDuration = 30;
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: bedrock("anthropic.claude-3-5-sonnet-20240620-v1:0"),
messages: convertToModelMessages(messages),
});
return result.toUIMessageStreamResponse();
}import { google } from "@ai-sdk/google";
import { convertToModelMessages, streamText } from "ai";
export const maxDuration = 30;
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: google("gemini-2.0-flash"),
messages: convertToModelMessages(messages),
});
return result.toUIMessageStreamResponse();
}import { vertex } from "@ai-sdk/google-vertex";
import { convertToModelMessages, streamText } from "ai";
export const maxDuration = 30;
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: vertex("gemini-1.5-pro"),
messages: convertToModelMessages(messages),
});
return result.toUIMessageStreamResponse();
}import { createOpenAI } from "@ai-sdk/openai";
import { convertToModelMessages, streamText } from "ai";
export const maxDuration = 30;
const groq = createOpenAI({
apiKey: process.env.GROQ_API_KEY ?? "",
baseURL: "https://api.groq.com/openai/v1",
});
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: groq("llama3-70b-8192"),
messages: convertToModelMessages(messages),
});
return result.toUIMessageStreamResponse();
}import { createOpenAI } from "@ai-sdk/openai";
import { convertToModelMessages, streamText } from "ai";
export const maxDuration = 30;
const fireworks = createOpenAI({
apiKey: process.env.FIREWORKS_API_KEY ?? "",
baseURL: "https://api.fireworks.ai/inference/v1",
});
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: fireworks("accounts/fireworks/models/firefunction-v2"),
messages: convertToModelMessages(messages),
});
return result.toUIMessageStreamResponse();
}import { cohere } from "@ai-sdk/cohere";
import { convertToModelMessages, streamText } from "ai";
export const maxDuration = 30;
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: cohere("command-r-plus"),
messages: convertToModelMessages(messages),
});
return result.toUIMessageStreamResponse();
}import { ollama } from "ollama-ai-provider-v2";
import { convertToModelMessages, streamText } from "ai";
export const maxDuration = 30;
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: ollama("llama3"),
messages: convertToModelMessages(messages),
});
return result.toUIMessageStreamResponse();
}import { chromeai } from "chrome-ai";
import { convertToModelMessages, streamText } from "ai";
export const maxDuration = 30;
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: chromeai(),
messages: convertToModelMessages(messages),
});
return result.toUIMessageStreamResponse();
}Define environment variables:
OPENAI_API_KEY="sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"ANTHROPIC_API_KEY="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"AZURE_RESOURCE_NAME="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
AZURE_API_KEY="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"AWS_ACCESS_KEY_ID="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
AWS_SECRET_ACCESS_KEY="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
AWS_REGION="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"GOOGLE_GENERATIVE_AI_API_KEY="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"GOOGLE_VERTEX_PROJECT="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
GOOGLE_VERTEX_LOCATION="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
GOOGLE_APPLICATION_CREDENTIALS="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"GROQ_API_KEY="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"FIREWORKS_API_KEY="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"COHERE_API_KEY="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"<none><none>If you aren't using Next.js, you can also deploy this endpoint to Cloudflare Workers, or any other serverless platform.
Use it in your app
import { AssistantRuntimeProvider } from "@assistant-ui/react";
import { useChatRuntime, AssistantChatTransport } from "@assistant-ui/react-ai-sdk";
import { ThreadList } from "@/components/assistant-ui/thread-list";
import { Thread } from "@/components/assistant-ui/thread";
const MyApp = () => {
const runtime = useChatRuntime({
transport: new AssistantChatTransport({
api: "/api/chat",
}),
});
return (
<AssistantRuntimeProvider runtime={runtime}>
<div>
<ThreadList />
<Thread />
</div>
</AssistantRuntimeProvider>
);
};// run `npx shadcn@latest add https://r.assistant-ui.com/assistant-modal.json`
import { AssistantRuntimeProvider } from "@assistant-ui/react";
import { useChatRuntime, AssistantChatTransport } from "@assistant-ui/react-ai-sdk";
import { AssistantModal } from "@/components/assistant-ui/assistant-modal";
const MyApp = () => {
const runtime = useChatRuntime({
transport: new AssistantChatTransport({
api: "/api/chat",
}),
});
return (
<AssistantRuntimeProvider runtime={runtime}>
<AssistantModal />
</AssistantRuntimeProvider>
);
};