"use client"; import { useEffect, useMemo, useState } from "react"; import { Bar, BarChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis, } from "recharts"; import { TrendingUp, Target, Thermometer, Hash } from "lucide-react"; type TrainingCity = { city_id: string; name: string; deb?: { hit_rate: number; mae: number; total_days: number } | null; }; const CHART_COLORS = { high: "#059669", mid: "#d97706", low: "#dc2626", blue: "#2563eb", purple: "#7c3aed", }; function barColor(hr: number) { if (hr >= 65) return CHART_COLORS.high; if (hr >= 45) return CHART_COLORS.mid; return CHART_COLORS.low; } export function TrainingDashboard({ isEn }: { isEn: boolean }) { const [data, setData] = useState(null); useEffect(() => { let cancelled = false; fetch("/api/ops/training/accuracy", { cache: "no-store", headers: { Accept: "application/json" }, }) .then(async (res) => { if (!res.ok) return null; return res.json() as Promise<{ accuracy: TrainingCity[] }>; }) .then((payload) => { if (cancelled || !payload?.accuracy) return; setData(payload.accuracy.filter((c) => c.deb && c.deb.total_days >= 5)); }) .catch(() => {}); return () => { cancelled = true; }; }, []); const debSorted = useMemo( () => (data || []).sort((a, b) => (b.deb?.hit_rate ?? 0) - (a.deb?.hit_rate ?? 0)), [data], ); const stats = useMemo(() => { if (!debSorted.length) return null; const avgHit = debSorted.reduce((s, c) => s + (c.deb?.hit_rate ?? 0), 0) / debSorted.length; const avgMae = debSorted.reduce((s, c) => s + (c.deb?.mae ?? 0), 0) / debSorted.length; const totalDays = debSorted.reduce((s, c) => s + (c.deb?.total_days ?? 0), 0); const high = debSorted.filter((c) => (c.deb?.hit_rate ?? 0) >= 65).length; const mid = debSorted.filter((c) => { const hr = c.deb?.hit_rate ?? 0; return hr >= 45 && hr < 65; }).length; const low = debSorted.filter((c) => (c.deb?.hit_rate ?? 0) < 45).length; return { avgHit, avgMae, totalDays, cities: debSorted.length, high, mid, low }; }, [debSorted]); const chartData = useMemo( () => debSorted.slice(0, 18).map((c) => ({ name: c.name, hit: Number((c.deb?.hit_rate ?? 0).toFixed(1)), mae: Number((c.deb?.mae ?? 0).toFixed(2)), fill: barColor(c.deb?.hit_rate ?? 0), })), [debSorted], ); const maeChartData = useMemo( () => [...debSorted] .sort((a, b) => (a.deb?.mae ?? 99) - (b.deb?.mae ?? 99)) .slice(0, 18) .map((c) => ({ name: c.name, mae: Number((c.deb?.mae ?? 0).toFixed(2)), })), [debSorted], ); return (
{/* Header */}

{isEn ? "DEB Training Accuracy" : "DEB 训练数据准确率"}

{isEn ? "DEB hit rate = forecast within settlement acceptance window" : "DEB 命中率 = 预报落入结算接受窗口的比例"}

{stats && (
{isEn ? "Updated" : "更新时间"}: {new Date().toLocaleDateString()}
)}
{/* Stats cards */} {stats && (
{[ { icon: Hash, label: isEn ? "Models" : "城市模型", value: stats.cities, color: "text-blue-600", bg: "bg-blue-50 border-blue-200" }, { icon: Target, label: isEn ? "Avg Hit Rate" : "平均命中率", value: `${stats.avgHit.toFixed(1)}%`, color: "text-emerald-600", bg: "bg-emerald-50 border-emerald-200" }, { icon: Thermometer, label: "Avg MAE", value: `${stats.avgMae.toFixed(1)}°`, color: "text-amber-600", bg: "bg-amber-50 border-amber-200" }, { icon: TrendingUp, label: isEn ? "Total Days" : "总训练天数", value: stats.totalDays.toLocaleString(), color: "text-purple-600", bg: "bg-purple-50 border-purple-200" }, ].map(({ icon: Icon, label, value, color, bg }) => (
{label}
{String(value)}
))}
)} {/* Distribution summary */} {stats && (
{[ { label: isEn ? "High (≥65%)" : "高 (≥65%)", count: stats.high, color: "emerald" }, { label: isEn ? "Mid (45-64%)" : "中 (45-64%)", count: stats.mid, color: "amber" }, { label: isEn ? "Low (<45%)" : "低 (<45%)", count: stats.low, color: "red" }, ].map(({ label, count, color }) => (
{count}
{label}
))}
)} {/* Charts */} {chartData.length > 0 && (
{/* Hit rate bar chart */}

{isEn ? "Hit Rate by City" : "各城市命中率"}

`${v}%`} /> [`${Number(value)}%`, isEn ? "Hit Rate" : "命中率"]} />
{/* MAE bar chart (lower is better) */}

{isEn ? "MAE by City (lower = better)" : "各城市 MAE (越低越好)"}

`${v}°`} /> [`${Number(value)}°`, "MAE"]} />
)} {/* Table */}
{debSorted.length ? debSorted.map((c, i) => { const hr = c.deb?.hit_rate ?? 0; const mae = c.deb?.mae ?? 0; const color = hr >= 65 ? "emerald" : hr >= 45 ? "amber" : "red"; return ( ); }) : ( )}
# {isEn ? "City" : "城市"} {isEn ? "Hit Rate" : "命中率"} MAE {isEn ? "Days" : "训练天数"}
{i + 1} {c.name}
{hr.toFixed(0)}%
{mae.toFixed(1)}° {c.deb?.total_days ?? 0}
{data === null ? (isEn ? "Loading training data..." : "加载训练数据中...") : (isEn ? "No training data available" : "暂无训练数据")}
{/* Footer note */}

{isEn ? "Training data updated daily. Hit rate = DEB prediction within settlement acceptance range." : "训练数据每日更新。命中率 = DEB 预报落入结算接受窗口的比例。"}

); }