// report.js — 报告详情页渲染:元信息、指标卡片、分布图、分组表、低分样本复核。 const Report = { distChart: null, currentDetail: null, activeGrouping: null, _switcherLoaded: false, // 加载并渲染指定运行的完整报告。 async render(runId) { const empty = document.getElementById("report-empty"); const content = document.getElementById("report-content"); // 加载历史报告下拉(仅首次) Report._loadSwitcher(runId); 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"; // 同步下拉选中项 const sel = document.getElementById("report-switcher-select"); if (sel) sel.value = runId; } catch (err) { empty.hidden = false; content.hidden = true; empty.innerHTML = `

加载报告失败:${App.escape(err.message)}

`; } }, // 加载并填充历史报告下拉选择框 async _loadSwitcher(currentRunId) { const sel = document.getElementById("report-switcher-select"); if (!sel) return; // 已加载过就只更新选中值,不重复请求 if (Report._switcherLoaded) { if (currentRunId) sel.value = currentRunId; return; } try { const data = await API.runs(); const runs = data.runs || []; sel.innerHTML = ""; if (runs.length === 0) { sel.innerHTML = ''; return; } runs.forEach((run) => { const opt = document.createElement("option"); opt.value = run.run_id; const timeStr = App.shortTime(run.finished_at); const meanText = run.metric_means ? Object.entries(run.metric_means) .filter(([, v]) => v !== null && v !== undefined) .slice(0, 2) .map(([k, v]) => `${App.shortMetric(k)}=${v.toFixed(2)}`) .join(" ") : ""; opt.textContent = `${run.scenario_name || run.run_id} ${timeStr}${meanText ? " [" + meanText + "]" : ""}`; sel.appendChild(opt); }); Report._switcherLoaded = true; if (currentRunId) sel.value = currentRunId; } catch (_e) { sel.innerHTML = ''; } // 绑定切换事件(只绑一次) sel.addEventListener("change", () => { const rid = sel.value; if (!rid) return; App.currentRunId = rid; App.enableReportNav(); Report.render(rid); }); }, // 顶部元信息条。 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 = `${head}${body}
`; }, // ④ 最低分样本逐条复核表(点击展开)。 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}
    `; }, // 导出 PDF:展开所有低分样本 → 打印 → 还原折叠状态 exportPdf() { // 1. 记录当前各 detail 展开状态,并全部展开 const details = document.querySelectorAll("#lowest-table .lowest-detail"); const wasHidden = Array.from(details).map((el) => el.hidden); details.forEach((el) => { el.hidden = false; }); // 2. 打印完成后还原折叠状态 const restore = () => { details.forEach((el, i) => { el.hidden = wasHidden[i]; }); window.removeEventListener("afterprint", restore); }; window.addEventListener("afterprint", restore); // 3. 触发打印(浏览器弹出打印对话框,用户选"另存为 PDF") window.print(); }, };