Jingrui77's picture
deploy: sync BayesScenParams Agent (2026-05-17T19:20:58Z)
b40eedf
Raw
History Blame Contribute Delete
25.3 kB
import { useMemo, useState } from "react";
import {
Activity,
AlertTriangle,
BarChart3,
CheckCircle2,
Database,
Download,
FileText,
GitCompare,
LineChart,
ShieldCheck,
Sparkles,
Users,
} from "lucide-react";
import { Card, CardHeader, Stat } from "@/components/ui/Card";
import { Badge } from "@/components/ui/Badge";
import { Button } from "@/components/ui/Button";
import { PriorPosteriorChart } from "@/components/charts/PriorPosteriorChart";
import { KDEChart } from "@/components/charts/KDEChart";
import { SensitivityChart } from "@/components/charts/SensitivityChart";
import { CorrelationHeatmap } from "@/components/charts/CorrelationHeatmap";
import { ExpertsRadarChart } from "@/components/charts/ExpertsRadarChart";
import { ForestPlot } from "@/components/charts/ForestPlot";
import { HistogramChart } from "@/components/charts/HistogramChart";
import { MarkdownReport } from "@/components/bayesian/MarkdownReport";
import { ManualOverride } from "@/components/bayesian/ManualOverride";
import { triggerDownload } from "@/lib/download";
import type {
ArtifactEvent,
AutoPipelineSummary,
BatchParametersSummary,
CorrelationMatrixSummary,
DataQualitySummary,
DatasetSummary,
ExpertDisagreementsSummary,
FusedPosteriorSummary,
KDEPoint,
KendallWSummary,
NormalityTestSummary,
OutliersSummary,
PosteriorSummary,
PriorSummary,
ReportGeneratedSummary,
ScenarioComparisonSummary,
SensitivitySummary,
} from "@/lib/types";
interface Props {
artifacts: ArtifactEvent[];
}
interface LatestState {
dataset?: DatasetSummary;
datasetValues?: number[];
prior?: PriorSummary;
kde?: KDEPoint[];
posterior?: PosteriorSummary;
sensitivity?: SensitivitySummary;
outliers?: OutliersSummary;
normality?: NormalityTestSummary;
dataQuality?: DataQualitySummary;
autoPipeline?: AutoPipelineSummary;
fusedPosterior?: FusedPosteriorSummary;
fusedExperts?: Array<{ name: string; judgments: number[] }>;
kendallW?: KendallWSummary;
disagreements?: ExpertDisagreementsSummary;
batch?: BatchParametersSummary;
correlation?: CorrelationMatrixSummary;
scenarioCompare?: ScenarioComparisonSummary;
report?: ReportGeneratedSummary;
reportMarkdown?: string;
}
function reduceArtifacts(arts: ArtifactEvent[]): LatestState {
const s: LatestState = {};
for (const a of arts) {
switch (a.type) {
case "dataset_loaded":
s.dataset = a.summary;
if (a.values && a.values.length > 0) s.datasetValues = a.values;
break;
case "prior_built":
s.prior = a.summary;
s.kde = a.kde;
break;
case "posterior_computed":
s.posterior = a.summary;
break;
case "sensitivity":
s.sensitivity = a.summary;
break;
case "outliers":
s.outliers = a.summary;
break;
case "normality_test":
s.normality = a.summary;
break;
case "data_quality":
s.dataQuality = a.summary;
break;
case "auto_pipeline":
s.autoPipeline = a.summary;
break;
case "fused_posterior":
s.fusedPosterior = a.summary;
// also reconstruct an "experts" array for the radar chart
// (we don't have individual judgments in the summary unless disagreements artifact came)
break;
case "kendall_w":
s.kendallW = a.summary;
break;
case "expert_disagreements":
s.disagreements = a.summary;
s.fusedExperts = a.summary.levels[0]?.individual_judgments.map((j) => ({
name: j.expert,
judgments: a.summary.levels.map((lv) =>
lv.individual_judgments.find((x) => x.expert === j.expert)?.judgment_idx ?? 2
),
}));
break;
case "batch_parameters":
s.batch = a.summary;
break;
case "correlation_matrix":
s.correlation = a.summary;
break;
case "scenario_comparison":
s.scenarioCompare = a.summary;
break;
case "report_generated":
s.report = a.summary;
s.reportMarkdown = a.markdown;
break;
}
}
return s;
}
const LEVEL_LABELS_ZH = ["极低", "低", "中等", "高", "极高"];
const JUDGMENT_LABELS_ZH = ["极不可能", "不太可能", "难以判断", "比较可能", "极有可能"];
export function Workspace({ artifacts }: Props) {
const st = useMemo(() => reduceArtifacts(artifacts), [artifacts]);
const [reportExpanded, setReportExpanded] = useState(true);
const [manualResult, setManualResult] = useState<{ mean: number; std: number } | null>(null);
const isEmpty = Object.values(st).every((v) => v === undefined);
if (isEmpty) {
return (
<div className="thin-scroll flex h-full flex-col items-center justify-center gap-3 p-8 text-center">
<div className="flex size-12 items-center justify-center rounded-full border border-white/5 bg-white/[0.02]">
<BarChart3 className="size-5 text-[color:var(--color-text-tertiary)]" />
</div>
<h3 className="text-[14px] font-medium text-[color:var(--color-text-secondary)]">
工作台
</h3>
<p className="max-w-[280px] text-[12px] leading-5 text-[color:var(--color-text-tertiary)]">
当 Agent 拉数据、做预处理、构建先验、计算后验、对比情景或生成报告时,对应图表和摘要会实时显示在这里。
</p>
</div>
);
}
const labels = st.prior?.level_labels_zh ?? LEVEL_LABELS_ZH;
const unit = st.dataset?.unit;
const downloadHandler = async (url: string, filename: string) => {
try {
await triggerDownload(url, filename);
} catch (e) {
alert(`下载失败: ${(e as Error).message}\n\n如果浏览器仍未触发下载,可在新标签页打开链接:${url}`);
}
};
return (
<div className="thin-scroll flex h-full flex-col gap-3 overflow-y-auto p-4">
{/* ============ Report (top, includes inline preview) ============ */}
{st.report && (
<Card>
<CardHeader
title={
<span className="flex items-center gap-1.5">
<FileText className="size-3.5 text-[color:var(--color-accent)]" />
分析报告 — {st.report.scenario_name}
</span>
}
subtitle={`${st.report.markdown_chars} 字符 Markdown · 三种格式可下载`}
right={<Badge tone="green">就绪</Badge>}
/>
<div className="grid grid-cols-3 gap-2">
{(["docx", "pdf", "html"] as const).map((kind) => {
const info = st.report!.downloads[kind];
return (
<Button
key={kind}
variant="secondary"
size="md"
className="w-full"
onClick={() => downloadHandler(info.url, info.filename)}
>
<Download className="size-4" />
{kind.toUpperCase()}
<span className="ml-1 text-[10px] opacity-60">
{(info.size / 1024).toFixed(1)}K
</span>
</Button>
);
})}
</div>
{st.reportMarkdown && (
<div className="mt-3 rounded-lg border border-white/8 bg-white/[0.015]">
<button
onClick={() => setReportExpanded((v) => !v)}
className="flex w-full items-center justify-between px-3 py-2 text-[11.5px] text-[color:var(--color-text-tertiary)] hover:bg-white/[0.04]"
>
<span>报告预览(点击{reportExpanded ? "收起" : "展开"})</span>
<span>{reportExpanded ? "▾" : "▸"}</span>
</button>
{reportExpanded && (
<div className="thin-scroll max-h-[600px] overflow-y-auto border-t border-white/5 px-4 py-3">
<MarkdownReport markdown={st.reportMarkdown} />
</div>
)}
</div>
)}
</Card>
)}
{/* ============ Dataset ============ */}
{st.dataset && (
<Card>
<CardHeader
title={
<span className="flex items-center gap-1.5">
<Database className="size-3.5 text-[color:var(--color-primary)]" />
{st.dataset.name || st.dataset.indicator_name || st.dataset.variable_name || "Dataset"}
</span>
}
subtitle={
<span className="flex flex-wrap gap-x-3 gap-y-0.5">
{st.dataset.source && <span>来源 · {st.dataset.source}</span>}
{st.dataset.period && <span>{st.dataset.period}</span>}
{st.dataset.unit && <span>{st.dataset.unit}</span>}
</span>
}
right={<Badge tone="blue">n = {st.dataset.n ?? st.dataset.n_used ?? "-"}</Badge>}
/>
{(st.dataset.min !== undefined || st.dataset.max !== undefined) && (
<div className="grid grid-cols-2 gap-2 sm:grid-cols-4">
<Stat label="MIN" value={st.dataset.min?.toFixed(2) ?? "—"} />
<Stat label="MAX" value={st.dataset.max?.toFixed(2) ?? "—"} />
<Stat label="N" value={st.dataset.n ?? st.dataset.n_used ?? "—"} />
<Stat label="UNIT" value={st.dataset.unit || "—"} />
</div>
)}
{/* Inline histogram of raw data */}
{st.datasetValues && st.datasetValues.length >= 8 && (
<div className="mt-3">
<div className="mb-1 text-[11px] uppercase tracking-wider text-[color:var(--color-text-tertiary)]">
原始数据分布(直方图 + KDE)
</div>
<HistogramChart
values={st.datasetValues}
kde={st.kde}
quantileValues={st.prior?.quantile_values}
unit={unit}
/>
</div>
)}
</Card>
)}
{/* ============ Data quality score ============ */}
{st.dataQuality && (
<Card>
<CardHeader
title={
<span className="flex items-center gap-1.5">
<ShieldCheck className="size-3.5 text-[color:var(--color-accent)]" />
数据质量评分(专利 §2.8.7)
</span>
}
right={
<Badge tone={st.dataQuality.score >= 75 ? "green" : st.dataQuality.score >= 60 ? "amber" : "red"}>
{st.dataQuality.score} / 100 · {st.dataQuality.grade}
</Badge>
}
/>
<div className="grid grid-cols-2 gap-2 sm:grid-cols-4">
<Stat label="N" value={st.dataQuality.n} />
<Stat label="MEAN" value={st.dataQuality.mean.toFixed(3)} />
<Stat label="STD" value={st.dataQuality.std.toFixed(3)} />
<Stat label="OUTLIERS" value={`${st.dataQuality.outlier_count_iqr} (${st.dataQuality.outlier_percentage_iqr.toFixed(1)}%)`} />
</div>
{st.dataQuality.issues.length > 0 && (
<div className="mt-3 rounded-md border border-[color:var(--color-warning)]/30 bg-[color:var(--color-warning)]/10 p-2.5 text-[11.5px] text-[color:var(--color-warning)]">
<div className="mb-1 flex items-center gap-1.5 font-semibold">
<AlertTriangle className="size-3.5" /> 发现问题
</div>
<ul className="ml-3 list-disc space-y-0.5">
{st.dataQuality.issues.map((s, i) => <li key={i}>{s}</li>)}
</ul>
</div>
)}
</Card>
)}
{/* ============ Auto preprocessing pipeline audit ============ */}
{st.autoPipeline && (
<Card>
<CardHeader
title={
<span className="flex items-center gap-1.5">
<Activity className="size-3.5 text-[color:var(--color-warning)]" />
自动预处理决策管线(专利 §2.8.8)
</span>
}
subtitle={`推荐先验方法:${st.autoPipeline.recommended_prior_method} · 质量分 ${st.autoPipeline.quality_score}/100`}
right={st.autoPipeline.winsorization_applied ? <Badge tone="amber">已 Winsorize</Badge> : <Badge tone="gray">原始数据</Badge>}
/>
<div className="space-y-1 text-[12px]">
{st.autoPipeline.audit_trail.map((step, i) => (
<div key={i} className="flex items-start gap-2 rounded-md border border-white/5 bg-white/[0.02] px-2.5 py-1.5">
<CheckCircle2 className="mt-0.5 size-3 shrink-0 text-[color:var(--color-accent)]" />
<div className="flex-1">
<span className="font-mono text-[10.5px] text-[color:var(--color-text-tertiary)]">
step {String(step.step)} · {String(step.name)}
</span>
</div>
</div>
))}
</div>
<div className="mt-3 text-[12px] text-[color:var(--color-text-secondary)]">
👉 {st.autoPipeline.next_step}
</div>
</Card>
)}
{/* ============ Outliers ============ */}
{st.outliers && !st.autoPipeline && (
<Card>
<CardHeader
title={<span className="flex items-center gap-1.5"><AlertTriangle className="size-3.5 text-[color:var(--color-warning)]" /> 异常值检测</span>}
subtitle={`方法:${st.outliers.method} · 边界 [${st.outliers.lower_bound.toFixed(2)}, ${st.outliers.upper_bound.toFixed(2)}]`}
right={<Badge tone={st.outliers.percentage > 10 ? "red" : st.outliers.count > 0 ? "amber" : "green"}>
{st.outliers.count} 个 ({st.outliers.percentage.toFixed(1)}%)
</Badge>}
/>
</Card>
)}
{/* ============ Normality test ============ */}
{st.normality && (
<Card>
<CardHeader
title={<span className="flex items-center gap-1.5"><LineChart className="size-3.5 text-[color:var(--color-primary)]" /> 正态性检验:{st.normality.test}</span>}
right={<Badge tone={st.normality.is_normal ? "green" : "amber"}>{st.normality.is_normal ? "近似正态" : "偏离正态"}</Badge>}
/>
<div className="grid grid-cols-3 gap-2">
<Stat label="STATISTIC" value={st.normality.statistic.toFixed(3)} />
<Stat label="P-VALUE" value={st.normality.p_value.toExponential(2)} />
<Stat label="N" value={st.normality.n} />
</div>
<p className="mt-2 text-[12px] text-[color:var(--color-text-secondary)]">{st.normality.interpretation}</p>
</Card>
)}
{/* ============ Prior KDE ============ */}
{st.prior && st.kde && (
<Card>
<CardHeader
title={<span className="flex items-center gap-1.5"><Sparkles className="size-3.5 text-[color:var(--color-primary)]" /> 先验分布(KDE + 5 分位点)</span>}
subtitle={`mean = ${st.prior.mean.toFixed(3)} · std = ${st.prior.std.toFixed(3)} · skew = ${st.prior.skewness.toFixed(2)} · kurt = ${st.prior.kurtosis.toFixed(2)}`}
/>
<KDEChart points={st.kde} quantileValues={st.prior.quantile_values} quantileLabels={labels} unit={unit} />
</Card>
)}
{/* ============ Posterior (with manual override) ============ */}
{st.posterior && st.prior && (
<Card>
<CardHeader
title={<span className="flex items-center gap-1.5"><BarChart3 className="size-3.5 text-[color:var(--color-accent)]" /> 先验 vs 后验</span>}
subtitle={`R = ${st.posterior.R} · ${st.posterior.judgments.map((j, i) => `${labels[i]}: ${JUDGMENT_LABELS_ZH[j]}`).join(" · ")}`}
right={<Badge tone="green">单专家</Badge>}
/>
<PriorPosteriorChart labels={labels} prior={st.prior.prior_weights} posterior={st.posterior.posterior_weights} quantileValues={st.prior.quantile_values} />
<div className="mt-3 grid grid-cols-2 gap-2 sm:grid-cols-4">
<Stat
label="后验均值"
value={(manualResult?.mean ?? st.posterior.posterior_mean).toFixed(3)}
hint={manualResult ? `手动调参中` : `Δ ${st.posterior.shift_mean >= 0 ? "+" : ""}${st.posterior.shift_mean.toFixed(3)}`}
tone={st.posterior.shift_mean >= 0 ? "accent" : "warn"}
/>
<Stat label="后验中位数" value={st.posterior.posterior_median.toFixed(3)} />
<Stat
label="后验标准差"
value={(manualResult?.std ?? st.posterior.posterior_std).toFixed(3)}
hint={`收窄 ${st.posterior.shrink_std_pct.toFixed(1)}%`}
/>
<Stat label="95% CI" value={`[${st.posterior.posterior_ci95[0].toFixed(2)}, ${st.posterior.posterior_ci95[1].toFixed(2)}]`} />
</div>
{st.posterior.judgment_rationale && (
<div className="mt-3 rounded-lg border border-white/5 bg-white/[0.02] p-3 text-[12.5px] leading-relaxed text-[color:var(--color-text-secondary)]">
<div className="mb-1 text-[10.5px] uppercase tracking-wider text-[color:var(--color-text-tertiary)]">Agent 的判断理由</div>
{st.posterior.judgment_rationale}
</div>
)}
{/* Manual override — runs locally, no Agent needed */}
{st.datasetValues && (
<ManualOverride
data={st.datasetValues}
initialJudgments={st.posterior.judgments}
initialR={st.posterior.R}
levelLabels={labels}
onResult={(r) =>
setManualResult({ mean: r.posterior.stats.mean, std: r.posterior.stats.std })
}
/>
)}
</Card>
)}
{/* ============ Multi-expert fused posterior ============ */}
{st.fusedPosterior && st.prior && (
<Card>
<CardHeader
title={<span className="flex items-center gap-1.5"><Users className="size-3.5 text-[color:var(--color-primary)]" /> 多专家融合后验({st.fusedPosterior.method} · n={st.fusedPosterior.n_experts})</span>}
subtitle={`专家:${st.fusedPosterior.expert_names.join(" · ")}`}
right={st.fusedPosterior.panel_agreement_index !== undefined && (
<Badge tone={st.fusedPosterior.panel_agreement_index > 0.6 ? "green" : "amber"}>
Panel Agreement {st.fusedPosterior.panel_agreement_index.toFixed(2)}
</Badge>
)}
/>
<PriorPosteriorChart labels={labels} prior={st.prior.prior_weights} posterior={st.fusedPosterior.posterior_weights} quantileValues={st.prior.quantile_values} />
<div className="mt-3 grid grid-cols-3 gap-2">
<Stat label="后验均值" value={st.fusedPosterior.posterior_mean.toFixed(3)} />
<Stat label="后验标准差" value={st.fusedPosterior.posterior_std.toFixed(3)} />
<Stat label="95% CI" value={`[${st.fusedPosterior.posterior_ci95[0].toFixed(2)}, ${st.fusedPosterior.posterior_ci95[1].toFixed(2)}]`} />
</div>
</Card>
)}
{/* ============ Experts Radar Chart ============ */}
{st.fusedExperts && st.fusedExperts.length >= 2 && (
<Card>
<CardHeader
title={<span className="flex items-center gap-1.5"><Users className="size-3.5 text-[color:var(--color-primary)]" /> 专家判断雷达图</span>}
subtitle="每位专家在 5 个分位水平上的判断轮廓"
/>
<ExpertsRadarChart levelLabels={labels} experts={st.fusedExperts} />
</Card>
)}
{/* ============ Kendall's W ============ */}
{st.kendallW && (
<Card>
<CardHeader
title={<span className="flex items-center gap-1.5"><Users className="size-3.5 text-[color:var(--color-warning)]" /> Kendall's W 一致性</span>}
right={<Badge tone={st.kendallW.W >= 0.7 ? "green" : st.kendallW.W >= 0.5 ? "amber" : "red"}>{st.kendallW.interpretation}</Badge>}
/>
<div className="grid grid-cols-3 gap-2">
<Stat label="W" value={st.kendallW.W.toFixed(3)} />
<Stat label="χ²" value={st.kendallW.chi_squared.toFixed(2)} />
<Stat label="p-value" value={st.kendallW.p_value.toExponential(2)} hint={st.kendallW.significant_at_0_05 ? "显著 (p<0.05)" : "不显著"} />
</div>
</Card>
)}
{/* ============ Expert disagreements ============ */}
{st.disagreements && (
<Card>
<CardHeader
title={<span className="flex items-center gap-1.5"><Users className="size-3.5 text-[color:var(--color-warning)]" /> 专家分歧水平</span>}
right={<Badge tone={st.disagreements.n_flagged === 0 ? "green" : "amber"}>{st.disagreements.n_flagged} 个高分歧水平</Badge>}
/>
<div className="space-y-1.5">
{st.disagreements.levels.map((lv) => (
<div key={lv.level} className={`rounded-md border px-2.5 py-1.5 ${lv.low_consensus ? "border-[color:var(--color-warning)]/40 bg-[color:var(--color-warning)]/10" : "border-white/5 bg-white/[0.02]"}`}>
<div className="flex items-center justify-between">
<span className="text-[12px] font-medium">{lv.level_label_zh}</span>
<span className="font-mono text-[11px] text-[color:var(--color-text-tertiary)]">range={lv.range} · σ={lv.std}</span>
</div>
<div className="mt-1 flex flex-wrap gap-1.5">
{lv.individual_judgments.map((j, i) => (
<Badge key={i} tone="gray">{j.expert}: {j.judgment_label_zh}</Badge>
))}
</div>
</div>
))}
</div>
<p className="mt-2 text-[12px] text-[color:var(--color-text-secondary)]">{st.disagreements.advice}</p>
</Card>
)}
{/* ============ Sensitivity ============ */}
{st.sensitivity && (
<Card>
<CardHeader
title={<span className="flex items-center gap-1.5"><Activity className="size-3.5 text-[color:var(--color-warning)]" /> 稳健性分析</span>}
subtitle={`R ∈ [${Math.min(...st.sensitivity.r_values)}, ${Math.max(...st.sensitivity.r_values)}] · 均值波动 ${st.sensitivity.mean_swing_pct.toFixed(1)}%`}
/>
<SensitivityChart rows={st.sensitivity.rows} />
</Card>
)}
{/* ============ Batch parameters + Forest plot ============ */}
{st.batch && (
<Card>
<CardHeader
title={<span className="flex items-center gap-1.5"><BarChart3 className="size-3.5 text-[color:var(--color-primary)]" /> 多参数 Forest plot({st.batch.n_parameters} 个)</span>}
subtitle="横向 95% 置信区间 · 实心方块 = 后验均值"
/>
<ForestPlot
rows={st.batch.parameters
.filter((p) => p.posterior_mean !== undefined && p.posterior_ci95)
.map((p) => ({
label: p.name + (p.unit ? ` (${p.unit})` : ""),
mean: p.posterior_mean!,
ci95Lower: p.posterior_ci95![0],
ci95Upper: p.posterior_ci95![1],
}))}
/>
</Card>
)}
{/* ============ Correlation matrix ============ */}
{st.correlation && (st.correlation.pearson || st.correlation.spearman) && (
<Card>
<CardHeader
title={<span className="flex items-center gap-1.5"><GitCompare className="size-3.5 text-[color:var(--color-primary)]" /> 参数相关性矩阵</span>}
/>
{st.correlation.pearson && (
<CorrelationHeatmap labels={st.correlation.labels} matrix={st.correlation.pearson} title="Pearson" />
)}
{st.correlation.spearman && (
<div className="mt-3">
<CorrelationHeatmap labels={st.correlation.labels} matrix={st.correlation.spearman} title="Spearman" />
</div>
)}
</Card>
)}
{/* ============ Scenario comparison ============ */}
{st.scenarioCompare && (
<Card>
<CardHeader
title={<span className="flex items-center gap-1.5"><GitCompare className="size-3.5 text-[color:var(--color-primary)]" /> 情景对比 · {st.scenarioCompare.label_a} vs {st.scenarioCompare.label_b}</span>}
right={<Badge tone="blue">{st.scenarioCompare.interpretation}</Badge>}
/>
<div className="grid grid-cols-2 gap-2 sm:grid-cols-4">
<Stat label="Δ MEAN" value={(st.scenarioCompare.delta_mean >= 0 ? "+" : "") + st.scenarioCompare.delta_mean.toFixed(3)} tone={st.scenarioCompare.delta_mean >= 0 ? "accent" : "warn"} />
<Stat label="KL DIV" value={st.scenarioCompare.kl_divergence_a_to_b.toFixed(4)} />
<Stat label="JS DIV" value={st.scenarioCompare.js_divergence.toFixed(4)} />
<Stat label="W₁" value={st.scenarioCompare.wasserstein_distance.toFixed(4)} />
</div>
</Card>
)}
</div>
);
}