import { useState, type CSSProperties } from 'react' import { Alert, Button, Select, Space, Spin, Table, Tag, Tooltip } from 'antd' import { type CampaignComparison, type CampaignListItem, type MetricDeltaPair, type MetricDiff, type ModelConfig, } from '../api' import { shortDateTime } from '../utils/date' import { colors, statusColors } from '../tokens' const TREND_META: Record = { improving: { label: '改善', color: 'green' }, stable: { label: '平稳', color: 'blue' }, regressing: { label: '退化', color: 'red' }, } const EVOLUTION_META: Record = { new: { label: '新增', color: 'red' }, persisting: { label: '持续', color: 'orange' }, resolved: { label: '消解', color: 'green' }, } const TRACKING_META: Record = { addressed: { label: '已落实', color: 'green' }, partial: { label: '部分落实', color: 'orange' }, unaddressed: { label: '未落实', color: 'default' }, new: { label: '新增', color: 'blue' }, } type MetricKey = 'pass_rate' | 'availability' | 'avg_latency_ms' const fmtPct = (v: number) => `${(v * 100).toFixed(1)}%` const signed = (d: number, body: string) => `${d > 0 ? '+' : ''}${body}` /** Single metric meta table: label, formatting and which direction is good. */ const METRICS: { key: MetricKey label: string goodDirection: 'up' | 'down' fmt: (v: number) => string fmtDelta: (d: number) => string }[] = [ { key: 'pass_rate', label: '通过率', goodDirection: 'up', fmt: fmtPct, fmtDelta: (d) => signed(d, `${(d * 100).toFixed(1)}pp`) }, { key: 'availability', label: '可用性', goodDirection: 'up', fmt: fmtPct, fmtDelta: (d) => signed(d, `${(d * 100).toFixed(1)}pp`) }, { key: 'avg_latency_ms', label: '平均时延', goodDirection: 'down', fmt: (v) => `${Math.round(v)}ms`, fmtDelta: (d) => signed(d, `${Math.round(d * 10) / 10}ms`) }, ] const boxStyle: CSSProperties = { border: `1px solid ${colors.border}`, borderRadius: 8, padding: '12px 14px', marginBottom: 16, } interface Props { comparison: CampaignComparison | null /** 活动已终态(按钮门控之一) */ terminal: boolean /** 当前活动智能分析已 completed(对比前提) */ analysisCompleted: boolean targetId: string currentCampaignId: string campaigns: CampaignListItem[] scenarioNames: Record modelConfigs: ModelConfig[] onGenerate: (baselineCampaignId?: string) => Promise } export default function PeriodComparisonSection({ comparison, terminal, analysisCompleted, targetId, currentCampaignId, campaigns, scenarioNames, modelConfigs, onGenerate, }: Props) { const [busy, setBusy] = useState(false) const [baselineChoice, setBaselineChoice] = useState(undefined) const generate = async (baselineCampaignId?: string) => { setBusy(true) try { await onGenerate(baselineCampaignId) } finally { setBusy(false) } } const baselineCandidates = campaigns .filter((c) => c.target_id === targetId && c.id !== currentCampaignId && c.completed_at != null && !(c.status === 'planned' || c.status === 'running')) .slice() .sort((a, b) => (b.completed_at ?? '').localeCompare(a.completed_at ?? '')) const button = (label: string, baselineCampaignId?: string, requireBaseline = false) => ( ) const metricDiffTable = (diff: MetricDiff) => { const rows = [ { key: 'overall', name: '整窗(总体)', ...diff.overall }, ...diff.scenarios.map((s) => ({ key: s.scenario_id, name: s.scenario_name || scenarioNames[s.scenario_id] || s.scenario_id.slice(0, 8), pass_rate: s.pass_rate, availability: s.availability, avg_latency_ms: s.avg_latency_ms, })), ] const renderPair = (metric: typeof METRICS[number]) => (_: unknown, r: typeof rows[number]) => { const pair: MetricDeltaPair = r[metric.key] const good = metric.goodDirection === 'down' ? (pair.delta ?? 0) < 0 : (pair.delta ?? 0) > 0 const deltaColor = pair.delta == null || pair.delta === 0 ? colors.textMuted : good ? statusColors.completed : statusColors.failed return ( {pair.baseline == null ? '—' : metric.fmt(pair.baseline)} {pair.current == null ? '—' : metric.fmt(pair.current)} {pair.delta == null ? '—' : metric.fmtDelta(pair.delta)} ) } return ( ({ title: m.label, key: m.key, render: renderPair(m) })), ]} style={{ marginBottom: 12 }} /> ) } const status = comparison?.status ?? 'none' if (status === 'generating') { return (
正在生成周期对比叙述,通常需要几十秒…
) } if (status === 'failed') { return (
) } const entry = comparison?.comparison ?? null if (!entry || !entry.result) { const autoBaseline = comparison?.auto_baseline ?? null return (
{autoBaseline ? <>已找到自动基线「{autoBaseline.name}」({shortDateTime(autoBaseline.completed_at)}),可生成对比叙述。 : '未找到自动基线(需同计划、正式线、更早完成且已有智能分析的历史活动)。可手动选择基线。'}
{autoBaseline ? button('生成对比') : ( <>