// app.js — 视图路由、运行列表渲染、健康检查。整个控制台的入口编排。 // 会话保持:URL hash 路由(#runs / #new / #profiles / #report/{runId}) // + sessionStorage 兜底,F5 刷新 / 浏览器前进后退均可恢复。 const App = { currentRunId: null, activeView: null, views: ["runs", "new", "report", "profiles", "apidocs"], titles: { runs: "运行列表", new: "新建评估", report: "报告详情", profiles: "LLM 配置", apidocs: "API 文档" }, // 初始化:绑定导航、从 URL/sessionStorage 恢复上次位置、启动健康检查。 init() { document.querySelectorAll(".nav-item").forEach((btn) => { btn.addEventListener("click", () => App.navigate(btn.dataset.view)); }); document.getElementById("refresh-btn").addEventListener("click", () => App.refreshCurrent()); Runner.init(); Profiles.init(); // 恢复上次会话(优先 URL hash,其次 sessionStorage) App._restoreSession(); App.checkHealth(); setInterval(App.checkHealth, 15000); // 浏览器前进 / 后退按钮 window.addEventListener("popstate", () => App._restoreSession()); }, // ---------------------------------------------------------------- // 路由 —— 有历史记录的主动导航(更新 URL hash) // ---------------------------------------------------------------- navigate(view, runId) { if (runId !== undefined) App.currentRunId = runId; const hash = App._buildHash(view, App.currentRunId); if (location.hash !== `#${hash}`) { history.pushState({ view, runId: App.currentRunId }, "", `#${hash}`); } App._doSwitch(view); }, // 供内部调用(不产生历史记录),例如刷新同一视图 switchView(view) { App._doSwitch(view); }, // 刷新当前视图数据 refreshCurrent() { App._doSwitch(App.activeView || "runs"); }, // ---------------------------------------------------------------- // 内部:实际切换 DOM + 触发数据加载 // ---------------------------------------------------------------- _doSwitch(view) { 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; // 持久化到 sessionStorage(URL 共享场景的备份) sessionStorage.setItem("rag_view", view); if (App.currentRunId) sessionStorage.setItem("rag_run_id", App.currentRunId); if (view === "runs") App.loadRuns(); if (view === "new") Runner.loadScenarios(); if (view === "report") Report.render(App.currentRunId); if (view === "profiles") Profiles.load(); }, // ---------------------------------------------------------------- // Hash 工具 // ---------------------------------------------------------------- _buildHash(view, runId) { if (view === "report" && runId) { return `report/${encodeURIComponent(runId)}`; } return view || "runs"; }, _parseHash() { const raw = location.hash.replace(/^#\/?/, ""); if (!raw) return { view: null, runId: null }; if (raw.startsWith("report/")) { const runId = decodeURIComponent(raw.slice("report/".length)); return { view: "report", runId }; } const view = App.views.includes(raw) ? raw : null; return { view, runId: null }; }, // 会话恢复:hash → sessionStorage → 默认 runs _restoreSession() { const { view: hView, runId: hRunId } = App._parseHash(); const view = hView || sessionStorage.getItem("rag_view") || "runs"; const runId = hRunId || sessionStorage.getItem("rag_run_id") || null; if (runId) { App.currentRunId = runId; App.enableReportNav(); } App._doSwitch(view); }, // ---------------------------------------------------------------- // 运行列表 // ---------------------------------------------------------------- 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" + (run.run_id === App.currentRunId ? " selected" : ""); card.addEventListener("click", () => { // 更新选中高亮 document.querySelectorAll(".run-card").forEach((c) => c.classList.remove("selected")); card.classList.add("selected"); App.enableReportNav(); App.navigate("report", run.run_id); }); 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 = `