Capstone Project: Stock Analysis Dashboard
Jan 3, 2026 β’ 30 min read
Congratulations! You have learned the theory. Now let's build the application. In this final module, we combine everything into a single AI Agent: The Financial Analyst.
1. Project Requirements
The application must satisfy these user stories:
- Search: "Compare AAPL and GOOGL revenue" β Renders a Multi-Line Chart.
- Analysis: "Why did the price drop?" β Renders a Text Summary streaming via RSC.
- Voice: User can speak to the dashboard to add new symbols.
- Action: "Buy 10 shares" β Renders a Confirmation Form β Executes Transaction.
2. The Architecture Stack
Tech Stack
- Framework: Next.js 14 (App Router)
- AI SDK: Vercel AI SDK 3.0 (RSC)
- Model: GPT-4o-mini (Fast & Cheap) or GPT-4o (Smart)
- Components: Recharts, Radix UI, Tailwind
- Database: Postgres (Prisma) for Chat History
Step 1: The Setup
npx create-next-app@latest financial-agent npm install ai openai zod recharts lucide-react clsx tailwind-merge framer-motion
Step 2: The AI State Definition
We define our state to hold a history of `ReactNode` (UI) and `CoreMessage` (LLM context). We also sync this state to a database so the user can refresh the page.
// app/action.tsx
import { createAI, getMutableAIState } from 'ai/rsc';
export const AI = createAI({
initialAIState: [],
initialUIState: [],
actions: {
submitUserMessage,
confirmPurchase
},
onSetAIState: async ({ state, done }) => {
'use server';
if (done) {
await saveChatToDB(state);
}
}
});Step 3: The Brain (submitUserMessage)
This is the orchestration layer. It uses `render()` (or `streamUI()` in newer versions) to map tools to components.
export async function submitUserMessage(content: string) {
'use server';
const aiState = getMutableAIState();
aiState.update([...aiState.get(), { role: 'user', content }]);
const ui = await render({
model: 'gpt-4o',
provider: openai,
messages: aiState.get(),
tools: {
get_stock_history: {
description: 'Get stock history for specific symbols',
parameters: z.object({ symbols: z.array(z.string()) }),
generate: async function* ({ symbols }) {
yield <BotCard><Skeleton /></BotCard>;
const data = await fetchValues(symbols);
return <BotCard><StockChart data={data} /></BotCard>;
}
},
buy_stock: {
description: 'Purchase stock',
parameters: z.object({ symbol: z.string(), quantity: z.number() }),
generate: async function* ({ symbol, quantity }) {
// Return a FORM, not a confirmation
return <BotCard><PurchaseForm symbol={symbol} quantity={quantity} /></BotCard>;
}
}
}
});
return { id: Date.now(), display: ui };
}Step 4: Deployment
Deploy to Vercel. Ensure you set your `OPENAI_API_KEY`. Because we use RSC, we can easily deploy to Edge (with some limitations) or Serverless (Node.js). For heavy data fetching, Node.js runtime is preferred.
Conclusion
You have now built a Generative UI application. You understand that the future of interfaces isn't about pointing and clickingβit's about intent. Your interface creates itself to meet the user's needs in real-time.