"use client"; import { useEffect, useMemo, useState } from "react"; import dynamic from "next/dynamic"; import { RefreshCcw, TrendingUp, TrendingDown, Target, Activity } from "lucide-react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { buildDebRecentRankingRows } from "@/lib/deb-training-ranking"; import { opsApi } from "@/lib/ops-api"; import type { SystemStatusPayload } from "@/types/ops"; import Link from "next/link"; const TrainingAccuracyCharts = dynamic( () => import("./TrainingAccuracyCharts").then((mod) => mod.TrainingAccuracyCharts), { ssr: false, loading: () =>
, }, ); function StatRow({ label, value }: { label: string; value: React.ReactNode }) { return (
{label} {value}
); } function KpiCard({ label, value, sub, icon: Icon, color }: { label: string; value: string | number; sub?: string; icon: React.ComponentType<{ className?: string }>; color: string; }) { return (
{value}
{label}
{sub ?
{sub}
: null}
); } interface CityAccuracy { city_id: string; name: string; deb?: { hit_rate: number; mae: number; total_days: number; details_str: string; } | null; deb_recent?: { recent_7d?: { hit_rate?: number | null; samples?: number; mae?: number | null; }; recent_14d?: { hit_rate?: number | null; samples?: number; mae?: number | null; }; trust_tier?: string; recommendation?: string; bias_direction?: string; reason?: string; } | null; mu?: { mae: number; hit_rate: number; brier_score: number | null; total_days: number; details_str: string; } | null; } interface DebSummary { historical?: { avg_hit_rate?: number | null; weighted_hit_rate?: number | null; avg_mae?: number | null; sample_days?: number; city_count?: number; }; usable_recent?: { window?: string; city_count?: number; samples?: number; hits?: number; hit_rate?: number | null; avg_mae?: number | null; recommendations?: { primary?: number; supporting?: number; }; }; recent_7d?: { hit_rate?: number | null; mae?: number | null; samples?: number; hits?: number; }; recent_14d?: { hit_rate?: number | null; mae?: number | null; samples?: number; hits?: number; }; } function debTrustBadgeClass(tier?: string) { if (tier === "high") return "bg-emerald-500/15 text-emerald-300 border-emerald-500/30"; if (tier === "medium") return "bg-amber-500/15 text-amber-300 border-amber-500/30"; if (tier === "low") return "bg-rose-500/15 text-rose-300 border-rose-500/30"; return "bg-slate-500/15 text-slate-300 border-slate-500/30"; } function debTrustLabel(tier?: string) { if (tier === "high") return "高可信"; if (tier === "medium") return "中可信"; if (tier === "low") return "低可信"; return "样本少"; } function debRecommendationLabel(recommendation?: string) { if (recommendation === "primary") return "主用"; if (recommendation === "supporting") return "辅助"; if (recommendation === "context_only") return "仅参考"; return "不足"; } function formatPct(value: number | null | undefined) { return value == null ? "—" : `${value.toFixed(0)}%`; } export function TrainingPageClient() { const [loading, setLoading] = useState(true); const [status, setStatus] = useState(null); const [accuracy, setAccuracy] = useState(null); const [debSummary, setDebSummary] = useState(null); const load = async () => { setLoading(true); try { const [s, accData] = await Promise.all([ opsApi.systemStatus() as Promise, opsApi.trainingAccuracy().catch(() => ({ accuracy: [] as CityAccuracy[], deb_summary: null })), ]); setStatus(s); setAccuracy((accData as { accuracy: CityAccuracy[] }).accuracy ?? []); setDebSummary((accData as { deb_summary?: DebSummary | null }).deb_summary ?? null); } catch { /* */ } setLoading(false); }; useEffect(() => { void load(); }, []); const kpis = useMemo(() => { if (!accuracy?.length) return null; const debCities = accuracy.filter((c) => c.deb); const avgHit = debCities.reduce((s, c) => s + (c.deb?.hit_rate ?? 0), 0) / debCities.length; const avgMae = debCities.reduce((s, c) => s + (c.deb?.mae ?? 0), 0) / debCities.length; const best = debCities.reduce((a, b) => ((a.deb?.hit_rate ?? 0) > (b.deb?.hit_rate ?? 0) ? a : b)); const worst = debCities.reduce((a, b) => ((a.deb?.mae ?? 0) > (b.deb?.mae ?? 0) ? a : b)); return { avgHit: debSummary?.historical?.avg_hit_rate ?? avgHit, avgMae: debSummary?.historical?.avg_mae ?? avgMae, usableRecent: debSummary?.usable_recent, recent7Hit: debSummary?.recent_7d?.hit_rate, recent14Hit: debSummary?.recent_14d?.hit_rate, best, worst, }; }, [accuracy, debSummary]); const usableWindowLabel = (window?: string) => window === "recent_14d" ? "DEB 可用近 14 天命中" : "DEB 可用近 7 天命中"; const debRecentRanked = useMemo(() => buildDebRecentRankingRows(accuracy || []), [accuracy]); const debRecentRankIndex = useMemo( () => new Map(debRecentRanked.map((row, index) => [row.cityId, index])), [debRecentRanked], ); const debChartData = useMemo(() => { return debRecentRanked .slice(0, 24) .map((c) => ({ name: c.name, cityId: c.cityId, hitRate: c.hitRate, mae: c.mae, days: c.samples, })); }, [debRecentRanked]); const muChartData = useMemo(() => { if (!accuracy?.length) return []; return accuracy .filter((c) => c.mu && c.mu.total_days >= 5 && c.mu.brier_score !== null) .sort((a, b) => ((a.mu?.brier_score ?? 1) - (b.mu?.brier_score ?? 1))) .map((c) => ({ name: c.name, cityId: c.city_id, brierScore: Number((c.mu?.brier_score ?? 0).toFixed(4)), hitRate: Number((c.mu?.hit_rate ?? 0).toFixed(1)), mae: Number((c.mu?.mae ?? 0).toFixed(1)), days: c.mu?.total_days ?? 0, })); }, [accuracy]); const sortedAccuracy = useMemo(() => { if (!accuracy?.length) return []; return [...accuracy].sort((a, b) => { const aDebRank = debRecentRankIndex.get(a.city_id) ?? 9999; const bDebRank = debRecentRankIndex.get(b.city_id) ?? 9999; if (aDebRank !== bDebRank) return aDebRank - bDebRank; const aMax = Math.max(a.deb?.hit_rate ?? 0, a.mu?.hit_rate ?? 0); const bMax = Math.max(b.deb?.hit_rate ?? 0, b.mu?.hit_rate ?? 0); return bMax - aMax; }); }, [accuracy, debRecentRankIndex]); if (loading) return
加载中...
; if (!status) return
加载失败
; const td = status.training_data; const truth = td?.truth_records; const features = td?.training_features; const coverage = td?.city_coverage; const modelCities = td?.model_cities; return (

训练数据

{/* Data volume KPIs */}
真值记录 训练特征 城市覆盖
{/* Accuracy KPI row */} {kpis ? (
) : null} {/* City coverage */} {modelCities ? (
最强城市 {modelCities.strongest?.length ? (
    {modelCities.strongest.map((c, i) => (
  • {c.city} 真值:{c.truth_rows ?? "—"} 特征:{c.feature_rows ?? "—"}
  • ))}
) : 无数据}
覆盖缺口 {modelCities.gaps?.length ? (
{modelCities.gaps.map((c) => ( {c} ))}
) : 无缺口}
) : null} {/* Detail table */} 模型融合与预测准确率详情
{sortedAccuracy.length > 0 ? ( sortedAccuracy.map((row) => ( )) ) : ( )}
城市 DEB 策略 近 7 / 14 天 DEB 命中 DEB MAE DEB 天数 μ 命中 μ MAE Brier μ 天数
{row.name} {row.city_id} {row.deb_recent ? ( {debTrustLabel(row.deb_recent.trust_tier)} · {debRecommendationLabel(row.deb_recent.recommendation)} ) : ( )} {row.deb_recent ? ( {formatPct(row.deb_recent.recent_7d?.hit_rate)} / {formatPct(row.deb_recent.recent_14d?.hit_rate)} ) : ( "—" )} {row.deb ? ( = 80 ? "bg-green-500/15 text-green-400" : row.deb.hit_rate >= 60 ? "bg-yellow-500/15 text-yellow-400" : "bg-red-500/15 text-red-400" }`}> {row.deb.hit_rate.toFixed(0)}% ) : ( )} {row.deb ? `${row.deb.mae.toFixed(1)}°` : "—"} {row.deb ? row.deb.total_days : "—"} {row.mu ? ( = 80 ? "bg-green-500/15 text-green-400" : row.mu.hit_rate >= 60 ? "bg-yellow-500/15 text-yellow-400" : "bg-red-500/15 text-red-400" }`}> {row.mu.hit_rate.toFixed(0)}% ) : ( )} {row.mu ? `${row.mu.mae.toFixed(1)}°` : "—"} {row.mu && row.mu.brier_score !== null ? ( {row.mu.brier_score.toFixed(3)} ) : ( "—" )} {row.mu ? row.mu.total_days : "—"}
无有效准确率记录
真值历史浏览

按城市和日期筛选查看历史真值记录。

打开真值历史 →
); }