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

222 lines
7.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// app.js — 视图路由、运行列表渲染、健康检查。整个控制台的入口编排。
// 会话保持URL hash 路由(#runs / #new / #profiles / #report/{runId}
// + sessionStorage 兜底F5 刷新 / 浏览器前进后退均可恢复。
const App = {
currentRunId: null,
activeView: null,
views: ["runs", "new", "report", "profiles"],
titles: { runs: "运行列表", new: "新建评估", report: "报告详情", profiles: "LLM 配置" },
// 初始化:绑定导航、从 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);