Chatbot

Tool

A collapsible component for displaying tool invocation details in AI chatbot interfaces.

The Tool component displays a collapsible interface for showing/hiding tool details. It is designed to take the ToolUIPart type from the AI SDK and display it in a collapsible interface.

"use client";import {  Tool,  ToolContent,  ToolHeader,  ToolInput,  ToolOutput,} from "@/components/ai-elements/elements/tool";import type { ToolUIPart } from "ai";import { nanoid } from "nanoid";const toolCall: ToolUIPart = {  type: "tool-database_query" as const,  toolCallId: nanoid(),  state: "output-available" as const,  input: {    query: "SELECT COUNT(*) FROM users WHERE created_at >= ?",    params: ["2024-01-01"],    database: "analytics",  },  output: `| User ID | Name | Email | Created At ||---------|------|-------|------------|| 1 | John Doe | john@example.com | 2024-01-15 || 2 | Jane Smith | jane@example.com | 2024-01-20 || 3 | Bob Wilson | bob@example.com | 2024-02-01 || 4 | Alice Brown | alice@example.com | 2024-02-10 || 5 | Charlie Davis | charlie@example.com | 2024-02-15 |`,  errorText: undefined,};const Example = () => (  <div style={{ height: "500px" }}>    <Tool>      <ToolHeader state={toolCall.state} type={toolCall.type} />      <ToolContent>        <ToolInput input={toolCall.input} />        {toolCall.state === "output-available" && (          <ToolOutput errorText={toolCall.errorText} output={toolCall.output} />        )}      </ToolContent>    </Tool>  </div>);export default Example;

Installation

npx ai-elements@latest add tool
npx shadcn@latest add @ai-elements/tool
"use client";import { Badge } from "@repo/shadcn-ui/components/ui/badge";import {  Collapsible,  CollapsibleContent,  CollapsibleTrigger,} from "@repo/shadcn-ui/components/ui/collapsible";import { cn } from "@repo/shadcn-ui/lib/utils";import type { ToolUIPart } from "ai";import {  CheckCircleIcon,  ChevronDownIcon,  CircleIcon,  ClockIcon,  WrenchIcon,  XCircleIcon,} from "lucide-react";import type { ComponentProps, ReactNode } from "react";import { isValidElement } from "react";import { CodeBlock } from "./code-block";export type ToolProps = ComponentProps<typeof Collapsible>;export const Tool = ({ className, ...props }: ToolProps) => (  <Collapsible    className={cn("not-prose mb-4 w-full rounded-md border", className)}    {...props}  />);export type ToolHeaderProps = {  title?: string;  type: ToolUIPart["type"];  state: ToolUIPart["state"];  className?: string;};const getStatusBadge = (status: ToolUIPart["state"]) => {  const labels = {    "input-streaming": "Pending",    "input-available": "Running",    "output-available": "Completed",    "output-error": "Error",  } as const;  const icons = {    "input-streaming": <CircleIcon className="size-4" />,    "input-available": <ClockIcon className="size-4 animate-pulse" />,    "output-available": <CheckCircleIcon className="size-4 text-green-600" />,    "output-error": <XCircleIcon className="size-4 text-red-600" />,  } as const;  return (    <Badge className="gap-1.5 rounded-full text-xs" variant="secondary">      {icons[status]}      {labels[status]}    </Badge>  );};export const ToolHeader = ({  className,  title,  type,  state,  ...props}: ToolHeaderProps) => (  <CollapsibleTrigger    className={cn(      "flex w-full items-center justify-between gap-4 p-3",      className    )}    {...props}  >    <div className="flex items-center gap-2">      <WrenchIcon className="size-4 text-muted-foreground" />      <span className="font-medium text-sm">        {title ?? type.split("-").slice(1).join("-")}      </span>      {getStatusBadge(state)}    </div>    <ChevronDownIcon className="size-4 text-muted-foreground transition-transform group-data-[state=open]:rotate-180" />  </CollapsibleTrigger>);export type ToolContentProps = ComponentProps<typeof CollapsibleContent>;export const ToolContent = ({ className, ...props }: ToolContentProps) => (  <CollapsibleContent    className={cn(      "data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-popover-foreground outline-none data-[state=closed]:animate-out data-[state=open]:animate-in",      className    )}    {...props}  />);export type ToolInputProps = ComponentProps<"div"> & {  input: ToolUIPart["input"];};export const ToolInput = ({ className, input, ...props }: ToolInputProps) => (  <div className={cn("space-y-2 overflow-hidden p-4", className)} {...props}>    <h4 className="font-medium text-muted-foreground text-xs uppercase tracking-wide">      Parameters    </h4>    <div className="rounded-md bg-muted/50">      <CodeBlock code={JSON.stringify(input, null, 2)} language="json" />    </div>  </div>);export type ToolOutputProps = ComponentProps<"div"> & {  output: ToolUIPart["output"];  errorText: ToolUIPart["errorText"];};export const ToolOutput = ({  className,  output,  errorText,  ...props}: ToolOutputProps) => {  if (!(output || errorText)) {    return null;  }  let Output = <div>{output as ReactNode}</div>;  if (typeof output === "object" && !isValidElement(output)) {    Output = (      <CodeBlock code={JSON.stringify(output, null, 2)} language="json" />    );  } else if (typeof output === "string") {    Output = <CodeBlock code={output} language="json" />;  }  return (    <div className={cn("space-y-2 p-4", className)} {...props}>      <h4 className="font-medium text-muted-foreground text-xs uppercase tracking-wide">        {errorText ? "Error" : "Result"}      </h4>      <div        className={cn(          "overflow-x-auto rounded-md text-xs [&_table]:w-full",          errorText            ? "bg-destructive/10 text-destructive"            : "bg-muted/50 text-foreground"        )}      >        {errorText && <div>{errorText}</div>}        {Output}      </div>    </div>  );};

