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 mcp
Add assistant-ui to an existing React project:
# Add assistant-ui to an existing React project
npx assistant-ui@latest init
Add 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 dev
Manual installation
We recommend npx assistant-ui init
to setup existing projects.
Add assistant-ui
npx assistant-ui add thread thread-list
Add the following packages:
npm install \
@assistant-ui/react \
@assistant-ui/react-markdown \
@assistant-ui/styles \
@radix-ui/react-avatar \
@radix-ui/react-dialog \
@radix-ui/react-slot \
@radix-ui/react-tooltip \
class-variance-authority \
clsx \
lucide-react \
motion \
remark-gfm \
zustand
Copy the following components into your project:
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva("aui-button", {
variants: {
variant: {
default: "aui-button-primary",
outline: "aui-button-outline",
ghost: "aui-button-ghost",
},
size: {
default: "aui-button-medium",
icon: "aui-button-icon",
},
},
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 TooltipPrimitive from "@radix-ui/react-tooltip";
import { cn } from "@/lib/utils";
const TooltipProvider = TooltipPrimitive.Provider;
const Tooltip = TooltipPrimitive.Root;
const TooltipTrigger = TooltipPrimitive.Trigger;
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn("aui-tooltip-content", className)}
{...props}
/>
</TooltipPrimitive.Portal>
));
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
import {
ArrowDownIcon,
ArrowUpIcon,
CheckIcon,
ChevronLeftIcon,
ChevronRightIcon,
CopyIcon,
PencilIcon,
RefreshCwIcon,
Square,
} from "lucide-react";
import {
ActionBarPrimitive,
BranchPickerPrimitive,
ComposerPrimitive,
ErrorPrimitive,
MessagePrimitive,
ThreadPrimitive,
} from "@assistant-ui/react";
import type { FC } from "react";
import { LazyMotion, MotionConfig, domAnimation } from "motion/react";
import * as m from "motion/react-m";
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 (
<LazyMotion features={domAnimation}>
<MotionConfig reducedMotion="user">
<ThreadPrimitive.Root
className="aui-root aui-thread-root"
style={{
["--thread-max-width" as string]: "44rem",
}}
>
<ThreadPrimitive.Viewport className="aui-thread-viewport">
<ThreadPrimitive.If empty>
<ThreadWelcome />
</ThreadPrimitive.If>
<ThreadPrimitive.Messages
components={{
UserMessage,
EditComposer,
AssistantMessage,
}}
/>
<ThreadPrimitive.If empty={false}>
<div className="aui-thread-viewport-spacer" />
</ThreadPrimitive.If>
<Composer />
</ThreadPrimitive.Viewport>
</ThreadPrimitive.Root>
</MotionConfig>
</LazyMotion>
);
};
const ThreadScrollToBottom: FC = () => {
return (
<ThreadPrimitive.ScrollToBottom asChild>
<TooltipIconButton
tooltip="Scroll to bottom"
variant="outline"
className="aui-thread-scroll-to-bottom"
>
<ArrowDownIcon />
</TooltipIconButton>
</ThreadPrimitive.ScrollToBottom>
);
};
const ThreadWelcome: FC = () => {
return (
<div className="aui-thread-welcome-root">
<div className="aui-thread-welcome-center">
<div className="aui-thread-welcome-message">
<m.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 10 }}
className="aui-thread-welcome-message-motion-1"
>
Hello there!
</m.div>
<m.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 10 }}
transition={{ delay: 0.1 }}
className="aui-thread-welcome-message-motion-2"
>
How can I help you today?
</m.div>
</div>
</div>
<ThreadSuggestions />
</div>
);
};
const ThreadSuggestions: FC = () => {
return (
<div className="aui-thread-welcome-suggestions">
{[
{
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) => (
<m.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 20 }}
transition={{ delay: 0.05 * index }}
key={`suggested-action-${suggestedAction.title}-${index}`}
className="aui-thread-welcome-suggestion-display"
>
<ThreadPrimitive.Suggestion
prompt={suggestedAction.action}
send
asChild
>
<Button
variant="ghost"
className="aui-thread-welcome-suggestion"
aria-label={suggestedAction.action}
>
<span className="aui-thread-welcome-suggestion-text-1">
{suggestedAction.title}
</span>
<span className="aui-thread-welcome-suggestion-text-2">
{suggestedAction.label}
</span>
</Button>
</ThreadPrimitive.Suggestion>
</m.div>
))}
</div>
);
};
const Composer: FC = () => {
return (
<div className="aui-composer-wrapper">
<ThreadScrollToBottom />
<ComposerPrimitive.Root className="aui-composer-root">
<ComposerAttachments />
<ComposerPrimitive.Input
placeholder="Send a message..."
className="aui-composer-input"
rows={1}
autoFocus
aria-label="Message input"
/>
<ComposerAction />
</ComposerPrimitive.Root>
</div>
);
};
const ComposerAction: FC = () => {
return (
<div className="aui-composer-action-wrapper">
<ComposerAddAttachment />
<ThreadPrimitive.If running={false}>
<ComposerPrimitive.Send asChild>
<TooltipIconButton
tooltip="Send message"
side="bottom"
type="submit"
variant="default"
size="icon"
className="aui-composer-send"
aria-label="Send message"
>
<ArrowUpIcon className="aui-composer-send-icon" />
</TooltipIconButton>
</ComposerPrimitive.Send>
</ThreadPrimitive.If>
<ThreadPrimitive.If running>
<ComposerPrimitive.Cancel asChild>
<Button
type="button"
variant="default"
size="icon"
className="aui-composer-cancel"
aria-label="Stop generating"
>
<Square className="aui-composer-cancel-icon" />
</Button>
</ComposerPrimitive.Cancel>
</ThreadPrimitive.If>
</div>
);
};
const MessageError: FC = () => {
return (
<MessagePrimitive.Error>
<ErrorPrimitive.Root className="aui-message-error-root">
<ErrorPrimitive.Message className="aui-message-error-message" />
</ErrorPrimitive.Root>
</MessagePrimitive.Error>
);
};
const AssistantMessage: FC = () => {
return (
<MessagePrimitive.Root asChild>
<div
className="aui-assistant-message-root"
data-role="assistant"
>
<div className="aui-assistant-message-content">
<MessagePrimitive.Parts
components={{
Text: MarkdownText,
tools: { Fallback: ToolFallback },
}}
/>
<MessageError />
</div>
<div className="aui-assistant-message-footer">
<BranchPicker />
<AssistantActionBar />
</div>
</div>
</MessagePrimitive.Root>
);
};
const AssistantActionBar: FC = () => {
return (
<ActionBarPrimitive.Root
hideWhenRunning
autohide="not-last"
autohideFloat="single-branch"
className="aui-assistant-action-bar-root"
>
<ActionBarPrimitive.Copy asChild>
<TooltipIconButton tooltip="Copy">
<MessagePrimitive.If copied>
<CheckIcon />
</MessagePrimitive.If>
<MessagePrimitive.If copied={false}>
<CopyIcon />
</MessagePrimitive.If>
</TooltipIconButton>
</ActionBarPrimitive.Copy>
<ActionBarPrimitive.Reload asChild>
<TooltipIconButton tooltip="Refresh">
<RefreshCwIcon />
</TooltipIconButton>
</ActionBarPrimitive.Reload>
</ActionBarPrimitive.Root>
);
};
const UserMessage: FC = () => {
return (
<MessagePrimitive.Root asChild>
<div
className="aui-user-message-root"
data-role="user"
>
<UserMessageAttachments />
<div className="aui-user-message-content-wrapper">
<div className="aui-user-message-content">
<MessagePrimitive.Parts />
</div>
<div className="aui-user-action-bar-wrapper">
<UserActionBar />
</div>
</div>
<BranchPicker className="aui-user-branch-picker" />
</div>
</MessagePrimitive.Root>
);
};
const UserActionBar: FC = () => {
return (
<ActionBarPrimitive.Root
hideWhenRunning
autohide="not-last"
className="aui-user-action-bar-root"
>
<ActionBarPrimitive.Edit asChild>
<TooltipIconButton tooltip="Edit" className="aui-user-action-edit">
<PencilIcon />
</TooltipIconButton>
</ActionBarPrimitive.Edit>
</ActionBarPrimitive.Root>
);
};
const EditComposer: FC = () => {
return (
<div className="aui-edit-composer-wrapper">
<ComposerPrimitive.Root className="aui-edit-composer-root">
<ComposerPrimitive.Input
className="aui-edit-composer-input"
autoFocus
/>
<div className="aui-edit-composer-footer">
<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>
</div>
);
};
const BranchPicker: FC<BranchPickerPrimitive.Root.Props> = ({
className,
...rest
}) => {
return (
<BranchPickerPrimitive.Root
hideWhenSingleBranch
className={cn("aui-branch-picker-root", className)}
{...rest}
>
<BranchPickerPrimitive.Previous asChild>
<TooltipIconButton tooltip="Previous">
<ChevronLeftIcon />
</TooltipIconButton>
</BranchPickerPrimitive.Previous>
<span className="aui-branch-picker-state">
<BranchPickerPrimitive.Number /> / <BranchPickerPrimitive.Count />
</span>
<BranchPickerPrimitive.Next asChild>
<TooltipIconButton tooltip="Next">
<ChevronRightIcon />
</TooltipIconButton>
</BranchPickerPrimitive.Next>
</BranchPickerPrimitive.Root>
);
};
"use client";
import { PropsWithChildren, useEffect, useState, type FC } from "react";
import Image from "next/image";
import { XIcon, PlusIcon, FileText } from "lucide-react";
import {
AttachmentPrimitive,
ComposerPrimitive,
MessagePrimitive,
useAssistantState,
useAssistantApi,
} from "@assistant-ui/react";
import { useShallow } from "zustand/shallow";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import {
Dialog,
DialogTitle,
DialogContent,
DialogTrigger,
} from "@/components/ui/dialog";
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
import { cn } from "@/lib/utils";
const useFileSrc = (file: File | undefined) => {
const [src, setSrc] = useState<string | undefined>(undefined);
useEffect(() => {
if (!file) {
setSrc(undefined);
return;
}
const objectUrl = URL.createObjectURL(file);
setSrc(objectUrl);
return () => {
URL.revokeObjectURL(objectUrl);
};
}, [file]);
return src;
};
const useAttachmentSrc = () => {
const { file, src } = useAssistantState(
useShallow(({ attachment }): { file?: File; src?: string } => {
if (attachment.type !== "image") return {};
if (attachment.file) return { file: attachment.file };
const src = attachment.content?.filter((c) => c.type === "image")[0]
?.image;
if (!src) return {};
return { src };
}),
);
return useFileSrc(file) ?? src;
};
type AttachmentPreviewProps = {
src: string;
};
const AttachmentPreview: FC<AttachmentPreviewProps> = ({ src }) => {
const [isLoaded, setIsLoaded] = useState(false);
return (
<Image
src={src}
alt="Image Preview"
width={1}
height={1}
className={
isLoaded
? "aui-attachment-preview-image-loaded"
: "aui-attachment-preview-image-loading"
}
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" asChild>
{children}
</DialogTrigger>
<DialogContent className="aui-attachment-preview-dialog-content">
<DialogTitle className="aui-sr-only">
Image Attachment Preview
</DialogTitle>
<div className="aui-attachment-preview">
<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">
<AvatarImage
src={src}
alt="Attachment preview"
className="aui-attachment-tile-image"
/>
<AvatarFallback delayMs={isImage ? 200 : 0}>
<FileText className="aui-attachment-tile-fallback-icon" />
</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",
isImage && "aui-attachment-root-composer",
)}
>
<AttachmentPreviewDialog>
<TooltipTrigger asChild>
<div
className={cn(
"aui-attachment-tile",
isComposer && "aui-attachment-tile-composer",
)}
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"
side="top"
>
<XIcon className="aui-attachment-remove-icon" />
</TooltipIconButton>
</AttachmentPrimitive.Remove>
);
};
export const UserMessageAttachments: FC = () => {
return (
<div className="aui-user-message-attachments-end">
<MessagePrimitive.Attachments components={{ Attachment: AttachmentUI }} />
</div>
);
};
export const ComposerAttachments: FC = () => {
return (
<div className="aui-composer-attachments">
<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"
aria-label="Add Attachment"
>
<PlusIcon className="aui-attachment-add-icon" />
</TooltipIconButton>
</ComposerPrimitive.AddAttachment>
);
};
import type { ToolCallMessagePartComponent } from "@assistant-ui/react";
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
export const ToolFallback: ToolCallMessagePartComponent = ({
toolName,
argsText,
result,
}) => {
const [isCollapsed, setIsCollapsed] = useState(true);
return (
<div className="aui-tool-fallback-root">
<div className="aui-tool-fallback-header">
<CheckIcon className="aui-tool-fallback-icon" />
<p className="aui-tool-fallback-title">
Used tool: <b>{toolName}</b>
</p>
<Button onClick={() => setIsCollapsed(!isCollapsed)}>
{isCollapsed ? <ChevronUpIcon /> : <ChevronDownIcon />}
</Button>
</div>
{!isCollapsed && (
<div className="aui-tool-fallback-content">
<div className="aui-tool-fallback-args-root">
<pre className="aui-tool-fallback-args-value">
{argsText}
</pre>
</div>
{result !== undefined && (
<div className="aui-tool-fallback-result-root">
<p className="aui-tool-fallback-result-header">
Result:
</p>
<pre className="aui-tool-fallback-result-content">
{typeof result === "string"
? result
: JSON.stringify(result, null, 2)}
</pre>
</div>
)}
</div>
)}
</div>
);
};
```tsx title="components/assistant-ui/thread-list.tsx"
import type { FC } from "react";
import {
ThreadListItemPrimitive,
ThreadListPrimitive,
} 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";
export const ThreadList: FC = () => {
return (
<ThreadListPrimitive.Root className="aui-root aui-thread-list-root">
<ThreadListNew />
<ThreadListItems />
</ThreadListPrimitive.Root>
);
};
const ThreadListNew: FC = () => {
return (
<ThreadListPrimitive.New asChild>
<Button className="aui-thread-list-new" variant="ghost">
<PlusIcon />
New Thread
</Button>
</ThreadListPrimitive.New>
);
};
const ThreadListItems: FC = () => {
return <ThreadListPrimitive.Items components={{ ThreadListItem }} />;
};
const ThreadListItem: FC = () => {
return (
<ThreadListItemPrimitive.Root className="aui-thread-list-item">
<ThreadListItemPrimitive.Trigger className="aui-thread-list-item-trigger">
<ThreadListItemTitle />
</ThreadListItemPrimitive.Trigger>
<ThreadListItemArchive />
</ThreadListItemPrimitive.Root>
);
};
const ThreadListItemTitle: FC = () => {
return (
<span className="aui-thread-list-item-title">
<ThreadListItemPrimitive.Title fallback="New Chat" />
</span>
);
};
const ThreadListItemArchive: FC = () => {
return (
<ThreadListItemPrimitive.Archive asChild>
<TooltipIconButton
className="aui-thread-list-item-archive"
variant="ghost"
tooltip="Archive thread"
>
<ArchiveIcon />
</TooltipIconButton>
</ThreadListItemPrimitive.Archive>
);
};
"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">
<span className="aui-code-header-language">{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", className)} {...props} />
),
h2: ({ className, ...props }) => (
<h2 className={cn("aui-md-h2", className)} {...props} />
),
h3: ({ className, ...props }) => (
<h3 className={cn("aui-md-h3", className)} {...props} />
),
h4: ({ className, ...props }) => (
<h4 className={cn("aui-md-h4", className)} {...props} />
),
h5: ({ className, ...props }) => (
<h5 className={cn("aui-md-h5", className)} {...props} />
),
h6: ({ className, ...props }) => (
<h6 className={cn("aui-md-h6", className)} {...props} />
),
p: ({ className, ...props }) => (
<p className={cn("aui-md-p", className)} {...props} />
),
a: ({ className, ...props }) => (
<a className={cn("aui-md-a", className)} {...props} />
),
blockquote: ({ className, ...props }) => (
<blockquote className={cn("aui-md-blockquote", className)} {...props} />
),
ul: ({ className, ...props }) => (
<ul className={cn("aui-md-ul", className)} {...props} />
),
ol: ({ className, ...props }) => (
<ol className={cn("aui-md-ol", className)} {...props} />
),
hr: ({ className, ...props }) => (
<hr className={cn("aui-md-hr", className)} {...props} />
),
table: ({ className, ...props }) => (
<table className={cn("aui-md-table", className)} {...props} />
),
th: ({ className, ...props }) => (
<th className={cn("aui-md-th", className)} {...props} />
),
td: ({ className, ...props }) => (
<td className={cn("aui-md-td", className)} {...props} />
),
tr: ({ className, ...props }) => (
<tr className={cn("aui-md-tr", className)} {...props} />
),
sup: ({ className, ...props }) => (
<sup className={cn("aui-md-sup", className)} {...props} />
),
pre: ({ className, ...props }) => (
<pre className={cn("aui-md-pre", className)} {...props} />
),
code: function Code({ className, ...props }) {
const isCodeBlock = useIsMarkdownCodeBlock();
return (
<code
className={cn(!isCodeBlock && "aui-md-inline-code", className)}
{...props}
/>
);
},
CodeHeader,
});
"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", className)}
ref={ref}
>
<Slottable>{children}</Slottable>
<span className="aui-sr-only">{tooltip}</span>
</Button>
</TooltipTrigger>
<TooltipContent side={side}>{tooltip}</TooltipContent>
</Tooltip>
);
});
TooltipIconButton.displayName = "TooltipIconButton";
import { type ClassValue, clsx } from "clsx";
export function cn(...inputs: ClassValue[]) {
return clsx(inputs);
}
The components above reference CSS class names like aui-thread-root
, aui-composer-input
, etc. These are normally replaced by our CLI with Tailwind class names, but in this case you'll use our pre-compiled CSS files without a need for Tailwind:
import "@assistant-ui/styles/index.css";
import "@assistant-ui/styles/markdown.css";
// import "@assistant-ui/styles/modal.css"; // for future reference, only if you use our modal component
Setup Backend Endpoint
Install provider SDK:
npm install ai @assistant-ui/react-ai-sdk @ai-sdk/openai
npm install ai @assistant-ui/react-ai-sdk @ai-sdk/anthropic
npm install ai @assistant-ui/react-ai-sdk @ai-sdk/azure
npm install ai @assistant-ui/react-ai-sdk @ai-sdk/amazon-bedrock
npm install ai @assistant-ui/react-ai-sdk @ai-sdk/google
npm install ai @assistant-ui/react-ai-sdk @ai-sdk/google-vertex
npm install ai @assistant-ui/react-ai-sdk @ai-sdk/openai
npm install ai @assistant-ui/react-ai-sdk @ai-sdk/openai
npm install ai @assistant-ui/react-ai-sdk @ai-sdk/cohere
npm install ai @assistant-ui/react-ai-sdk ollama-ai-provider
npm install ai @assistant-ui/react-ai-sdk chrome-ai
Add 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";
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 } 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({
api: "/api/chat",
});
return (
<AssistantRuntimeProvider runtime={runtime}>
<div>
<ThreadList />
<Thread />
</div>
</AssistantRuntimeProvider>
);
};
// run `npx shadcn@latest add "https://r.assistant-ui.com/assistant-modal"`
import { AssistantRuntimeProvider } from "@assistant-ui/react";
import { useChatRuntime } from "@assistant-ui/react-ai-sdk";
import { AssistantModal } from "@/components/assistant-ui/assistant-modal";
const MyApp = () => {
const runtime = useChatRuntime({
api: "/api/chat",
});
return (
<AssistantRuntimeProvider runtime={runtime}>
<AssistantModal />
</AssistantRuntimeProvider>
);
};