Chatbot Components
Chat UI components including Message, PromptInput, Conversation, and supporting components
Chatbot Components
Message Component Suite
The Message component suite provides components for displaying messages from users and AI assistants, managing response branches, adding action buttons, and rendering markdown content.
CSS Requirement
After adding the Message component, add to globals.css:
@source "../node_modules/streamdown/dist/*.js";
Required for MessageResponse component streaming styles.
Component Hierarchy
Message
├── MessageContent
│ └── MessageResponse
├── MessageActions
│ └── MessageAction
├── MessageBranch
│ ├── MessageBranchContent
│ ├── MessageBranchSelector
│ ├── MessageBranchPrevious
│ ├── MessageBranchNext
│ └── MessageBranchPage
└── MessageToolbar
Features
- Displays messages from user and AI assistant with distinct styling
- Minimalist flat design with user messages in secondary background
- Response branching with navigation controls for multiple AI response versions
- Markdown rendering with GFM support (tables, task lists, strikethrough), math equations, smart streaming
- Action buttons for retry, like, dislike, copy, share with tooltips
- File attachments display with images and generic files
- Code blocks with syntax highlighting and copy-to-clipboard
- Keyboard accessible with proper ARIA labels
- Responsive design and seamless light/dark theme integration
<Message />
| Prop | Type | Description |
|------|------|-------------|
| from | UIMessage["role"] | "user", "assistant", or "system" |
| ...props | React.HTMLAttributes<HTMLDivElement> | Spread to root div |
<MessageContent />
| Prop | Type | Description |
|------|------|-------------|
| ...props | React.HTMLAttributes<HTMLDivElement> | Spread to content div |
<MessageResponse />
| Prop | Type | Default |
|------|------|---------|
| children | string | Markdown content to render |
| parseIncompleteMarkdown | boolean | true |
| className | string | CSS class names |
| components | object | Custom React components for markdown elements |
| allowedImagePrefixes | string[] | ["*"] |
| allowedLinkPrefixes | string[] | ["*"] |
| defaultOrigin | string | Default origin for relative URLs |
| rehypePlugins | array | [rehypeKatex] |
| remarkPlugins | array | [remarkGfm, remarkMath] |
| ...props | React.HTMLAttributes<HTMLDivElement> | Spread to root div |
<MessageActions />
| Prop | Type | Description |
|------|------|-------------|
| ...props | React.HTMLAttributes<HTMLDivElement> | Spread to root div |
<MessageAction />
| Prop | Type | Description |
|------|------|-------------|
| tooltip | string | Optional tooltip text on hover |
| label | string | Accessible label for screen readers |
| ...props | React.ComponentProps<typeof Button> | Spread to Button component |
<MessageBranch />
| Prop | Type | Default |
|------|------|---------|
| defaultBranch | number | 0 |
| onBranchChange | (branchIndex: number) => void | Callback when branch changes |
| ...props | React.HTMLAttributes<HTMLDivElement> | Spread to root div |
<MessageBranchContent />
| Prop | Type | Description |
|------|------|-------------|
| ...props | React.HTMLAttributes<HTMLDivElement> | Spread to root div |
<MessageBranchSelector />
| Prop | Type | Description |
|------|------|-------------|
| ...props | React.ComponentProps<typeof ButtonGroup> | Spread to ButtonGroup component |
<MessageBranchPrevious />
| Prop | Type | Description |
|------|------|-------------|
| ...props | React.ComponentProps<typeof Button> | Spread to Button component |
<MessageBranchNext />
| Prop | Type | Description |
|------|------|-------------|
| ...props | React.ComponentProps<typeof Button> | Spread to Button component |
<MessageBranchPage />
| Prop | Type | Description |
|------|------|-------------|
| ...props | React.HTMLAttributes<HTMLSpanElement> | Spread to span element |
<MessageToolbar />
Container for placing actions and branch selectors below a message. Lays out children in a horizontal row with space-between alignment.
| Prop | Type | Description |
|------|------|-------------|
| ...props | React.ComponentProps<"div"> | Spread to div element |
Usage Example
"use client";
import { useState } from "react";
import { useChat } from "@ai-sdk/react";
import { Fragment } from "react";
import {
Message,
MessageContent,
MessageResponse,
MessageActions,
MessageAction,
} from "@/components/ai-elements/message";
import {
Conversation,
ConversationContent,
ConversationScrollButton,
} from "@/components/ai-elements/conversation";
import {
PromptInput,
PromptInputMessage,
PromptInputTextarea,
PromptInputSubmit,
} from "@/components/ai-elements/prompt-input";
import { RefreshCcwIcon, CopyIcon } from "lucide-react";
const ChatDemo = () => {
const [input, setInput] = useState("");
const { messages, sendMessage, status, regenerate } = useChat();
const handleSubmit = (message: PromptInputMessage) => {
if (message.text.trim()) {
sendMessage({ text: message.text });
setInput("");
}
};
return (
<div className="max-w-4xl mx-auto p-6 rounded-lg border h-[600px]">
<div className="flex flex-col h-full">
<Conversation>
<ConversationContent>
{messages.map((message, messageIndex) => (
<Fragment key={message.id}>
{message.parts.map((part, i) => {
switch (part.type) {
case "text":
const isLastMessage = messageIndex === messages.length - 1;
return (
<Fragment key={`${message.id}-${i}`}>
<Message from={message.role}>
<MessageContent>
<MessageResponse>{part.text}</MessageResponse>
</MessageContent>
</Message>
{message.role === "assistant" && isLastMessage && (
<MessageActions>
<MessageAction onClick={() => regenerate()} label="Retry">
<RefreshCcwIcon className="size-3" />
</MessageAction>
<MessageAction
onClick={() => navigator.clipboard.writeText(part.text)}
label="Copy"
>
<CopyIcon className="size-3" />
</MessageAction>
</MessageActions>
)}
</Fragment>
);
default:
return null;
}
})}
</Fragment>
))}
</ConversationContent>
<ConversationScrollButton />
</Conversation>
<PromptInput onSubmit={handleSubmit} className="mt-4">
<PromptInputTextarea
value={input}
placeholder="Say something..."
onChange={(e) => setInput(e.currentTarget.value)}
/>
<PromptInputSubmit
status={status === "streaming" ? "streaming" : "ready"}
disabled={!input.trim()}
/>
</PromptInput>
</div>
</div>
);
};
PromptInput Component
Allows users to send messages with file attachments. Includes textarea, file upload, submit button, and model selection dropdown.
Component Hierarchy
PromptInput
├── PromptInputHeader
├── PromptInputBody
│ └── PromptInputTextarea
├── PromptInputFooter
│ ├── PromptInputTools
│ │ ├── PromptInputActionMenu
│ │ ├── PromptInputButton
│ │ └── PromptInputSelect
│ └── PromptInputSubmit
└── PromptInputProvider
Features
- Auto-resizing textarea that adjusts height based on content
- File attachment support with drag-and-drop
- Built-in screenshot capture action
- Image preview for image attachments
- Configurable file constraints (max files, max size, accepted types)
- Automatic submit button icons based on status
- Keyboard shortcuts (Enter submit, Shift+Enter new line)
- Customizable min/max height for textarea
- Flexible toolbar with custom actions and tools
- Built-in model selection dropdown
- Built-in native speech recognition (Web Speech API)
- Optional
PromptInputProviderfor lifted state management - Form automatically resets on submit
- Global document drop support (opt-in)
<PromptInput />
| Prop | Type | Description |
|------|------|-------------|
| onSubmit | (message: PromptInputMessage, event: FormEvent) => void | Form submission handler |
| accept | string | File types to accept (e.g., "image/*") |
| multiple | boolean | Allow multiple file selection |
| globalDrop | boolean | Accept file drops anywhere on document |
| syncHiddenInput | boolean | Render hidden input for native form posts |
| maxFiles | number | Maximum number of files |
| maxFileSize | number | Maximum file size in bytes |
| onError | (err: { code, message }) => void | File validation error handler |
| ...props | React.HTMLAttributes<HTMLFormElement> | Spread to root form |
<PromptInputTextarea />
| Prop | Type | Description |
|------|------|-------------|
| ...props | React.ComponentProps<typeof Textarea> | Spread to Textarea component |
<PromptInputFooter />
| Prop | Type | Description |
|------|------|-------------|
| ...props | React.HTMLAttributes<HTMLDivElement> | Spread to toolbar div |
<PromptInputTools />
| Prop | Type | Description |
|------|------|-------------|
| ...props | React.HTMLAttributes<HTMLDivElement> | Spread to tools div |
<PromptInputButton />
| Prop | Type | Description |
|------|------|-------------|
| tooltip | string \| { content, shortcut?, side? } | Tooltip on hover |
| ...props | React.ComponentProps<typeof Button> | Spread to Button component |
<PromptInputSubmit />
| Prop | Type | Description |
|------|------|-------------|
| status | ChatStatus | Current chat status |
| ...props | React.ComponentProps<typeof Button> | Spread to Button component |
<PromptInputSelect />
| Prop | Type | Description |
|------|------|-------------|
| ...props | React.ComponentProps<typeof Select> | Spread to Select component |
<PromptInputActionMenu />
| Prop | Type | Description |
|------|------|-------------|
| ...props | React.ComponentProps<typeof DropdownMenu> | Spread to DropdownMenu |
<PromptInputActionMenuTrigger />
| Prop | Type | Description |
|------|------|-------------|
| ...props | React.ComponentProps<typeof Button> | Spread to Button component |
<PromptInputHeader />
| Prop | Type | Description |
|------|------|-------------|
| ...props | Omit<React.ComponentProps<typeof InputGroupAddon>, "align"> | Spread to InputGroupAddon |
<PromptInputProvider />
| Prop | Type | Description |
|------|------|-------------|
| initialInput | string | Initial text input value |
| children | React.ReactNode | Child components with access to context |
Optional provider that lifts PromptInput state outside of PromptInput.
Hooks
usePromptInputAttachments
Access and manage file attachments within a PromptInput context.
const attachments = usePromptInputAttachments();
attachments.files; // Array of current attachments
attachments.add(files); // Add new files
attachments.remove(id); // Remove an attachment by ID
attachments.clear(); // Clear all attachments
attachments.openFileDialog(); // Open file selection dialog
usePromptInputController
Available when using PromptInputProvider.
const controller = usePromptInputController();
controller.textInput.value; // Current text input value
controller.textInput.setInput(value); // Set text input value
controller.textInput.clear(); // Clear text input
controller.attachments; // Same as usePromptInputAttachments
Usage with AI SDK
"use client";
import { useState } from "react";
import { useChat } from "@ai-sdk/react";
import {
PromptInput,
PromptInputMessage,
PromptInputTextarea,
PromptInputSubmit,
PromptInputBody,
PromptInputFooter,
PromptInputTools,
PromptInputActionMenu,
PromptInputActionMenuTrigger,
PromptInputActionMenuContent,
PromptInputActionAddAttachments,
PromptInputActionAddScreenshot,
usePromptInputAttachments,
Attachment,
AttachmentPreview,
AttachmentRemove,
Attachments,
} from "@/components/ai-elements/prompt-input";
import {
Conversation,
ConversationContent,
ConversationScrollButton,
} from "@/components/ai-elements/conversation";
import {
Message,
MessageContent,
MessageResponse,
} from "@/components/ai-elements/message";
const models = [
{ id: "gpt-4o", name: "GPT-4o" },
{ id: "claude-opus-4-20250514", name: "Claude 4 Opus" },
];
const PromptInputDemo = () => {
const [text, setText] = useState("");
const [model, setModel] = useState(models[0].id);
const [useWebSearch, setUseWebSearch] = useState(false);
const { messages, status, sendMessage } = useChat();
const attachments = usePromptInputAttachments();
const handleSubmit = (message: PromptInputMessage) => {
const hasText = Boolean(message.text);
const hasAttachments = Boolean(message.files?.length);
if (!(hasText || hasAttachments)) {
return;
}
sendMessage(
{ text: message.text || "Sent with attachments", files: message.files },
{ body: { model, webSearch: useWebSearch } }
);
setText("");
};
return (
<div className="max-w-4xl mx-auto p-6 rounded-lg border h-[600px]">
<div className="flex flex-col h-full">
<Conversation>
<ConversationContent>
{messages.map((message) => (
<Message from={message.role} key={message.id}>
<MessageContent>
{message.parts.map((part, i) => {
if (part.type === "text") {
return (
<MessageResponse key={`${message.id}-${i}`}>
{part.text}
</MessageResponse>
);
}
return null;
})}
</MessageContent>
</Message>
))}
</ConversationContent>
<ConversationScrollButton />
</Conversation>
<PromptInput onSubmit={handleSubmit} globalDrop multiple>
<PromptInputHeader>
{attachments.files.length > 0 && (
<Attachments variant="inline">
{attachments.files.map((attachment) => (
<Attachment
key={attachment.id}
data={attachment}
onRemove={() => attachments.remove(attachment.id)}
>
<AttachmentPreview />
<AttachmentRemove />
</Attachment>
))}
</Attachments>
)}
</PromptInputHeader>
<PromptInputBody>
<PromptInputTextarea
onChange={(e) => setText(e.target.value)}
value={text}
/>
</PromptInputBody>
<PromptInputFooter>
<PromptInputTools>
<PromptInputActionMenu>
<PromptInputActionMenuTrigger />
<PromptInputActionMenuContent>
<PromptInputActionAddAttachments />
<PromptInputActionAddScreenshot />
</PromptInputActionMenuContent>
</PromptInputActionMenu>
<PromptInputSelect value={model} onValueChange={setModel}>
<PromptInputSelectTrigger>
<PromptInputSelectValue />
</PromptInputSelectTrigger>
<PromptInputSelectContent>
{models.map((m) => (
<PromptInputSelectItem key={m.id} value={m.id}>
{m.name}
</PromptInputSelectItem>
))}
</PromptInputSelectContent>
</PromptInputSelect>
</PromptInputTools>
<PromptInputSubmit disabled={!text && !status} status={status} />
</PromptInputFooter>
</PromptInput>
</div>
</div>
);
};
Backend Route
import { streamText, UIMessage, convertToModelMessages } from "ai";
export const maxDuration = 30;
export async function POST(req: Request) {
const { model, messages, webSearch } = await req.json();
const result = streamText({
model: webSearch ? "perplexity/sonar" : model,
messages: await convertToModelMessages(messages),
});
return result.toUIMessageStreamResponse();
}
Conversation Component
Scrollable container for messages with scroll-to-bottom button.
Component Hierarchy
Conversation
├── ConversationContent
└── ConversationScrollButton