import { Fragment, useEffect, useState, type ReactNode } from "react"; import { adminEvals, type EvalDay, type EvalSummary, type PoorEval } from "../api"; // 自动评测观测:真数据(来自 sundynix_eval,评测经 JetStream eval 流持久落库)。 // 质量趋势 + 计数总览 + 错题本(低分评测 + 评语 + 纠偏标记)。 // 注:纠偏前后全文轨迹后端未持久化,错题本展示评语(Reason)与「已纠偏」标记,不含 before/after 对照。 const pct = (v: number) => `${Math.round(v * 100)}%`; const LEVEL_BADGE: Record = { poor: "bg-rose-50 text-rose-600", warn: "bg-amber-50 text-amber-600", ok: "bg-emerald-50 text-emerald-600" }; const LEVEL_LABEL: Record = { poor: "低分", warn: "警告", ok: "合格" }; const mmdd = (ymd: string) => (ymd.length === 8 ? `${ymd.slice(4, 6)}-${ymd.slice(6, 8)}` : ymd); export function EvalsPage() { const [days, setDays] = useState(14); const [trend, setTrend] = useState([]); const [summary, setSummary] = useState(null); const [poor, setPoor] = useState([]); const [open, setOpen] = useState(null); // 展开评语的 task_id const [loading, setLoading] = useState(true); const [err, setErr] = useState(""); useEffect(() => { setLoading(true); adminEvals(days) .then((r) => { setTrend(r.trend); setSummary(r.summary); setPoor(r.poor); setErr(""); }) .catch((e) => setErr((e as Error).message)) .finally(() => setLoading(false)); }, [days]); if (loading) return
加载评测数据中…
; if (err) return
评测加载失败:{err}
; const s = summary!; const correctRate = s.poor + s.warn > 0 ? s.corrected / (s.poor + s.warn) : 0; return (

自动评测

质量趋势 · 忠实度 · 低分错题本与纠偏成效(全平台真实评测)

{[7, 14, 30].map((d) => ( ))}
{/* 总览计数 */}
{/* 质量 & 忠实度趋势 */}

质量与忠实度趋势

综合分 忠实度 低分数
{/* 错题本 */}

错题本(低分/警告评测)

点行展开评语;「已纠偏」= 自动重生成后采纳
{poor.length === 0 ? (
区间内没有低分评测 —— 质量良好
) : (
{poor.map((r) => ( setOpen(open === r.task_id ? null : r.task_id)} className="cursor-pointer border-b border-gray-50 hover:bg-gray-50"> {open === r.task_id && ( )} ))}
时间 任务 / 租户 综合 规则 质量 忠实 等级
{r.created_at}
{r.task_id}
{r.tenant_name || "—"}
{pct(r.overall)} {pct(r.rule)} {pct(r.llm)} {r.sources > 0 ? pct(r.faithful) : "—"}
{LEVEL_LABEL[r.level] ?? r.level} {r.corrected && 已纠偏}
评测评语

{r.reason || "(无评语)"}

检索来源 {r.sources} 处 · 提交者 {r.owner || "—"}
)}
); } const TONE: Record = { violet: "text-violet-600", emerald: "text-emerald-600", rose: "text-rose-500", cyan: "text-cyan-600" }; function Stat({ label, value, sub, tone }: { label: string; value: ReactNode; sub?: ReactNode; tone: string }) { return (
{label}
{value}
{sub &&
{sub}
}
); } // TrendChart:综合分/忠实度折线(左轴 [0,1])+ 低分条数背景条。纯 SVG,无依赖。 function TrendChart({ trend }: { trend: EvalDay[] }) { if (trend.length === 0) return
区间内暂无评测
; const w = 720, h = 160, pad = 24; const n = trend.length; const x = (i: number) => pad + (n === 1 ? (w - 2 * pad) / 2 : (i * (w - 2 * pad)) / (n - 1)); const y = (v: number) => h - pad - v * (h - 2 * pad); const maxPoor = Math.max(1, ...trend.map((d) => d.poor_count)); const line = (get: (d: EvalDay) => number) => trend.map((d, i) => `${i === 0 ? "M" : "L"}${x(i).toFixed(1)},${y(get(d)).toFixed(1)}`).join(" "); return (
{[0, 0.5, 1].map((g) => ( {g} ))} {/* 低分条数背景条 */} {trend.map((d, i) => ( ))} d.avg_overall)} fill="none" stroke="#7c3aed" strokeWidth={2} /> d.avg_faithful || 0)} fill="none" stroke="#06b6d4" strokeWidth={2} strokeDasharray="3 2" /> {trend.map((d, i) => ( {mmdd(d.day)} ))}
); }