Spaces:
Sleeping
Sleeping
| 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> | |
| ); | |
| } | |