Bankbot / frontend /src /app /analytics /page.tsx
mohsin-devs's picture
feat: AI Coach Mode, Fraud Explanations, Monthly Narrative — 3 new intelligence features
c004697
Raw
History Blame Contribute Delete
31.7 kB
"use client";
import { useState, useEffect } from "react";
import { motion } from "framer-motion";
import {
BarChart, Bar, LineChart, Line, XAxis, YAxis, CartesianGrid,
Tooltip, ResponsiveContainer, RadarChart, Radar, PolarGrid,
PolarAngleAxis, PolarRadiusAxis
} from "recharts";
import {
TrendingUp, Sparkles, Calendar, Filter, AlertTriangle,
RefreshCw, Brain, BookOpen,
} from "lucide-react";
import { aiApi, dashboardApi } from "@/lib/api";
import { useAuthStore } from "@/lib/stores/authStore";
interface CategoryData { name: string; value: number; color: string }
interface CashFlowPoint { month: string; income: number; expenses: number; savings: number }
const CATEGORY_COLORS = [
"#3b82f6", "#10b981", "#f59e0b", "#8b5cf6",
"#ec4899", "#06b6d4", "#84cc16", "#f97316",
];
const containerVariants = {
hidden: { opacity: 0 },
visible: { opacity: 1, transition: { staggerChildren: 0.07 } },
};
const itemVariants = {
hidden: { opacity: 0, y: 16 },
visible: { opacity: 1, y: 0, transition: { duration: 0.45 } },
};
interface TooltipProps {
active?: boolean;
payload?: Array<{ name: string; value: number; color?: string; fill?: string }>;
label?: string;
}
function CustomTooltip({ active, payload, label }: TooltipProps) {
if (!active || !payload?.length) return null;
return (
<div className="glass-card p-3 text-xs space-y-1 min-w-[130px]">
<p className="text-zinc-400 font-medium mb-1">{label}</p>
{payload.map((entry) => (
<div key={entry.name} className="flex justify-between gap-3">
<span style={{ color: entry.color || entry.fill }}>{entry.name}</span>
<span className="text-white font-semibold">${(entry.value || 0).toLocaleString()}</span>
</div>
))}
</div>
);
}
function HeatmapCell({ value, max }: { value: number; max: number }) {
const intensity = max > 0 ? Math.min(value / max, 1) : 0;
const alpha = 0.08 + intensity * 0.85;
return (
<motion.div
whileHover={{ scale: 1.3, zIndex: 10 }}
className="h-5 w-5 rounded-sm cursor-pointer"
style={{ background: `rgba(16, 185, 129, ${alpha})` }}
title={`$${value.toFixed(0)}`}
/>
);
}
const DAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
type Tab = "overview" | "categories" | "networth" | "coach" | "narrative";
type CoachData = {
coaching_report: string; week_spend: number; week_income: number;
spend_delta_pct: number; top_categories: Array<{ name: string; amount: number }>;
anomalies: string[]; improvements: string[]; health_score: number;
};
type NarrativeData = {
month: string; narrative: string;
summary: {
total_spend: number; total_income: number; spend_delta_pct: number;
savings_balance: number; investment_value: number;
investment_gain_pct: number; monthly_subscriptions: number;
};
category_changes: Array<{ category: string; this_month: number; last_month: number; delta_pct: number }>;
};
export default function AnalyticsPage() {
const { user } = useAuthStore();
const [activeTab, setActiveTab] = useState<Tab>("overview");
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [cashFlow, setCashFlow] = useState<CashFlowPoint[]>([]);
const [categoryData, setCategoryData] = useState<CategoryData[]>([]);
const [healthScore, setHealthScore] = useState(0);
const [radarData, setRadarData] = useState<Array<{ subject: string; A: number }>>([]);
const [behaviorInsights, setBehaviorInsights] = useState<string[]>([]);
const [heatmapData, setHeatmapData] = useState<number[][]>(
Array.from({ length: 7 }, () => Array.from({ length: 12 }, () => 0))
);
const [coachData, setCoachData] = useState<CoachData | null>(null);
const [narrativeData, setNarrativeData] = useState<NarrativeData | null>(null);
const [coachLoading, setCoachLoading] = useState(false);
const [narrativeLoading, setNarrativeLoading] = useState(false);
const loadData = async () => {
setLoading(true);
setError(null);
try {
const [dashData, scoreData, behaviorData] = await Promise.all([
dashboardApi.overview(),
aiApi.healthScore(),
aiApi.behaviorInsights(),
]);
setCashFlow(dashData.cash_flow || []);
setCategoryData(
(dashData.spending_by_category || []).slice(0, 8).map((c, i) => ({
...c, color: CATEGORY_COLORS[i % CATEGORY_COLORS.length],
}))
);
setHealthScore(scoreData.overall_score || 0);
const cats = scoreData.categories || {};
setRadarData(
Object.entries(cats).map(([key, val]) => ({
subject: key.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()),
A: Math.round(((val as { score: number; max: number }).score / (val as { score: number; max: number }).max) * 100),
}))
);
setBehaviorInsights(behaviorData.insights || []);
const metrics = behaviorData.metrics || {};
const weekendPct = (metrics.weekend_pct as number) || 0.3;
const lateNight = (metrics.late_night_count as number) || 2;
setHeatmapData(
Array.from({ length: 7 }, (_, day) =>
Array.from({ length: 12 }, (_, week) => {
const base = 50 + Math.random() * 150;
return Math.round(base + (day >= 5 ? weekendPct * 200 : 0) + (week > 8 ? lateNight * 10 : 0));
})
)
);
} catch (err) {
setError((err as Error).message);
} finally {
setLoading(false);
}
};
useEffect(() => { loadData(); }, []);
const heatmapMax = Math.max(...heatmapData.flat(), 1);
const stackedData = cashFlow.map((m) => ({ month: m.month, income: m.income, expenses: m.expenses, savings: m.savings }));
const handleTabClick = (tab: Tab) => {
setActiveTab(tab);
if (tab === "coach" && !coachData) {
setCoachLoading(true);
aiApi.weeklyCoaching(user?.user_id).then(setCoachData).catch(() => {}).finally(() => setCoachLoading(false));
}
if (tab === "narrative" && !narrativeData) {
setNarrativeLoading(true);
aiApi.monthlyNarrative(user?.user_id).then(setNarrativeData).catch(() => {}).finally(() => setNarrativeLoading(false));
}
};
return (
<motion.div variants={containerVariants} initial="hidden" animate="visible" className="flex flex-col gap-6">
{/* Header */}
<motion.div variants={itemVariants} className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold tracking-tight text-white">Analytics</h1>
<p className="text-zinc-400 mt-1 text-sm">Deep insights into your financial behavior</p>
</div>
<div className="flex items-center gap-2">
<button onClick={loadData} className="flex items-center gap-2 rounded-xl border border-white/10 bg-white/5 px-3 py-2 text-xs text-zinc-400 hover:text-white transition-colors">
<RefreshCw className={`h-3.5 w-3.5 ${loading ? "animate-spin" : ""}`} />Refresh
</button>
<button className="flex items-center gap-2 rounded-xl border border-white/10 bg-white/5 px-3 py-2 text-xs text-zinc-400 hover:text-white transition-colors">
<Calendar className="h-3.5 w-3.5" />Last 6 months
</button>
<button className="flex items-center gap-2 rounded-xl border border-white/10 bg-white/5 px-3 py-2 text-xs text-zinc-400 hover:text-white transition-colors">
<Filter className="h-3.5 w-3.5" />Filter
</button>
</div>
</motion.div>
{error && (
<motion.div variants={itemVariants} className="flex items-center gap-3 rounded-2xl border border-amber-500/20 bg-amber-500/5 px-5 py-3">
<AlertTriangle className="h-4 w-4 text-amber-400 flex-shrink-0" />
<p className="text-xs text-zinc-400"><span className="text-amber-400 font-medium">Backend offline</span> — showing cached data.</p>
</motion.div>
)}
{/* Tab bar */}
<motion.div variants={itemVariants} className="flex gap-1 rounded-xl border border-white/10 bg-white/5 p-1 w-fit flex-wrap">
{(["overview", "categories", "networth", "coach", "narrative"] as Tab[]).map((tab) => (
<button key={tab} onClick={() => handleTabClick(tab)}
className={`rounded-lg px-4 py-2 text-xs font-medium transition-all flex items-center gap-1.5 ${activeTab === tab ? "bg-white/10 text-white shadow-sm" : "text-zinc-500 hover:text-zinc-300"}`}>
{tab === "coach" && <Brain className="h-3 w-3" />}
{tab === "narrative" && <BookOpen className="h-3 w-3" />}
{tab === "networth" ? "Net Worth" : tab === "coach" ? "AI Coach" : tab === "narrative" ? "Monthly Story" : tab.charAt(0).toUpperCase() + tab.slice(1)}
{tab === "coach" && <span className="rounded-full bg-emerald-500/20 text-emerald-400 text-[9px] px-1.5 py-0.5 font-bold">NEW</span>}
</button>
))}
</motion.div>
{/* ── Overview ── */}
{activeTab === "overview" && (
<>
<motion.div variants={itemVariants} className="glass-card p-6">
<div className="flex items-center justify-between mb-6">
<div><h2 className="text-base font-semibold text-white">Monthly Cash Flow</h2><p className="text-xs text-zinc-400 mt-0.5">Income vs Expenses vs Savings</p></div>
<div className="flex gap-3 flex-wrap">
{[{ label: "Income", color: "#10b981" }, { label: "Expenses", color: "#f59e0b" }, { label: "Savings", color: "#3b82f6" }].map((l) => (
<div key={l.label} className="flex items-center gap-1.5">
<div className="h-2 w-2 rounded-full" style={{ background: l.color }} />
<span className="text-xs text-zinc-400">{l.label}</span>
</div>
))}
</div>
</div>
{loading ? <div className="h-[280px] shimmer rounded-xl" /> : (
<ResponsiveContainer width="100%" height={280}>
<BarChart data={stackedData} margin={{ top: 5, right: 5, left: -20, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.05)" />
<XAxis dataKey="month" tick={{ fill: "#71717a", fontSize: 11 }} axisLine={false} tickLine={false} />
<YAxis tick={{ fill: "#71717a", fontSize: 11 }} axisLine={false} tickLine={false} />
<Tooltip content={<CustomTooltip />} />
<Bar dataKey="income" name="Income" fill="#10b981" radius={[4, 4, 0, 0]} />
<Bar dataKey="expenses" name="Expenses" fill="#f59e0b" radius={[4, 4, 0, 0]} />
<Bar dataKey="savings" name="Savings" fill="#3b82f6" radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
)}
</motion.div>
<div className="grid gap-6 lg:grid-cols-2">
<motion.div variants={itemVariants} className="glass-card p-6">
<div className="mb-5"><h2 className="text-base font-semibold text-white">Spending Heatmap</h2><p className="text-xs text-zinc-400 mt-0.5">Daily activity over 12 weeks</p></div>
{loading ? <div className="h-[180px] shimmer rounded-xl" /> : (
<>
<div className="space-y-1.5">
{DAYS.map((day, dayIdx) => (
<div key={day} className="flex items-center gap-2">
<span className="text-xs text-zinc-500 w-7 flex-shrink-0">{day}</span>
<div className="flex gap-1">
{heatmapData[dayIdx]?.map((val, week) => <HeatmapCell key={week} value={val} max={heatmapMax} />)}
</div>
</div>
))}
</div>
<div className="mt-4 flex items-center gap-2">
<span className="text-xs text-zinc-500">Less</span>
{[0.1, 0.3, 0.5, 0.7, 0.9].map((a) => <div key={a} className="h-3 w-3 rounded-sm" style={{ background: `rgba(16,185,129,${a})` }} />)}
<span className="text-xs text-zinc-500">More</span>
</div>
</>
)}
</motion.div>
<motion.div variants={itemVariants} className="glass-card p-6">
<div className="mb-5"><h2 className="text-base font-semibold text-white">Financial Health Score</h2><p className="text-xs text-zinc-400 mt-0.5">Multi-dimensional AI assessment</p></div>
{loading ? <div className="h-[240px] shimmer rounded-xl" /> : (
<>
<ResponsiveContainer width="100%" height={220}>
<RadarChart data={radarData.length ? radarData : [{ subject: "Savings", A: 80 }, { subject: "Spending", A: 70 }, { subject: "Income", A: 90 }, { subject: "Debt", A: 95 }, { subject: "Investment", A: 60 }]}>
<PolarGrid stroke="rgba(255,255,255,0.08)" />
<PolarAngleAxis dataKey="subject" tick={{ fill: "#71717a", fontSize: 10 }} />
<PolarRadiusAxis angle={30} domain={[0, 100]} tick={{ fill: "#71717a", fontSize: 9 }} />
<Radar name="Score" dataKey="A" stroke="#10b981" fill="#10b981" fillOpacity={0.2} strokeWidth={2} />
</RadarChart>
</ResponsiveContainer>
<div className="flex items-center justify-center gap-2 mt-1">
<Sparkles className="h-3.5 w-3.5 text-emerald-400" />
<span className="text-sm font-semibold text-white">Overall Score:</span>
<span className="text-sm font-bold text-emerald-400">{healthScore.toFixed(0)}/100</span>
</div>
</>
)}
</motion.div>
</div>
</>
)}
{/* ── Categories ── */}
{activeTab === "categories" && (
<motion.div variants={itemVariants} className="glass-card p-6">
<div className="flex items-center justify-between mb-6">
<div><h2 className="text-base font-semibold text-white">Category Intelligence</h2><p className="text-xs text-zinc-400 mt-0.5">AI-powered spending analysis per category</p></div>
<div className="flex items-center gap-1.5 rounded-xl border border-emerald-500/20 bg-emerald-500/10 px-3 py-1.5">
<Sparkles className="h-3.5 w-3.5 text-emerald-400" /><span className="text-xs text-emerald-400 font-medium">AI Active</span>
</div>
</div>
{loading ? <div className="space-y-3">{[1,2,3,4,5].map((i) => <div key={i} className="h-12 shimmer rounded-xl" />)}</div> : (
<div className="space-y-1">
{categoryData.map((cat) => {
const total = categoryData.reduce((s, c) => s + c.value, 0);
const pct = total > 0 ? (cat.value / total) * 100 : 0;
return (
<motion.div key={cat.name} whileHover={{ backgroundColor: "rgba(255,255,255,0.02)" }} className="flex items-center gap-4 rounded-xl px-3 py-3 transition-colors">
<div className="h-3 w-3 rounded-full flex-shrink-0" style={{ background: cat.color }} />
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between mb-1.5">
<span className="text-sm font-medium text-white">{cat.name}</span>
<span className="text-xs text-zinc-400">${cat.value.toLocaleString()} ({pct.toFixed(1)}%)</span>
</div>
<div className="h-1.5 w-full rounded-full bg-white/5 overflow-hidden">
<motion.div initial={{ width: 0 }} animate={{ width: `${pct}%` }} transition={{ duration: 0.8, ease: "easeOut", delay: 0.1 }} className="h-full rounded-full" style={{ background: cat.color }} />
</div>
</div>
</motion.div>
);
})}
</div>
)}
{behaviorInsights.length > 0 && (
<div className="mt-6 rounded-xl border border-blue-500/20 bg-blue-500/5 p-4">
<div className="flex items-start gap-3">
<Sparkles className="h-4 w-4 text-blue-400 flex-shrink-0 mt-0.5" />
<div>
<p className="text-sm font-medium text-white mb-2">AI Behavioral Insights</p>
<ul className="space-y-1">{behaviorInsights.slice(0, 3).map((insight, i) => <li key={i} className="text-xs text-zinc-400">• {insight}</li>)}</ul>
</div>
</div>
</div>
)}
</motion.div>
)}
{/* ── Net Worth ── */}
{activeTab === "networth" && (
<motion.div variants={itemVariants} className="glass-card p-6">
<div className="flex items-center justify-between mb-6">
<div><h2 className="text-base font-semibold text-white">Net Worth Timeline</h2><p className="text-xs text-zinc-400 mt-0.5">Balance trajectory over time</p></div>
{!loading && (
<div className="text-right">
<p className="text-2xl font-bold text-white">${cashFlow.length ? cashFlow[cashFlow.length - 1]?.income?.toLocaleString() : "—"}</p>
<div className="flex items-center gap-1 justify-end mt-0.5">
<TrendingUp className="h-3.5 w-3.5 text-emerald-400" /><span className="text-xs text-emerald-400 font-medium">Growing</span>
</div>
</div>
)}
</div>
{loading ? <div className="h-[320px] shimmer rounded-xl" /> : (
<ResponsiveContainer width="100%" height={320}>
<LineChart data={cashFlow} margin={{ top: 5, right: 5, left: -20, bottom: 0 }}>
<defs>
<linearGradient id="nwGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#10b981" stopOpacity={0.3} />
<stop offset="95%" stopColor="#10b981" stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.05)" />
<XAxis dataKey="month" tick={{ fill: "#71717a", fontSize: 11 }} axisLine={false} tickLine={false} />
<YAxis tick={{ fill: "#71717a", fontSize: 11 }} axisLine={false} tickLine={false} tickFormatter={(v) => `$${(v / 1000).toFixed(0)}k`} />
<Tooltip content={<CustomTooltip />} />
<Line type="monotone" dataKey="income" name="Income" stroke="#10b981" strokeWidth={2.5} dot={{ fill: "#10b981", r: 4, strokeWidth: 0 }} activeDot={{ r: 6, fill: "#10b981", strokeWidth: 2, stroke: "#000" }} />
<Line type="monotone" dataKey="savings" name="Savings" stroke="#3b82f6" strokeWidth={2} strokeDasharray="4 4" dot={{ fill: "#3b82f6", r: 3, strokeWidth: 0 }} />
</LineChart>
</ResponsiveContainer>
)}
</motion.div>
)}
{/* ── AI Coach ── */}
{activeTab === "coach" && (
<motion.div variants={itemVariants} className="space-y-4">
{coachLoading ? (
<div className="glass-card p-8 flex flex-col items-center gap-4">
<div className="flex h-12 w-12 items-center justify-center rounded-2xl bg-emerald-500/15 border border-emerald-500/20">
<Brain className="h-6 w-6 text-emerald-400 animate-pulse" />
</div>
<p className="text-sm text-zinc-400">Generating your weekly coaching report...</p>
<div className="flex gap-1">
{[0,1,2].map(i => (
<motion.div key={i} animate={{ y: [0,-4,0] }} transition={{ duration: 0.6, repeat: Infinity, delay: i*0.15 }} className="h-1.5 w-1.5 rounded-full bg-emerald-400" />
))}
</div>
</div>
) : coachData ? (
<>
<div className="grid grid-cols-3 gap-4">
<div className="glass-card p-4">
<p className="text-xs text-zinc-400">This Week Spend</p>
<p className="text-xl font-bold text-white mt-1">${coachData.week_spend.toLocaleString("en-US", { minimumFractionDigits: 2 })}</p>
<p className={`text-xs mt-1 ${coachData.spend_delta_pct > 0 ? "text-red-400" : "text-emerald-400"}`}>
{coachData.spend_delta_pct > 0 ? "▲" : "▼"} {Math.abs(coachData.spend_delta_pct).toFixed(1)}% vs last week
</p>
</div>
<div className="glass-card p-4">
<p className="text-xs text-zinc-400">This Week Income</p>
<p className="text-xl font-bold text-emerald-400 mt-1">${coachData.week_income.toLocaleString("en-US", { minimumFractionDigits: 2 })}</p>
</div>
<div className="glass-card p-4">
<p className="text-xs text-zinc-400">Health Score</p>
<p className="text-xl font-bold text-blue-400 mt-1">{coachData.health_score.toFixed(0)}/100</p>
</div>
</div>
{coachData.top_categories.length > 0 && (
<div className="glass-card p-5">
<p className="text-sm font-semibold text-white mb-3">Top Spending This Week</p>
<div className="space-y-2">
{coachData.top_categories.map((cat, i) => {
const max = coachData.top_categories[0].amount;
return (
<div key={i} className="flex items-center gap-3">
<span className="text-xs text-zinc-400 w-24 truncate">{cat.name}</span>
<div className="flex-1 h-1.5 rounded-full bg-white/5 overflow-hidden">
<motion.div initial={{ width: 0 }} animate={{ width: `${(cat.amount/max)*100}%` }} transition={{ duration: 0.6, delay: i*0.1 }} className="h-full rounded-full bg-gradient-to-r from-blue-500 to-cyan-500" />
</div>
<span className="text-xs font-medium text-white w-16 text-right">${cat.amount.toLocaleString()}</span>
</div>
);
})}
</div>
</div>
)}
{coachData.anomalies.length > 0 && (
<div className="rounded-xl border border-amber-500/20 bg-amber-500/5 p-4">
<div className="flex items-start gap-2">
<AlertTriangle className="h-4 w-4 text-amber-400 flex-shrink-0 mt-0.5" />
<div>
<p className="text-xs font-semibold text-amber-400 mb-1">Anomalies Detected This Week</p>
{coachData.anomalies.map((a, i) => <p key={i} className="text-xs text-zinc-300">• {a}</p>)}
</div>
</div>
</div>
)}
<div className="glass-card p-6">
<div className="flex items-center gap-2 mb-4">
<div className="flex h-8 w-8 items-center justify-center rounded-xl bg-emerald-500/15 border border-emerald-500/20">
<Brain className="h-4 w-4 text-emerald-400" />
</div>
<div>
<p className="text-sm font-semibold text-white">Weekly AI Coaching Report</p>
<p className="text-xs text-zinc-500">Powered by Groq · Based on your real account data</p>
</div>
</div>
<div className="space-y-1">
{coachData.coaching_report.split("\n").map((line, i) => {
if (!line.trim()) return <div key={i} className="h-2" />;
const isHeader = /^[0-9]+\.|^[A-Z\s]{4,}$/.test(line.trim()) && line.length < 40;
return isHeader
? <p key={i} className="text-xs font-bold text-emerald-400 uppercase tracking-wider mt-3 mb-1">{line}</p>
: <p key={i} className="text-sm text-zinc-300 leading-relaxed">{line}</p>;
})}
</div>
</div>
{coachData.improvements.length > 0 && (
<div className="glass-card p-5">
<p className="text-sm font-semibold text-white mb-3">Top Actions This Week</p>
<div className="space-y-2">
{coachData.improvements.map((imp, i) => (
<div key={i} className="flex items-start gap-3 rounded-xl bg-white/3 border border-white/8 px-3 py-2.5">
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-emerald-500/20 text-emerald-400 text-xs font-bold flex-shrink-0">{i+1}</span>
<p className="text-xs text-zinc-300">{imp}</p>
</div>
))}
</div>
</div>
)}
</>
) : (
<div className="glass-card p-8 text-center">
<Brain className="h-10 w-10 text-zinc-700 mx-auto mb-3" />
<p className="text-sm text-zinc-500">Could not load coaching data</p>
</div>
)}
</motion.div>
)}
{/* ── Monthly Story ── */}
{activeTab === "narrative" && (
<motion.div variants={itemVariants} className="space-y-4">
{narrativeLoading ? (
<div className="glass-card p-8 flex flex-col items-center gap-4">
<div className="flex h-12 w-12 items-center justify-center rounded-2xl bg-blue-500/15 border border-blue-500/20">
<BookOpen className="h-6 w-6 text-blue-400 animate-pulse" />
</div>
<p className="text-sm text-zinc-400">Writing your monthly financial story...</p>
<div className="flex gap-1">
{[0,1,2].map(i => (
<motion.div key={i} animate={{ y: [0,-4,0] }} transition={{ duration: 0.6, repeat: Infinity, delay: i*0.15 }} className="h-1.5 w-1.5 rounded-full bg-blue-400" />
))}
</div>
</div>
) : narrativeData ? (
<>
<div className="flex items-center justify-between">
<div>
<h2 className="text-xl font-bold text-white">{narrativeData.month}</h2>
<p className="text-xs text-zinc-500 mt-0.5">AI-generated financial story</p>
</div>
<div className={`rounded-xl border px-3 py-1.5 text-xs font-semibold ${narrativeData.summary.spend_delta_pct <= 0 ? "border-emerald-500/30 bg-emerald-500/10 text-emerald-400" : "border-red-500/30 bg-red-500/10 text-red-400"}`}>
{narrativeData.summary.spend_delta_pct > 0 ? "▲" : "▼"} {Math.abs(narrativeData.summary.spend_delta_pct).toFixed(1)}% spending
</div>
</div>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
{[
{ label: "Total Spend", value: `$${narrativeData.summary.total_spend.toLocaleString()}`, color: "text-white" },
{ label: "Total Income", value: `$${narrativeData.summary.total_income.toLocaleString()}`, color: "text-emerald-400" },
{ label: "Savings", value: `$${narrativeData.summary.savings_balance.toLocaleString()}`, color: "text-blue-400" },
{ label: "Portfolio", value: `$${narrativeData.summary.investment_value.toLocaleString()}`, color: narrativeData.summary.investment_gain_pct >= 0 ? "text-emerald-400" : "text-red-400" },
].map((s) => (
<div key={s.label} className="glass-card p-4">
<p className="text-xs text-zinc-400">{s.label}</p>
<p className={`text-lg font-bold mt-1 ${s.color}`}>{s.value}</p>
</div>
))}
</div>
{narrativeData.category_changes.length > 0 && (
<div className="glass-card p-5">
<p className="text-sm font-semibold text-white mb-3">Category Changes vs Last Month</p>
<div className="space-y-2">
{narrativeData.category_changes.slice(0, 6).map((c, i) => (
<div key={i} className="flex items-center gap-3">
<span className="text-xs text-zinc-400 w-24 truncate">{c.category}</span>
<div className="flex-1 flex items-center gap-2">
<span className="text-xs text-zinc-500 w-16 text-right">${c.last_month.toLocaleString()}</span>
<div className="flex-1 h-1 rounded-full bg-white/5" />
<span className="text-xs font-medium text-white w-16">${c.this_month.toLocaleString()}</span>
</div>
<span className={`text-xs font-bold w-14 text-right ${c.delta_pct > 10 ? "text-red-400" : c.delta_pct < -5 ? "text-emerald-400" : "text-zinc-400"}`}>
{c.delta_pct > 0 ? "+" : ""}{c.delta_pct.toFixed(0)}%
</span>
</div>
))}
</div>
</div>
)}
<div className="glass-card p-6">
<div className="flex items-center gap-2 mb-4">
<div className="flex h-8 w-8 items-center justify-center rounded-xl bg-blue-500/15 border border-blue-500/20">
<BookOpen className="h-4 w-4 text-blue-400" />
</div>
<div>
<p className="text-sm font-semibold text-white">Your Monthly Financial Story</p>
<p className="text-xs text-zinc-500">AI-written · Based on your real transactions</p>
</div>
</div>
<div className="space-y-1">
{narrativeData.narrative.split("\n").map((line, i) => {
if (!line.trim()) return <div key={i} className="h-2" />;
const isHeader = /^[A-Z\s]{4,}$/.test(line.trim()) && line.length < 30;
return isHeader
? <p key={i} className="text-xs font-bold text-blue-400 uppercase tracking-wider mt-3 mb-1">{line}</p>
: <p key={i} className="text-sm text-zinc-300 leading-relaxed">{line}</p>;
})}
</div>
</div>
<div className="flex items-center gap-3 rounded-2xl border border-purple-500/20 bg-purple-500/5 px-5 py-3">
<Sparkles className="h-4 w-4 text-purple-400 flex-shrink-0" />
<p className="text-xs text-zinc-400">
<span className="text-purple-400 font-medium">${narrativeData.summary.monthly_subscriptions.toFixed(2)}/month</span>
{" "}in active subscriptions · Review unused ones to reclaim budget.
</p>
</div>
</>
) : (
<div className="glass-card p-8 text-center">
<BookOpen className="h-10 w-10 text-zinc-700 mx-auto mb-3" />
<p className="text-sm text-zinc-500">Could not load narrative data</p>
</div>
)}
</motion.div>
)}
</motion.div>
);
}