"use client";
import { useState, useMemo, useEffect, useCallback } from "react";
import { motion, AnimatePresence } from "framer-motion";
import {
AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip,
ResponsiveContainer, ReferenceLine
} from "recharts";
import { Sparkles, TrendingUp, TrendingDown, Zap, RotateCcw, ChevronRight, AlertTriangle } from "lucide-react";
import { aiApi } from "@/lib/api";
// ─── Types ────────────────────────────────────────────────────────────────────
interface SimParams {
monthlyIncome: number;
monthlyExpenses: number;
savingsRate: number;
investmentReturn: number;
loanPayment: number;
emergencyMonths: number;
}
const DEFAULT_PARAMS: SimParams = {
monthlyIncome: 10200,
monthlyExpenses: 6100,
savingsRate: 40,
investmentReturn: 7,
loanPayment: 450,
emergencyMonths: 6,
};
const scenarios = [
{
id: "aggressive",
label: "Aggressive Saver",
icon: "🚀",
color: "text-emerald-400",
bg: "bg-emerald-500/10 border-emerald-500/20",
params: { savingsRate: 55, monthlyExpenses: 4600, investmentReturn: 9 },
},
{
id: "balanced",
label: "Balanced Growth",
icon: "⚖️",
color: "text-blue-400",
bg: "bg-blue-500/10 border-blue-500/20",
params: { savingsRate: 40, monthlyExpenses: 6100, investmentReturn: 7 },
},
{
id: "conservative",
label: "Conservative",
icon: "🛡️",
color: "text-purple-400",
bg: "bg-purple-500/10 border-purple-500/20",
params: { savingsRate: 25, monthlyExpenses: 7600, investmentReturn: 4 },
},
];
// ─── Local projection engine (instant feedback while API loads) ───────────────
function projectBalance(params: SimParams, startBalance: number, months = 36) {
const monthlySavings = params.monthlyIncome * (params.savingsRate / 100);
const monthlyReturn = params.investmentReturn / 100 / 12;
let balance = startBalance;
return Array.from({ length: months + 1 }, (_, i) => {
if (i > 0) balance = balance * (1 + monthlyReturn) + monthlySavings - params.loanPayment;
return { month: i === 0 ? "Now" : `M${i}`, balance: Math.round(balance), savings: Math.round(monthlySavings * i) };
});
}
// ─── Custom Tooltip ───────────────────────────────────────────────────────────
interface TooltipProps {
active?: boolean;
payload?: Array<{ name: string; value: number; stroke?: string; fill?: string }>;
label?: string;
}
function CustomTooltip({ active, payload, label }: TooltipProps) {
if (!active || !payload?.length) return null;
return (
{label}
{payload.map((entry) => (
{entry.name}
${entry.value?.toLocaleString()}
))}
);
}
// ─── Slider ───────────────────────────────────────────────────────────────────
function Slider({ label, value, min, max, step, format, onChange, color = "emerald" }: {
label: string; value: number; min: number; max: number; step: number;
format: (v: number) => string; onChange: (v: number) => void; color?: string;
}) {
const pct = ((value - min) / (max - min)) * 100;
const colorMap: Record = {
emerald: "#10b981", blue: "#3b82f6", purple: "#8b5cf6", amber: "#f59e0b", red: "#ef4444",
};
const c = colorMap[color] || colorMap.emerald;
return (
{label}
{format(value)}
onChange(Number(e.target.value))}
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer" style={{ zIndex: 10 }} />
);
}
// ─── Metric Card ──────────────────────────────────────────────────────────────
function MetricCard({ label, value, sub, positive, icon }: {
label: string; value: string; sub: string; positive: boolean; icon: React.ReactNode;
}) {
return (
{value}
{sub}
);
}
// ─── Main Page ────────────────────────────────────────────────────────────────
export default function SimulatorPage() {
const [params, setParams] = useState(DEFAULT_PARAMS);
const [activeScenario, setActiveScenario] = useState("balanced");
const [startBalance, setStartBalance] = useState(24563);
const [apiScenarios, setApiScenarios] = useState | null>(null);
const [loadingApi, setLoadingApi] = useState(false);
const [apiError, setApiError] = useState(false);
// Load real starting balance and API scenarios on mount
useEffect(() => {
const loadApiData = async () => {
setLoadingApi(true);
try {
const [twinData, scenariosData] = await Promise.all([
aiApi.twinPredict(),
aiApi.twinScenarios(6),
]);
// Extract starting balance from twin prediction
const projections = twinData.monthly_projections;
if (projections && projections.length > 0) {
setStartBalance(projections[0].balance || 24563);
}
// Extract scenario end values
if (scenariosData.scenarios) {
const s = scenariosData.scenarios;
const getEnd = (arr: number[]) => arr[arr.length - 1] || 0;
setApiScenarios({
conservative: getEnd(s.conservative || []),
expected: getEnd(s.expected || []),
optimistic: getEnd(s.optimistic || []),
});
}
setApiError(false);
} catch {
setApiError(true);
} finally {
setLoadingApi(false);
}
};
loadApiData();
}, []);
const projection = useMemo(() => projectBalance(params, startBalance), [params, startBalance]);
const baseProjection = useMemo(() => projectBalance(DEFAULT_PARAMS, startBalance), [startBalance]);
const finalBalance = projection[projection.length - 1].balance;
const baseBalance = baseProjection[baseProjection.length - 1].balance;
const delta = finalBalance - baseBalance;
const monthlySavings = params.monthlyIncome * (params.savingsRate / 100);
const emergencyTarget = params.monthlyExpenses * params.emergencyMonths;
const monthsToEmergency = Math.ceil(Math.max(0, emergencyTarget - startBalance) / monthlySavings);
const update = (key: keyof SimParams) => (v: number) => {
setParams((p) => ({ ...p, [key]: v }));
setActiveScenario(null);
};
const applyScenario = (s: typeof scenarios[0]) => {
setParams((p) => ({ ...p, ...s.params }));
setActiveScenario(s.id);
};
const reset = useCallback(() => {
setParams(DEFAULT_PARAMS);
setActiveScenario("balanced");
}, []);
const comparisonData = projection.map((p, i) => ({
...p,
base: baseProjection[i]?.balance,
}));
return (
{/* Header */}
What-If Simulator
Model financial scenarios and see your future balance in real-time
{loadingApi && · Loading real data... }
Reset
{/* API error notice */}
{apiError && (
Using local projections — backend offline. Sliders still work with estimated data.
)}
{/* API Scenario Cards (from backend) */}
{apiScenarios && (
AI 6-Month Forecast (from your real data)
{[
{ label: "Conservative", value: apiScenarios.conservative, color: "text-zinc-400" },
{ label: "Expected", value: apiScenarios.expected, color: "text-blue-400" },
{ label: "Optimistic", value: apiScenarios.optimistic, color: "text-emerald-400" },
].map((s) => (
{s.label}
${s.value.toLocaleString()}
))}
)}
{/* Scenario Presets */}
{scenarios.map((s) => (
applyScenario(s)}
className={`flex items-center gap-3 rounded-2xl border p-4 text-left transition-all ${s.bg} ${
activeScenario === s.id ? "ring-1 ring-white/20" : ""
}`}
>
{s.icon}
{activeScenario === s.id &&
}
))}
{/* Controls */}
Adjust Parameters
`$${v.toLocaleString()}`} onChange={update("monthlyIncome")} color="emerald" />
`$${v.toLocaleString()}`} onChange={update("monthlyExpenses")} color="amber" />
`${v}%`} onChange={update("savingsRate")} color="blue" />
`${v}%`} onChange={update("investmentReturn")} color="purple" />
`$${v.toLocaleString()}`} onChange={update("loanPayment")} color="red" />
`${v}mo`} onChange={update("emergencyMonths")} color="emerald" />
{/* Chart + Metrics */}
{/* Metrics */}
= 0 ? "+" : ""}$${Math.abs(delta / 1000).toFixed(1)}k vs baseline`}
positive={delta >= 0}
icon={ }
/>
= 20}
icon={ }
/>
}
/>
{/* Projection Chart */}
36-Month Projection
Your scenario vs baseline
`$${(v / 1000).toFixed(0)}k`} />
} />
{/* Dynamic AI Insight */}
AI Insight
{params.savingsRate >= 50
? `At ${params.savingsRate}% savings rate, you'll reach $100k in approximately ${Math.ceil((100000 - startBalance) / (params.monthlyIncome * params.savingsRate / 100))} months. That's an aggressive but achievable goal.`
: params.savingsRate >= 30
? `Your ${params.savingsRate}% savings rate is above average. Increasing by just 5% more would add $${Math.round(params.monthlyIncome * 0.05 * 36).toLocaleString()} to your 3-year balance.`
: `At ${params.savingsRate}% savings rate, consider reducing discretionary spending. Even a 5% increase would significantly accelerate your goals.`}
Ask AI
);
}