Chatbot Components (Continued)
Additional chatbot components including Attachments, ModelSelector, Suggestion, Reasoning
Chatbot Components (Continued)
Attachments Component
A flexible, composable attachment component for displaying files, images, videos, audio, and source documents.
Features
- Three display variants: grid (thumbnails), inline (badges), and list (rows)
- Supports both
FileUIPartandSourceDocumentUIPartfrom AI SDK - Automatic media type detection (image, video, audio, document, source)
- Hover card support for inline previews
- Remove button with customizable callback
- Composable architecture for maximum flexibility
- Accessible with proper ARIA labels
Usage
"use client";
import {
Attachments,
Attachment,
AttachmentPreview,
AttachmentInfo,
AttachmentRemove,
} from "@/components/ai-elements/attachments";
import type { FileUIPart } from "ai";
interface MessageProps {
attachments: (FileUIPart & { id: string })[];
onRemove?: (id: string) => void;
}
const MessageAttachments = ({ attachments, onRemove }: MessageProps) => (
<Attachments variant="grid">
{attachments.map((file) => (
<Attachment
key={file.id}
data={file}
onRemove={onRemove ? () => onRemove(file.id) : undefined}
>
<AttachmentPreview />
<AttachmentRemove />
</Attachment>
))}
</Attachments>
);
<Attachments />
Container component that sets the layout variant.
| Prop | Type | Default |
|------|------|---------|
| variant | "grid" \| "inline" \| "list" | "grid" |
| ...props | React.HTMLAttributes<HTMLDivElement> | Spread to div |
<Attachment />
Individual attachment item wrapper.
| Prop | Type | Description |
|------|------|-------------|
| data | (FileUIPart & { id: string }) \| (SourceDocumentUIPart & { id: string }) | Attachment data |
| onRemove | () => void | Callback when remove clicked |
| ...props | React.HTMLAttributes<HTMLDivElement> | Spread to div |
<AttachmentPreview />
Displays the media preview (image, video, or icon).
| Prop | Type | Description |
|------|------|-------------|
| fallbackIcon | React.ReactNode | Icon when no preview available |
| ...props | React.HTMLAttributes<HTMLDivElement> | Spread to div |
<AttachmentInfo />
Displays the filename and optional media type.
| Prop | Type | Default |
|------|------|---------|
| showMediaType | boolean | false |
| ...props | React.HTMLAttributes<HTMLDivElement> | Spread to div |
<AttachmentRemove />
Remove button that appears on hover.
| Prop | Type | Default |
|------|------|---------|
| label | string | "Remove" |
| ...props | React.ComponentProps<typeof Button> | Spread to Button |
<AttachmentHoverCard />
Wrapper for hover preview functionality.
| Prop | Type | Default |
|------|------|---------|
| openDelay | number | 0 |
| closeDelay | number | 0 |
| ...props | React.ComponentProps<typeof HoverCard> | Spread to HoverCard |
<AttachmentHoverCardTrigger />
| Prop | Type | Description |
|------|------|-------------|
| ...props | React.ComponentProps<typeof HoverCardTrigger> | Spread to HoverCardTrigger |
<AttachmentHoverCardContent />
| Prop | Type | Default |
|------|------|---------|
| align | "start" \| "center" \| "end" | "start" |
| ...props | React.ComponentProps<typeof HoverCardContent> | Spread to HoverCardContent |
<AttachmentEmpty />
Empty state component when no attachments are present.
| Prop | Type | Description |
|------|------|-------------|
| ...props | React.HTMLAttributes<HTMLDivElement> | Spread to div |
Utility Functions
getMediaCategory(data)
Returns the media category for an attachment.
import { getMediaCategory } from "@/components/ai-elements/attachments";
const category = getMediaCategory(attachment);
// Returns: "image" | "video" | "audio" | "document" | "source" | "unknown"
getAttachmentLabel(data)
Returns the display label for an attachment.
import { getAttachmentLabel } from "@/components/ai-elements/attachments";
const label = getAttachmentLabel(attachment);
// Returns filename or fallback like "Image" or "Attachment"
ModelSelector Component
A searchable command palette for selecting AI models. Built on cmdk library.
Features
- Searchable interface with keyboard navigation
- Fuzzy search filtering across model names
- Grouped model organization by provider
- Keyboard shortcuts support
- Empty state handling
- Built on cmdk for accessibility
<ModelSelector />
| Prop | Type | Description |
|------|------|-------------|
| ...props | React.ComponentProps<typeof Dialog> | Spread to Dialog |
<ModelSelectorTrigger />
| Prop | Type | Description |
|------|------|-------------|
| ...props | React.ComponentProps<typeof DialogTrigger> | Spread to DialogTrigger |
<ModelSelectorContent />
| Prop | Type | Default |
|------|------|---------|
| title | ReactNode | "Model Selector" |
| ...props | React.ComponentProps<typeof DialogContent> | Spread to DialogContent |
<ModelSelectorDialog />
| Prop | Type | Description |
|------|------|-------------|
| ...props | React.ComponentProps<typeof CommandDialog> | Spread to CommandDialog |
Suggestion Component
A horizontal row of clickable suggestions for user interaction.
Features
- Horizontal row of clickable suggestion buttons
- Customizable styling with variant and size options
- Flexible layout that wraps on smaller screens
- onClick callback that emits the selected suggestion string
- Responsive design with mobile-friendly touch targets
Usage
"use client";
import {
PromptInput,
PromptInputMessage,
PromptInputTextarea,
PromptInputSubmit,
} from "@/components/ai-elements/prompt-input";
import { Suggestion, Suggestions } from "@/components/ai-elements/suggestion";
import { useState } from "react";
import { useChat } from "@ai-sdk/react";
const suggestions = [
"Can you explain how to play tennis?",
"What is the weather in Tokyo?",
"How do I make a really good fish taco?",
];
const SuggestionDemo = () => {
const [input, setInput] = useState("");
const { sendMessage, status } = useChat();
const handleSubmit = (message: PromptInputMessage) => {
if (message.text.trim()) {
sendMessage({ text: message.text });
setInput("");
}
};
const handleSuggestionClick = (suggestion: string) => {
sendMessage({ text: suggestion });
};
return (
<div className="max-w-4xl mx-auto p-6 rounded-lg border h-[600px]">
<div className="flex flex-col gap-4">
<Suggestions>
{suggestions.map((suggestion) => (
<Suggestion
key={suggestion}
onClick={handleSuggestionClick}
suggestion={suggestion}
/>
))}
</Suggestions>
<PromptInput onSubmit={handleSubmit}>
<PromptInputTextarea
value={input}
placeholder="Say something..."
onChange={(e) => setInput(e.currentTarget.value)}
/>
<PromptInputSubmit
status={status === "streaming" ? "streaming" : "ready"}
disabled={!input.trim()}
/>
</PromptInput>
</div>
</div>
);
};
<Suggestions />
| Prop | Type | Description |
|------|------|-------------|
| ...props | React.ComponentProps<typeof ScrollArea> | Spread to ScrollArea |
<Suggestion />
| Prop | Type | Description |
|------|------|-------------|
| suggestion | string | The suggestion string to display and emit |
| onClick | (suggestion: string) => void | Callback when clicked |
| ...props | Omit<React.ComponentProps<typeof Button>, "onClick"> | Spread to Button |
Reasoning Component
A collapsible component that displays AI reasoning content, automatically opening during streaming and closing when finished.
When to Use
Use Reasoning when your model outputs thinking content as a single block or continuous stream (Deepseek R1, Claude with extended thinking).
Use Chain of Thought when your model outputs discrete, labeled steps (search queries, tool calls, distinct thought stages).
Features
- Automatically opens when streaming content and closes when finished
- Manual toggle control for user interaction
- Smooth animations and transitions powered by Radix UI
- Visual streaming indicator with pulsing animation
- Built on shadcn/ui Collapsible primitives
Usage
"use client";
import {
Reasoning,
ReasoningContent,
ReasoningTrigger,
} from "@/components/ai-elements/reasoning";
import {
Conversation,
ConversationContent,
ConversationScrollButton,
} from "@/components/ai-elements/conversation";
import {
PromptInput,
PromptInputMessage,
PromptInputTextarea,
PromptInputSubmit,
} from "@/components/ai-elements/prompt-input";
import { Spinner } from "@/components/ui/spinner";
import {
Message,
MessageContent,
MessageResponse,
} from "@/components/ai-elements/message";
import { useState } from "react";
import { useChat } from "@ai-sdk/react";
import type { UIMessage } from "ai";
const MessageParts = ({
message,
isLastMessage,
isStreaming,
}: {
message: UIMessage;
isLastMessage: boolean;
isStreaming: boolean;
}) => {
const reasoningParts = message.parts.filter(
(part) => part.type === "reasoning"
);
const reasoningText = reasoningParts.map((part) => part.text).join("\n\n");
const hasReasoning = reasoningParts.length > 0;
const lastPart = message.parts.at(-1);
const isReasoningStreaming =
isLastMessage && isStreaming && lastPart?.type === "reasoning";
return (
<>
{hasReasoning && (
<Reasoning className="w-full" isStreaming={isReasoningStreaming}>
<ReasoningTrigger />
<ReasoningContent>{reasoningText}</ReasoningContent>
</Reasoning>
)}
{message.parts.map((part, i) => {
if (part.type === "text") {
return (
<MessageResponse key={`${message.id}-${i}`}>
{part.text}
</MessageResponse>
);
}
return null;
})}
</>
);
};
const ReasoningDemo = () => {
const [input, setInput] = useState("");
const { messages, sendMessage, status } = useChat();
const handleSubmit = (message: PromptInputMessage) => {
sendMessage({ text: message.text });
setInput("");
};
const isStreaming = status === "streaming";
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, index) => (
<Message from={message.role} key={message.id}>
<MessageContent>
<MessageParts
message={message}
isLastMessage={index === messages.length - 1}
isStreaming={isStreaming}
/>
</MessageContent>
</Message>
))}
{status === "submitted" && <Spinner />}
</ConversationContent>
<ConversationScrollButton />
</Conversation>
<PromptInput onSubmit={handleSubmit}>
<PromptInputTextarea
value={input}
placeholder="Say something..."
onChange={(e) => setInput(e.currentTarget.value)}
/>
<PromptInputSubmit
status={isStreaming ? "streaming" : "ready"}
disabled={!input.trim()}
/>
</PromptInput>
</div>
</div>
);
};
Backend Route
import { streamText, UIMessage, convertToModelMessages } from "ai";
export const maxDuration = 30;
export async function POST(req: Request) {
const { model, messages } = await req.json();
const result = streamText({
model: "deepseek/deepseek-r1",
messages: await convertToModelMessages(messages),
});
return result.toUIMessageStreamResponse({
sendReasoning: true,
});
}
<Reasoning />
| Prop | Type | Default |
|------|------|---------|
| isStreaming | boolean | false |
| open | boolean | Controlled open state |
| defaultOpen | boolean | true |
| onOpenChange | (open: boolean) => void | Callback when open changes |
| duration | number | Duration in seconds |
| ...props | React.ComponentProps<typeof Collapsible> | Spread to Collapsible |
<ReasoningTrigger />
| Prop | Type | Description |
|------|------|-------------|
| getThinkingMessage | (isStreaming, duration?) => ReactNode | Customize thinking message |
| ...props | React.ComponentProps<typeof CollapsibleTrigger> | Spread to CollapsibleTrigger |
<ReasoningContent />
| Prop | Type | Description |
|------|------|-------------|
| children | string | Reasoning text to display |
| ...props | React.ComponentProps<typeof CollapsibleContent> | Spread to CollapsibleContent |
useReasoning Hook
const { isStreaming, isOpen, setIsOpen, duration } = useReasoning();
| Return Value | Type | Description |
|--------------|------|-------------|
| isStreaming | boolean | Whether reasoning is streaming |
| isOpen | boolean | Whether panel is open |
| setIsOpen | (open: boolean) => void | Set open state |
| duration | number \| undefined | Duration in seconds |