// report.js — 报告详情页渲染:元信息、指标卡片、分布图、分组表、低分样本复核。
const Report = {
distChart: null,
currentDetail: null,
activeGrouping: null,
// 加载并渲染指定运行的完整报告。
async render(runId) {
const empty = document.getElementById("report-empty");
const content = document.getElementById("report-content");
if (!runId) {
empty.hidden = false;
content.hidden = true;
return;
}
empty.hidden = true;
content.hidden = false;
content.style.opacity = "0.4";
try {
const detail = await API.runDetail(runId);
Report.currentDetail = detail;
Report.renderMeta(detail.summary);
Report.renderMetricCards(detail.summary, detail.report);
Report.renderDistribution(detail.report);
Report.renderGroupings(detail.report);
Report.renderLowest(detail.report);
Report.renderAdvice(detail.summary, detail.report);
content.style.opacity = "1";
} catch (err) {
empty.hidden = false;
content.hidden = true;
empty.innerHTML = `
加载报告失败:${App.escape(err.message)}
`;
}
},
// 顶部元信息条。
renderMeta(summary) {
const el = document.getElementById("report-meta");
el.innerHTML = `
${App.escape(summary.scenario_name || summary.run_id)}
● completed
run_id: ${App.escape(summary.run_id)}
${App.escape(summary.mode || "—")} · judge: ${App.escape(summary.judge_model || "—")}
· ${summary.total_samples} 样本 (${summary.valid_samples} 有效 / ${summary.invalid_samples} 无效)
· ${App.escape(App.shortTime(summary.finished_at))}
`;
},
// ① 指标均值卡片。
renderMetricCards(summary, report) {
const wrap = document.getElementById("metric-cards");
wrap.innerHTML = "";
const metrics = report.metrics && report.metrics.length ? report.metrics : summary.metrics;
metrics.forEach((metric) => {
const value = report.metric_means ? report.metric_means[metric] : null;
const cls = App.scoreClass(value);
const text = value === null || value === undefined ? "n/a" : value.toFixed(2);
const card = document.createElement("div");
card.className = "metric-card";
card.innerHTML = `
${text}
${App.escape(metric)}
`;
wrap.appendChild(card);
});
},
// ② 分数分布直方图(可切换指标)。
renderDistribution(report) {
const select = document.getElementById("dist-metric-select");
const distributions = report.distributions || {};
const metricsWithDist = Object.keys(distributions);
select.innerHTML = "";
if (metricsWithDist.length === 0) {
Report._drawDistChart([], []);
return;
}
metricsWithDist.forEach((metric) => {
const opt = document.createElement("option");
opt.value = metric;
opt.textContent = metric;
select.appendChild(opt);
});
select.onchange = () => Report._updateDistChart(select.value);
Report._updateDistChart(metricsWithDist[0]);
},
// 用选定指标的分箱数据刷新直方图。
_updateDistChart(metric) {
const distributions = Report.currentDetail.report.distributions || {};
const bins = distributions[metric] || [];
const labels = bins.map((b) => b.label);
const counts = bins.map((b) => b.count);
const colors = bins.map((b) => Report._binColor(b.lower));
Report._drawDistChart(labels, counts, colors);
},
// 低分箱偏红、高分箱偏绿,直观暴露长尾。
_binColor(lower) {
if (lower >= 0.8) return "#16a34a";
if (lower >= 0.6) return "#84cc16";
if (lower >= 0.4) return "#eab308";
if (lower >= 0.2) return "#f97316";
return "#dc2626";
},
// 实际绘制 Chart.js 柱状图。
_drawDistChart(labels, counts, colors) {
const canvas = document.getElementById("dist-chart");
if (Report.distChart) Report.distChart.destroy();
Report.distChart = new Chart(canvas, {
type: "bar",
data: {
labels,
datasets: [{ data: counts, backgroundColor: colors || "#009999", borderRadius: 4 }],
},
options: {
responsive: true,
plugins: { legend: { display: false } },
scales: {
y: { beginAtZero: true, ticks: { precision: 0 }, grid: { color: "#f1f5f9" } },
x: { grid: { display: false } },
},
},
});
},
// ③ 分组均值(difficulty / question_type / language)。
renderGroupings(report) {
const tabsEl = document.getElementById("grouping-tabs");
const tableEl = document.getElementById("grouping-table");
const groupings = report.groupings || {};
const fields = Object.keys(groupings);
tabsEl.innerHTML = "";
if (fields.length === 0) {
tableEl.innerHTML = '数据集未包含可分组字段(difficulty / question_type)。
';
return;
}
const fieldLabels = { difficulty: "难度", question_type: "类型", language: "语言" };
Report.activeGrouping = fields[0];
fields.forEach((field) => {
const tab = document.createElement("button");
tab.className = "grouping-tab" + (field === Report.activeGrouping ? " active" : "");
tab.textContent = fieldLabels[field] || field;
tab.onclick = () => {
Report.activeGrouping = field;
tabsEl.querySelectorAll(".grouping-tab").forEach((t) => t.classList.remove("active"));
tab.classList.add("active");
Report._drawGroupTable(report, field);
};
tabsEl.appendChild(tab);
});
Report._drawGroupTable(report, Report.activeGrouping);
},
// 渲染单个分组字段的均值表。
_drawGroupTable(report, field) {
const tableEl = document.getElementById("grouping-table");
const stats = report.groupings[field] || [];
const metrics = report.metrics || [];
let head = "| 组 | 样本 | ";
metrics.forEach((m) => (head += `${App.escape(App.shortMetric(m))} | `));
head += "
";
let body = "";
stats.forEach((stat) => {
body += `| ${App.escape(stat.key)} | ${stat.count} | `;
metrics.forEach((m) => {
const v = stat.means ? stat.means[m] : null;
const cls = App.scoreClass(v);
const text = v === null || v === undefined ? "—" : v.toFixed(2);
body += `${text} | `;
});
body += "
";
});
tableEl.innerHTML = ``;
},
// ④ 最低分样本逐条复核表(点击展开)。
renderLowest(report) { const wrap = document.getElementById("lowest-table");
const samples = report.lowest_samples || [];
wrap.innerHTML = "";
if (samples.length === 0) {
wrap.innerHTML = '暂无可复核样本。
';
return;
}
const metrics = report.metrics || [];
samples.forEach((sample, idx) => {
const row = document.createElement("div");
row.className = "lowest-row";
const scoreBadges = metrics
.map((m) => {
const v = sample.metrics ? sample.metrics[m] : null;
const cls = App.scoreClass(v);
const text = v === null || v === undefined ? "—" : v.toFixed(2);
return `${text}`;
})
.join("");
row.innerHTML = `
${App.escape(sample.sample_id)}
${App.escape(sample.question || "—")}
${scoreBadges}
`;
const detail = document.createElement("div");
detail.className = "lowest-detail";
detail.hidden = true;
detail.innerHTML = Report._detailHtml(sample);
row.addEventListener("click", () => {
detail.hidden = !detail.hidden;
});
wrap.appendChild(row);
wrap.appendChild(detail);
});
},
// 单条样本的展开详情:question / contexts / answer / ground_truth。
_detailHtml(sample) {
const contexts = (sample.contexts || [])
.map((c, i) => `[${i + 1}] ${App.escape(c)}
`)
.join("");
const errorBlock = sample.error
? `错误 error
${App.escape(sample.error)}
`
: "";
return `
问题 question
${App.escape(sample.question || "—")}
检索片段 contexts
${contexts || "(空)"}
生成答案 answer
${App.escape(sample.answer || "—")}
标准答案 ground_truth
${App.escape(sample.ground_truth || "—")}
${errorBlock}
`;
},
// ⑤ 优化建议(仅 optimization_advice.md 存在时渲染)。
renderAdvice(summary, report) {
const section = document.getElementById("advice-section");
const body = document.getElementById("advice-body");
const modelLabel = document.getElementById("advice-model-label");
const md = report.advice_markdown || "";
if (!md.trim()) {
section.hidden = true;
return;
}
section.hidden = false;
modelLabel.textContent = summary.judge_model ? `judge: ${summary.judge_model}` : "";
// 简单 Markdown → HTML 转换(标题、列表、分隔线、粗体)
const escaped = md
.replace(/&/g, "&").replace(//g, ">");
const html = escaped
.replace(/^#{3}\s+(.+)$/gm, "$1
")
.replace(/^#{2}\s+(.+)$/gm, "$1
")
.replace(/^#{1}\s+(.+)$/gm, "$1
")
.replace(/^---+$/gm, "
")
.replace(/\*\*(.+?)\*\*/g, "$1")
.replace(/^- (.+)$/gm, "$1")
.replace(/([^]*?<\/li>\n?)+/g, (m) => ``)
.replace(/\n\n+/g, "\n
\n");
body.innerHTML = `${html}
`;
},
};