Usage

import {
  Tool,
  ToolContent,
  ToolHeader,
  ToolOutput,
  ToolInput,
} from '@/components/ai-elements/tool';
<Tool>
  <ToolHeader type="tool-call" state={'output-available' as const} />
  <ToolContent>
    <ToolInput input="Input to tool call" />
    <ToolOutput errorText="Error" output="Output from tool call" />
  </ToolContent>
</Tool>

Usage in AI SDK

Build a simple stateful weather app that renders the last message in a tool using useChat.

Add the following component to your frontend:

app/page.tsx
'use client';

import { useChat } from '@ai-sdk/react';
import { DefaultChatTransport, type ToolUIPart } from 'ai';
import { Button } from '@/components/ui/button';
import { Response } from '@/components/ai-elements/response';
import {
  Tool,
  ToolContent,
  ToolHeader,
  ToolInput,
  ToolOutput,
} from '@/components/ai-elements/tool';

type WeatherToolInput = {
  location: string;
  units: 'celsius' | 'fahrenheit';
};

type WeatherToolOutput = {
  location: string;
  temperature: string;
  conditions: string;
  humidity: string;
  windSpeed: string;
  lastUpdated: string;
};

type WeatherToolUIPart = ToolUIPart<{
  fetch_weather_data: {
    input: WeatherToolInput;
    output: WeatherToolOutput;
  };
}>;

