Files
siemens_ragas/webapp/static/js/app.js

222 lines
7.5 KiB
JavaScript
Raw Normal View History

// 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;
// 持久化到 sessionStorageURL 共享场景的备份)
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 = '<p class="muted">加载中…</p>';
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 = `<p class="muted">加载失败:${App.escape(err.message)}</p>`;
}
},
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 `<span class="metric-chip" title="${App.escape(m)}">${App.escape(App.shortMetric(m))} <b class="${cls}">${text}</b></span>`;
})
.join("");
card.innerHTML = `
<div class="run-card-head">
<div class="run-card-title">${App.escape(run.scenario_name || run.run_id)}</div>
</div>
<div class="run-card-meta">
<div>${App.escape(run.mode || "—")} · judge: ${App.escape(run.judge_model || "—")}</div>
<div>${run.valid_samples} 有效 / ${run.invalid_samples} 无效 · ${App.escape(App.shortTime(run.finished_at))}</div>
</div>
<div class="run-card-metrics">${chips}</div>
`;
return card;
},
// ----------------------------------------------------------------
// 工具方法
// ----------------------------------------------------------------
enableReportNav() {
const btn = document.querySelector('.nav-item[data-view="report"]');
if (btn) btn.disabled = false;
},
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);
},
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);