Project: Interactive Financial Dashboard with Generative UI
In this project, we will build an interactive financial analyzer. When a user asks "Show my expense trends for this quarter", the agent will execute the data tool and stream a financial chart component directly into the chat history.
1. Defining the Chart Component
First, create a clean bar chart component using standard Tailwind styles:
// components/ExpenseChart.tsx
import React from "react";
interface DataPoint {
month: string;
amount: number;
}
export function ExpenseChart(props: { title: string; dataset: DataPoint[] }) {
return (
<div className="border rounded-2xl p-5 bg-gradient-to-br from-slate-900 to-slate-950 text-white shadow-xl my-4">
<h3 className="text-sm font-semibold tracking-wider text-gray-400 uppercase">{props.title}</h3>
<div className="mt-6 flex items-end gap-3 h-32 justify-around border-b border-gray-800 pb-2">
{props.dataset.map((point, idx) => {
// Calculate height percentage based on max value
const barHeight = Math.min(100, (point.amount / 3000) * 100);
return (
<div key={idx} className="flex flex-col items-center flex-1 group">
{/* Tooltip on hover */}
<span className="text-[10px] text-emerald-400 opacity-0 group-hover:opacity-100 transition mb-1">
${point.amount}
</span>
<div
style={{ height: `${barHeight}%` }}
className="w-full bg-emerald-500 rounded-t-sm transition-all duration-500 hover:bg-emerald-400"
/>
<span className="text-[10px] text-gray-500 mt-2">{point.month}</span>
</div>
);
})}
</div>
</div>
);
}2. Implementing the Route Handler
Register the data fetching tool on the server route:
// app/api/financial-agent/route.ts
import { streamText, tool } from "ai";
import { z } from "zod";
import { defaultModel } from "../../../lib/models";
export async function POST(req: Request) {
const { messages } = await req.json();
const result = await streamText({
model: defaultModel,
messages,
system: "You are a corporate accountant assistant. Help users analyze their costs.",
tools: {
getExpenseMetrics: tool({
description: "Retrieve monthly operational expenses statistics.",
parameters: z.object({
department: z.string().describe("The business sector (e.g. Sales, Engineering)."),
}),
execute: async ({ department }) => {
// Mocking PostgreSQL aggregation queries
return {
title: `${department} department quarterly cost summary`,
metrics: [
{ month: "Jan", amount: 1200 },
{ month: "Feb", amount: 2400 },
{ month: "Mar", amount: 1850 },
],
};
},
}),
},
});
return result.toDataStreamResponse();
}3. Rendering the Chart inside Chat Console
Read toolInvocations on the frontend and render ExpenseChart:
// app/dashboard/FinanceAgent.tsx
"use client";
import { useChat } from "ai/react";
import { ExpenseChart } from "../../components/ExpenseChart";
export default function FinanceAgent() {
const { messages, input, handleInputChange, handleSubmit } = useChat({
api: "/api/financial-agent",
});
return (
<div className="max-w-xl mx-auto p-6 bg-white border rounded-2xl shadow mt-8">
<h2 className="font-bold text-lg border-b pb-2 mb-4">Financial Agent</h2>
<div className="space-y-4 h-[350px] overflow-y-auto">
{messages.map((m) => (
<div key={m.id} className="p-3 bg-gray-50 rounded-lg">
<strong>{m.role === "user" ? "You" : "Accountant"}:</strong>
{m.content && <p className="mt-1 text-sm">{m.content}</p>}
{/* Loop and render matched dynamic UI component */}
{m.toolInvocations?.map((ti) => {
if (ti.state === "result" && ti.toolName === "getExpenseMetrics") {
return (
<ExpenseChart
key={ti.toolCallId}
title={ti.result.title}
dataset={ti.result.metrics}
/>
);
}
return null;
})}
</div>
))}
</div>
<form onSubmit={handleSubmit} className="mt-4 flex gap-2">
<input value={input} onChange={handleInputChange} className="flex-1 border p-2 rounded-xl text-sm" placeholder="Ask e.g. Show Sales quarterly stats..." />
<button type="submit" className="bg-indigo-600 text-white px-4 py-2 rounded-xl text-sm">Submit</button>
</form>
</div>
);
}Published on Last updated: