// app.js — 视图路由、运行列表渲染、健康检查。整个控制台的入口编排。 const App = { currentRunId: null, views: ["runs", "new", "report", "profiles"], titles: { runs: "运行列表", new: "新建评估", report: "报告详情", profiles: "LLM 配置" }, // 初始化:绑定导航、加载首屏、启动健康检查。 init() { document.querySelectorAll(".nav-item").forEach((btn) => { btn.addEventListener("click", () => App.switchView(btn.dataset.view)); }); document.getElementById("refresh-btn").addEventListener("click", () => App.refreshCurrent()); Runner.init(); Profiles.init(); App.switchView("runs"); App.checkHealth(); setInterval(App.checkHealth, 15000); }, // 切换主视图,并同步导航高亮与标题。 switchView(view) { if (view === "report" && !App.currentRunId) { // 没有选中的运行时,报告页显示占位。 } App.views.forEach((name) => { const el = document.getElementById(`view-${name}`); if (el) el.hidden = name !== view; }); document.querySelectorAll(".nav-item").forEach((btn) => { btn.classList.toggle("active", btn.dataset.view === view); }); document.getElementById("view-title").textContent = App.titles[view] || view; App.activeView = view; if (view === "runs") App.loadRuns(); if (view === "new") Runner.loadScenarios(); if (view === "report") Report.render(App.currentRunId); if (view === "profiles") Profiles.load(); }, // 刷新当前视图的数据。 refreshCurrent() { App.switchView(App.activeView || "runs"); }, // 加载并渲染运行列表。 async loadRuns() { const container = document.getElementById("runs-container"); const empty = document.getElementById("runs-empty"); container.innerHTML = '

加载中…

'; try { const data = await API.runs(); const runs = data.runs || []; if (runs.length === 0) { container.innerHTML = ""; empty.hidden = false; return; } empty.hidden = true; container.innerHTML = ""; runs.forEach((run) => container.appendChild(App.renderRunCard(run))); } catch (err) { container.innerHTML = `

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

`; } }, // 构造一张运行卡片。 renderRunCard(run) { const card = document.createElement("div"); card.className = "run-card"; card.addEventListener("click", () => { App.currentRunId = run.run_id; App.enableReportNav(); App.switchView("report"); }); const chips = (run.metrics || []) .map((m) => { const val = run.metric_means ? run.metric_means[m] : null; const cls = App.scoreClass(val); const text = val === null || val === undefined ? "n/a" : val.toFixed(2); return `${App.escape(App.shortMetric(m))} ${text}`; }) .join(""); card.innerHTML = `
${App.escape(run.scenario_name || run.run_id)}
${App.escape(run.mode || "—")} · judge: ${App.escape(run.judge_model || "—")}
${run.valid_samples} 有效 / ${run.invalid_samples} 无效 · ${App.escape(App.shortTime(run.finished_at))}
${chips}
`; return card; }, // 启用报告导航项(选中运行后)。 enableReportNav() { const btn = document.querySelector('.nav-item[data-view="report"]'); if (btn) btn.disabled = false; }, // 根据分值返回 good/warn/bad/na 配色类。 scoreClass(value) { if (value === null || value === undefined) return "na"; if (value >= 0.8) return "good"; if (value >= 0.65) return "warn"; return "bad"; }, // 指标名缩写,节省卡片横向空间。 shortMetric(name) { const map = { faithfulness: "faith.", answer_relevancy: "ans.rel.", context_recall: "ctx.recall", context_precision: "ctx.prec.", noise_sensitivity: "noise.sens.", factual_correctness: "fact.corr.", semantic_similarity: "sem.sim.", }; return map[name] || name; }, // 截取时间戳到分钟,便于阅读。 shortTime(iso) { if (!iso) return "—"; return String(iso).replace("T", " ").slice(0, 16); }, // 简单 HTML 转义,防止注入。 escape(text) { const div = document.createElement("div"); div.textContent = text == null ? "" : String(text); return div.innerHTML; }, // 健康检查,更新左下角状态点。 async checkHealth() { const dot = document.getElementById("health-dot"); const label = document.getElementById("health-text"); try { await API.health(); dot.className = "dot ok"; label.textContent = "服务正常"; } catch (_e) { dot.className = "dot bad"; label.textContent = "服务离线"; } }, }; document.addEventListener("DOMContentLoaded", App.init);