ai-sdk-elements/Components

Chatbot Components (Continued)

Additional chatbot components including Attachments, ModelSelector, Suggestion, Reasoning

attachmentsmodel-selectorsuggestionreasoning

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 FileUIPart and SourceDocumentUIPart from 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 |