/** * AgentsPanel: 五路专家意见 —— 可折叠 + 状态摘要 + 权重条形图 + 单路详情。 * * P3-1: 从 MatchPredictPanel.PredictionPanel 拆出,渲染逻辑原样搬迁。 */ import { useState } from 'react' import type { AgentReport, Prediction } from '../types' import { AGENT_LABELS, CN_NUM } from '../types' const STATUS_BADGE: Record = { ok: { label: '正常', cls: 'text-ink-500' }, no_data: { label: '无数据', cls: 'text-ink-400' }, error: { label: '调用失败', cls: 'text-press' }, parse_error: { label: '解析失败', cls: 'text-press' }, } const SUFFICIENCY_LABEL: Record = { high: '充分', medium: '一般', low: '偏少', none: '无', } /** home_edge(-1~1,正=利主队)的可视化:以中线为原点的双向细条 */ function EdgeBar({ value }: { value: number }) { const v = Math.max(-1, Math.min(1, value)) const half = Math.abs(v) * 50 return (
= 0 ? 'bg-press' : 'bg-ink-600'}`} style={ v >= 0 ? { left: '50%', width: `${half}%` } : { right: '50%', width: `${half}%` } } />
) } /** 单路专家意见:汉字编号 + 细线行 */ function AgentCard({ report: r, no }: { report: AgentReport; no: string }) { const badge = STATUS_BADGE[r.status] ?? { label: r.status, cls: 'text-ink-400' } const inactive = r.status !== 'ok' return (
{no} {AGENT_LABELS[r.agent] ?? r.agent} {badge.label} {r.status === 'ok' && r.subjective_confidence !== null && ( 信心 {Math.round(r.subjective_confidence * 100)}% )} {r.status === 'ok' && r.probable_score && ( {r.probable_score} )}
{inactive && (

{r.status === 'no_data' && '该维度没有可用数据,已跳过 LLM 分析以节省额度(不影响其他专家)。'} {r.status === 'error' && '该专家调用失败,本次结论未纳入其视角(fail-open 设计,不阻断整体预测)。'} {r.status === 'parse_error' && '模型输出未通过格式校验,该报告已丢弃。'}

)} {!inactive && r.home_edge !== null && (
主队优势 0 ? 'text-press' : r.home_edge < 0 ? 'text-ink-700' : 'text-ink-500'}`}> {r.home_edge > 0 ? '+' : ''}{r.home_edge.toFixed(2)}
利客队 利主队
)} {r.analysis && (

{r.analysis}

)} {r.key_evidence.length > 0 && (
    {r.key_evidence.map((e, i) => (
  • {e}
  • ))}
)} {r.exp_home_goals !== null && r.exp_away_goals !== null && (

进球期望 {r.exp_home_goals.toFixed(1)} - {r.exp_away_goals.toFixed(1)}

)} {!inactive && (

数据充分度 {SUFFICIENCY_LABEL[r.data_sufficiency] ?? r.data_sufficiency} | {r.model} {r.latency_ms !== null && {r.latency_ms}ms}

)}
) } export function AgentsPanel({ prediction }: { prediction: Prediction }) { const [expertsOpen, setExpertsOpen] = useState(false) const degraded = prediction.status === 'degraded' || prediction.status === 'failed' const reports = prediction.agent_outputs ?? [] const okReports = reports.filter(r => r.status === 'ok') if (reports.length === 0) return null return (
{!degraded && prediction.agent_weights && Object.keys(prediction.agent_weights).length > 0 && (
终裁权重分布 {Object.entries(prediction.agent_weights) .sort((a, b) => b[1] - a[1]) .map(([k, v]) => (
{AGENT_LABELS[k] ?? k}
{Math.round(v * 100)}%
))}
)} {expertsOpen && (
{reports.map((r, i) => ( ))}
)}
) }