Skip to main content
Give your AI tools custom UI components that show loading states and rich results.

Overview

Tools let the LLM perform actions (API calls, calculations, etc). This pattern connects a backend tool to a frontend component that renders its input/output with proper loading states. File convention: lib/ai/tools/{name}.tscomponents/part/{name}.tsx

How it works

  1. Define tool with Vercel AI SDK’s tool() - schema + execute function
  2. Register in lib/ai/tools/tools.ts
  3. Route tool type to component in components/message-parts.tsx
  4. Render with loading/complete states based on tool.state
The AI SDK streams tool calls as parts with two states:
Statetool.stateAvailable
Loading"input-available"tool.input
Complete"output-available"tool.input + tool.output

Code

1. Tool Definition

lib/ai/tools/get-weather.ts
import { tool } from "ai";
import { z } from "zod";

export const getWeather = tool({
  description: "Get the current weather at a location",
  inputSchema: z.object({
    latitude: z.number(),
    longitude: z.number(),
  }),
  execute: async ({
    latitude,
    longitude,
  }: {
    latitude: number;
    longitude: number;
  }) => {
    const response = await fetch(
      `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&current=temperature_2m&hourly=temperature_2m&daily=sunrise,sunset&timezone=auto`
    );

    const weatherData = await response.json();
    return weatherData as WeatherAtLocation;
  },
});

export type WeatherAtLocation = {
  latitude: number;
  longitude: number;
  generationtime_ms: number;
  utc_offset_seconds: number;
  timezone: string;
  timezone_abbreviation: string;
  elevation: number;
  current_units: {
    time: string;
    interval: string;
    temperature_2m: string;
  };
  current: {
    time: string;
    interval: number;
    temperature_2m: number;
  };
  hourly_units: {
    time: string;
    temperature_2m: string;
  };
  hourly: {
    time: string[];
    temperature_2m: number[];
  };
  daily_units: {
    time: string;
    sunrise: string;
    sunset: string;
  };
  daily: {
    time: string[];
    sunrise: string[];
    sunset: string[];
  };
};

2. Register Tool

lib/ai/tools/tools.ts
import { getWeather } from "./get-weather";

export function getTools(
  {
    /* ...params */
  }
) {
  return {
    getWeather,
    // ... other tools
  };
}

3. Route to Component

components/message-parts.tsx
import { Weather } from "./part/weather";

function ToolPart({
  part,
  messageId,
  isReadonly,
}: {
  part: ToolUIPart<ChatTools>;
  messageId: string;
  isReadonly: boolean;
}) {
  if (part.type === "tool-getWeather") {
    return <Weather tool={part} />;
  }

  // ... other tools

  return null;
}

4. UI Component

components/part/weather.tsx
"use client";

import type { WeatherAtLocation } from "@/lib/ai/tools/get-weather";
import type { ChatMessage } from "@/lib/ai/types";

export type WeatherTool = Extract<
  ChatMessage["parts"][number],
  { type: "tool-getWeather" }
>;

export function Weather({ tool }: { tool: WeatherTool }) {
  const isLoading = tool.state === "input-available";

  if (isLoading) {
    return <div className="skeleton rounded-2xl bg-blue-400 p-4 h-24 w-48" />;
  }

  const data = tool.output;

  return (
    <div className="rounded-2xl bg-blue-400 p-4 text-white">
      <div className="text-4xl">
        {Math.round(data.current.temperature_2m)}
        {data.current_units.temperature_2m}
      </div>
    </div>
  );
}