Bankbot / frontend /src /app /docs /page.tsx
mohsin-devs's picture
fix: docs page JSX build error β€” escape curly braces in CodeBlock template literals
2de64c6
Raw
History Blame Contribute Delete
53.7 kB
"use client";
import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import {
BookOpen, ChevronRight, Terminal, Cpu, Zap, Shield,
BarChart2, Globe, Server, Database, Code2, GitBranch,
Layers, AlertTriangle, CheckCircle2, ArrowRight,
Building2, Sparkles, Lock, Activity, FileText,
MonitorSmartphone, Wifi, RefreshCw, Target,
} from "lucide-react";
import { useThemeStore } from "@/lib/stores/themeStore";
// ─── Section types ────────────────────────────────────────────────────────────
interface Section {
id: string;
icon: React.ElementType;
label: string;
color: string;
}
const SECTIONS: Section[] = [
{ id: "overview", icon: BookOpen, label: "Overview", color: "text-emerald-500" },
{ id: "quickstart", icon: Terminal, label: "Quick Start", color: "text-blue-500" },
{ id: "how-it-works", icon: Cpu, label: "How It Works", color: "text-purple-500" },
{ id: "tech-stack", icon: Layers, label: "Tech Stack", color: "text-amber-500" },
{ id: "features", icon: Zap, label: "Features", color: "text-cyan-500" },
{ id: "architecture", icon: Server, label: "Architecture", color: "text-orange-500" },
{ id: "ai-engine", icon: Sparkles, label: "AI Engine", color: "text-pink-500" },
{ id: "security", icon: Shield, label: "Security", color: "text-red-500" },
{ id: "database", icon: Database, label: "Database & Models", color: "text-teal-500" },
{ id: "api", icon: Code2, label: "API Reference", color: "text-violet-500" },
{ id: "deployment", icon: Globe, label: "Deployment", color: "text-lime-500" },
{ id: "use-cases", icon: Target, label: "Use Cases", color: "text-sky-500" },
{ id: "vs-real-world", icon: Building2, label: "vs Real Banking Apps", color: "text-rose-500" },
{ id: "limitations", icon: AlertTriangle, label: "Limitations", color: "text-yellow-500" },
];
// ─── Helpers ──────────────────────────────────────────────────────────────────
function H1({ children }: { children: React.ReactNode }) {
return <h1 className="text-3xl font-bold tracking-tight mb-2" style={{ color: "var(--fg)" }}>{children}</h1>;
}
function H2({ children, id }: { children: React.ReactNode; id?: string }) {
return <h2 id={id} className="text-xl font-bold mt-8 mb-3 flex items-center gap-2" style={{ color: "var(--fg)" }}>{children}</h2>;
}
function H3({ children }: { children: React.ReactNode }) {
return <h3 className="text-base font-semibold mt-5 mb-2" style={{ color: "var(--fg)" }}>{children}</h3>;
}
function P({ children }: { children: React.ReactNode }) {
return <p className="text-sm leading-relaxed mb-3" style={{ color: "var(--fg-muted)" }}>{children}</p>;
}
function Code({ children }: { children: React.ReactNode }) {
return (
<code
className="px-1.5 py-0.5 rounded text-xs font-mono"
style={{ background: "var(--card-bg)", border: "1px solid var(--border)", color: "var(--fg)" }}
>
{children}
</code>
);
}
function CodeBlock({ children, lang = "bash" }: { children: string; lang?: string }) {
const { theme } = useThemeStore();
const isLight = theme === "light";
return (
<div
className="rounded-xl border text-xs font-mono overflow-x-auto mb-4"
style={{
background: isLight ? "#1e1e2e" : "#0d0d0d",
borderColor: "var(--border)",
}}
>
<div
className="flex items-center gap-1.5 px-4 py-2 border-b"
style={{ borderColor: isLight ? "rgba(255,255,255,0.1)" : "rgba(255,255,255,0.07)" }}
>
{["#ff5f57","#febc2e","#28c840"].map((c) => (
<div key={c} className="h-2.5 w-2.5 rounded-full" style={{ background: c }} />
))}
<span className="ml-2 text-[10px] text-zinc-500">{lang}</span>
</div>
<pre className="px-4 py-3 leading-relaxed text-zinc-200 whitespace-pre-wrap">{children}</pre>
</div>
);
}
function Table({ headers, rows }: { headers: string[]; rows: string[][] }) {
return (
<div className="overflow-x-auto mb-4">
<table className="w-full text-xs border-collapse">
<thead>
<tr style={{ borderBottom: "1px solid var(--border)" }}>
{headers.map((h) => (
<th key={h} className="text-left px-3 py-2 font-semibold" style={{ color: "var(--fg-muted)" }}>{h}</th>
))}
</tr>
</thead>
<tbody>
{rows.map((row, i) => (
<tr key={i} style={{ borderBottom: "1px solid var(--border)" }}>
{row.map((cell, j) => (
<td key={j} className="px-3 py-2 text-xs" style={{ color: "var(--fg-muted)" }}>{cell}</td>
))}
</tr>
))}
</tbody>
</table>
</div>
);
}
function Badge({ children, color }: { children: React.ReactNode; color: string }) {
return (
<span className={`inline-flex items-center rounded-full border px-2 py-0.5 text-[10px] font-bold uppercase ${color}`}>
{children}
</span>
);
}
function InfoBox({ icon: Icon, title, children, color }: { icon: React.ElementType; title: string; children: React.ReactNode; color: string }) {
return (
<div className="rounded-xl border p-4 mb-4" style={{ background: "var(--card-bg)", borderColor: "var(--border)" }}>
<div className={`flex items-center gap-2 mb-2 ${color}`}>
<Icon className="h-4 w-4" />
<span className="text-sm font-semibold">{title}</span>
</div>
<div className="text-sm" style={{ color: "var(--fg-muted)" }}>{children}</div>
</div>
);
}
// ─── Content sections ─────────────────────────────────────────────────────────
function SectionOverview() {
return (
<div>
<H1>BankBot AI β€” Documentation</H1>
<P>
BankBot AI is a <strong>production-grade, AI-native financial operating system</strong> built as a
portfolio and educational project. It simulates the core features of a modern digital bank with a
real AI layer on top β€” real-time chat, fraud detection, forecasting, and document analysis β€” all
running in a single Docker container on Hugging Face Spaces.
</P>
<P>
The project demonstrates how to architect a full-stack AI application with a FastAPI backend,
a Next.js 14 frontend, multi-provider LLM fallback chains, JWT authentication, WebSocket
streaming, and a glassmorphism UI β€” deployable anywhere from a local machine to cloud platforms.
</P>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3 my-6">
{[
{ icon: MonitorSmartphone, label: "Next.js 14 Frontend", sub: "TypeScript Β· Tailwind Β· Framer Motion", color: "text-blue-500", bg: "bg-blue-500/10 border-blue-500/20" },
{ icon: Server, label: "FastAPI Backend", sub: "Python 3.11 Β· SQLAlchemy Β· JWT", color: "text-emerald-500", bg: "bg-emerald-500/10 border-emerald-500/20" },
{ icon: Sparkles, label: "4-Tier AI Chain", sub: "OpenAI β†’ Groq β†’ Ollama β†’ Rules", color: "text-purple-500", bg: "bg-purple-500/10 border-purple-500/20" },
].map((item) => (
<div key={item.label} className={`rounded-xl border p-4 ${item.bg}`}>
<item.icon className={`h-5 w-5 mb-2 ${item.color}`} />
<p className="text-sm font-semibold" style={{ color: "var(--fg)" }}>{item.label}</p>
<p className="text-xs mt-0.5" style={{ color: "var(--fg-muted)" }}>{item.sub}</p>
</div>
))}
</div>
<H2>Demo Account</H2>
<CodeBlock lang="credentials">{`Email: alex@bankbot.dev
Password: BankBot2026!
Pre-loaded data:
β€’ $59,637 across 3 accounts (checking Β· savings Β· investment)
β€’ 160 transactions across 6 months
β€’ 1 fraud alert (Tech Store NYC Β· $847 Β· 78% risk score)
β€’ 4 financial goals (Emergency Fund Β· Vacation Β· MacBook Β· Down Payment)
β€’ 4 investments (S&P 500 Β· AAPL Β· BTC Β· US Treasuries)
β€’ 6 subscriptions (Netflix Β· Spotify Β· Adobe CC Β· Planet Fitness Β· iCloud Β· LinkedIn)
β€’ 6 notifications (3 unread)`}</CodeBlock>
</div>
);
}
function SectionQuickStart() {
return (
<div>
<H1>Quick Start</H1>
<P>Three ways to run BankBot AI β€” pick whichever fits your situation.</P>
<H2>Option 1 β€” Hugging Face (No setup)</H2>
<P>The easiest option. Just open the link and log in with the demo credentials.</P>
<InfoBox icon={CheckCircle2} title="Live Demo" color="text-emerald-500">
<a href="https://mohsin-devs-bankbot.hf.space/" target="_blank" rel="noreferrer"
className="text-emerald-500 underline">https://mohsin-devs-bankbot.hf.space/</a>
<br />AI backend: Groq (llama-3.3-70b) Β· DB: SQLite Β· Cache: in-memory
</InfoBox>
<H2>Option 2 β€” Local Development</H2>
<P>Requires Python 3.11+ and Node.js 18+. Run these once:</P>
<CodeBlock lang="bash">{`# 1. Clone the repo
git clone https://github.com/mohsinkp02/Bankbot-AI.git
cd Bankbot-AI
# 2. Backend setup
cd backend
python -m venv venv
venv\\Scripts\\activate # Windows
# source venv/bin/activate # macOS / Linux
pip install -r requirements.txt
# 3. Configure environment (copy example and edit)
copy .env.example .env
# Open .env and add at minimum:
# GROQ_API_KEY=gsk_... ← free at console.groq.com
# JWT_SECRET_KEY=any-long-random-string
# 4. Seed demo data
python app/scripts/seed_demo.py
# 5. Start backend (keep this terminal open)
uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload`}</CodeBlock>
<CodeBlock lang="bash">{`# 6. In a NEW terminal β€” frontend setup
cd frontend
npm install --legacy-peer-deps
npm run dev`}</CodeBlock>
<CodeBlock lang="text">{`Then open:
Frontend β†’ http://localhost:3000
API Docs β†’ http://localhost:8000/docs
Metrics β†’ http://localhost:8000/api/metrics
Health β†’ http://localhost:8000/health`}</CodeBlock>
<H2>Option 3 β€” Docker Compose</H2>
<P>Runs everything (Nginx + FastAPI + Next.js + SQLite) in containers.</P>
<CodeBlock lang="bash">{`# 1. Copy and configure environment
copy .env.example .env
# Add GROQ_API_KEY and JWT_SECRET_KEY
# 2. Build and start
docker compose up -d
# 3. Seed demo data
docker compose exec backend python app/scripts/seed_demo.py
# 4. Open http://localhost:3000
# Stop
docker compose down`}</CodeBlock>
<H2>Environment Variables Reference</H2>
<Table
headers={["Variable", "Required", "Default", "Description"]}
rows={[
["GROQ_API_KEY", "Recommended", "β€”", "Free LLM from console.groq.com"],
["OPENAI_API_KEY", "Optional", "β€”", "OpenAI GPT-4o-mini (priority 1)"],
["JWT_SECRET_KEY", "Yes (prod)", "dev-secret","Signs JWT tokens"],
["DATABASE_URL", "Optional", "SQLite", "PostgreSQL URL (Neon / Supabase)"],
["REDIS_URL", "Optional", "in-memory","Redis URL for caching"],
["BACKEND_CORS_ORIGINS", "Optional", '["http://localhost:3000"]', "Allowed frontend origins"],
["ACCESS_TOKEN_EXPIRE_MINUTES", "Optional","60", "JWT access token lifetime"],
]}
/>
</div>
);
}
function SectionHowItWorks() {
return (
<div>
<H1>How It Works</H1>
<P>
BankBot AI is a <strong>full-stack monorepo</strong> with a clear separation of concerns.
The frontend never talks directly to the database β€” everything goes through the FastAPI backend
which handles auth, business logic, AI orchestration, and caching.
</P>
<H2>Request Lifecycle</H2>
<CodeBlock lang="text">{String.raw`User action (e.g. load dashboard)
β”‚
β–Ό
Next.js page component
β†’ calls dashboardApi.overview()
β†’ fetch("/api/dashboard/overview", { Authorization: Bearer <token> })
β”‚
β–Ό ← Nginx routes /api/* to FastAPI
FastAPI route handler
β†’ validates JWT token
β†’ checks Redis cache (key: "dashboard:overview:USER_ID")
β”‚
β”Œβ”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”
β”‚ Cache HIT β”‚ β†’ returns JSON in ~10ms
β”‚ Cache MISS β”‚ β†’ queries SQLite/PostgreSQL
β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ accounts + transactions + goals + fraud_logs
β”‚
β–Ό
runs AI briefing (Groq β†’ offline fallback)
sets cache (TTL 2 min)
returns JSON response
β”‚
β–Ό
React component receives data
β†’ renders dashboard with charts, cards, fraud shield`}</CodeBlock>
<H2>AI Chat Flow</H2>
<P>
The AI Assistant builds a <strong>full financial context prompt</strong> from the user's live data
before every message β€” so it always knows your real balance, goals, and spending patterns.
</P>
<CodeBlock lang="text">{`User types "How much did I spend on food this month?"
β”‚
β–Ό
POST /api/ai/chat (with message + session_id + language)
β”‚
β–Ό
Backend fetches user's financial context:
β€’ accounts + balances
β€’ last 50 transactions
β€’ active goals + progress
β€’ investments
β€’ subscriptions
β€’ fraud alerts
β”‚
β–Ό
Builds system prompt:
"You are BankBot. User Alex has $59,637 total.
This month: Food $487.30 across 12 transactions.
Top merchant: Chipotle ($143). ..."
β”‚
β–Ό
AI Provider chain:
1. OpenAI gpt-4o-mini (if OPENAI_API_KEY set)
2. Groq llama-3.3-70b (if GROQ_API_KEY set) ← HF demo uses this
3. Ollama llama3 (if running locally)
4. Rule-based fallback (always available)
β”‚
β–Ό
Response streamed back β†’ saved to chat memory β†’ displayed`}</CodeBlock>
<H2>Fraud Detection Flow</H2>
<CodeBlock lang="text">{`Every transaction is scored when it enters the system:
Amount spike? >3.5x user avg β†’ +40 pts
>2.0x user avg β†’ +20 pts
Night timing? 11PM – 4AM β†’ +25 pts
Rapid-fire? <3 min gap β†’ +20 pts
Duplicate? same merchant + amount in 10 min β†’ +30 pts
β”‚
Score β‰₯ 50 β†’ status: "flagged" β†’ notification created
Score 30-49 β†’ status: "suspicious"
Score < 30 β†’ status: "verified"`}</CodeBlock>
<H2>Caching Strategy</H2>
<Table
headers={["Endpoint", "Cache Key", "TTL", "Why"]}
rows={[
["/api/dashboard/overview", "dashboard:overview:{uid}", "2 min", "Heavy DB query, high traffic"],
["/api/ai/coaching/score", "ai:coaching:score:{uid}", "10 min", "LLM call, slow to compute"],
["/api/ai/coaching/briefing", "ai:coaching:briefing:{uid}", "1 hour", "Expensive AI generation"],
["/api/ai/behavior/insights", "ai:behavior:insights:{uid}", "10 min", "Pattern analysis is heavy"],
["/api/ai/twin/predict", "ai:twin:predict:{uid}", "5 min", "Moderate cost"],
["/api/ai/subscriptions", "ai:subs:optimize:{uid}", "10 min", "Stable subscription data"],
]}
/>
<P>
Cache backend: <strong>Redis β†’ in-memory dict fallback</strong>. If Redis is not configured
the app silently falls back to an in-process Python dict. No config change needed.
</P>
</div>
);
}
function SectionTechStack() {
return (
<div>
<H1>Tech Stack</H1>
<H2>Frontend</H2>
<Table
headers={["Library", "Version", "Purpose"]}
rows={[
["Next.js", "14.2", "React framework β€” App Router, SSR, standalone output"],
["TypeScript", "5.x", "Type safety across the entire frontend"],
["Tailwind CSS", "3.4", "Utility-first styling + custom CSS variables for theming"],
["Framer Motion", "12.x", "Page transitions, card animations, stagger effects"],
["Recharts", "3.x", "Area charts, pie charts, sparklines"],
["Zustand", "5.x", "Lightweight global state β€” auth, theme, language, dashboard"],
["Radix UI", "latest", "Accessible dialog, slider, tooltip primitives"],
["Lucide React", "latest", "Icon system"],
]}
/>
<H2>Backend</H2>
<Table
headers={["Library", "Version", "Purpose"]}
rows={[
["FastAPI", "0.111", "Async HTTP + WebSocket API framework"],
["Uvicorn", "0.29", "ASGI server with hot-reload"],
["SQLAlchemy", "2.0", "ORM + connection pooling"],
["Alembic", "1.13", "Database migrations"],
["python-jose", "3.3", "JWT encode/decode (HS256)"],
["passlib[bcrypt]", "1.7", "Password hashing (rounds=12)"],
["openai", "1.30", "OpenAI API client"],
["groq", "0.9", "Groq API client (llama-3.3-70b)"],
["redis", "5.0", "Redis client with in-memory fallback"],
["pydantic", "2.7", "Request/response validation and settings"],
["pypdf / PyMuPDF", "latest","PDF text extraction"],
["python-docx", "1.1", "DOCX text extraction"],
]}
/>
<H2>Infrastructure</H2>
<Table
headers={["Tool", "Role"]}
rows={[
["Docker", "Single-container build: Node build stage + Python runtime stage"],
["Nginx", "Reverse proxy: port 7860 β†’ Next.js (3000) or FastAPI (8000)"],
["Supervisord", "Process manager: runs Nginx + FastAPI + Next.js in one container"],
["SQLite", "Default database β€” zero config, file-based, auto-fallback"],
["PostgreSQL", "Production database option (Neon / Supabase / Render)"],
["Hugging Face Spaces", "Free GPU-less Docker hosting β€” public URL in minutes"],
["GitHub Actions", "CI: lint + build checks on push"],
]}
/>
<H2>Why These Choices</H2>
<InfoBox icon={Zap} title="Next.js App Router + FastAPI" color="text-blue-500">
Separating frontend and backend gives clean API contracts and allows each to be deployed
independently (Vercel + Render) or together in Docker. The <Code>standalone</Code> output
mode shrinks the Docker image by ~70% vs a full node_modules copy.
</InfoBox>
<InfoBox icon={Database} title="SQLite as default" color="text-emerald-500">
SQLite needs zero infrastructure β€” perfect for demos, local dev, and HF Spaces.
The same ORM code works with PostgreSQL in production with one env var change.
No migration needed between environments.
</InfoBox>
<InfoBox icon={Sparkles} title="Groq over OpenAI as default" color="text-purple-500">
Groq's free tier runs llama-3.3-70b at 500+ tokens/second β€” fast enough for real-time
streaming and free enough for a portfolio project. OpenAI is supported as priority 1
if a key is provided.
</InfoBox>
</div>
);
}
function SectionFeatures() {
return (
<div>
<H1>Features</H1>
<H2><BarChart2 className="h-5 w-5 text-blue-500" /> Dashboard</H2>
<P>
A single API call (<Code>GET /api/dashboard/overview</Code>) returns everything the dashboard
needs: total balance, 4 stat cards, 6-month cash flow chart, category spending pie,
5 recent transactions, health score, and AI daily briefing. Cold: ~65ms. Cached: ~10ms.
</P>
<H2><Sparkles className="h-5 w-5 text-emerald-500" /> AI Financial Twin</H2>
<P>
The AI assistant has full context of your finances injected into every prompt. It knows
your actual balance, top spending categories, active goals, investments, and fraud alerts.
It responds in English, Hindi, or Marathi depending on your language setting.
</P>
<P>
File attachment in chat: attach a PDF bank statement, CSV, DOCX invoice, or image directly
in the chat input. The backend extracts text, runs an AI analysis, and you can ask questions
about the document in the same conversation thread.
</P>
<H2><Shield className="h-5 w-5 text-red-500" /> Fraud Detection</H2>
<P>
Rule-based scoring engine that evaluates every transaction on 4 dimensions: amount vs
personal average, time-of-day, transaction velocity, and duplicate detection. Flagged
transactions create notifications and appear in the Security page with AI explanations.
</P>
<H2><Activity className="h-5 w-5 text-purple-500" /> Financial Health Score</H2>
<P>
A 100-point composite score across 6 weighted dimensions: savings consistency (20pts),
debt ratio (20pts), spending discipline (20pts), emergency fund coverage (20pts),
investment diversification (10pts), subscription hygiene (10pts).
</P>
<H2><Zap className="h-5 w-5 text-amber-500" /> What-If Simulator</H2>
<P>
6 real-time sliders (income, rent, food, transport, entertainment, savings target) that
generate an instant 36-month balance projection. Changes are calculated client-side for
immediate feedback, with an AI commentary generated on demand.
</P>
<H2><Globe className="h-5 w-5 text-cyan-500" /> Multi-Language UI</H2>
<P>
Full UI translation in English, Hindi, and Marathi. The AI assistant also responds in
the selected language. The <Code>html lang</Code> attribute updates automatically on
language change for proper browser accessibility support.
</P>
<H2><MonitorSmartphone className="h-5 w-5 text-slate-500" /> Dark / Light Mode</H2>
<P>
Full theme system built on CSS custom properties. Every surface β€” sidebar, navbar, cards,
charts, tooltips, notifications β€” adapts cleanly between dark and light mode with a single
DOM class toggle. Persisted to localStorage.
</P>
</div>
);
}
function SectionArchitecture() {
return (
<div>
<H1>Architecture</H1>
<H2>Single-Container HF Deployment</H2>
<CodeBlock lang="text">{`Internet β†’ HF Spaces (port 7860)
β”‚
β–Ό
Nginx (port 7860)
β”Œβ”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”
β–Ό β–Ό
Next.js (3000) FastAPI (8000)
(frontend) (backend API)
β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”
β–Ό β–Ό
SQLite in-memory cache
(auto-seeded) (Redis fallback)`}</CodeBlock>
<H2>Local / Docker Compose</H2>
<CodeBlock lang="text">{`Browser β†’ localhost:3000 (Next.js)
Browser β†’ localhost:8000 (FastAPI) ← direct in dev mode
Docker mode (docker compose up):
nginx: localhost:80 β†’ routes /api/* to fastapi, rest to nextjs
fastapi: localhost:8000
nextjs: localhost:3000
postgres: localhost:5432 (optional, override with DATABASE_URL)
redis: localhost:6379 (optional, override with REDIS_URL)`}</CodeBlock>
<H2>Directory Structure</H2>
<CodeBlock lang="text">{`BankBot New/
β”œβ”€β”€ Dockerfile # Single-container build (HF Spaces)
β”œβ”€β”€ docker-compose.yml # Multi-service local/prod
β”œβ”€β”€ hf/
β”‚ β”œβ”€β”€ nginx.conf # Nginx reverse proxy config
β”‚ β”œβ”€β”€ supervisord.conf # Process manager config
β”‚ └── start.sh # Container startup script
β”œβ”€β”€ backend/
β”‚ β”œβ”€β”€ app/
β”‚ β”‚ β”œβ”€β”€ main.py # FastAPI app + middleware stack
β”‚ β”‚ β”œβ”€β”€ ai/ # AI modules (chat, fraud, coaching, ...)
β”‚ β”‚ β”œβ”€β”€ auth/ # JWT auth router
β”‚ β”‚ β”œβ”€β”€ dashboard/ # Dashboard aggregation router
β”‚ β”‚ β”œβ”€β”€ database/ # SQLAlchemy models + migrations
β”‚ β”‚ β”œβ”€β”€ middleware/ # Logging, caching, rate limiting
β”‚ β”‚ └── scripts/ # seed_demo.py
β”‚ └── requirements.txt
└── frontend/
β”œβ”€β”€ src/
β”‚ β”œβ”€β”€ app/ # Next.js App Router pages
β”‚ β”‚ β”œβ”€β”€ page.tsx # Dashboard
β”‚ β”‚ β”œβ”€β”€ chat/ # AI Assistant
β”‚ β”‚ β”œβ”€β”€ analytics/ # Spending intelligence
β”‚ β”‚ β”œβ”€β”€ simulator/ # What-If engine
β”‚ β”‚ β”œβ”€β”€ security/ # Fraud alerts
β”‚ β”‚ β”œβ”€β”€ settings/ # User preferences
β”‚ β”‚ └── docs/ # This page
β”‚ β”œβ”€β”€ components/
β”‚ β”‚ β”œβ”€β”€ layout/ # Sidebar, Navbar, AppShell, DashboardLayout
β”‚ β”‚ └── ui/ # Shadcn primitives
β”‚ └── lib/
β”‚ β”œβ”€β”€ api.ts # Typed fetch client for all endpoints
β”‚ └── stores/ # Zustand: auth, theme, language, dashboard
└── package.json`}</CodeBlock>
</div>
);
}
function SectionAIEngine() {
return (
<div>
<H1>AI Engine</H1>
<P>
BankBot's AI layer is a <strong>4-tier fallback chain</strong> with automatic provider
detection. It never crashes β€” if all cloud providers fail, it falls back to a deterministic
rule-based engine that still produces useful answers using real database data.
</P>
<H2>Provider Priority</H2>
<CodeBlock lang="text">{`1. OpenAI (gpt-4o-mini) if OPENAI_API_KEY is set
↓ on error / unavailable
2. Groq (llama-3.3-70b) if GROQ_API_KEY is set
↓ on error / unavailable
3. Ollama (llama3:latest) if Ollama running on localhost:11434
↓ on error / unavailable
4. Rule-based fallback always available β€” uses real DB data`}</CodeBlock>
<H2>Context Injection</H2>
<P>Before every chat message, the backend builds a system prompt from live user data:</P>
<CodeBlock lang="python">{"system_prompt = f\"\"\"\nYou are BankBot, an elite AI Financial Analyst.\nALWAYS communicate in {language}.\n\nLIVE USER DATA:\n Name: {user.name}\n Personality: {user.financial_personality}\n Health Score: {score}/100\n Total Balance: ${total_balance:,.2f}\n Checking: ${checking:,.2f}\n Savings: ${savings:,.2f}\n Investment: ${investment:,.2f}\n\nMONTHLY ACTIVITY:\n Income: ${monthly_income:,.2f}\n Expenses: ${monthly_expenses:,.2f}\n Savings Rate: {savings_rate:.1f}%\n\nTOP SPENDING: {top_categories}\nACTIVE GOALS: {goals_summary}\nINVESTMENTS: {investments_summary}\nFRAUD ALERTS: {fraud_count} pending\n\nRULES:\n 1. Never give generic advice β€” use real numbers above\n 2. Keep answers concise and actionable\n 3. Respond in {language}\n\"\"\""}</CodeBlock>
<H2>Document Analysis</H2>
<P>
Files attached in the AI chat are uploaded to <Code>POST /api/documents/upload</Code>,
which extracts text (PDF via pypdf + PyMuPDF + Tesseract OCR fallback, DOCX via python-docx,
CSV row-by-row) and runs a structured analysis prompt. The extracted text is capped at 50,000
characters for storage and 6,000 characters per AI prompt to stay within token limits.
</P>
<H2>Streaming</H2>
<P>
The WebSocket endpoint (<Code>WS /api/ai/chat/ws</Code>) streams tokens from the AI provider
as they arrive. The HTTP fallback (<Code>POST /api/ai/chat</Code>) returns the full response
at once. The frontend simulates word-by-word streaming on HTTP responses using a 28ms interval.
</P>
</div>
);
}
function SectionSecurity() {
return (
<div>
<H1>Security</H1>
<H2>Authentication Flow</H2>
<CodeBlock lang="text">{`POST /api/auth/login
β†’ validates email + bcrypt hash (rounds=12)
β†’ returns access_token (JWT, 60min) + refresh_token (JWT, 7 days)
All protected requests:
Authorization: Bearer <access_token>
β†’ FastAPI verifies signature + expiry on every request
Token expired?
β†’ POST /api/auth/refresh with refresh_token
β†’ returns new access_token (refresh_token unchanged)
Frontend (api.ts):
β†’ auto-retry on 401: tries refresh, then clears tokens + redirects to /login`}</CodeBlock>
<H2>Middleware Stack</H2>
<P>Every request passes through this stack before reaching a route handler:</P>
<CodeBlock lang="text">{`1. Nginx β€” rate limit (30 req/min API, 10 req/min auth)
2. FastAPI β€” CORS validation (allowed origins from BACKEND_CORS_ORIGINS)
3. FastAPI β€” rate limiter (120 req/min per IP, in-process)
4. FastAPI β€” security headers:
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
Referrer-Policy: strict-origin-when-cross-origin
5. FastAPI β€” request logger (structured JSON with request-id)
6. FastAPI β€” process-time header (X-Process-Time: 12.4ms)
7. Route β€” JWT validation (if protected)
8. Handler β€” business logic`}</CodeBlock>
<InfoBox icon={AlertTriangle} title="Demo Note" color="text-amber-500">
The HF demo uses an ephemeral JWT_SECRET_KEY generated at container start. This means
sessions don't survive a container restart. Set a persistent JWT_SECRET_KEY in HF Secrets
to fix this for real usage.
</InfoBox>
</div>
);
}
function SectionDatabase() {
return (
<div>
<H1>Database & Models</H1>
<P>
BankBot uses SQLAlchemy 2.0 with auto-fallback: if <Code>DATABASE_URL</Code> points to
PostgreSQL and the connection fails, it silently falls back to SQLite. All models work
identically on both databases.
</P>
<H2>Core Models</H2>
<Table
headers={["Model", "Key Fields", "Relationships"]}
rows={[
["User", "id (UUID), email, password_hash, profile_data (JSON), financial_personality", "has many: Accounts, Goals, Investments, Subscriptions, Notifications"],
["Account", "id, user_id, type (checking/savings/investment), balance, currency, status", "belongs to: User Β· has many: Transactions"],
["Transaction", "id, account_id, amount, type (credit/debit), category, merchant, timestamp, tags (JSON)", "belongs to: Account Β· may have: FraudLog"],
["FraudLog", "id, transaction_id, risk_score (0–1), suspicious_activity_details, status", "belongs to: Transaction"],
["Goal", "id, user_id, title, target_amount, current_amount, target_date, ai_generated_plan (JSON)", "belongs to: User"],
["Investment", "id, user_id, asset_name, type, amount_invested, current_value, portfolio_allocation, ai_risk_analysis (JSON)", "belongs to: User"],
["Subscription", "id, user_id, merchant, amount, billing_cycle, active, ai_usage_detection (JSON)", "belongs to: User"],
["Notification", "id, user_id, title, message, type, read_status, created_at", "belongs to: User"],
["ChatMessage", "id, user_id, session_id, role (user/assistant), content, created_at", "belongs to: User"],
["UploadedDocument","id, user_id, filename, file_type, extracted_text, ai_summary, ai_insights (JSON)", "belongs to: User Β· has many: DocumentMessages"],
]}
/>
<H2>Seed Data</H2>
<P>
Running <Code>python app/scripts/seed_demo.py</Code> creates the <Code>alex@bankbot.dev</Code> demo
account with 160 realistic transactions across 6 months, all goals, investments, subscriptions,
notifications, and one fraud alert. The script is idempotent β€” it deletes the existing demo user
first, then re-creates everything clean.
</P>
</div>
);
}
function SectionAPI() {
return (
<div>
<H1>API Reference</H1>
<P>
Interactive Swagger UI available at <Code>http://localhost:8000/docs</Code> when running locally.
All protected endpoints require <Code>Authorization: Bearer {'<token>'}</Code>.
</P>
<H2>Core Endpoints</H2>
<Table
headers={["Method", "Path", "Auth", "Description"]}
rows={[
["GET", "/health", "No", "Health check β€” status, db, cache, uptime"],
["GET", "/api/status", "No", "Runtime info β€” AI backend, DB type, version"],
["GET", "/api/metrics", "No", "Live observability β€” request counts, AI latency, cache hit ratio"],
["GET", "/docs", "No", "Interactive Swagger UI"],
["POST", "/api/auth/register", "No", "Create account β†’ returns JWT pair"],
["POST", "/api/auth/login", "No", "Login (form-encoded) β†’ returns JWT pair"],
["POST", "/api/auth/refresh", "No", "Refresh access token"],
["GET", "/api/auth/me", "Yes", "Current user profile"],
["PATCH","/api/auth/settings", "Yes", "Update name / preferences"],
["GET", "/api/dashboard/overview", "Yes", "Full dashboard data (cached 2min)"],
["GET", "/api/transactions/", "Yes", "Paginated transactions (filter by category/type)"],
["GET", "/api/notifications/", "Yes", "Notifications + unread count"],
["PATCH","/api/notifications/{id}/read", "Yes", "Mark notification read"],
["PATCH","/api/notifications/read-all", "Yes", "Mark all notifications read"],
["DELETE","/api/notifications/{id}", "Yes", "Dismiss notification"],
["GET", "/api/ai/coaching/score", "Yes", "Financial health score (cached 10min)"],
["GET", "/api/ai/coaching/briefing", "Yes", "AI daily briefing (cached 1hr)"],
["GET", "/api/ai/behavior/insights", "Yes", "Spending behavior analysis"],
["GET", "/api/ai/twin/predict", "Yes", "30-day balance forecast"],
["GET", "/api/ai/twin/future", "Yes", "Long-term projection (param: months)"],
["GET", "/api/ai/twin/scenarios", "Yes", "Conservative/expected/optimistic scenarios"],
["GET", "/api/ai/fraud/analysis", "Yes", "All fraud alerts for user"],
["POST", "/api/ai/chat", "Yes", "HTTP chat (non-streaming)"],
["WS", "/api/ai/chat/ws", "Yes", "Streaming WebSocket chat"],
["POST", "/api/payments/create", "Yes", "Create payment with fraud scoring"],
["POST", "/api/payments/transfer", "Yes", "Internal account transfer"],
["GET", "/api/payments/history", "Yes", "Payment history"],
["GET", "/api/goals", "Yes", "User financial goals + progress"],
["POST", "/api/goals/{id}/contribute", "Yes", "Add contribution to a goal"],
["POST", "/api/loans/eligibility", "Yes", "ML-based loan eligibility prediction"],
["GET", "/api/memory/history", "Yes", "Chat history (all sessions)"],
["POST", "/api/memory/save", "Yes", "Save a chat message"],
["DELETE","/api/memory/clear", "Yes", "Clear chat history"],
["POST", "/api/documents/upload", "Yes", "Upload + analyze document"],
["POST", "/api/documents/chat/{id}", "Yes", "Ask question about a document"],
["GET", "/api/documents/history", "Yes", "Previously uploaded documents"],
]}
/>
</div>
);
}
function SectionDeployment() {
return (
<div>
<H1>Deployment</H1>
<H2>Hugging Face Spaces (Current)</H2>
<P>
The project deploys as a single Docker container to HF Spaces. The <Code>Dockerfile</Code>
has two build stages: Node.js 20 compiles the Next.js standalone bundle, then a Python 3.11
slim image assembles the final runtime with Nginx, supervisord, Node, and all Python
dependencies.
</P>
<CodeBlock lang="bash">{`# Push to the hf remote triggers an automatic rebuild:
git push hf hf-deploy2:main
# HF detects the new SHA, builds the Docker image (~5 min),
# then starts the container. Live URL:
# https://mohsin-devs-bankbot.hf.space/`}</CodeBlock>
<H2>Vercel + Render (Split Deploy)</H2>
<CodeBlock lang="bash">{`# Frontend β†’ Vercel
cd frontend
npx vercel --prod
# Set env var: NEXT_PUBLIC_API_URL=https://your-backend.onrender.com
# Backend β†’ Render
# 1. Push to GitHub
# 2. Render.com β†’ New Web Service β†’ connect repo
# 3. Render reads backend/render.yaml automatically
# 4. Set secrets: GROQ_API_KEY, JWT_SECRET_KEY
# 5. Render provisions PostgreSQL + Redis from render.yaml`}</CodeBlock>
<H2>Persistent Database Options</H2>
<Table
headers={["Option", "Cost", "Setup", "Notes"]}
rows={[
["SQLite (default)", "Free", "None", "Resets on HF Space restart"],
["Neon PostgreSQL", "Free tier", "DATABASE_URL secret", "Persistent, 3GB free"],
["Supabase", "Free tier", "DATABASE_URL secret", "Persistent, managed backups"],
["Render PostgreSQL","Free tier", "Auto via render.yaml", "Best for Render deploys"],
]}
/>
</div>
);
}
function SectionUseCases() {
return (
<div>
<H1>Use Cases</H1>
<P>
BankBot AI was built with specific audiences in mind. Here's who it's for and what
they can do with it.
</P>
<H2>1. Portfolio Project / Viva Demonstration</H2>
<P>
The primary use case. The project demonstrates end-to-end full-stack development: database
design, REST + WebSocket APIs, JWT auth, LLM integration with fallback chains, Docker
containerization, CI/CD, and a production-quality UI.
</P>
<P>
For a college viva or technical interview, you can walk through: the ER diagram, the fraud
detection algorithm, the AI context injection, the caching strategy, the Docker multi-stage
build, or the theme system β€” each one a self-contained deep-dive topic.
</P>
<H2>2. Learning Full-Stack AI Development</H2>
<P>
The codebase is structured and commented as a learning resource. Each module is independent:
you can study just the auth system, just the WebSocket streaming, or just the Zustand stores
without needing to understand the whole system.
</P>
<H2>3. Starter Template for Fintech Projects</H2>
<P>
The backend routers, database models, JWT auth flow, and AI orchestration layer can be
adapted for real financial tools. The 4-tier AI fallback chain is especially reusable β€”
swap the financial domain prompts for any other domain.
</P>
<H2>4. AI Chatbot Architecture Reference</H2>
<P>
The pattern of injecting live database context into every LLM prompt (rather than relying
on RAG or fine-tuning) is a practical technique for domain-specific chatbots. BankBot
shows how to do this cleanly with SQLAlchemy + FastAPI + streaming responses.
</P>
<H2>5. Demo for Non-Technical Stakeholders</H2>
<P>
The polished UI, realistic data, and immediate AI responses make it effective as a product
demo or mockup for fintech pitches β€” showing what a modern banking interface with AI could
look like, without building a real bank.
</P>
</div>
);
}
function SectionVsRealWorld() {
return (
<div>
<H1>BankBot vs Real Banking Apps</H1>
<P>
This is the most important section for understanding what BankBot is and what it isn't.
It's a realistic simulation, not a production banking system.
</P>
<H2>What BankBot Does That Real Banks Do</H2>
<Table
headers={["Feature", "BankBot", "Real Bank"]}
rows={[
["JWT authentication", "βœ… Full implementation", "βœ… OAuth2 / proprietary"],
["Transaction history", "βœ… Paginated, filterable", "βœ… Same concept"],
["Financial health score", "βœ… Rule-based 100-pt system", "βœ… Credit score systems"],
["Fraud detection alerts", "βœ… Real-time scoring + alerts", "βœ… ML models + human review"],
["Goal tracking", "βœ… Progress + AI plan", "βœ… Savings goals / pots"],
["Multi-language support", "βœ… EN / HI / MR", "βœ… Varies by region"],
["API-first architecture", "βœ… REST + WebSocket", "βœ… Open Banking APIs"],
["Dark / light theme", "βœ… Full CSS variable system", "βœ… Most modern apps"],
]}
/>
<H2>Where BankBot Differs from Real Banking</H2>
<Table
headers={["Area", "BankBot", "Real Banking System"]}
rows={[
["Money movement", "Simulated β€” no actual fund transfer", "Real money, regulatory compliance (PCI-DSS, PSD2)"],
["Identity verification", "Email + password only", "KYC: ID docs, face match, address proof"],
["Data persistence", "SQLite resets on HF restart", "ACID-compliant PostgreSQL clusters, backups, DR"],
["AI advice", "Informational only, not regulated", "Licensed financial advisors, fiduciary duty"],
["Security audit", "Basic JWT + bcrypt, no pen testing", "SOC 2, ISO 27001, regular security audits"],
["Scale", "Single-container, 1 worker, ~100 users", "Kubernetes, load balancers, millions of users"],
["Transaction data", "Seeded synthetic data", "Real transaction feeds (Plaid, bank APIs)"],
["Fraud ML", "Rule-based scoring (4 heuristics)", "Deep learning on billions of transactions"],
["Payments", "Fake payment records, no actual routing", "SWIFT, ACH, SEPA, UPI, real settlement"],
["Regulatory compliance", "None", "RBI, FCA, OCC, FDIC depending on jurisdiction"],
["Investment data", "Static seeded values", "Real-time market feeds (Bloomberg, Reuters)"],
]}
/>
<InfoBox icon={AlertTriangle} title="Important Disclaimer" color="text-amber-500">
BankBot AI is an educational demo. It does not handle real money, store real financial data,
or provide regulated financial advice. AI responses are generated by a language model and
may contain errors. Do not use it to make actual financial decisions.
</InfoBox>
<H2>What Makes BankBot Technically Interesting</H2>
<P>Despite being a demo, several parts of BankBot reflect real production patterns:</P>
<CodeBlock lang="text">{`βœ… 4-tier AI fallback chain β€” same pattern used in production LLM apps
βœ… Cache-aside with TTL per endpoint β€” standard Redis pattern
βœ… JWT access + refresh token flow β€” same as real OAuth2 implementations
βœ… WebSocket with heartbeat + auto-reconnect β€” production WS pattern
βœ… Multi-stage Docker build β€” Next.js standalone output saves ~200MB
βœ… Structured JSON logging with request-id β€” standard observability pattern
βœ… Middleware stack order β€” CORS β†’ rate limit β†’ auth β†’ handler
βœ… Alembic migrations β€” same tool used in production Python backends
βœ… pydantic-settings for env validation β€” standard FastAPI pattern`}</CodeBlock>
</div>
);
}
function SectionLimitations() {
return (
<div>
<H1>Known Limitations</H1>
<P>
These are deliberate simplifications made to keep the project manageable as a portfolio
piece. Each one has a note on how it would be addressed in production.
</P>
<Table
headers={["Limitation", "Current State", "Production Fix"]}
rows={[
["Data resets", "SQLite on HF Space resets on restart", "Set DATABASE_URL to Neon/Supabase PostgreSQL"],
["Single worker", "Uvicorn runs with 1 worker on HF free tier", "Multiple workers + Gunicorn, or Kubernetes pods"],
["No real-time market", "Investment values are static seeded data", "WebSocket feed from Alpha Vantage / Polygon.io"],
["No email/SMS", "Notifications are in-app only", "Twilio SMS, SendGrid email for critical alerts"],
["No 2FA", "Email + password only", "TOTP (Google Authenticator), SMS OTP"],
["AI rate limits", "Groq free tier: 30 req/min, may queue under load", "Paid tier, or multiple provider keys"],
["No audit log", "User actions not logged for compliance", "Append-only audit trail in separate table"],
["No account linking", "Only the seeded demo account exists per user", "Plaid / Open Banking API integration"],
["Session not persistent","Chat session IDs reset if localStorage cleared", "Store session_id server-side in DB"],
["Image OCR quality", "Tesseract OCR can fail on low-res images", "Google Vision API or AWS Textract"],
]}
/>
<H2>Reporting Issues</H2>
<P>
The GitHub repository is at{" "}
<a href="https://github.com/mohsinkp02/Bankbot-AI" target="_blank" rel="noreferrer"
className="text-emerald-500 underline">
github.com/mohsinkp02/Bankbot-AI
</a>
. Open an issue for bugs or feature suggestions.
</P>
</div>
);
}
// ─── Section registry ─────────────────────────────────────────────────────────
const SECTION_COMPONENTS: Record<string, React.ComponentType> = {
"overview": SectionOverview,
"quickstart": SectionQuickStart,
"how-it-works": SectionHowItWorks,
"tech-stack": SectionTechStack,
"features": SectionFeatures,
"architecture": SectionArchitecture,
"ai-engine": SectionAIEngine,
"security": SectionSecurity,
"database": SectionDatabase,
"api": SectionAPI,
"deployment": SectionDeployment,
"use-cases": SectionUseCases,
"vs-real-world": SectionVsRealWorld,
"limitations": SectionLimitations,
};
// ─── Main docs page ───────────────────────────────────────────────────────────
export default function DocsPage() {
const { theme } = useThemeStore();
const isLight = theme === "light";
const [activeSection, setActiveSection] = useState("overview");
const ActiveComponent = SECTION_COMPONENTS[activeSection] ?? SectionOverview;
return (
<div className="flex h-[calc(100vh-4rem)] -m-8 overflow-hidden">
{/* ── Left nav ────────────────────────────────────────────────────────── */}
<div
className="hidden lg:flex flex-col w-56 flex-shrink-0 border-r overflow-y-auto py-4"
style={{ background: "var(--sidebar-bg)", borderColor: "var(--border)" }}
>
<div className="px-4 mb-3">
<div className="flex items-center gap-2">
<BookOpen className="h-4 w-4 text-emerald-500" />
<span className="text-sm font-bold" style={{ color: "var(--fg)" }}>Documentation</span>
</div>
<p className="text-[10px] mt-0.5" style={{ color: "var(--fg-subtle)" }}>BankBot AI v2.0</p>
</div>
<nav className="px-2 space-y-0.5">
{SECTIONS.map((sec) => {
const isActive = activeSection === sec.id;
return (
<button
key={sec.id}
onClick={() => setActiveSection(sec.id)}
className={`w-full flex items-center gap-2.5 rounded-xl px-3 py-2 text-xs font-medium text-left transition-all ${
isActive
? isLight ? "bg-emerald-50 border border-emerald-200/80" : "bg-white/10 border border-white/10"
: isLight ? "hover:bg-black/5" : "hover:bg-white/5"
}`}
style={{ color: isActive ? "var(--fg)" : "var(--fg-subtle)" }}
>
<sec.icon className={`h-3.5 w-3.5 flex-shrink-0 ${isActive ? "text-emerald-500" : sec.color}`} />
<span>{sec.label}</span>
{isActive && <ChevronRight className="h-3 w-3 ml-auto text-emerald-500" />}
</button>
);
})}
</nav>
</div>
{/* ── Content ─────────────────────────────────────────────────────────── */}
<div className="flex-1 overflow-y-auto">
<div className="max-w-3xl mx-auto px-6 lg:px-10 py-8">
{/* Mobile section picker */}
<div className="lg:hidden mb-6 flex flex-wrap gap-2">
{SECTIONS.map((sec) => (
<button
key={sec.id}
onClick={() => setActiveSection(sec.id)}
className={`flex items-center gap-1.5 rounded-xl border px-3 py-1.5 text-xs font-medium transition-all ${
activeSection === sec.id
? "border-emerald-500/50 bg-emerald-500/10 text-emerald-500"
: ""
}`}
style={activeSection !== sec.id ? { borderColor: "var(--border)", color: "var(--fg-subtle)" } : undefined}
>
<sec.icon className="h-3 w-3" />
{sec.label}
</button>
))}
</div>
{/* Section breadcrumb */}
<div className="flex items-center gap-2 mb-6 text-xs" style={{ color: "var(--fg-subtle)" }}>
<BookOpen className="h-3.5 w-3.5" />
<span>BankBot AI</span>
<ChevronRight className="h-3 w-3" />
<span className="text-emerald-500 font-medium">
{SECTIONS.find(s => s.id === activeSection)?.label}
</span>
</div>
{/* Animated section content */}
<AnimatePresence mode="wait">
<motion.div
key={activeSection}
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -8 }}
transition={{ duration: 0.2, ease: "easeOut" }}
>
<ActiveComponent />
</motion.div>
</AnimatePresence>
{/* Next section nav */}
<div className="mt-10 pt-6 border-t flex items-center justify-between" style={{ borderColor: "var(--border)" }}>
{(() => {
const idx = SECTIONS.findIndex(s => s.id === activeSection);
const prev = SECTIONS[idx - 1];
const next = SECTIONS[idx + 1];
return (
<>
{prev ? (
<button
onClick={() => setActiveSection(prev.id)}
className="flex items-center gap-2 text-xs transition-colors hover:text-emerald-500"
style={{ color: "var(--fg-muted)" }}
>
<ArrowRight className="h-3.5 w-3.5 rotate-180" />
<div className="text-left">
<div style={{ color: "var(--fg-subtle)" }}>Previous</div>
<div className="font-medium">{prev.label}</div>
</div>
</button>
) : <div />}
{next ? (
<button
onClick={() => setActiveSection(next.id)}
className="flex items-center gap-2 text-xs transition-colors hover:text-emerald-500 text-right"
style={{ color: "var(--fg-muted)" }}
>
<div>
<div style={{ color: "var(--fg-subtle)" }}>Next</div>
<div className="font-medium">{next.label}</div>
</div>
<ArrowRight className="h-3.5 w-3.5" />
</button>
) : <div />}
</>
);
})()}
</div>
</div>
</div>
</div>
);
}