const Example = () => {
  const { messages, sendMessage, status } = useChat({
    transport: new DefaultChatTransport({
      api: '/api/weather',
    }),
  });

  const handleWeatherClick = () => {
    sendMessage({ text: 'Get weather data for San Francisco in fahrenheit' });
  };

  const latestMessage = messages[messages.length - 1];
  const weatherTool = latestMessage?.parts?.find(
    (part) => part.type === 'tool-fetch_weather_data',
  ) as WeatherToolUIPart | undefined;

  return (
    <div className="max-w-4xl mx-auto p-6 relative size-full rounded-lg border h-[600px]">
      <div className="flex flex-col h-full">
        <div className="space-y-4">
          <Button onClick={handleWeatherClick} disabled={status !== 'ready'}>
            Get Weather for San Francisco
          </Button>

          {weatherTool && (
            <Tool defaultOpen={true}>
              <ToolHeader type="tool-fetch_weather_data" state={weatherTool.state} />
              <ToolContent>
                <ToolInput input={weatherTool.input} />
                <ToolOutput
                  output={
                    <Response>
                      {formatWeatherResult(weatherTool.output)}
                    </Response>
                  }
                  errorText={weatherTool.errorText}
                />
              </ToolContent>
            </Tool>
          )}
        </div>
      </div>
    </div>
  );
};

function formatWeatherResult(result: WeatherToolOutput): string {
  return `**Weather for ${result.location}**

**Temperature:** ${result.temperature}  
**Conditions:** ${result.conditions}  
**Humidity:** ${result.humidity}  
**Wind Speed:** ${result.windSpeed}  

*Last updated: ${result.lastUpdated}*`;
}

export default Example;

Add the following route to your backend:

app/api/weather/route.tsx
import { streamText, UIMessage, convertToModelMessages } from 'ai';
import { z } from 'zod';

// Allow streaming responses up to 30 seconds
export const maxDuration = 30;

export async function POST(req: Request) {
  const { messages }: { messages: UIMessage[] } = await req.json();

  const result = streamText({
    model: 'openai/gpt-4o',
    messages: convertToModelMessages(messages),
    tools: {
      fetch_weather_data: {
        description: 'Fetch weather information for a specific location',
        parameters: z.object({
          location: z
            .string()
            .describe('The city or location to get weather for'),
          units: z
            .enum(['celsius', 'fahrenheit'])
            .default('celsius')
            .describe('Temperature units'),
        }),
        inputSchema: z.object({
          location: z.string(),
          units: z.enum(['celsius', 'fahrenheit']).default('celsius'),
        }),
        execute: async ({ location, units }) => {
          await new Promise((resolve) => setTimeout(resolve, 1500));

          const temp =
            units === 'celsius'
              ? Math.floor(Math.random() * 35) + 5
              : Math.floor(Math.random() * 63) + 41;

          return {
            location,
            temperature: `${temp}°${units === 'celsius' ? 'C' : 'F'}`,
            conditions: 'Sunny',
            humidity: `12%`,
            windSpeed: `35 ${units === 'celsius' ? 'km/h' : 'mph'}`,
            lastUpdated: new Date().toLocaleString(),
          };
        },
      },
    },
  });

  return result.toUIMessageStreamResponse();
}

Features

  • Collapsible interface for showing/hiding tool details
  • Visual status indicators with icons and badges
  • Support for multiple tool execution states (pending, running, completed, error)
  • Formatted parameter display with JSON syntax highlighting
  • Result and error handling with appropriate styling
  • Composable structure for flexible layouts
  • Accessible keyboard navigation and screen reader support
  • Consistent styling that matches your design system
  • Auto-opens completed tools by default for better UX

Examples

Input Streaming (Pending)

Shows a tool in its initial state while parameters are being processed.

"use client";import { Tool, ToolContent, ToolHeader, ToolInput } from "@/components/ai-elements/elements/tool";import { nanoid } from "nanoid";const toolCall = {  type: "tool-web_search" as const,  toolCallId: nanoid(),  state: "input-streaming" as const,  input: {    query: "latest AI market trends 2024",    max_results: 10,    include_snippets: true,  },  output: undefined,  errorText: undefined,};const Example = () => (  <div style={{ height: "500px" }}>    <Tool>      <ToolHeader state={toolCall.state} type={toolCall.type} />      <ToolContent>        <ToolInput input={toolCall.input} />      </ToolContent>    </Tool>  </div>);export default Example;

Input Available (Running)

Shows a tool that's actively executing with its parameters.

"use client";import { Tool, ToolContent, ToolHeader, ToolInput } from "@/components/ai-elements/elements/tool";import { nanoid } from "nanoid";const toolCall = {  type: "tool-image_generation" as const,  toolCallId: nanoid(),  state: "input-available" as const,  input: {    prompt: "A futuristic cityscape at sunset with flying cars",    style: "digital_art",    resolution: "1024x1024",    quality: "high",  },  output: undefined,  errorText: undefined,};const Example = () => (  <div style={{ height: "500px" }}>    <Tool>      <ToolHeader state={toolCall.state} type={toolCall.type} />      <ToolContent>        <ToolInput input={toolCall.input} />      </ToolContent>    </Tool>  </div>);export default Example;

Output Available (Completed)

Shows a completed tool with successful results. Opens by default to show the results. In this instance, the output is a JSON object, so we can use the CodeBlock component to display it.

"use client";import { CodeBlock } from "@/components/ai-elements/elements/code-block";import {  Tool,  ToolContent,  ToolHeader,  ToolInput,  ToolOutput,} from "@/components/ai-elements/elements/tool";import type { ToolUIPart } from "ai";import { nanoid } from "nanoid";const toolCall: ToolUIPart = {  type: "tool-database_query" as const,  toolCallId: nanoid(),  state: "output-available" as const,  input: {    query: "SELECT COUNT(*) FROM users WHERE created_at >= ?",    params: ["2024-01-01"],    database: "analytics",  },  output: [    {      "User ID": 1,      Name: "John Doe",      Email: "john@example.com",      "Created At": "2024-01-15",    },    {      "User ID": 2,      Name: "Jane Smith",      Email: "jane@example.com",      "Created At": "2024-01-20",    },    {      "User ID": 3,      Name: "Bob Wilson",      Email: "bob@example.com",      "Created At": "2024-02-01",    },    {      "User ID": 4,      Name: "Alice Brown",      Email: "alice@example.com",      "Created At": "2024-02-10",    },    {      "User ID": 5,      Name: "Charlie Davis",      Email: "charlie@example.com",      "Created At": "2024-02-15",    },  ],  errorText: undefined,};const Example = () => (  <div style={{ height: "500px" }}>    <Tool>      <ToolHeader state={toolCall.state} type={toolCall.type} />      <ToolContent>        <ToolInput input={toolCall.input} />        {toolCall.state === "output-available" && (          <ToolOutput            errorText={toolCall.errorText}            output={              <CodeBlock                code={JSON.stringify(toolCall.output)}                language="json"              />            }          />        )}      </ToolContent>    </Tool>  </div>);export default Example;

Output Error

Shows a tool that encountered an error during execution. Opens by default to display the error.

"use client";import {  Tool,  ToolContent,  ToolHeader,  ToolInput,  ToolOutput,} from "@/components/ai-elements/elements/tool";import type { ToolUIPart } from "ai";const toolCall: ToolUIPart = {  type: "tool-api_request" as const,  toolCallId: "api_request_1",  state: "output-error" as const,  input: {    url: "https://api.example.com/data",    method: "GET",    headers: {      Authorization: "Bearer token123",      "Content-Type": "application/json",    },    timeout: 5000,  },  output: undefined,  errorText:    "Connection timeout: The request took longer than 5000ms to complete. Please check your network connection and try again.",};const Example = () => (  <div style={{ height: "500px" }}>    <Tool>      <ToolHeader state={toolCall.state} type={toolCall.type} />      <ToolContent>        <ToolInput input={toolCall.input} />        {toolCall.state === "output-error" && (          <ToolOutput errorText={toolCall.errorText} output={toolCall.output} />        )}      </ToolContent>    </Tool>  </div>);export default Example;

Props

<Tool />

Prop

Type

<ToolHeader />

Prop

Type

<ToolContent />

Prop

Type

<ToolInput />

Prop

Type

<ToolOutput />

Prop

Type