Back to roadmaps vercel-ai-sdk Course

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: