"use generative"; import { cn } from "@/lib/utils"; import { WeatherWidget, type TemperatureUnit, type WeatherWidgetPayload, } from "@/components/tool-ui/weather-widget/runtime"; import { fetchWeatherWidgetFromOpenMeteo, geocodeLocationWithOpenMeteo, } from "@/lib/open-meteo-weather-adapter"; import { MapPin, CloudSun, AlertCircle } from "lucide-react"; import { z } from "zod"; import { defineToolkit, unstable_interactableTool, useAuiState, type ToolCallMessagePartComponent, } from "@assistant-ui/react"; import { JSONGenerativeUI, defineGenerativeComponents, generativeUIToJSX, } from "@assistant-ui/react-generative-ui"; import { ToolErrorCard, ToolStatusCard, ToolTraceCard } from "@/lib/tool-trace"; import { Notepad } from "@/components/tool-ui/notepad"; const weatherFormatSchema = z.enum(["fahrenheit", "celsius"]); const notepadSchema = z.object({ title: z.string().describe("A short title for the text."), content: z.string().describe("The full plain text."), }); type GeocodeLocationArgs = { query: string; }; type GeocodeLocationResult = Awaited< ReturnType >; type GetWeatherArgs = { location: string; latitude: number; longitude: number; }; type GetWeatherResult = | { success: true; id: string; location: string; widget: WeatherWidgetPayload; } | { success: false; error: string; }; const GeocodeLocationToolUI: ToolCallMessagePartComponent< GeocodeLocationArgs, GeocodeLocationResult > = ({ toolName, args, result }) => { const icon = ; if (result?.success === false) { return ( ); } if (!result) { return ( ); } const { name, latitude, longitude } = result.result; return ( ); }; const GetWeatherToolUI: ToolCallMessagePartComponent< GetWeatherArgs, GetWeatherResult > = ({ toolName, args, result }) => { const icon = ; if (result?.success === false) { return ( ); } if (!result) { return ( ); } const current = result.widget?.current; const unitSymbol = result.widget?.units.temperature === "celsius" ? "C" : "F"; return ( ); }; // The user-facing component library the model renders through the `present` // tool. `Weather` shows the rich card for a `get_weather` result by `id`. const generative = new JSONGenerativeUI({ library: defineGenerativeComponents({ Weather: { description: "Show the user a rich weather card for a `get_weather` result.", properties: z.object({ id: z.string().describe("The `id` returned by `get_weather`."), format: weatherFormatSchema .optional() .describe("Temperature format to display in the weather card."), }), render: (props) => , }, }), }); export default defineToolkit({ // Weather data powered by Open-Meteo (https://open-meteo.com/) geocode_location: { description: "Geocode a location name into latitude/longitude (Open-Meteo). Pass the " + "coordinates to `get_weather`.", parameters: z.object({ query: z.string(), }), execute: async ({ query }) => geocodeLocationWithOpenMeteo(query), render: GeocodeLocationToolUI, }, get_weather: { description: "Fetch the weather for coordinates from `geocode_location`. Returns an " + '`id`; call `present` with `{ $type: "Weather", id }` to show the user a card.', parameters: z.object({ location: z.string(), latitude: z.number(), longitude: z.number(), }), execute: async ({ location, latitude, longitude }) => { const weather = await fetchWeatherWidgetFromOpenMeteo({ query: location, latitude, longitude, }); if (!weather.success) { return { success: false as const, error: weather.error }; } return { success: true as const, id: crypto.randomUUID().slice(0, 8), location, widget: weather.widget, }; }, render: GetWeatherToolUI, }, present: generative.present({ display: "standalone" }), notepad: unstable_interactableTool({ description: "A live notepad whose drafted text the user sees and can edit. Open one " + "whenever you write or draft prose for the user — a note, message, post, " + "release notes, a description — and revise it with `update_notepad` " + "rather than opening a new one. Opening the notepad and every " + "`update_notepad` call display the latest draft to the user directly, so " + "keep the text in the notepad and never repeat it in your reply.", stateSchema: notepadSchema, render: (props) => , }), }); const WeatherCard = ({ id, format, }: { id: string; format?: TemperatureUnit; }) => { // The payload lives on the `get_weather` result; the `Weather` component only // carries the `id`. Scan the whole thread (the two calls usually land in // separate assistant messages) for the matching result. const source = useAuiState((s) => { for (const message of s.thread.messages) { for (const part of message.content) { if ( part.type === "tool-call" && part.toolName === "get_weather" && (part.result as any)?.id === id ) { return part.result as any; } } } return undefined; }); if (source?.success === false) { return ( Weather unavailable {source.error} ); } if (!source?.widget) { return ( Preparing weather... ); } const widget = format ? convertWeatherWidgetFormat(source.widget, format) : source.widget; const generativeNode = { $type: "Weather", id, ...(format !== undefined && { format }), }; return (

present({generativeUIToJSX(generativeNode)})

); }; const convertTemperature = ( value: number, from: TemperatureUnit, to: TemperatureUnit, ) => { if (from === to) return value; return to === "celsius" ? ((value - 32) * 5) / 9 : (value * 9) / 5 + 32; }; const convertWeatherWidgetFormat = ( widget: WeatherWidgetPayload, format: TemperatureUnit, ): WeatherWidgetPayload => { const sourceFormat = widget.units.temperature; if (sourceFormat === format) return widget; return { ...widget, units: { ...widget.units, temperature: format }, current: { ...widget.current, temperature: convertTemperature( widget.current.temperature, sourceFormat, format, ), tempMin: convertTemperature(widget.current.tempMin, sourceFormat, format), tempMax: convertTemperature(widget.current.tempMax, sourceFormat, format), }, forecast: widget.forecast.map((day) => ({ ...day, tempMin: convertTemperature(day.tempMin, sourceFormat, format), tempMax: convertTemperature(day.tempMax, sourceFormat, format), })), }; }; // Shared Tool Card Components const ToolCard = ({ children, variant = "default", }: { children: React.ReactNode; variant?: "default" | "error"; }) => (
{children}
); const ToolCardDescription = ({ children }: { children: React.ReactNode }) => ( {children} ); const ToolCardIcon = ({ children, loading = false, }: { children: React.ReactNode; loading?: boolean; }) => (
{children}
); const ToolCardContent = ({ children }: { children: React.ReactNode }) => (
{children}
); const ToolCardTitle = ({ children, mono = false, }: { children: React.ReactNode; mono?: boolean; }) => ( {children} );