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:
# Minimal starter
npx assistant-ui@latest create -t minimal
# Assistant Cloud - with persistence and thread management
npx assistant-ui@latest create -t cloud
# Assistant Cloud + Clerk authentication
npx assistant-ui@latest create -t cloud-clerk
# LangGraph starter template
npx assistant-ui@latest create -t langgraph
# MCP starter template
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 { Reasoning, ReasoningContent, ReasoningRoot, ReasoningText, ReasoningTrigger,} from "@/components/assistant-ui/reasoning";import { ToolFallback } from "@/components/assistant-ui/tool-fallback";import { ToolGroupContent, ToolGroupRoot, ToolGroupTrigger,} from "@/components/assistant-ui/tool-group";import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";import { Button } from "@/components/ui/button";import { cn } from "@/lib/utils";import { ActionBarMorePrimitive, ActionBarPrimitive, AuiIf, type AssistantState, BranchPickerPrimitive, ComposerPrimitive, ErrorPrimitive, groupPartByType, MessagePrimitive, SuggestionPrimitive, ThreadPrimitive, type ToolCallMessagePartComponent, useAuiState,} from "@assistant-ui/react";import { ArrowDownIcon, ArrowUpIcon, CheckIcon, ChevronLeftIcon, ChevronRightIcon, CopyIcon, DownloadIcon, MicIcon, MoreHorizontalIcon, PencilIcon, RefreshCwIcon, SquareIcon,} from "lucide-react";import { createContext, useContext, type ComponentType, type FC, type PropsWithChildren,} from "react";export type ThreadGroupPart = MessagePrimitive.GroupedParts.GroupPart;/** * Optional component overrides for the thread. `AssistantMessage` and * `Welcome` replace whole sections; the remaining slots override how the * assistant message renders tool calls and part groups. Tool UIs registered * by name (toolkit `render`, `useAssistantDataUI`) take precedence over * `ToolFallback`. */export type ThreadComponents = { AssistantMessage?: ComponentType | undefined; Welcome?: ComponentType | undefined; ToolFallback?: ToolCallMessagePartComponent | undefined; ToolGroup?: | ComponentType<PropsWithChildren<{ group: ThreadGroupPart }>> | undefined; ReasoningGroup?: | ComponentType<PropsWithChildren<{ group: ThreadGroupPart }>> | undefined;};export type ThreadProps = { components?: ThreadComponents | undefined;};const EMPTY_COMPONENTS: ThreadComponents = {};const ThreadComponentsContext = createContext<ThreadComponents>(EMPTY_COMPONENTS);// Startup exposes a loading placeholder thread; treat it as a new chat so// the composer mounts centered. Loads after startup keep the docked layout.const isNewChatView = (s: AssistantState) => s.thread.messages.length === 0 && (!s.thread.isLoading || s.threads.isLoading);export const Thread: FC<ThreadProps> = ({ components = EMPTY_COMPONENTS }) => { const isEmpty = useAuiState(isNewChatView); return ( <ThreadComponentsContext.Provider value={components}> <ThreadRoot isEmpty={isEmpty} /> </ThreadComponentsContext.Provider> );};const ThreadRoot: FC<{ isEmpty: boolean }> = ({ isEmpty }) => { const { Welcome = ThreadWelcome } = useContext(ThreadComponentsContext); return ( <ThreadPrimitive.Root className="aui-root aui-thread-root bg-background @container flex h-full flex-col" style={{ ["--thread-max-width" as string]: "44rem", ["--composer-padding" as string]: "8px", }} > <ThreadPrimitive.Viewport turnAnchor="top" data-slot="aui_thread-viewport" className="relative flex flex-1 flex-col overflow-x-auto overflow-y-scroll scroll-smooth" > <div className={cn( "mx-auto flex w-full max-w-(--thread-max-width) flex-1 flex-col px-4 pt-4", isEmpty && "justify-center", )} > <AuiIf condition={isNewChatView}> <Welcome /> </AuiIf> <div data-slot="aui_message-group" className="mb-14 flex flex-col gap-y-6 empty:hidden" > <ThreadPrimitive.Messages> {() => <ThreadMessage />} </ThreadPrimitive.Messages> </div> <ThreadPrimitive.ViewportFooter className={cn( "aui-thread-viewport-footer bg-background flex flex-col gap-4 overflow-visible pb-4 md:pb-6", !isEmpty && "sticky bottom-0 mt-auto rounded-t-xl", )} > <ThreadScrollToBottom /> <Composer /> <AuiIf condition={(s) => isNewChatView(s) && s.composer.isEmpty}> <ThreadSuggestions /> </AuiIf> </ThreadPrimitive.ViewportFooter> </div> </ThreadPrimitive.Viewport> </ThreadPrimitive.Root> );};const ThreadMessage: FC = () => { const { AssistantMessage: AssistantMessageComponent = AssistantMessage } = useContext(ThreadComponentsContext); const role = useAuiState((s) => s.message.role); const isEditing = useAuiState((s) => s.message.composer.isEditing); if (isEditing) return <EditComposer />; if (role === "user") return <UserMessage />; return <AssistantMessageComponent />;};const ThreadScrollToBottom: FC = () => { return ( <ThreadPrimitive.ScrollToBottom asChild> <TooltipIconButton tooltip="Scroll to bottom" variant="outline" className="aui-thread-scroll-to-bottom dark:border-border dark:bg-background dark:hover:bg-accent absolute -top-12 z-10 self-center rounded-full p-4 disabled:invisible" > <ArrowDownIcon /> </TooltipIconButton> </ThreadPrimitive.ScrollToBottom> );};const ThreadWelcome: FC = () => { return ( <div className="aui-thread-welcome-root mb-6 flex flex-col items-center px-4 text-center"> <h1 className="aui-thread-welcome-message-inner fade-in slide-in-from-bottom-1 animate-in fill-mode-both text-2xl font-semibold duration-200"> How can I help you today? </h1> </div> );};const ThreadSuggestions: FC = () => { return ( <div className="aui-thread-welcome-suggestions flex w-full flex-wrap items-center justify-center gap-2 px-4"> <ThreadPrimitive.Suggestions> {() => <ThreadSuggestionItem />} </ThreadPrimitive.Suggestions> </div> );};const ThreadSuggestionItem: FC = () => { return ( <div className="aui-thread-welcome-suggestion-display fade-in slide-in-from-bottom-2 animate-in fill-mode-both duration-200"> <SuggestionPrimitive.Trigger send asChild> <Button variant="ghost" className="aui-thread-welcome-suggestion text-foreground hover:bg-muted border-border/60 h-auto gap-1.5 rounded-full border px-3.5 py-1.5 text-sm font-normal whitespace-nowrap transition-colors" > <SuggestionPrimitive.Title className="aui-thread-welcome-suggestion-text-1" /> <SuggestionPrimitive.Description className="aui-thread-welcome-suggestion-text-2 empty:hidden" /> </Button> </SuggestionPrimitive.Trigger> </div> );};const Composer: FC = () => { return ( <ComposerPrimitive.Root className="aui-composer-root relative flex w-full flex-col"> <ComposerPrimitive.AttachmentDropzone asChild> <div data-slot="aui_composer-shell" className="bg-background border-border/60 data-[dragging=true]:border-ring data-[dragging=true]:bg-accent/50 focus-within:border-border dark:border-muted-foreground/15 dark:bg-muted/30 dark:focus-within:border-muted-foreground/30 flex w-full flex-col gap-2 rounded-3xl border p-(--composer-padding) shadow-[0_4px_16px_-8px_rgba(0,0,0,0.08),0_1px_2px_rgba(0,0,0,0.04)] transition-[border-color,box-shadow] focus-within:shadow-[0_6px_24px_-8px_rgba(0,0,0,0.12),0_1px_2px_rgba(0,0,0,0.05)] data-[dragging=true]:border-dashed dark:shadow-none" > <ComposerAttachments /> <ComposerPrimitive.Input placeholder="Send a message..." className="aui-composer-input placeholder:text-muted-foreground/80 max-h-32 min-h-10 w-full resize-none bg-transparent px-2.5 py-1 text-base outline-none" rows={1} autoFocus aria-label="Message input" /> <ComposerAction /> </div> </ComposerPrimitive.AttachmentDropzone> </ComposerPrimitive.Root> );};const ComposerAction: FC = () => { return ( <div className="aui-composer-action-wrapper relative flex items-center justify-between"> <ComposerAddAttachment /> <div className="flex items-center gap-1.5"> <AuiIf condition={(s) => s.thread.capabilities.dictation}> <AuiIf condition={(s) => s.composer.dictation == null}> <ComposerPrimitive.Dictate asChild> <TooltipIconButton tooltip="Voice input" side="bottom" type="button" variant="ghost" size="icon" className="aui-composer-dictate size-7 rounded-full" aria-label="Start voice input" > <MicIcon className="aui-composer-dictate-icon size-4" /> </TooltipIconButton> </ComposerPrimitive.Dictate> </AuiIf> <AuiIf condition={(s) => s.composer.dictation != null}> <ComposerPrimitive.StopDictation asChild> <TooltipIconButton tooltip="Stop dictation" side="bottom" type="button" variant="ghost" size="icon" className="aui-composer-stop-dictation text-destructive size-7 rounded-full" aria-label="Stop voice input" > <SquareIcon className="aui-composer-stop-dictation-icon size-3.5 animate-pulse fill-current" /> </TooltipIconButton> </ComposerPrimitive.StopDictation> </AuiIf> </AuiIf> <AuiIf condition={(s) => !s.thread.isRunning}> <ComposerPrimitive.Send asChild> <TooltipIconButton tooltip="Send message" side="bottom" type="button" variant="default" size="icon" className="aui-composer-send size-7 rounded-full" aria-label="Send message" > <ArrowUpIcon className="aui-composer-send-icon size-4.5" /> </TooltipIconButton> </ComposerPrimitive.Send> </AuiIf> <AuiIf condition={(s) => s.thread.isRunning}> <ComposerPrimitive.Cancel asChild> <Button type="button" variant="default" size="icon" className="aui-composer-cancel size-7 rounded-full" aria-label="Stop generating" > <SquareIcon className="aui-composer-cancel-icon size-3.5 fill-current" /> </Button> </ComposerPrimitive.Cancel> </AuiIf> </div> </div> );};const MessageError: FC = () => { return ( <MessagePrimitive.Error> <ErrorPrimitive.Root className="aui-message-error-root border-destructive bg-destructive/10 text-destructive dark:bg-destructive/5 mt-2 rounded-md border p-3 text-sm dark:text-red-200"> <ErrorPrimitive.Message className="aui-message-error-message line-clamp-2" /> </ErrorPrimitive.Root> </MessagePrimitive.Error> );};const AssistantMessage: FC = () => { const { ToolFallback: ToolFallbackComponent = ToolFallback, ToolGroup, ReasoningGroup, } = useContext(ThreadComponentsContext); // reserves space for action bar and compensates with `-mb` for consistent msg spacing // keeps hovered action bar from shifting layout (autohide doesn't support absolute positioning well) // for pt-[n] use -mb-[n + 6] & min-h-[n + 6] to preserve compensation const ACTION_BAR_PT = "pt-1.5"; const ACTION_BAR_HEIGHT = `-mb-7.5 min-h-7.5 ${ACTION_BAR_PT}`; return ( <MessagePrimitive.Root data-slot="aui_assistant-message-root" data-role="assistant" className="fade-in slide-in-from-bottom-1 animate-in relative duration-150" > <div data-slot="aui_assistant-message-content" // [contain-intrinsic-size:auto_24px] fixes issue #4104, don't change without checking for regressions className="text-foreground px-2 leading-relaxed wrap-break-word [contain-intrinsic-size:auto_24px] [content-visibility:auto]" > <MessagePrimitive.GroupedParts groupBy={groupPartByType({ reasoning: ["group-chainOfThought", "group-reasoning"], "tool-call": ["group-chainOfThought", "group-tool"], "standalone-tool-call": [], })} > {({ part, children }) => { switch (part.type) { case "group-chainOfThought": return <div data-slot="aui_chain-of-thought">{children}</div>; case "group-tool": if (ToolGroup) { return <ToolGroup group={part}>{children}</ToolGroup>; } return ( <ToolGroupRoot variant="ghost"> <ToolGroupTrigger count={part.indices.length} active={part.status.type === "running"} /> <ToolGroupContent>{children}</ToolGroupContent> </ToolGroupRoot> ); case "group-reasoning": { if (ReasoningGroup) { return ( <ReasoningGroup group={part}>{children}</ReasoningGroup> ); } const running = part.status.type === "running"; return ( <ReasoningRoot streaming={running}> <ReasoningTrigger active={running} /> <ReasoningContent aria-busy={running}> <ReasoningText>{children}</ReasoningText> </ReasoningContent> </ReasoningRoot> ); } case "text": return <MarkdownText />; case "reasoning": return <Reasoning {...part} />; case "tool-call": return part.toolUI ?? <ToolFallbackComponent {...part} />; case "data": return part.dataRendererUI; case "indicator": return ( <span data-slot="aui_assistant-message-indicator" className="animate-pulse font-sans" aria-label="Assistant is working" > {"●"} </span> ); default: return null; } }} </MessagePrimitive.GroupedParts> <MessageError /> </div> <div data-slot="aui_assistant-message-footer" className={cn("ms-2 flex items-center", ACTION_BAR_HEIGHT)} > <BranchPicker /> <AssistantActionBar /> </div> </MessagePrimitive.Root> );};const AssistantActionBar: FC = () => { return ( <ActionBarPrimitive.Root hideWhenRunning autohide="not-last" className="aui-assistant-action-bar-root text-muted-foreground animate-in fade-in col-start-3 row-start-2 -ms-1 flex gap-1 duration-200" > <ActionBarPrimitive.Copy asChild> <TooltipIconButton tooltip="Copy"> <AuiIf condition={(s) => s.message.isCopied}> <CheckIcon className="animate-in zoom-in-50 fade-in duration-200 ease-out" /> </AuiIf> <AuiIf condition={(s) => !s.message.isCopied}> <CopyIcon className="animate-in zoom-in-75 fade-in duration-150" /> </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" sideOffset={6} className="aui-action-bar-more-content bg-popover/95 text-popover-foreground data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=closed]:animate-out 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 z-50 min-w-[8rem] overflow-hidden rounded-xl border p-1.5 shadow-lg backdrop-blur-sm" > <ActionBarPrimitive.ExportMarkdown asChild> <ActionBarMorePrimitive.Item className="aui-action-bar-more-item hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground flex cursor-pointer items-center gap-2 rounded-lg px-2.5 py-1.5 text-sm outline-none select-none"> <DownloadIcon className="size-4" /> Export as Markdown </ActionBarMorePrimitive.Item> </ActionBarPrimitive.ExportMarkdown> </ActionBarMorePrimitive.Content> </ActionBarMorePrimitive.Root> </ActionBarPrimitive.Root> );};const UserMessage: FC = () => { return ( <MessagePrimitive.Root data-slot="aui_user-message-root" className="fade-in slide-in-from-bottom-1 animate-in grid auto-rows-auto grid-cols-[minmax(72px,1fr)_auto] content-start gap-y-2 px-2 duration-150 [contain-intrinsic-size:auto_60px] [content-visibility:auto] [&: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 peer bg-muted text-foreground rounded-xl px-4 py-2 wrap-break-word empty:hidden"> <MessagePrimitive.Parts /> </div> <div className="aui-user-action-bar-wrapper absolute start-0 top-1/2 -translate-x-full -translate-y-1/2 pe-2 peer-empty:hidden rtl:translate-x-full"> <UserActionBar /> </div> </div> <BranchPicker data-slot="aui_user-branch-picker" className="col-span-full col-start-1 row-start-3 -me-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"> <PencilIcon /> </TooltipIconButton> </ActionBarPrimitive.Edit> </ActionBarPrimitive.Root> );};const EditComposer: FC = () => { return ( <MessagePrimitive.Root data-slot="aui_edit-composer-wrapper" className="flex flex-col px-2" > <ComposerPrimitive.Root className="aui-edit-composer-root bg-background border-border/60 dark:border-muted-foreground/15 dark:bg-muted/30 ms-auto flex w-full max-w-[85%] flex-col rounded-3xl border shadow-[0_4px_16px_-8px_rgba(0,0,0,0.08),0_1px_2px_rgba(0,0,0,0.04)] dark:shadow-none"> <ComposerPrimitive.Input className="aui-edit-composer-input text-foreground min-h-14 w-full resize-none bg-transparent px-4 pt-3 pb-1 text-base outline-none" autoFocus /> <div className="aui-edit-composer-footer mx-2.5 mb-2.5 flex items-center gap-1.5 self-end"> <ComposerPrimitive.Cancel asChild> <Button variant="ghost" size="sm" className="h-8 rounded-full px-3.5" > Cancel </Button> </ComposerPrimitive.Cancel> <ComposerPrimitive.Send asChild> <Button size="sm" className="h-8 rounded-full px-3.5"> 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 text-muted-foreground -ms-2 me-2 inline-flex items-center 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, useAuiState,} from "@assistant-ui/react";import { ArchiveIcon, MoreHorizontalIcon, PlusIcon, TrashIcon,} from "lucide-react";import { Fragment, useMemo, type FC } from "react";export const ThreadList: FC = () => { return ( <ThreadListPrimitive.Root className="aui-root aui-thread-list-root flex flex-col gap-0.5"> <ThreadListNew /> <AuiIf condition={(s) => s.threads.isLoading}> <ThreadListSkeleton /> </AuiIf> <AuiIf condition={(s) => !s.threads.isLoading}> <ThreadListItems /> </AuiIf> </ThreadListPrimitive.Root> );};const DAY_IN_MS = 86_400_000;const dateGroupLabel = ( date: Date | undefined, startOfToday: number,): string => { if (!date || date.getTime() >= startOfToday) return "Today"; if (date.getTime() >= startOfToday - DAY_IN_MS) return "Yesterday"; return "Earlier";};type ThreadListGroup = { label: string; indices: number[] };const ThreadListItems: FC = () => { const threadIds = useAuiState((s) => s.threads.threadIds); const threadItems = useAuiState((s) => s.threads.threadItems); const groups = useMemo<ThreadListGroup[] | null>(() => { const itemsById = new Map(threadItems.map((item) => [item.id, item])); const dates = threadIds.map((id) => itemsById.get(id)?.lastMessageAt); if (!dates.some(Boolean)) return null; const now = new Date(); const startOfToday = new Date( now.getFullYear(), now.getMonth(), now.getDate(), ).getTime(); const time = (index: number) => dates[index]?.getTime() ?? Number.MAX_SAFE_INTEGER; const indices = threadIds .map((_, index) => index) .sort((a, b) => time(b) - time(a)); const result: ThreadListGroup[] = []; for (const index of indices) { const label = dateGroupLabel(dates[index], startOfToday); const lastGroup = result[result.length - 1]; if (lastGroup?.label === label) { lastGroup.indices.push(index); } else { result.push({ label, indices: [index] }); } } return result; }, [threadIds, threadItems]); if (!groups) { return ( <ThreadListPrimitive.Items> {() => <ThreadListItem />} </ThreadListPrimitive.Items> ); } return groups.map((group) => ( <Fragment key={group.label}> <div className="aui-thread-list-group-label text-muted-foreground px-2.5 pt-3 pb-1 text-xs font-medium"> {group.label} </div> {group.indices.map((index) => ( <ThreadListPrimitive.ItemByIndex key={threadIds[index]} index={index} components={{ ThreadListItem }} /> ))} </Fragment> ));};const ThreadListNew: FC = () => { return ( <ThreadListPrimitive.New asChild> <Button variant="ghost" className="aui-thread-list-new hover:bg-muted data-active:bg-muted h-8 justify-start gap-2 rounded-md px-2.5 text-sm font-normal" > <PlusIcon className="size-4" /> New Thread </Button> </ThreadListPrimitive.New> );};const ThreadListSkeleton: FC = () => { return ( <div className="flex flex-col gap-0.5"> {Array.from({ length: 5 }, (_, i) => ( <div key={i} role="status" aria-label="Loading threads" className="aui-thread-list-skeleton-wrapper flex h-8 items-center px-2.5" > <Skeleton className="aui-thread-list-skeleton h-3.5 w-full" /> </div> ))} </div> );};const ThreadListItem: FC = () => { return ( <ThreadListItemPrimitive.Root className="aui-thread-list-item group hover:bg-muted focus-visible:bg-muted data-active:bg-muted flex h-8 items-center gap-1 rounded-md transition-colors focus-visible:outline-none"> <ThreadListItemPrimitive.Trigger className="aui-thread-list-item-trigger flex h-full min-w-0 flex-1 items-center px-2.5 text-start text-sm"> <span className="aui-thread-list-item-title min-w-0 flex-1 truncate"> <ThreadListItemPrimitive.Title fallback="New Chat" /> </span> </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 data-[state=open]:bg-accent me-1.5 size-6 p-0 opacity-0 transition-opacity group-hover:opacity-100 group-data-active:opacity-100 data-[state=open]:opacity-100" > <MoreHorizontalIcon className="size-3.5" /> <span className="sr-only">More options</span> </Button> </ThreadListItemMorePrimitive.Trigger> <ThreadListItemMorePrimitive.Content side="right" align="start" sideOffset={6} className="aui-thread-list-item-more-content bg-popover/95 text-popover-foreground data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=closed]:animate-out 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 z-50 min-w-[8rem] overflow-hidden rounded-xl border p-1.5 shadow-lg backdrop-blur-sm" > <ThreadListItemPrimitive.Archive asChild> <ThreadListItemMorePrimitive.Item className="aui-thread-list-item-more-item hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground flex cursor-pointer items-center gap-2 rounded-lg px-2.5 py-1.5 text-sm outline-none select-none"> <ArchiveIcon className="size-4" /> Archive </ThreadListItemMorePrimitive.Item> </ThreadListItemPrimitive.Archive> <ThreadListItemPrimitive.Delete asChild> <ThreadListItemMorePrimitive.Item className="aui-thread-list-item-more-item text-destructive hover:bg-destructive/10 hover:text-destructive focus:bg-destructive/10 focus:text-destructive flex cursor-pointer items-center gap-2 rounded-lg px-2.5 py-1.5 text-sm outline-none select-none"> <TrashIcon className="size-4" /> Delete </ThreadListItemMorePrimitive.Item> </ThreadListItemPrimitive.Delete> </ThreadListItemMorePrimitive.Content> </ThreadListItemMorePrimitive.Root> );};assistant-ui dependencies
npm install @assistant-ui/react @assistant-ui/react-markdown class-variance-authority radix-ui remark-gfm tw-shimmer zustand"use client";import { type 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((s): { file?: File; src?: string } => { if (s.attachment.type !== "image") return {}; if (s.attachment.file) return { file: s.attachment.file }; const src = s.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="Attachment 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 hover:bg-accent/50 cursor-pointer transition-colors" asChild > {children} </DialogTrigger> <DialogContent className="aui-attachment-preview-dialog-content [&>button]:bg-foreground/60 [&_svg]:text-background [&>button]:hover:[&_svg]:text-destructive p-2 sm:max-w-3xl [&>button]:rounded-full [&>button]:p-1 [&>button]:opacity-100 [&>button]:ring-0!"> <DialogTitle className="aui-sr-only sr-only"> Image Attachment Preview </DialogTitle> <div className="aui-attachment-preview bg-background relative mx-auto flex max-h-[80dvh] w-full items-center justify-center overflow-hidden"> <AttachmentPreview src={src} /> </div> </DialogContent> </Dialog> );};const AttachmentThumb: FC = () => { 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> <FileText className="aui-attachment-tile-fallback-icon text-muted-foreground size-8" /> </AvatarFallback> </Avatar> );};const AttachmentUI: FC = () => { const aui = useAui(); const isComposer = aui.attachment.source !== "message"; const isImage = useAuiState((s) => s.attachment.type === "image"); const typeLabel = useAuiState((s) => { const type = s.attachment.type; switch (type) { case "image": return "Image"; case "document": return "Document"; case "file": return "File"; default: return type; } }); return ( <Tooltip> <AttachmentPrimitive.Root className={cn( "aui-attachment-root relative", isImage && !isComposer && "aui-attachment-root-message only:*:first:size-24", )} > <AttachmentPreviewDialog> <TooltipTrigger asChild> <div className="aui-attachment-tile bg-muted size-14 cursor-pointer overflow-hidden rounded-md border transition-opacity hover:opacity-75" role="button" tabIndex={0} 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 text-muted-foreground hover:[&_svg]:text-destructive absolute end-1.5 top-1.5 size-3.5 rounded-full bg-white opacity-100 shadow-sm hover:bg-white! [&_svg]:text-black" 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> {() => <AttachmentUI />} </MessagePrimitive.Attachments> </div> );};export const ComposerAttachments: FC = () => { return ( <div className="aui-composer-attachments flex w-full flex-row items-center gap-2 overflow-x-auto empty:hidden"> <ComposerPrimitive.Attachments> {() => <AttachmentUI />} </ComposerPrimitive.Attachments> </div> );};export const ComposerAddAttachment: FC = () => { return ( <ComposerPrimitive.AddAttachment asChild> <TooltipIconButton tooltip="Add Attachment" side="bottom" variant="ghost" size="icon" className="aui-composer-add-attachment hover:bg-muted-foreground/15 dark:border-muted-foreground/15 dark:hover:bg-muted-foreground/30 size-7 rounded-full p-1 text-xs font-semibold" aria-label="Add Attachment" > <PlusIcon className="aui-attachment-add-icon size-4.5 stroke-[1.5px]" /> </TooltipIconButton> </ComposerPrimitive.AddAttachment> );};"use client";import { type ComponentPropsWithRef, forwardRef } from "react";import { Slot } from "radix-ui";import { Tooltip, TooltipContent, TooltipProvider, 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 ( <TooltipProvider delayDuration={0}> <Tooltip> <TooltipTrigger asChild> <Button variant="ghost" size="icon" {...rest} className={cn( "aui-button-icon size-6 p-1 active:scale-90", className, )} ref={ref} > <Slot.Slottable>{children}</Slot.Slottable> <span className="aui-sr-only sr-only">{tooltip}</span> </Button> </TooltipTrigger> <TooltipContent side={side}>{tooltip}</TooltipContent> </Tooltip> </TooltipProvider> );});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 { SyntaxHighlighter } from "@/components/assistant-ui/shiki-highlighter";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} defer /> );};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 border-border/50 bg-muted/50 mt-3 flex items-center justify-between rounded-t-xl border border-b-0 px-3.5 py-1.5 text-xs"> <span className="aui-code-header-language text-muted-foreground font-medium lowercase"> {language} </span> <TooltipIconButton tooltip="Copy" onClick={onCopy}> {!isCopied && ( <CopyIcon className="animate-in zoom-in-75 fade-in duration-150" /> )} {isCopied && ( <CheckIcon className="animate-in zoom-in-50 fade-in duration-200 ease-out" /> )} </TooltipIconButton> </div> );};const useCopyToClipboard = ({ copiedDuration = 3000,}: { copiedDuration?: number;} = {}) => { const [isCopied, setIsCopied] = useState<boolean>(false); const copyToClipboard = (value: string) => { if (!value || typeof navigator === "undefined" || !navigator.clipboard) { return; } navigator.clipboard.writeText(value).then( () => { setIsCopied(true); setTimeout(() => setIsCopied(false), copiedDuration); }, () => {}, ); }; return { isCopied, copyToClipboard };};const defaultComponents = memoizeMarkdownComponents({ SyntaxHighlighter, h1: ({ className, ...props }) => ( <h1 className={cn( "aui-md-h1 mt-5 mb-2 scroll-m-20 text-xl font-semibold first:mt-0 last:mb-0", className, )} {...props} /> ), h2: ({ className, ...props }) => ( <h2 className={cn( "aui-md-h2 mt-5 mb-2 scroll-m-20 text-lg font-semibold first:mt-0 last:mb-0", className, )} {...props} /> ), h3: ({ className, ...props }) => ( <h3 className={cn( "aui-md-h3 mt-4 mb-1.5 scroll-m-20 text-base font-semibold first:mt-0 last:mb-0", className, )} {...props} /> ), h4: ({ className, ...props }) => ( <h4 className={cn( "aui-md-h4 mt-3.5 mb-1 scroll-m-20 text-base font-medium first:mt-0 last:mb-0", className, )} {...props} /> ), h5: ({ className, ...props }) => ( <h5 className={cn( "aui-md-h5 mt-3 mb-1 text-sm font-semibold first:mt-0 last:mb-0", className, )} {...props} /> ), h6: ({ className, ...props }) => ( <h6 className={cn( "aui-md-h6 mt-3 mb-1 text-sm font-medium first:mt-0 last:mb-0", className, )} {...props} /> ), p: ({ className, ...props }) => ( <p className={cn( "aui-md-p my-3 leading-relaxed first:mt-0 last:mb-0", className, )} {...props} /> ), a: ({ className, ...props }) => ( <a className={cn( "aui-md-a text-primary hover:text-primary/80 underline underline-offset-2", className, )} {...props} /> ), blockquote: ({ className, ...props }) => ( <blockquote className={cn( "aui-md-blockquote border-muted-foreground/30 text-muted-foreground my-3 border-s-2 ps-4", className, )} {...props} /> ), ul: ({ className, ...props }) => ( <ul className={cn( "aui-md-ul marker:text-muted-foreground my-3 ms-5 list-disc [&>li]:mt-1", className, )} {...props} /> ), ol: ({ className, ...props }) => ( <ol className={cn( "aui-md-ol marker:text-muted-foreground my-3 ms-5 list-decimal [&>li]:mt-1", className, )} {...props} /> ), hr: ({ className, ...props }) => ( <hr className={cn("aui-md-hr border-muted-foreground/20 my-3", className)} {...props} /> ), table: ({ className, ...props }) => ( <table className={cn( "aui-md-table my-3 w-full border-separate border-spacing-0 overflow-y-auto", className, )} {...props} /> ), th: ({ className, ...props }) => ( <th className={cn( "aui-md-th bg-muted px-3 py-1.5 text-start font-medium first:rounded-ss-lg last:rounded-se-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-s border-b px-3 py-1.5 text-start last:border-e [[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-es-lg [&:last-child>td:last-child]:rounded-ee-lg", className, )} {...props} /> ), li: ({ className, ...props }) => ( <li className={cn("aui-md-li leading-relaxed", className)} {...props} /> ), strong: ({ className, ...props }) => ( <strong className={cn("aui-md-strong font-semibold", 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 border-border/50 bg-muted/30 overflow-x-auto rounded-t-none rounded-b-xl border border-t-0 p-3.5 text-[13px] leading-relaxed", className, )} {...props} /> ), code: function Code({ className, ...props }) { const isCodeBlock = useIsMarkdownCodeBlock(); return ( <code className={cn( !isCodeBlock && "aui-md-inline-code bg-muted rounded-md px-1.5 py-0.5 font-mono text-[0.85em]", className, )} {...props} /> ); }, CodeHeader,});"use client";import { createContext, memo, useCallback, useContext, useEffect, useLayoutEffect, useRef, useState,} from "react";import { cva, type VariantProps } from "class-variance-authority";import { BrainIcon, ChevronDownIcon } from "lucide-react";import { useScrollLock, useAuiState, type ReasoningMessagePartComponent, type ReasoningGroupComponent,} from "@assistant-ui/react";import { MarkdownText } from "@/components/assistant-ui/markdown-text";import { Collapsible, CollapsibleContent, CollapsibleTrigger,} from "@/components/ui/collapsible";import { cn } from "@/lib/utils";const ANIMATION_DURATION = 200;const ReasoningPreviewContext = createContext(false);const reasoningVariants = cva("aui-reasoning-root mb-4 w-full", { variants: { variant: { outline: "rounded-lg border px-3 py-2", ghost: "", muted: "bg-muted/50 rounded-lg px-3 py-2", }, }, defaultVariants: { variant: "outline", },});export type ReasoningRootProps = Omit< React.ComponentProps<typeof Collapsible>, "open" | "onOpenChange"> & VariantProps<typeof reasoningVariants> & { open?: boolean; onOpenChange?: (open: boolean) => void; defaultOpen?: boolean; /** * Whether the reasoning is currently streaming. When provided, it * supersedes `defaultOpen`: the disclosure auto-opens while streaming * with a bottom-pinned live preview, auto-collapses when streaming * ends, and the first manual toggle takes over permanently. */ streaming?: boolean; };function ReasoningRoot({ className, variant, open: controlledOpen, onOpenChange: controlledOnOpenChange, defaultOpen = false, streaming, children, ...props}: ReasoningRootProps) { const collapsibleRef = useRef<HTMLDivElement>(null); const initialOpenRef = useRef(defaultOpen); const [userOpen, setUserOpen] = useState<boolean | null>(null); const lockScroll = useScrollLock(collapsibleRef, ANIMATION_DURATION); const isControlled = controlledOpen !== undefined; const isOpen = isControlled ? controlledOpen : (userOpen ?? streaming ?? initialOpenRef.current); const isAutoMode = isControlled || userOpen === null; const isPreview = streaming === true && isOpen && isAutoMode; const prevStreamingRef = useRef(streaming); useLayoutEffect(() => { if (prevStreamingRef.current === streaming) return; prevStreamingRef.current = streaming; if (!isControlled && userOpen === null) lockScroll(); }, [streaming, isControlled, userOpen, lockScroll]); const handleOpenChange = useCallback( (open: boolean) => { lockScroll(); if (!isControlled) { setUserOpen(open); } controlledOnOpenChange?.(open); }, [lockScroll, isControlled, controlledOnOpenChange], ); return ( <Collapsible ref={collapsibleRef} data-slot="reasoning-root" data-variant={variant} open={isOpen} onOpenChange={handleOpenChange} className={cn( "group/reasoning-root", reasoningVariants({ variant, className }), )} style={ { "--animation-duration": `${ANIMATION_DURATION}ms`, } as React.CSSProperties } {...props} > <ReasoningPreviewContext.Provider value={isPreview}> {children} </ReasoningPreviewContext.Provider> </Collapsible> );}function ReasoningFade({ side = "bottom", className, ...props}: React.ComponentProps<"div"> & { side?: "top" | "bottom" }) { if (side === "top") { return ( <div data-slot="reasoning-fade" className={cn( "aui-reasoning-fade pointer-events-none absolute inset-x-0 top-0 z-10 h-8", "bg-[linear-gradient(to_bottom,var(--color-background),transparent)]", "group-data-[variant=muted]/reasoning-root:bg-[linear-gradient(to_bottom,hsl(var(--muted)/0.5),transparent)]", "fade-in-0 animate-in", "duration-(--animation-duration)", className, )} {...props} /> ); } return ( <div data-slot="reasoning-fade" className={cn( "aui-reasoning-fade pointer-events-none absolute inset-x-0 bottom-0 z-10 h-8", "bg-[linear-gradient(to_top,var(--color-background),transparent)]", "group-data-[variant=muted]/reasoning-root:bg-[linear-gradient(to_top,hsl(var(--muted)/0.5),transparent)]", "fade-in-0 animate-in", "group-data-[state=open]/collapsible-content:animate-out", "group-data-[state=open]/collapsible-content:fade-out-0", "group-data-[state=open]/collapsible-content:delay-[calc(var(--animation-duration)*0.75)]", "group-data-[state=open]/collapsible-content:fill-mode-forwards", "duration-(--animation-duration)", "group-data-[state=open]/collapsible-content:duration-(--animation-duration)", className, )} {...props} /> );}function ReasoningTrigger({ active, duration, className, ...props}: React.ComponentProps<typeof CollapsibleTrigger> & { active?: boolean; duration?: number;}) { const durationText = duration ? ` (${duration}s)` : ""; return ( <CollapsibleTrigger data-slot="reasoning-trigger" className={cn( "aui-reasoning-trigger group/trigger text-muted-foreground hover:text-foreground flex max-w-[75%] items-center gap-2 py-1 text-sm transition-colors", className, )} {...props} > <BrainIcon data-slot="reasoning-trigger-icon" className="aui-reasoning-trigger-icon size-4 shrink-0" /> <span data-slot="reasoning-trigger-label" className="aui-reasoning-trigger-label-wrapper relative inline-block leading-none" > <span>Reasoning{durationText}</span> {active ? ( <span aria-hidden data-slot="reasoning-trigger-shimmer" className="aui-reasoning-trigger-shimmer shimmer pointer-events-none absolute inset-0 motion-reduce:animate-none" > Reasoning{durationText} </span> ) : null} </span> <ChevronDownIcon data-slot="reasoning-trigger-chevron" className={cn( "aui-reasoning-trigger-chevron mt-0.5 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 ReasoningContent({ className, children, ...props}: React.ComponentProps<typeof CollapsibleContent>) { const isPreview = useContext(ReasoningPreviewContext); return ( <CollapsibleContent data-slot="reasoning-content" className={cn( "aui-reasoning-content text-muted-foreground 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} > {isPreview ? <ReasoningFade side="top" /> : null} {children} <ReasoningFade /> </CollapsibleContent> );}function ReasoningText({ className, children, ...props}: React.ComponentProps<"div">) { const isPreview = useContext(ReasoningPreviewContext); const scrollRef = useRef<HTMLDivElement>(null); const contentRef = useRef<HTMLDivElement>(null); useEffect(() => { if (!isPreview) return; const scrollEl = scrollRef.current; const contentEl = contentRef.current; if (!scrollEl || !contentEl) return; const pin = () => { scrollEl.scrollTop = scrollEl.scrollHeight; }; pin(); const observer = new ResizeObserver(pin); observer.observe(contentEl); return () => observer.disconnect(); }, [isPreview]); return ( <div ref={scrollRef} data-slot="reasoning-text" className={cn( "aui-reasoning-text relative z-0 max-h-64 overflow-y-auto ps-6 pt-2 pb-2 leading-relaxed", "transform-gpu transition-[transform,opacity]", "group-data-[state=open]/collapsible-content:animate-in", "group-data-[state=closed]/collapsible-content:animate-out", "group-data-[state=open]/collapsible-content:fade-in-0", "group-data-[state=closed]/collapsible-content:fade-out-0", "group-data-[state=open]/collapsible-content:slide-in-from-top-4", "group-data-[state=closed]/collapsible-content:slide-out-to-top-4", "group-data-[state=open]/collapsible-content:duration-(--animation-duration)", "group-data-[state=closed]/collapsible-content:duration-(--animation-duration)", className, )} {...props} > <div ref={contentRef} className="aui-reasoning-text-content space-y-4"> {children} </div> </div> );}const ReasoningImpl: ReasoningMessagePartComponent = () => <MarkdownText />;const ReasoningGroupImpl: ReasoningGroupComponent = ({ children, startIndex, endIndex,}) => { const isReasoningStreaming = useAuiState((s) => { if (s.message.status?.type !== "running") return false; const lastIndex = s.message.parts.length - 1; if (lastIndex < 0) return false; const lastType = s.message.parts[lastIndex]?.type; if (lastType !== "reasoning") return false; return lastIndex >= startIndex && lastIndex <= endIndex; }); return ( <ReasoningRoot streaming={isReasoningStreaming}> <ReasoningTrigger active={isReasoningStreaming} /> <ReasoningContent aria-busy={isReasoningStreaming}> <ReasoningText>{children}</ReasoningText> </ReasoningContent> </ReasoningRoot> );};const Reasoning = memo( ReasoningImpl,) as unknown as ReasoningMessagePartComponent & { Root: typeof ReasoningRoot; Trigger: typeof ReasoningTrigger; Content: typeof ReasoningContent; Text: typeof ReasoningText; Fade: typeof ReasoningFade;};Reasoning.displayName = "Reasoning";Reasoning.Root = ReasoningRoot;Reasoning.Trigger = ReasoningTrigger;Reasoning.Content = ReasoningContent;Reasoning.Text = ReasoningText;Reasoning.Fade = ReasoningFade;/** * @deprecated This wrapper targets the legacy `components.ReasoningGroup` * prop on `<MessagePrimitive.Parts>`. Use `<MessagePrimitive.GroupedParts>` * with a `groupBy` returning `"group-reasoning"` and compose `ReasoningRoot` * / `ReasoningTrigger` / `ReasoningContent` / `ReasoningText` directly. * See `thread.tsx` for an example. */const ReasoningGroup = memo(ReasoningGroupImpl);ReasoningGroup.displayName = "ReasoningGroup";export { Reasoning, ReasoningGroup, ReasoningRoot, ReasoningTrigger, ReasoningContent, ReasoningText, ReasoningFade, reasoningVariants,};"use client";import { memo, useCallback, useRef, useState } from "react";import { AlertCircleIcon, CheckIcon, ChevronDownIcon, LoaderIcon, XCircleIcon,} from "lucide-react";import { useScrollLock, type ToolCallMessagePart, type ToolCallMessagePartProps, type ToolCallMessagePartStatus, type ToolCallMessagePartComponent,} from "@assistant-ui/react";import { Collapsible, CollapsibleContent, CollapsibleTrigger,} from "@/components/ui/collapsible";import { cn } from "@/lib/utils";import { Button } from "@/components/ui/button";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) => { 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", 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 text-muted-foreground hover:text-foreground flex w-fit items-center gap-2 py-1 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 text-start 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="flex flex-col gap-2 ps-6 pt-1 pb-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", className)} {...props} > <pre className="aui-tool-fallback-args-value bg-muted/50 text-muted-foreground rounded-md p-2.5 text-xs 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", className)} {...props} > <p className="aui-tool-fallback-result-header text-muted-foreground text-xs font-medium"> Result: </p> <pre className="aui-tool-fallback-result-content bg-muted/50 text-muted-foreground mt-1 rounded-md p-2.5 text-xs 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", className)} {...props} > <p className="aui-tool-fallback-error-header text-muted-foreground font-semibold"> {headerText} </p> <p className="aui-tool-fallback-error-reason text-muted-foreground"> {errorText} </p> </div> );}const APPROVED_RESULT = "Approved by user";const DENIED_RESULT = "User denied tool execution";function ToolFallbackApproval({ className, addResult, resume, interrupt, approval, respondToApproval, ...props}: React.ComponentProps<"div"> & Partial< Pick<ToolCallMessagePartProps, "addResult" | "resume" | "respondToApproval"> > & { interrupt?: ToolCallMessagePart["interrupt"]; approval?: ToolCallMessagePart["approval"]; }) { const [submitted, setSubmitted] = useState(false); if (approval != null && approval.approved !== undefined) return null; const respond = (approved: boolean) => { if (submitted) return; if ( approval != null && approval.approved === undefined && respondToApproval ) { respondToApproval({ approved }); } else if (interrupt) { resume?.({ approved }); } else { addResult?.(approved ? APPROVED_RESULT : DENIED_RESULT); } setSubmitted(true); }; return ( <div data-slot="tool-fallback-approval" className={cn( "aui-tool-fallback-approval flex items-center gap-2 pt-1", className, )} {...props} > <Button size="sm" onClick={() => respond(true)} disabled={submitted}> Allow </Button> <Button size="sm" variant="outline" onClick={() => respond(false)} disabled={submitted} > Deny </Button> </div> );}const ToolFallbackImpl: ToolCallMessagePartComponent = ({ toolName, argsText, result, status, addResult, resume, interrupt, approval, respondToApproval,}) => { const isCancelled = status?.type === "incomplete" && status.reason === "cancelled"; const isRequiresAction = status?.type === "requires-action"; const [open, setOpen] = useState(isRequiresAction); const [prevRequiresAction, setPrevRequiresAction] = useState(isRequiresAction); if (isRequiresAction !== prevRequiresAction) { setPrevRequiresAction(isRequiresAction); if (isRequiresAction) setOpen(true); } return ( <ToolFallbackRoot open={open} onOpenChange={setOpen}> <ToolFallbackTrigger toolName={toolName} status={status} /> <ToolFallbackContent> <ToolFallbackError status={status} /> <ToolFallbackArgs argsText={argsText} className={cn(isCancelled && "opacity-60")} /> {isRequiresAction && ( <ToolFallbackApproval addResult={addResult} resume={resume} interrupt={interrupt} approval={approval} respondToApproval={respondToApproval} /> )} {!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; Approval: typeof ToolFallbackApproval;};ToolFallback.displayName = "ToolFallback";ToolFallback.Root = ToolFallbackRoot;ToolFallback.Trigger = ToolFallbackTrigger;ToolFallback.Content = ToolFallbackContent;ToolFallback.Args = ToolFallbackArgs;ToolFallback.Result = ToolFallbackResult;ToolFallback.Error = ToolFallbackError;ToolFallback.Approval = ToolFallbackApproval;export { ToolFallback, ToolFallbackRoot, ToolFallbackTrigger, ToolFallbackContent, ToolFallbackArgs, ToolFallbackResult, ToolFallbackError, ToolFallbackApproval,};"use client";import { memo, useCallback, useRef, useState, type FC, type PropsWithChildren,} from "react";import { ChevronDownIcon, LoaderIcon } from "lucide-react";import { cva, type VariantProps } from "class-variance-authority";import { useScrollLock } from "@assistant-ui/react";import { Collapsible, CollapsibleContent, CollapsibleTrigger,} from "@/components/ui/collapsible";import { cn } from "@/lib/utils";const ANIMATION_DURATION = 200;const toolGroupVariants = cva("aui-tool-group-root group/tool-group w-full", { variants: { variant: { outline: "rounded-lg border py-3", ghost: "", muted: "border-muted-foreground/30 bg-muted/30 rounded-lg border py-3", }, }, defaultVariants: { variant: "outline" },});export type ToolGroupRootProps = Omit< React.ComponentProps<typeof Collapsible>, "open" | "onOpenChange"> & VariantProps<typeof toolGroupVariants> & { open?: boolean; onOpenChange?: (open: boolean) => void; defaultOpen?: boolean; };function ToolGroupRoot({ className, variant, open: controlledOpen, onOpenChange: controlledOnOpenChange, defaultOpen = false, children, ...props}: ToolGroupRootProps) { 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) => { lockScroll(); if (!isControlled) { setUncontrolledOpen(open); } controlledOnOpenChange?.(open); }, [lockScroll, isControlled, controlledOnOpenChange], ); return ( <Collapsible ref={collapsibleRef} data-slot="tool-group-root" data-variant={variant ?? "outline"} open={isOpen} onOpenChange={handleOpenChange} className={cn( toolGroupVariants({ variant }), "group/tool-group-root", className, )} style={ { "--animation-duration": `${ANIMATION_DURATION}ms`, } as React.CSSProperties } {...props} > {children} </Collapsible> );}function ToolGroupTrigger({ count, active = false, className, ...props}: React.ComponentProps<typeof CollapsibleTrigger> & { count: number; active?: boolean;}) { const label = `${count} tool ${count === 1 ? "call" : "calls"}`; return ( <CollapsibleTrigger data-slot="tool-group-trigger" className={cn( "aui-tool-group-trigger group/trigger flex items-center gap-2 text-sm transition-colors", "group-data-[variant=ghost]/tool-group-root:text-muted-foreground group-data-[variant=ghost]/tool-group-root:hover:text-foreground group-data-[variant=ghost]/tool-group-root:py-1", "group-data-[variant=outline]/tool-group-root:w-full group-data-[variant=outline]/tool-group-root:px-4", "group-data-[variant=muted]/tool-group-root:w-full group-data-[variant=muted]/tool-group-root:px-4", className, )} {...props} > {active && ( <LoaderIcon data-slot="tool-group-trigger-loader" className="aui-tool-group-trigger-loader size-4 shrink-0 animate-spin" /> )} <span data-slot="tool-group-trigger-label" className={cn( "aui-tool-group-trigger-label-wrapper relative inline-block text-start leading-none font-medium", "group-data-[variant=ghost]/tool-group-root:font-normal", "group-data-[variant=outline]/tool-group-root:grow", "group-data-[variant=muted]/tool-group-root:grow", )} > <span>{label}</span> {active && ( <span aria-hidden data-slot="tool-group-trigger-shimmer" className="aui-tool-group-trigger-shimmer shimmer pointer-events-none absolute inset-0 motion-reduce:animate-none" > {label} </span> )} </span> <ChevronDownIcon data-slot="tool-group-trigger-chevron" className={cn( "aui-tool-group-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 ToolGroupContent({ className, children, ...props}: React.ComponentProps<typeof CollapsibleContent>) { return ( <CollapsibleContent data-slot="tool-group-content" className={cn( "aui-tool-group-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={cn( "mt-2 flex flex-col gap-2", "group-data-[variant=ghost]/tool-group-root:mt-1 group-data-[variant=ghost]/tool-group-root:gap-1", "group-data-[variant=outline]/tool-group-root:mt-3 group-data-[variant=outline]/tool-group-root:border-t group-data-[variant=outline]/tool-group-root:px-4 group-data-[variant=outline]/tool-group-root:pt-3", "group-data-[variant=muted]/tool-group-root:mt-3 group-data-[variant=muted]/tool-group-root:border-t group-data-[variant=muted]/tool-group-root:px-4 group-data-[variant=muted]/tool-group-root:pt-3", )} > {children} </div> </CollapsibleContent> );}type ToolGroupComponent = FC< PropsWithChildren<{ startIndex: number; endIndex: number }>> & { Root: typeof ToolGroupRoot; Trigger: typeof ToolGroupTrigger; Content: typeof ToolGroupContent;};const ToolGroupImpl: FC< PropsWithChildren<{ startIndex: number; endIndex: number }>> = ({ children, startIndex, endIndex }) => { const toolCount = endIndex - startIndex + 1; return ( <ToolGroupRoot> <ToolGroupTrigger count={toolCount} /> <ToolGroupContent>{children}</ToolGroupContent> </ToolGroupRoot> );};/** * @deprecated This wrapper targets the legacy `components.ToolGroup` prop * on `<MessagePrimitive.Parts>`. Use `<MessagePrimitive.GroupedParts>` with * a `groupBy` returning `"group-tool"` and compose `ToolGroupRoot` / * `ToolGroupTrigger` / `ToolGroupContent` directly. See `thread.tsx`. */const ToolGroup = memo(ToolGroupImpl) as unknown as ToolGroupComponent;ToolGroup.displayName = "ToolGroup";ToolGroup.Root = ToolGroupRoot;ToolGroup.Trigger = ToolGroupTrigger;ToolGroup.Content = ToolGroupContent;export { ToolGroup, ToolGroupRoot, ToolGroupTrigger, ToolGroupContent, toolGroupVariants,};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/groqnpm install ai @assistant-ui/react-ai-sdk @ai-sdk/fireworksnpm 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 { frontendTools } from "@assistant-ui/react-ai-sdk";
import { convertToModelMessages, streamText } from "ai";
export const maxDuration = 30;
export async function POST(req: Request) {
const { messages, system, tools } = await req.json();
const result = streamText({
model: openai("gpt-5.4-nano"),
system,
messages: await convertToModelMessages(messages),
tools: frontendTools(tools),
});
return result.toUIMessageStreamResponse();
}import { anthropic } from "@ai-sdk/anthropic";
import { frontendTools } from "@assistant-ui/react-ai-sdk";
import { convertToModelMessages, streamText } from "ai";
export const maxDuration = 30;
export async function POST(req: Request) {
const { messages, system, tools } = await req.json();
const result = streamText({
model: anthropic("claude-sonnet-4-6"),
system,
messages: await convertToModelMessages(messages),
tools: frontendTools(tools),
});
return result.toUIMessageStreamResponse();
}import { azure } from "@ai-sdk/azure";
import { frontendTools } from "@assistant-ui/react-ai-sdk";
import { convertToModelMessages, streamText } from "ai";
export const maxDuration = 30;
export async function POST(req: Request) {
const { messages, system, tools } = await req.json();
const result = streamText({
model: azure("your-deployment-name"),
system,
messages: await convertToModelMessages(messages),
tools: frontendTools(tools),
});
return result.toUIMessageStreamResponse();
}import { bedrock } from "@ai-sdk/amazon-bedrock";
import { frontendTools } from "@assistant-ui/react-ai-sdk";
import { convertToModelMessages, streamText } from "ai";
export const maxDuration = 30;
export async function POST(req: Request) {
const { messages, system, tools } = await req.json();
const result = streamText({
model: bedrock("anthropic.claude-sonnet-4-6-v1:0"),
system,
messages: await convertToModelMessages(messages),
tools: frontendTools(tools),
});
return result.toUIMessageStreamResponse();
}import { google } from "@ai-sdk/google";
import { frontendTools } from "@assistant-ui/react-ai-sdk";
import { convertToModelMessages, streamText } from "ai";
export const maxDuration = 30;
export async function POST(req: Request) {
const { messages, system, tools } = await req.json();
const result = streamText({
model: google("gemini-2.0-flash"),
system,
messages: await convertToModelMessages(messages),
tools: frontendTools(tools),
});
return result.toUIMessageStreamResponse();
}import { vertex } from "@ai-sdk/google-vertex";
import { frontendTools } from "@assistant-ui/react-ai-sdk";
import { convertToModelMessages, streamText } from "ai";
export const maxDuration = 30;
export async function POST(req: Request) {
const { messages, system, tools } = await req.json();
const result = streamText({
model: vertex("gemini-2.0-flash"),
system,
messages: await convertToModelMessages(messages),
tools: frontendTools(tools),
});
return result.toUIMessageStreamResponse();
}import { groq } from "@ai-sdk/groq";
import { frontendTools } from "@assistant-ui/react-ai-sdk";
import { convertToModelMessages, streamText } from "ai";
export const maxDuration = 30;
export async function POST(req: Request) {
const { messages, system, tools } = await req.json();
const result = streamText({
model: groq("llama-3.3-70b-versatile"),
system,
messages: await convertToModelMessages(messages),
tools: frontendTools(tools),
});
return result.toUIMessageStreamResponse();
}import { fireworks } from "@ai-sdk/fireworks";
import { frontendTools } from "@assistant-ui/react-ai-sdk";
import { convertToModelMessages, streamText } from "ai";
export const maxDuration = 30;
export async function POST(req: Request) {
const { messages, system, tools } = await req.json();
const result = streamText({
model: fireworks("accounts/fireworks/models/llama-v3p3-70b-instruct"),
system,
messages: await convertToModelMessages(messages),
tools: frontendTools(tools),
});
return result.toUIMessageStreamResponse();
}import { cohere } from "@ai-sdk/cohere";
import { frontendTools } from "@assistant-ui/react-ai-sdk";
import { convertToModelMessages, streamText } from "ai";
export const maxDuration = 30;
export async function POST(req: Request) {
const { messages, system, tools } = await req.json();
const result = streamText({
model: cohere("command-r-plus"),
system,
messages: await convertToModelMessages(messages),
tools: frontendTools(tools),
});
return result.toUIMessageStreamResponse();
}import { ollama } from "ollama-ai-provider-v2";
import { frontendTools } from "@assistant-ui/react-ai-sdk";
import { convertToModelMessages, streamText } from "ai";
export const maxDuration = 30;
export async function POST(req: Request) {
const { messages, system, tools } = await req.json();
const result = streamText({
model: ollama("llama3"),
system,
messages: await convertToModelMessages(messages),
tools: frontendTools(tools),
});
return result.toUIMessageStreamResponse();
}import { chromeai } from "chrome-ai";
import { frontendTools } from "@assistant-ui/react-ai-sdk";
import { convertToModelMessages, streamText } from "ai";
export const maxDuration = 30;
export async function POST(req: Request) {
const { messages, system, tools } = await req.json();
const result = streamText({
model: chromeai(),
system,
messages: await convertToModelMessages(messages),
tools: frontendTools(tools),
});
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";
export default function 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";
export default function MyApp() {
const runtime = useChatRuntime({
transport: new AssistantChatTransport({
api: "/api/chat",
}),
});
return (
<AssistantRuntimeProvider runtime={runtime}>
<AssistantModal />
</AssistantRuntimeProvider>
);
}