- tsconfig 排除 *.test.ts(生产构建不编译测试) - OutcomePanel TeamSideTag 导入路径 ../../ → ../../../ - AgentsPanel/OutcomePanel/ReasoningPanel types 导入 ../../ → ../ - MatchPredictPanel JSDoc 注释 **. → /** (P3-1 编辑损坏) 前端构建成功,容器 healthy。
127 lines
4.8 KiB
TypeScript
127 lines
4.8 KiB
TypeScript
/**
|
|
* OutcomePanel: 预测主结论 —— 比分 / 胜平负 / 置信度 / 成本。
|
|
*
|
|
* P3-1: 从 MatchPredictPanel.PredictionPanel 拆出,渲染逻辑原样搬迁。
|
|
*/
|
|
import TeamSideTag from '../../../components/TeamSideTag'
|
|
import type { Match, Prediction } from '../types'
|
|
import { OUTCOME_LABEL } from '../types'
|
|
|
|
/** 置信度细线:0~1 数值的低调可视化 */
|
|
function Meter({ value }: { value: number }) {
|
|
const pct = Math.max(0, Math.min(100, Math.round(value * 100)))
|
|
return (
|
|
<div className="h-px w-full bg-ink-200" role="presentation">
|
|
<div className="h-px bg-press transition-[width] duration-500" style={{ width: `${pct}%` }} />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
/** 胜平负一行文字:选中的红字加方块标记,未选中的退灰 */
|
|
function OutcomeLine({
|
|
pick,
|
|
confidence,
|
|
}: {
|
|
pick: string | null
|
|
confidence: number | null
|
|
}) {
|
|
const options = ['1', 'X', '2'] as const
|
|
return (
|
|
<div>
|
|
<div className="flex items-baseline justify-center gap-6 sm:gap-10">
|
|
{options.map(o => {
|
|
const on = pick === o
|
|
return (
|
|
<div key={o} className="flex flex-col items-center gap-1">
|
|
<span className={`flex items-center gap-1.5 text-sm ${on ? 'font-semibold text-press' : 'text-ink-400'}`}>
|
|
{on && <span className="inline-block h-2 w-2 bg-press" aria-hidden="true" />}
|
|
{OUTCOME_LABEL[o]}
|
|
</span>
|
|
{on && confidence !== null && (
|
|
<span className="text-2xs tabular-nums text-ink-500">
|
|
置信 {Math.round(confidence * 100)}%
|
|
</span>
|
|
)}
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
{pick && confidence !== null && (
|
|
<div className="mx-auto mt-3 max-w-xs">
|
|
<Meter value={confidence} />
|
|
<p className="mt-1 text-center text-2xs text-ink-400">主观置信度,非统计概率</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
/** 预测成本展示:耗时 + token + 限流余量 */
|
|
function PredictionCost({ prediction }: { prediction: Prediction }) {
|
|
const latency = prediction.latency_ms != null ? `${(prediction.latency_ms / 1000).toFixed(1)}s` : null
|
|
const tokens = prediction.prompt_tokens != null || prediction.completion_tokens != null
|
|
? `${prediction.prompt_tokens ?? '?'}/${prediction.completion_tokens ?? '?'}`
|
|
: null
|
|
|
|
if (!latency && !tokens && prediction.rate_limit_remaining == null) return null
|
|
|
|
return (
|
|
<div className="border-t border-ink-200 pt-3 text-2xs text-ink-500">
|
|
<div className="flex flex-wrap items-center justify-center gap-x-4 gap-y-1">
|
|
{latency && (
|
|
<span className="inline-flex items-center gap-1">
|
|
<span aria-hidden="true" className="opacity-60">⏱</span>耗时 {latency}
|
|
</span>
|
|
)}
|
|
{tokens && (
|
|
<span className="inline-flex items-center gap-1">
|
|
<span aria-hidden="true" className="opacity-60">Tok</span>prompt/completion: {tokens}
|
|
</span>
|
|
)}
|
|
{prediction.rate_limit_remaining != null && prediction.rate_limit_remaining <= 3 && (
|
|
<span className="text-press" title="每分钟最多 10 次预测">
|
|
剩余配额: {prediction.rate_limit_remaining}/10(分钟)
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function OutcomePanel({ prediction, match }: { prediction: Prediction; match: Match }) {
|
|
const homeName = match.home_team_zh || match.home_team
|
|
const awayName = match.away_team_zh || match.away_team
|
|
const degraded = prediction.status === 'degraded' || prediction.status === 'failed'
|
|
|
|
return (
|
|
<>
|
|
{!degraded && (
|
|
<div className="text-center">
|
|
<p className="font-serif text-5xl font-bold tabular-nums leading-none text-ink-900 sm:text-6xl">
|
|
{prediction.pred_home_goals ?? '-'}
|
|
<span className="mx-3 font-normal text-ink-300">:</span>
|
|
{prediction.pred_away_goals ?? '-'}
|
|
</p>
|
|
<p className="mt-3 text-2xs tracking-[0.5em] text-ink-400">预测比分</p>
|
|
{prediction.alt_pred_home_goals != null && prediction.alt_pred_away_goals != null && (
|
|
<p className="mt-2 text-2xs tabular-nums text-ink-400">
|
|
备选{' '}
|
|
<span className="font-serif text-sm font-bold tabular-nums text-ink-600">
|
|
{prediction.alt_pred_home_goals}<span className="mx-0.5 font-normal text-ink-300">:</span>{prediction.alt_pred_away_goals}
|
|
</span>
|
|
</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{!degraded && (
|
|
<div className="border-y border-ink-200 py-4">
|
|
<OutcomeLine pick={prediction.pred_1x2} confidence={prediction.subjective_confidence} />
|
|
</div>
|
|
)}
|
|
|
|
{!degraded && <PredictionCost prediction={prediction} />}
|
|
</>
|
|
)
|
|
}
|