Getting Started
Start with a new project

Initialize assistant-ui
Create a new project:
# Create a new project with the default template
npx assistant-ui@latest create
# Or choose one of the following templates:
# Assistant Cloud for baked in 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 assistant-ui to an existing React project:
# Add assistant-ui to an existing React project
npx assistant-ui@latest initAdd API key
Add a new .env file to your project with your OpenAI API key:
OPENAI_API_KEY="sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# chat history -- sign up for free on https://cloud.assistant-ui.com
# NEXT_PUBLIC_ASSISTANT_BASE_URL="https://..."Start the app
npm run devManual installation
We recommend npx assistant-ui init to setup existing projects.
Add assistant-ui
npx shadcn@latest add @assistant-ui/thread @assistant-ui/thread-listPrerequisites
- 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/reactyarn add @assistant-ui/reactpnpm add @assistant-ui/reactbun add @assistant-ui/reactxpm add @assistant-ui/reactimport { ArrowDownIcon, ArrowUpIcon, CheckIcon, ChevronLeftIcon, ChevronRightIcon, CopyIcon, PencilIcon, RefreshCwIcon, Square,} from "lucide-react";import { ActionBarPrimitive, AssistantIf, BranchPickerPrimitive, ComposerPrimitive, ErrorPrimitive, MessagePrimitive, ThreadPrimitive,} from "@assistant-ui/react";import type { FC } from "react";import { Button } from "@/components/ui/button";import { MarkdownText } from "@/components/assistant-ui/markdown-text";import { ToolFallback } from "@/components/assistant-ui/tool-fallback";import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";import { ComposerAddAttachment, ComposerAttachments, UserMessageAttachments,} from "@/components/assistant-ui/attachment";import { cn } from "@/lib/utils";export const Thread: FC = () => { return ( <ThreadPrimitive.Root className="aui-root aui-thread-root @container flex h-full flex-col bg-background" style={{ ["--thread-max-width" as string]: "44rem", }} > <ThreadPrimitive.Viewport turnAnchor="top" className="aui-thread-viewport relative flex flex-1 flex-col overflow-x-auto overflow-y-scroll scroll-smooth px-4 pt-4" > <AssistantIf condition={({ thread }) => thread.isEmpty}> <ThreadWelcome /> </AssistantIf> <ThreadPrimitive.Messages components={{ UserMessage, EditComposer, AssistantMessage, }} /> <ThreadPrimitive.ViewportFooter className="aui-thread-viewport-footer sticky bottom-0 mx-auto mt-4 flex w-full max-w-(--thread-max-width) flex-col gap-4 overflow-visible rounded-t-3xl bg-background pb-4 md:pb-6"> <ThreadScrollToBottom /> <Composer /> </ThreadPrimitive.ViewportFooter> </ThreadPrimitive.Viewport> </ThreadPrimitive.Root> );};const ThreadScrollToBottom: FC = () => { return ( <ThreadPrimitive.ScrollToBottom asChild> <TooltipIconButton tooltip="Scroll to bottom" variant="outline" className="aui-thread-scroll-to-bottom -top-12 absolute z-10 self-center rounded-full p-4 disabled:invisible dark:bg-background dark:hover:bg-accent" > <ArrowDownIcon /> </TooltipIconButton> </ThreadPrimitive.ScrollToBottom> );};const ThreadWelcome: FC = () => { return ( <div className="aui-thread-welcome-root mx-auto my-auto flex w-full max-w-(--thread-max-width) grow flex-col"> <div className="aui-thread-welcome-center flex w-full grow flex-col items-center justify-center"> <div className="aui-thread-welcome-message flex size-full flex-col justify-center px-8"> <div className="aui-thread-welcome-message-inner fade-in slide-in-from-bottom-2 animate-in font-semibold text-2xl duration-300 ease-out"> Hello there! </div> <div className="aui-thread-welcome-message-inner fade-in slide-in-from-bottom-2 animate-in text-2xl text-muted-foreground/65 delay-100 duration-300 ease-out"> How can I help you today? </div> </div> </div> <ThreadSuggestions /> </div> );};const ThreadSuggestions: FC = () => { return ( <div className="aui-thread-welcome-suggestions grid w-full @md:grid-cols-2 gap-2 pb-4"> {[ { title: "What's the weather", label: "in San Francisco?", action: "What's the weather in San Francisco?", }, { title: "Explain React hooks", label: "like useState and useEffect", action: "Explain React hooks like useState and useEffect", }, { title: "Write a SQL query", label: "to find top customers", action: "Write a SQL query to find top customers", }, { title: "Create a meal plan", label: "for healthy weight loss", action: "Create a meal plan for healthy weight loss", }, ].map((suggestedAction, index) => ( <div key={`suggested-action-${suggestedAction.title}-${index}`} className="aui-thread-welcome-suggestion-display fade-in slide-in-from-bottom-4 @md:nth-[n+3]:block nth-[n+3]:hidden animate-in fill-mode-both duration-300 ease-out" style={{ animationDelay: `${index * 50}ms` }} > <ThreadPrimitive.Suggestion prompt={suggestedAction.action} send asChild > <Button variant="ghost" className="aui-thread-welcome-suggestion h-auto w-full flex-1 @md:flex-col flex-wrap items-start justify-start gap-1 rounded-3xl border px-5 py-4 text-left text-sm dark:hover:bg-accent/60" aria-label={suggestedAction.action} > <span className="aui-thread-welcome-suggestion-text-1 font-medium"> {suggestedAction.title} </span> <span className="aui-thread-welcome-suggestion-text-2 text-muted-foreground"> {suggestedAction.label} </span> </Button> </ThreadPrimitive.Suggestion> </div> ))} </div> );};const Composer: FC = () => { return ( <ComposerPrimitive.Root className="aui-composer-root relative flex w-full flex-col"> <ComposerPrimitive.AttachmentDropzone className="aui-composer-attachment-dropzone flex w-full flex-col rounded-3xl border border-input bg-background px-1 pt-2 shadow-xs outline-none transition-[color,box-shadow] has-[textarea:focus-visible]:border-ring has-[textarea:focus-visible]:ring-[3px] has-[textarea:focus-visible]:ring-ring/50 data-[dragging=true]:border-ring data-[dragging=true]:border-dashed data-[dragging=true]:bg-accent/50 dark:bg-background"> <ComposerAttachments /> <ComposerPrimitive.Input placeholder="Send a message..." className="aui-composer-input mb-1 max-h-32 min-h-16 w-full resize-none bg-transparent px-3.5 pt-1.5 pb-3 text-base outline-none placeholder:text-muted-foreground focus-visible:ring-0" rows={1} autoFocus aria-label="Message input" /> <ComposerAction /> </ComposerPrimitive.AttachmentDropzone> </ComposerPrimitive.Root> );};const ComposerAction: FC = () => { return ( <div className="aui-composer-action-wrapper relative mx-1 mt-2 mb-2 flex items-center justify-between"> <ComposerAddAttachment /> <AssistantIf condition={({ thread }) => !thread.isRunning}> <ComposerPrimitive.Send asChild> <TooltipIconButton tooltip="Send message" side="bottom" type="submit" variant="default" size="icon" className="aui-composer-send size-[34px] rounded-full p-1" aria-label="Send message" > <ArrowUpIcon className="aui-composer-send-icon size-5" /> </TooltipIconButton> </ComposerPrimitive.Send> </AssistantIf> <AssistantIf condition={({ thread }) => thread.isRunning}> <ComposerPrimitive.Cancel asChild> <Button type="button" variant="default" size="icon" className="aui-composer-cancel size-[34px] rounded-full border border-muted-foreground/60 hover:bg-primary/75 dark:border-muted-foreground/90" aria-label="Stop generating" > <Square className="aui-composer-cancel-icon size-3.5 fill-white dark:fill-black" /> </Button> </ComposerPrimitive.Cancel> </AssistantIf> </div> );};const MessageError: FC = () => { return ( <MessagePrimitive.Error> <ErrorPrimitive.Root className="aui-message-error-root mt-2 rounded-md border border-destructive bg-destructive/10 p-3 text-destructive text-sm dark:bg-destructive/5 dark:text-red-200"> <ErrorPrimitive.Message className="aui-message-error-message line-clamp-2" /> </ErrorPrimitive.Root> </MessagePrimitive.Error> );};const AssistantMessage: FC = () => { return ( <MessagePrimitive.Root className="aui-assistant-message-root fade-in slide-in-from-bottom-1 relative mx-auto w-full max-w-(--thread-max-width) animate-in py-4 duration-150 ease-out" data-role="assistant" > <div className="aui-assistant-message-content wrap-break-word mx-2 text-foreground leading-7"> <MessagePrimitive.Parts components={{ Text: MarkdownText, tools: { Fallback: ToolFallback }, }} /> <MessageError /> </div> <div className="aui-assistant-message-footer mt-2 ml-2 flex"> <BranchPicker /> <AssistantActionBar /> </div> </MessagePrimitive.Root> );};const AssistantActionBar: FC = () => { return ( <ActionBarPrimitive.Root hideWhenRunning autohide="not-last" autohideFloat="single-branch" className="aui-assistant-action-bar-root -ml-1 col-start-3 row-start-2 flex gap-1 text-muted-foreground data-floating:absolute data-floating:rounded-md data-floating:border data-floating:bg-background data-floating:p-1 data-floating:shadow-sm" > <ActionBarPrimitive.Copy asChild> <TooltipIconButton tooltip="Copy"> <AssistantIf condition={({ message }) => message.isCopied}> <CheckIcon /> </AssistantIf> <AssistantIf condition={({ message }) => !message.isCopied}> <CopyIcon /> </AssistantIf> </TooltipIconButton> </ActionBarPrimitive.Copy> <ActionBarPrimitive.Reload asChild> <TooltipIconButton tooltip="Refresh"> <RefreshCwIcon /> </TooltipIconButton> </ActionBarPrimitive.Reload> </ActionBarPrimitive.Root> );};const UserMessage: FC = () => { return ( <MessagePrimitive.Root className="aui-user-message-root fade-in slide-in-from-bottom-1 mx-auto grid w-full max-w-(--thread-max-width) animate-in auto-rows-auto grid-cols-[minmax(72px,1fr)_auto] content-start gap-y-2 px-2 py-4 duration-150 ease-out [&:where(>*)]:col-start-2" data-role="user" > <UserMessageAttachments /> <div className="aui-user-message-content-wrapper relative col-start-2 min-w-0"> <div className="aui-user-message-content wrap-break-word rounded-3xl bg-muted px-5 py-2.5 text-foreground"> <MessagePrimitive.Parts /> </div> <div className="aui-user-action-bar-wrapper -translate-x-full -translate-y-1/2 absolute top-1/2 left-0 pr-2"> <UserActionBar /> </div> </div> <BranchPicker className="aui-user-branch-picker -mr-1 col-span-full col-start-1 row-start-3 justify-end" /> </MessagePrimitive.Root> );};const UserActionBar: FC = () => { return ( <ActionBarPrimitive.Root hideWhenRunning autohide="not-last" className="aui-user-action-bar-root flex flex-col items-end" > <ActionBarPrimitive.Edit asChild> <TooltipIconButton tooltip="Edit" className="aui-user-action-edit p-4"> <PencilIcon /> </TooltipIconButton> </ActionBarPrimitive.Edit> </ActionBarPrimitive.Root> );};const EditComposer: FC = () => { return ( <MessagePrimitive.Root className="aui-edit-composer-wrapper mx-auto flex w-full max-w-(--thread-max-width) flex-col gap-4 px-2"> <ComposerPrimitive.Root className="aui-edit-composer-root ml-auto flex w-full max-w-7/8 flex-col rounded-xl bg-muted"> <ComposerPrimitive.Input className="aui-edit-composer-input flex min-h-[60px] w-full resize-none bg-transparent p-4 text-foreground outline-none" autoFocus /> <div className="aui-edit-composer-footer mx-3 mb-3 flex items-center justify-center gap-2 self-end"> <ComposerPrimitive.Cancel asChild> <Button variant="ghost" size="sm" aria-label="Cancel edit"> Cancel </Button> </ComposerPrimitive.Cancel> <ComposerPrimitive.Send asChild> <Button size="sm" aria-label="Update message"> Update </Button> </ComposerPrimitive.Send> </div> </ComposerPrimitive.Root> </MessagePrimitive.Root> );};const BranchPicker: FC<BranchPickerPrimitive.Root.Props> = ({ className, ...rest}) => { return ( <BranchPickerPrimitive.Root hideWhenSingleBranch className={cn( "aui-branch-picker-root -ml-2 mr-2 inline-flex items-center text-muted-foreground text-xs", className, )} {...rest} > <BranchPickerPrimitive.Previous asChild> <TooltipIconButton tooltip="Previous"> <ChevronLeftIcon /> </TooltipIconButton> </BranchPickerPrimitive.Previous> <span className="aui-branch-picker-state font-medium"> <BranchPickerPrimitive.Number /> / <BranchPickerPrimitive.Count /> </span> <BranchPickerPrimitive.Next asChild> <TooltipIconButton tooltip="Next"> <ChevronRightIcon /> </TooltipIconButton> </BranchPickerPrimitive.Next> </BranchPickerPrimitive.Root> );};import type { FC } from "react";import { ThreadListItemPrimitive, ThreadListPrimitive, useAssistantState,} from "@assistant-ui/react";import { ArchiveIcon, PlusIcon } from "lucide-react";import { Button } from "@/components/ui/button";import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";import { Skeleton } from "@/components/ui/skeleton";export const ThreadList: FC = () => { return ( <ThreadListPrimitive.Root className="aui-root aui-thread-list-root flex flex-col items-stretch gap-1.5"> <ThreadListNew /> <ThreadListItems /> </ThreadListPrimitive.Root> );};const ThreadListNew: FC = () => { return ( <ThreadListPrimitive.New asChild> <Button className="aui-thread-list-new flex items-center justify-start gap-1 rounded-lg px-2.5 py-2 text-start hover:bg-muted data-active:bg-muted" variant="ghost" > <PlusIcon /> New Thread </Button> </ThreadListPrimitive.New> );};const ThreadListItems: FC = () => { const isLoading = useAssistantState(({ threads }) => threads.isLoading); if (isLoading) { return <ThreadListSkeleton />; } return <ThreadListPrimitive.Items components={{ ThreadListItem }} />;};const ThreadListSkeleton: FC = () => { return ( <> {Array.from({ length: 5 }, (_, i) => ( <div key={i} role="status" aria-label="Loading threads" aria-live="polite" className="aui-thread-list-skeleton-wrapper flex items-center gap-2 rounded-md px-3 py-2" > <Skeleton className="aui-thread-list-skeleton h-[22px] grow" /> </div> ))} </> );};const ThreadListItem: FC = () => { return ( <ThreadListItemPrimitive.Root className="aui-thread-list-item flex items-center gap-2 rounded-lg transition-all hover:bg-muted focus-visible:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring data-active:bg-muted"> <ThreadListItemPrimitive.Trigger className="aui-thread-list-item-trigger grow px-3 py-2 text-start"> <ThreadListItemTitle /> </ThreadListItemPrimitive.Trigger> <ThreadListItemArchive /> </ThreadListItemPrimitive.Root> );};const ThreadListItemTitle: FC = () => { return ( <span className="aui-thread-list-item-title text-sm"> <ThreadListItemPrimitive.Title fallback="New Chat" /> </span> );};const ThreadListItemArchive: FC = () => { return ( <ThreadListItemPrimitive.Archive asChild> <TooltipIconButton className="aui-thread-list-item-archive mr-3 ml-auto size-4 p-0 text-foreground hover:text-primary" variant="ghost" tooltip="Archive thread" > <ArchiveIcon /> </TooltipIconButton> </ThreadListItemPrimitive.Archive> );};assistant-ui dependencies
npm install @assistant-ui/react @assistant-ui/react-markdown remark-gfm zustandyarn add @assistant-ui/react @assistant-ui/react-markdown remark-gfm zustandpnpm add @assistant-ui/react @assistant-ui/react-markdown remark-gfm zustandbun add @assistant-ui/react @assistant-ui/react-markdown remark-gfm zustandxpm add @assistant-ui/react @assistant-ui/react-markdown remark-gfm zustand"use client";import { PropsWithChildren, useEffect, useState, type FC } from "react";import Image from "next/image";import { XIcon, PlusIcon, FileText } from "lucide-react";import { AttachmentPrimitive, ComposerPrimitive, MessagePrimitive, useAssistantState, useAssistantApi,} from "@assistant-ui/react";import { useShallow } from "zustand/shallow";import { Tooltip, TooltipContent, TooltipTrigger,} from "@/components/ui/tooltip";import { Dialog, DialogTitle, DialogContent, DialogTrigger,} from "@/components/ui/dialog";import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";import { cn } from "@/lib/utils";const useFileSrc = (file: File | undefined) => { const [src, setSrc] = useState<string | undefined>(undefined); useEffect(() => { if (!file) { setSrc(undefined); return; } const objectUrl = URL.createObjectURL(file); setSrc(objectUrl); return () => { URL.revokeObjectURL(objectUrl); }; }, [file]); return src;};const useAttachmentSrc = () => { const { file, src } = useAssistantState( useShallow(({ attachment }): { file?: File; src?: string } => { if (attachment.type !== "image") return {}; if (attachment.file) return { file: attachment.file }; const src = attachment.content?.filter((c) => c.type === "image")[0] ?.image; if (!src) return {}; return { src }; }), ); return useFileSrc(file) ?? src;};type AttachmentPreviewProps = { src: string;};const AttachmentPreview: FC<AttachmentPreviewProps> = ({ src }) => { const [isLoaded, setIsLoaded] = useState(false); return ( <Image src={src} alt="Image Preview" width={1} height={1} className={ isLoaded ? "aui-attachment-preview-image-loaded block h-auto max-h-[80vh] w-auto max-w-full object-contain" : "aui-attachment-preview-image-loading hidden" } onLoadingComplete={() => setIsLoaded(true)} priority={false} /> );};const AttachmentPreviewDialog: FC<PropsWithChildren> = ({ children }) => { const src = useAttachmentSrc(); if (!src) return children; return ( <Dialog> <DialogTrigger className="aui-attachment-preview-trigger cursor-pointer transition-colors hover:bg-accent/50" asChild > {children} </DialogTrigger> <DialogContent className="aui-attachment-preview-dialog-content p-2 sm:max-w-3xl [&>button]:rounded-full [&>button]:bg-foreground/60 [&>button]:p-1 [&>button]:opacity-100 [&>button]:ring-0! [&_svg]:text-background [&>button]:hover:[&_svg]:text-destructive"> <DialogTitle className="aui-sr-only sr-only"> Image Attachment Preview </DialogTitle> <div className="aui-attachment-preview relative mx-auto flex max-h-[80dvh] w-full items-center justify-center overflow-hidden bg-background"> <AttachmentPreview src={src} /> </div> </DialogContent> </Dialog> );};const AttachmentThumb: FC = () => { const isImage = useAssistantState( ({ attachment }) => attachment.type === "image", ); const src = useAttachmentSrc(); return ( <Avatar className="aui-attachment-tile-avatar h-full w-full rounded-none"> <AvatarImage src={src} alt="Attachment preview" className="aui-attachment-tile-image object-cover" /> <AvatarFallback delayMs={isImage ? 200 : 0}> <FileText className="aui-attachment-tile-fallback-icon size-8 text-muted-foreground" /> </AvatarFallback> </Avatar> );};const AttachmentUI: FC = () => { const api = useAssistantApi(); const isComposer = api.attachment.source === "composer"; const isImage = useAssistantState( ({ attachment }) => attachment.type === "image", ); const typeLabel = useAssistantState(({ attachment }) => { const type = attachment.type; switch (type) { case "image": return "Image"; case "document": return "Document"; case "file": return "File"; default: const _exhaustiveCheck: never = type; throw new Error(`Unknown attachment type: ${_exhaustiveCheck}`); } }); return ( <Tooltip> <AttachmentPrimitive.Root className={cn( "aui-attachment-root relative", isImage && "aui-attachment-root-composer only:[&>#attachment-tile]:size-24", )} > <AttachmentPreviewDialog> <TooltipTrigger asChild> <div className={cn( "aui-attachment-tile size-14 cursor-pointer overflow-hidden rounded-[14px] border bg-muted transition-opacity hover:opacity-75", isComposer && "aui-attachment-tile-composer border-foreground/20", )} role="button" id="attachment-tile" aria-label={`${typeLabel} attachment`} > <AttachmentThumb /> </div> </TooltipTrigger> </AttachmentPreviewDialog> {isComposer && <AttachmentRemove />} </AttachmentPrimitive.Root> <TooltipContent side="top"> <AttachmentPrimitive.Name /> </TooltipContent> </Tooltip> );};const AttachmentRemove: FC = () => { return ( <AttachmentPrimitive.Remove asChild> <TooltipIconButton tooltip="Remove file" className="aui-attachment-tile-remove absolute top-1.5 right-1.5 size-3.5 rounded-full bg-white text-muted-foreground opacity-100 shadow-sm hover:bg-white! [&_svg]:text-black hover:[&_svg]:text-destructive" side="top" > <XIcon className="aui-attachment-remove-icon size-3 dark:stroke-[2.5px]" /> </TooltipIconButton> </AttachmentPrimitive.Remove> );};export const UserMessageAttachments: FC = () => { return ( <div className="aui-user-message-attachments-end col-span-full col-start-1 row-start-1 flex w-full flex-row justify-end gap-2"> <MessagePrimitive.Attachments components={{ Attachment: AttachmentUI }} /> </div> );};export const ComposerAttachments: FC = () => { return ( <div className="aui-composer-attachments mb-2 flex w-full flex-row items-center gap-2 overflow-x-auto px-1.5 pt-0.5 pb-1 empty:hidden"> <ComposerPrimitive.Attachments components={{ Attachment: AttachmentUI }} /> </div> );};export const ComposerAddAttachment: FC = () => { return ( <ComposerPrimitive.AddAttachment asChild> <TooltipIconButton tooltip="Add Attachment" side="bottom" variant="ghost" size="icon" className="aui-composer-add-attachment size-[34px] rounded-full p-1 font-semibold text-xs hover:bg-muted-foreground/15 dark:border-muted-foreground/15 dark:hover:bg-muted-foreground/30" aria-label="Add Attachment" > <PlusIcon className="aui-attachment-add-icon size-5 stroke-[1.5px]" /> </TooltipIconButton> </ComposerPrimitive.AddAttachment> );};"use client";import { ComponentPropsWithRef, forwardRef } from "react";import { Slottable } from "@radix-ui/react-slot";import { Tooltip, TooltipContent, TooltipTrigger,} from "@/components/ui/tooltip";import { Button } from "@/components/ui/button";import { cn } from "@/lib/utils";export type TooltipIconButtonProps = ComponentPropsWithRef<typeof Button> & { tooltip: string; side?: "top" | "bottom" | "left" | "right";};export const TooltipIconButton = forwardRef< HTMLButtonElement, TooltipIconButtonProps>(({ children, tooltip, side = "bottom", className, ...rest }, ref) => { return ( <Tooltip> <TooltipTrigger asChild> <Button variant="ghost" size="icon" {...rest} className={cn("aui-button-icon size-6 p-1", className)} ref={ref} > <Slottable>{children}</Slottable> <span className="aui-sr-only sr-only">{tooltip}</span> </Button> </TooltipTrigger> <TooltipContent side={side}>{tooltip}</TooltipContent> </Tooltip> );});TooltipIconButton.displayName = "TooltipIconButton";"use client";import "@assistant-ui/react-markdown/styles/dot.css";import { type CodeHeaderProps, MarkdownTextPrimitive, unstable_memoizeMarkdownComponents as memoizeMarkdownComponents, useIsMarkdownCodeBlock,} from "@assistant-ui/react-markdown";import remarkGfm from "remark-gfm";import { type FC, memo, useState } from "react";import { CheckIcon, CopyIcon } from "lucide-react";import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";import { cn } from "@/lib/utils";const MarkdownTextImpl = () => { return ( <MarkdownTextPrimitive remarkPlugins={[remarkGfm]} className="aui-md" components={defaultComponents} /> );};export const MarkdownText = memo(MarkdownTextImpl);const CodeHeader: FC<CodeHeaderProps> = ({ language, code }) => { const { isCopied, copyToClipboard } = useCopyToClipboard(); const onCopy = () => { if (!code || isCopied) return; copyToClipboard(code); }; return ( <div className="aui-code-header-root mt-4 flex items-center justify-between gap-4 rounded-t-lg bg-muted-foreground/15 px-4 py-2 font-semibold text-foreground text-sm dark:bg-muted-foreground/20"> <span className="aui-code-header-language lowercase [&>span]:text-xs"> {language} </span> <TooltipIconButton tooltip="Copy" onClick={onCopy}> {!isCopied && <CopyIcon />} {isCopied && <CheckIcon />} </TooltipIconButton> </div> );};const useCopyToClipboard = ({ copiedDuration = 3000,}: { copiedDuration?: number;} = {}) => { const [isCopied, setIsCopied] = useState<boolean>(false); const copyToClipboard = (value: string) => { if (!value) return; navigator.clipboard.writeText(value).then(() => { setIsCopied(true); setTimeout(() => setIsCopied(false), copiedDuration); }); }; return { isCopied, copyToClipboard };};const defaultComponents = memoizeMarkdownComponents({ h1: ({ className, ...props }) => ( <h1 className={cn( "aui-md-h1 mb-8 scroll-m-20 font-extrabold text-4xl tracking-tight last:mb-0", className, )} {...props} /> ), h2: ({ className, ...props }) => ( <h2 className={cn( "aui-md-h2 mt-8 mb-4 scroll-m-20 font-semibold text-3xl tracking-tight first:mt-0 last:mb-0", className, )} {...props} /> ), h3: ({ className, ...props }) => ( <h3 className={cn( "aui-md-h3 mt-6 mb-4 scroll-m-20 font-semibold text-2xl tracking-tight first:mt-0 last:mb-0", className, )} {...props} /> ), h4: ({ className, ...props }) => ( <h4 className={cn( "aui-md-h4 mt-6 mb-4 scroll-m-20 font-semibold text-xl tracking-tight first:mt-0 last:mb-0", className, )} {...props} /> ), h5: ({ className, ...props }) => ( <h5 className={cn( "aui-md-h5 my-4 font-semibold text-lg first:mt-0 last:mb-0", className, )} {...props} /> ), h6: ({ className, ...props }) => ( <h6 className={cn( "aui-md-h6 my-4 font-semibold first:mt-0 last:mb-0", className, )} {...props} /> ), p: ({ className, ...props }) => ( <p className={cn( "aui-md-p mt-5 mb-5 leading-7 first:mt-0 last:mb-0", className, )} {...props} /> ), a: ({ className, ...props }) => ( <a className={cn( "aui-md-a font-medium text-primary underline underline-offset-4", className, )} {...props} /> ), blockquote: ({ className, ...props }) => ( <blockquote className={cn("aui-md-blockquote border-l-2 pl-6 italic", className)} {...props} /> ), ul: ({ className, ...props }) => ( <ul className={cn("aui-md-ul my-5 ml-6 list-disc [&>li]:mt-2", className)} {...props} /> ), ol: ({ className, ...props }) => ( <ol className={cn("aui-md-ol my-5 ml-6 list-decimal [&>li]:mt-2", className)} {...props} /> ), hr: ({ className, ...props }) => ( <hr className={cn("aui-md-hr my-5 border-b", className)} {...props} /> ), table: ({ className, ...props }) => ( <table className={cn( "aui-md-table my-5 w-full border-separate border-spacing-0 overflow-y-auto", className, )} {...props} /> ), th: ({ className, ...props }) => ( <th className={cn( "aui-md-th bg-muted px-4 py-2 text-left font-bold first:rounded-tl-lg last:rounded-tr-lg [[align=center]]:text-center [[align=right]]:text-right", className, )} {...props} /> ), td: ({ className, ...props }) => ( <td className={cn( "aui-md-td border-b border-l px-4 py-2 text-left last:border-r [[align=center]]:text-center [[align=right]]:text-right", className, )} {...props} /> ), tr: ({ className, ...props }) => ( <tr className={cn( "aui-md-tr m-0 border-b p-0 first:border-t [&:last-child>td:first-child]:rounded-bl-lg [&:last-child>td:last-child]:rounded-br-lg", className, )} {...props} /> ), sup: ({ className, ...props }) => ( <sup className={cn("aui-md-sup [&>a]:text-xs [&>a]:no-underline", className)} {...props} /> ), pre: ({ className, ...props }) => ( <pre className={cn( "aui-md-pre overflow-x-auto rounded-t-none! rounded-b-lg bg-black p-4 text-white", className, )} {...props} /> ), code: function Code({ className, ...props }) { const isCodeBlock = useIsMarkdownCodeBlock(); return ( <code className={cn( !isCodeBlock && "aui-md-inline-code rounded border bg-muted font-semibold", className, )} {...props} /> ); }, CodeHeader,});import type { ToolCallMessagePartComponent } from "@assistant-ui/react";import { CheckIcon, ChevronDownIcon, ChevronUpIcon, XCircleIcon,} from "lucide-react";import { useState } from "react";import { Button } from "@/components/ui/button";import { cn } from "@/lib/utils";export const ToolFallback: ToolCallMessagePartComponent = ({ toolName, argsText, result, status,}) => { const [isCollapsed, setIsCollapsed] = useState(true); const isCancelled = status?.type === "incomplete" && status.reason === "cancelled"; const cancelledReason = isCancelled && status.error ? typeof status.error === "string" ? status.error : JSON.stringify(status.error) : null; return ( <div className={cn( "aui-tool-fallback-root mb-4 flex w-full flex-col gap-3 rounded-lg border py-3", isCancelled && "border-muted-foreground/30 bg-muted/30", )} > <div className="aui-tool-fallback-header flex items-center gap-2 px-4"> {isCancelled ? ( <XCircleIcon className="aui-tool-fallback-icon size-4 text-muted-foreground" /> ) : ( <CheckIcon className="aui-tool-fallback-icon size-4" /> )} <p className={cn( "aui-tool-fallback-title grow", isCancelled && "text-muted-foreground line-through", )} > {isCancelled ? "Cancelled tool: " : "Used tool: "} <b>{toolName}</b> </p> <Button onClick={() => setIsCollapsed(!isCollapsed)}> {isCollapsed ? <ChevronUpIcon /> : <ChevronDownIcon />} </Button> </div> {!isCollapsed && ( <div className="aui-tool-fallback-content flex flex-col gap-2 border-t pt-2"> {cancelledReason && ( <div className="aui-tool-fallback-cancelled-root px-4"> <p className="aui-tool-fallback-cancelled-header font-semibold text-muted-foreground"> Cancelled reason: </p> <p className="aui-tool-fallback-cancelled-reason text-muted-foreground"> {cancelledReason} </p> </div> )} <div className={cn( "aui-tool-fallback-args-root px-4", isCancelled && "opacity-60", )} > <pre className="aui-tool-fallback-args-value whitespace-pre-wrap"> {argsText} </pre> </div> {!isCancelled && result !== undefined && ( <div className="aui-tool-fallback-result-root border-t border-dashed px-4 pt-2"> <p className="aui-tool-fallback-result-header font-semibold"> Result: </p> <pre className="aui-tool-fallback-result-content whitespace-pre-wrap"> {typeof result === "string" ? result : JSON.stringify(result, null, 2)} </pre> </div> )} </div> )} </div> );};shadcn/ui dependencies
npm install @radix-ui/react-avatar @radix-ui/react-dialog @radix-ui/react-slot @radix-ui/react-tooltipyarn add @radix-ui/react-avatar @radix-ui/react-dialog @radix-ui/react-slot @radix-ui/react-tooltippnpm add @radix-ui/react-avatar @radix-ui/react-dialog @radix-ui/react-slot @radix-ui/react-tooltipbun add @radix-ui/react-avatar @radix-ui/react-dialog @radix-ui/react-slot @radix-ui/react-tooltipxpm add @radix-ui/react-avatar @radix-ui/react-dialog @radix-ui/react-slot @radix-ui/react-tooltipimport * as React from "react";import { Slot } from "@radix-ui/react-slot";import { cva, type VariantProps } from "class-variance-authority";import { cn } from "@/lib/utils";const buttonVariants = cva( "inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md font-medium text-sm no-underline outline-none transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0", { variants: { variant: { default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", destructive: "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40", outline: "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50", secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", link: "text-primary underline-offset-4 hover:underline", }, size: { default: "h-9 px-4 py-2 has-[>svg]:px-3", sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5", lg: "h-10 rounded-md px-6 has-[>svg]:px-4", icon: "size-9", }, }, defaultVariants: { variant: "default", size: "default", }, },);function Button({ className, variant, size, asChild = false, ...props}: React.ComponentProps<"button"> & VariantProps<typeof buttonVariants> & { asChild?: boolean; }) { const Comp = asChild ? Slot : "button"; return ( <Comp data-slot="button" className={cn(buttonVariants({ variant, size, className }))} {...props} /> );}export { Button, buttonVariants };"use client";import * as React from "react";import * as DialogPrimitive from "@radix-ui/react-dialog";import { XIcon } from "lucide-react";import { cn } from "@/lib/utils";function Dialog({ ...props}: React.ComponentProps<typeof DialogPrimitive.Root>) { return <DialogPrimitive.Root data-slot="dialog" {...props} />;}function DialogTrigger({ ...props}: React.ComponentProps<typeof DialogPrimitive.Trigger>) { return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;}function DialogPortal({ ...props}: React.ComponentProps<typeof DialogPrimitive.Portal>) { return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;}function DialogClose({ ...props}: React.ComponentProps<typeof DialogPrimitive.Close>) { return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;}function DialogOverlay({ className, ...props}: React.ComponentProps<typeof DialogPrimitive.Overlay>) { return ( <DialogPrimitive.Overlay data-slot="dialog-overlay" className={cn( "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80 data-[state=closed]:animate-out data-[state=open]:animate-in", className, )} {...props} /> );}function DialogContent({ className, children, ...props}: React.ComponentProps<typeof DialogPrimitive.Content>) { return ( <DialogPortal data-slot="dialog-portal"> <DialogOverlay /> <DialogPrimitive.Content data-slot="dialog-content" className={cn( "data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg duration-200 data-[state=closed]:animate-out data-[state=open]:animate-in sm:max-w-lg", className, )} {...props} > {children} <DialogPrimitive.Close className="absolute top-4 right-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0"> <XIcon /> <span className="sr-only">Close</span> </DialogPrimitive.Close> </DialogPrimitive.Content> </DialogPortal> );}function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="dialog-header" className={cn("flex flex-col gap-2 text-center sm:text-left", className)} {...props} /> );}function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="dialog-footer" className={cn( "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className, )} {...props} /> );}function DialogTitle({ className, ...props}: React.ComponentProps<typeof DialogPrimitive.Title>) { return ( <DialogPrimitive.Title data-slot="dialog-title" className={cn("font-semibold text-lg leading-none", className)} {...props} /> );}function DialogDescription({ className, ...props}: React.ComponentProps<typeof DialogPrimitive.Description>) { return ( <DialogPrimitive.Description data-slot="dialog-description" className={cn("text-muted-foreground text-sm", className)} {...props} /> );}export { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger,};"use client";import * as React from "react";import * as TooltipPrimitive from "@radix-ui/react-tooltip";import { cn } from "@/lib/utils";function TooltipProvider({ delayDuration = 0, ...props}: React.ComponentProps<typeof TooltipPrimitive.Provider>) { return ( <TooltipPrimitive.Provider data-slot="tooltip-provider" delayDuration={delayDuration} {...props} /> );}function Tooltip({ ...props}: React.ComponentProps<typeof TooltipPrimitive.Root>) { return ( <TooltipProvider> <TooltipPrimitive.Root data-slot="tooltip" {...props} /> </TooltipProvider> );}function TooltipTrigger({ ...props}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) { return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;}function TooltipContent({ className, sideOffset = 0, children, ...props}: React.ComponentProps<typeof TooltipPrimitive.Content>) { return ( <TooltipPrimitive.Portal> <TooltipPrimitive.Content data-slot="tooltip-content" sideOffset={sideOffset} className={cn( "fade-in-0 zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) animate-in text-balance rounded-md bg-foreground px-3 py-1.5 text-background text-xs data-[state=closed]:animate-out", className, )} {...props} > {children} <TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground" /> </TooltipPrimitive.Content> </TooltipPrimitive.Portal> );}export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };"use client";import * as React from "react";import * as AvatarPrimitive from "@radix-ui/react-avatar";import { cn } from "@/lib/utils";function Avatar({ className, ...props}: React.ComponentProps<typeof AvatarPrimitive.Root>) { return ( <AvatarPrimitive.Root data-slot="avatar" className={cn( "relative flex size-8 shrink-0 overflow-hidden rounded-full", className, )} {...props} /> );}function AvatarImage({ className, ...props}: React.ComponentProps<typeof AvatarPrimitive.Image>) { return ( <AvatarPrimitive.Image data-slot="avatar-image" className={cn("aspect-square size-full", className)} {...props} /> );}function AvatarFallback({ className, ...props}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) { return ( <AvatarPrimitive.Fallback data-slot="avatar-fallback" className={cn( "flex size-full items-center justify-center rounded-full bg-muted", className, )} {...props} /> );}export { Avatar, AvatarImage, AvatarFallback };import { cn } from "@/lib/utils";function Skeleton({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="skeleton" className={cn("animate-pulse rounded-md bg-accent", className)} {...props} /> );}export { Skeleton };Setup Backend Endpoint
Install provider SDK:
npm install ai @assistant-ui/react-ai-sdk @ai-sdk/openaiyarn add ai @assistant-ui/react-ai-sdk @ai-sdk/openaipnpm add ai @assistant-ui/react-ai-sdk @ai-sdk/openaibun add ai @assistant-ui/react-ai-sdk @ai-sdk/openaixpm add ai @assistant-ui/react-ai-sdk @ai-sdk/openainpm install ai @assistant-ui/react-ai-sdk @ai-sdk/anthropicyarn add ai @assistant-ui/react-ai-sdk @ai-sdk/anthropicpnpm add ai @assistant-ui/react-ai-sdk @ai-sdk/anthropicbun add ai @assistant-ui/react-ai-sdk @ai-sdk/anthropicxpm add ai @assistant-ui/react-ai-sdk @ai-sdk/anthropicnpm install ai @assistant-ui/react-ai-sdk @ai-sdk/azureyarn add ai @assistant-ui/react-ai-sdk @ai-sdk/azurepnpm add ai @assistant-ui/react-ai-sdk @ai-sdk/azurebun add ai @assistant-ui/react-ai-sdk @ai-sdk/azurexpm add ai @assistant-ui/react-ai-sdk @ai-sdk/azurenpm install ai @assistant-ui/react-ai-sdk @ai-sdk/amazon-bedrockyarn add ai @assistant-ui/react-ai-sdk @ai-sdk/amazon-bedrockpnpm add ai @assistant-ui/react-ai-sdk @ai-sdk/amazon-bedrockbun add ai @assistant-ui/react-ai-sdk @ai-sdk/amazon-bedrockxpm add ai @assistant-ui/react-ai-sdk @ai-sdk/amazon-bedrocknpm install ai @assistant-ui/react-ai-sdk @ai-sdk/googleyarn add ai @assistant-ui/react-ai-sdk @ai-sdk/googlepnpm add ai @assistant-ui/react-ai-sdk @ai-sdk/googlebun add ai @assistant-ui/react-ai-sdk @ai-sdk/googlexpm add ai @assistant-ui/react-ai-sdk @ai-sdk/googlenpm install ai @assistant-ui/react-ai-sdk @ai-sdk/google-vertexyarn add ai @assistant-ui/react-ai-sdk @ai-sdk/google-vertexpnpm add ai @assistant-ui/react-ai-sdk @ai-sdk/google-vertexbun add ai @assistant-ui/react-ai-sdk @ai-sdk/google-vertexxpm add ai @assistant-ui/react-ai-sdk @ai-sdk/google-vertexnpm install ai @assistant-ui/react-ai-sdk @ai-sdk/openaiyarn add ai @assistant-ui/react-ai-sdk @ai-sdk/openaipnpm add ai @assistant-ui/react-ai-sdk @ai-sdk/openaibun add ai @assistant-ui/react-ai-sdk @ai-sdk/openaixpm add ai @assistant-ui/react-ai-sdk @ai-sdk/openainpm install ai @assistant-ui/react-ai-sdk @ai-sdk/openaiyarn add ai @assistant-ui/react-ai-sdk @ai-sdk/openaipnpm add ai @assistant-ui/react-ai-sdk @ai-sdk/openaibun add ai @assistant-ui/react-ai-sdk @ai-sdk/openaixpm add ai @assistant-ui/react-ai-sdk @ai-sdk/openainpm install ai @assistant-ui/react-ai-sdk @ai-sdk/cohereyarn add ai @assistant-ui/react-ai-sdk @ai-sdk/coherepnpm add ai @assistant-ui/react-ai-sdk @ai-sdk/coherebun add ai @assistant-ui/react-ai-sdk @ai-sdk/coherexpm add ai @assistant-ui/react-ai-sdk @ai-sdk/coherenpm install ai @assistant-ui/react-ai-sdk ollama-ai-provider-v2yarn add ai @assistant-ui/react-ai-sdk ollama-ai-provider-v2pnpm add ai @assistant-ui/react-ai-sdk ollama-ai-provider-v2bun add ai @assistant-ui/react-ai-sdk ollama-ai-provider-v2xpm add ai @assistant-ui/react-ai-sdk ollama-ai-provider-v2npm install ai @assistant-ui/react-ai-sdk chrome-aiyarn add ai @assistant-ui/react-ai-sdk chrome-aipnpm add ai @assistant-ui/react-ai-sdk chrome-aibun add ai @assistant-ui/react-ai-sdk chrome-aixpm add 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 @assistant-ui/assistant-modal`
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>
);
};