Part 2: Generative UI
In the previous step, we set up the frontend to connect to a LangGraph Cloud endpoint.
In this step, we will set up a component to display stock ticker information.

For reference, this the corresponding code in the backend:
export const priceSnapshotTool = tool(
async (input) => {
const data = await callFinancialDatasetAPI<SnapshotResponse>({
endpoint: "/prices/snapshot",
params: {
ticker: input.ticker,
},
});
return JSON.stringify(data, null);
},
{
name: "price_snapshot",
description:
"Retrieves the current stock price and related market data for a given company.",
schema: z.object({
ticker: z.string().describe("The ticker of the company. Example: 'AAPL'"),
}),
},
);
PriceSnapshotTool
We create a new file under /components/tools/price-snapshot/PriceSnapshotTool.tsx
to define the tool.
First, we define the tool arguments and result types:
type PriceSnapshotToolArgs = {
ticker: string;
};
type PriceSnapshotToolResult = {
snapshot: {
price: number;
day_change: number;
day_change_percent: number;
time: string;
};
};
Then, we use makeAssistantToolUI
to define the tool UI:
"use client";
import { makeAssistantToolUI } from "@assistant-ui/react";
export const PriceSnapshotTool = makeAssistantToolUI<
PriceSnapshotToolArgs,
string
>({
toolName: "price_snapshot",
render: function PriceSnapshotUI({ args, result }) {
return (
<div className="mb-4 flex flex-col items-center">
<pre className="whitespace-pre-wrap break-all text-center">
price_snapshot({JSON.stringify(args)})
</pre>
</div>
);
},
});
This simply displays the tool name and arguments passed to it, but not the result.
Bind tool UI
import { PriceSnapshotTool } from "@/components/tools/price-snapshot/PriceSnapshotTool";
export default function Home() {
return (
<div className="flex h-full flex-col">
<Thread
...
tools={[PriceSnapshotTool]}
/>
</div>
);
}
Try it out!
Ask the assistant for the current stock price of Tesla. You should see the following text appear:
price_snapshot({ticker: "TSLA"})
Next, we will visualize the function's result.
Visualizing tool results
Install dependencies
The tool result component relies on shadcn/ui's Card
component. We will install it as a dependency.
npx shadcn@latest add card
You will be prompted to setup a components.json
file, after this step, a card
UI component will be installed in your project.
Add PriceSnapshot
We create a new file under /components/tools/price-snapshot/price-snapshot.tsx
to define the new tool result UI.
"use client";
import { ArrowDownIcon, ArrowUpIcon } from "lucide-react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
type PriceSnapshotToolArgs = {
ticker: string;
};
type PriceSnapshotToolResult = {
price: number;
day_change: number;
day_change_percent: number;
time: string;
};
export function PriceSnapshot({
ticker,
price,
day_change,
day_change_percent,
time,
}: PriceSnapshotToolArgs & PriceSnapshotToolResult) {
const isPositiveChange = day_change >= 0;
const changeColor = isPositiveChange ? "text-green-600" : "text-red-600";
const ArrowIcon = isPositiveChange ? ArrowUpIcon : ArrowDownIcon;
return (
<Card className="mx-auto w-full max-w-md">
<CardHeader>
<CardTitle className="text-2xl font-bold">{ticker}</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-4">
<div className="col-span-2">
<p className="text-3xl font-semibold">${price?.toFixed(2)}</p>
</div>
<div>
<p className="text-muted-foreground text-sm">Day Change</p>
<p
className={`flex items-center text-lg font-medium ${changeColor}`}
>
<ArrowIcon className="mr-1 h-4 w-4" />$
{Math.abs(day_change)?.toFixed(2)} (
{Math.abs(day_change_percent)?.toFixed(2)}%)
</p>
</div>
<div>
<p className="text-muted-foreground text-sm">Last Updated</p>
<p className="text-lg font-medium">
{new Date(time).toLocaleTimeString()}
</p>
</div>
</div>
</CardContent>
</Card>
);
}
Update PriceSnapshotTool
We will import the new <PriceSnapshot />
component and use it in the render
function whenever a tool result is available.
"use client";
import { PriceSnapshot } from "./price-snapshot";
import { makeAssistantToolUI } from "@assistant-ui/react";
type PriceSnapshotToolArgs = {
ticker: string;
};
type PriceSnapshotToolResult = {
snapshot: {
price: number;
day_change: number;
day_change_percent: number;
time: string;
};
};
export const PriceSnapshotTool = makeAssistantToolUI<
PriceSnapshotToolArgs,
string
>({
toolName: "price_snapshot",
render: function PriceSnapshotUI({ args, result }) {
let resultObj: PriceSnapshotToolResult | { error: string };
try {
resultObj = result ? JSON.parse(result) : {};
} catch (e) {
resultObj = { error: result! };
}
return (
<div className="mb-4 flex flex-col items-center gap-2">
<pre className="whitespace-pre-wrap break-all text-center">
price_snapshot({JSON.stringify(args)})
</pre>
{"snapshot" in resultObj && (
<PriceSnapshot ticker={args.ticker} {...resultObj.snapshot} />
)}
{"error" in resultObj && (
<p className="text-red-500">{resultObj.error}</p>
)}
</div>
);
},
});
Try it out!
Ask the assistant for the current stock price of Tesla. You should see the tool result appear:

Fallback tool UI
Instead of defining a custom tool UI for every tool, we can also define a fallback UI for all tools that are not explicitly defined.
This requires shadcn/ui's Button
component. We will install it as a dependency.
npx shadcn@latest add button
Then create a new file under /components/tools/ToolFallback.tsx
to define the fallback UI.
import { ToolCallContentPartComponent } from "@assistant-ui/react";
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
import { useState } from "react";
import { Button } from "../ui/button";
export const ToolFallback: ToolCallContentPartComponent = ({
toolName,
argsText,
result,
}) => {
const [isCollapsed, setIsCollapsed] = useState(true);
return (
<div className="mb-4 flex w-full flex-col gap-3 rounded-lg border py-3">
<div className="flex items-center gap-2 px-4">
<CheckIcon className="size-4" />
<p className="">
Used tool: <b>{toolName}</b>
</p>
<div className="flex-grow" />
<Button onClick={() => setIsCollapsed(!isCollapsed)}>
{isCollapsed ? <ChevronUpIcon /> : <ChevronDownIcon />}
</Button>
</div>
{!isCollapsed && (
<div className="flex flex-col gap-2 border-t pt-2">
<div className="px-4">
<pre className="whitespace-pre-wrap">{argsText}</pre>
</div>
{result !== undefined && (
<div className="border-t border-dashed px-4 pt-2">
<p className="font-semibold">Result:</p>
<pre className="whitespace-pre-wrap">
{typeof result === "string"
? result
: JSON.stringify(result, null, 2)}
</pre>
</div>
)}
</div>
)}
</div>
);
};
Bind fallback UI
import { ToolFallback } from "@/components/tools/ToolFallback";
export default function Home() {
return (
<div className="flex h-full flex-col">
<Thread
...
assistantMessage={{ components: { Text: MarkdownText, ToolFallback } }}
/>
</div>
);
}