"use client"; import { useEffect, useMemo, useState } from "react"; type TrainingCity = { city_id: string; name: string; deb?: { hit_rate: number; mae: number; total_days: number } | null; }; 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); return { avgHit, avgMae, totalDays, cities: debSorted.length }; }, [debSorted]); return (

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

{isEn ? "Per-city DEB prediction accuracy. Hit rate measures whether the forecast fell within the settlement window." : "各城市 DEB 预报命中率。命中率衡量预报是否落入结算接受窗口。"}

{stats && (
{[ [isEn ? "Cities" : "城市数", stats.cities], [isEn ? "Avg Hit" : "平均命中率", `${stats.avgHit.toFixed(1)}%`], ["Avg MAE", `${stats.avgMae.toFixed(1)}°`], [isEn ? "Days" : "训练天数", stats.totalDays], ].map(([label, value]) => (
{label}
{String(value)}
))}
)}
{debSorted.length ? debSorted.map((c) => { const hr = c.deb?.hit_rate ?? 0; return ( ); }) : ( )}
{isEn ? "City" : "城市"} {isEn ? "Hit Rate" : "命中率"} MAE {isEn ? "Days" : "训练天数"}
{c.name}
= 60 ? "bg-emerald-500" : hr >= 30 ? "bg-amber-400" : "bg-red-400" }`} style={{ width: `${Math.min(100, hr)}%` }} />
= 60 ? "text-emerald-700" : hr >= 30 ? "text-amber-700" : "text-red-600" }`}> {hr.toFixed(0)}%
{(c.deb?.mae ?? 0).toFixed(1)}° {c.deb?.total_days ?? 0}
{data === null ? (isEn ? "Loading..." : "加载中...") : (isEn ? "No training data" : "暂无训练数据")}
); }