- webapp/: FastAPI backend with runs/scenarios/evaluations API routers; services for run_reader, report_builder, scenario_scanner, task_manager (lazy ragas import — server boots even without ragas); Pydantic models - webapp/static/: single-page console (layout A: left-nav + main area); report detail with metric cards, Chart.js distribution histogram, grouping table, lowest-score sample review; trigger evaluation + log polling - webmain.py: uvicorn entry point (alongside existing main.py CLI) - start.bat: Windows one-click launcher with env checks and auto-browser open - rag_eval/datasets/: implement missing loader + normalizer modules (load_dataset_records, normalize_records) required by evaluator - scripts/seed_sample_run.py: generate realistic demo run artifacts - .gitignore: exclude datasets/ data files but keep rag_eval/datasets/ source Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com>
153 lines
4.8 KiB
JavaScript
153 lines
4.8 KiB
JavaScript
// app.js — 视图路由、运行列表渲染、健康检查。整个控制台的入口编排。
|
|
|
|
const App = {
|
|
currentRunId: null,
|
|
views: ["runs", "new", "report"],
|
|
titles: { runs: "运行列表", new: "新建评估", report: "报告详情" },
|
|
|
|
// 初始化:绑定导航、加载首屏、启动健康检查。
|
|
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();
|
|
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);
|
|
},
|
|
|
|
// 刷新当前视图的数据。
|
|
refreshCurrent() {
|
|
App.switchView(App.activeView || "runs");
|
|
},
|
|
|
|
// 加载并渲染运行列表。
|
|
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";
|
|
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 `<span class="metric-chip">${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;
|
|
},
|
|
|
|
// 根据分值返回 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.",
|
|
};
|
|
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);
|