- 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>
134 lines
4.5 KiB
JavaScript
134 lines
4.5 KiB
JavaScript
// runner.js — 新建评估视图:列出场景、触发评估、轮询任务状态与日志。
|
|
|
|
const Runner = {
|
|
selectedScenario: null,
|
|
pollTimer: null,
|
|
|
|
// 绑定运行按钮。
|
|
init() {
|
|
document.getElementById("run-btn").addEventListener("click", () => Runner.trigger());
|
|
document.getElementById("view-report-btn").addEventListener("click", () => {
|
|
if (Runner.lastRunId) {
|
|
App.currentRunId = Runner.lastRunId;
|
|
App.enableReportNav();
|
|
App.switchView("report");
|
|
}
|
|
});
|
|
},
|
|
|
|
// 加载并渲染可触发的场景列表。
|
|
async loadScenarios() {
|
|
const list = document.getElementById("scenario-list");
|
|
list.innerHTML = '<p class="muted">加载中…</p>';
|
|
try {
|
|
const data = await API.scenarios();
|
|
const scenarios = data.scenarios || [];
|
|
if (scenarios.length === 0) {
|
|
list.innerHTML = '<p class="muted">未在 scenarios/ 下找到场景文件。</p>';
|
|
return;
|
|
}
|
|
list.innerHTML = "";
|
|
scenarios.forEach((sc) => list.appendChild(Runner.renderScenarioItem(sc)));
|
|
} catch (err) {
|
|
list.innerHTML = `<p class="muted">加载失败:${App.escape(err.message)}</p>`;
|
|
}
|
|
},
|
|
|
|
// 构造单个场景条目。
|
|
renderScenarioItem(sc) {
|
|
const item = document.createElement("div");
|
|
const invalid = !!sc.error;
|
|
item.className = "scenario-item" + (invalid ? " invalid" : "");
|
|
|
|
const modeTag = sc.mode
|
|
? `<span class="tag mode-${App.escape(sc.mode)}">${App.escape(sc.mode)}</span>`
|
|
: "";
|
|
const metricCount = (sc.metrics || []).length;
|
|
|
|
item.innerHTML = `
|
|
<div>
|
|
<div class="scenario-name">${App.escape(sc.scenario_name || sc.path)}</div>
|
|
<div class="scenario-path">${App.escape(sc.path)}</div>
|
|
${sc.error ? `<div class="scenario-path" style="color:#dc2626">${App.escape(sc.error)}</div>` : ""}
|
|
</div>
|
|
<div class="scenario-tags">
|
|
${modeTag}
|
|
<span class="tag">${metricCount} 指标</span>
|
|
</div>
|
|
`;
|
|
|
|
if (!invalid) {
|
|
item.addEventListener("click", () => {
|
|
document.querySelectorAll(".scenario-item").forEach((el) => el.classList.remove("selected"));
|
|
item.classList.add("selected");
|
|
Runner.selectedScenario = sc.path;
|
|
document.getElementById("selected-scenario").textContent = sc.path;
|
|
document.getElementById("run-btn").disabled = false;
|
|
});
|
|
}
|
|
return item;
|
|
},
|
|
|
|
// 触发评估并开始轮询。
|
|
async trigger() {
|
|
if (!Runner.selectedScenario) return;
|
|
const runBtn = document.getElementById("run-btn");
|
|
runBtn.disabled = true;
|
|
|
|
const panel = document.getElementById("task-panel");
|
|
const logBox = document.getElementById("task-log");
|
|
const statusBadge = document.getElementById("task-status");
|
|
const reportBtn = document.getElementById("view-report-btn");
|
|
panel.hidden = false;
|
|
reportBtn.hidden = true;
|
|
logBox.textContent = "";
|
|
Runner._setStatus(statusBadge, "queued");
|
|
|
|
try {
|
|
const resp = await API.triggerEvaluation(Runner.selectedScenario);
|
|
Runner.poll(resp.task_id);
|
|
} catch (err) {
|
|
Runner._setStatus(statusBadge, "failed");
|
|
logBox.textContent = `触发失败:${err.message}`;
|
|
runBtn.disabled = false;
|
|
}
|
|
},
|
|
|
|
// 周期性轮询任务状态,刷新日志与徽标。
|
|
poll(taskId) {
|
|
const logBox = document.getElementById("task-log");
|
|
const statusBadge = document.getElementById("task-status");
|
|
const reportBtn = document.getElementById("view-report-btn");
|
|
const runBtn = document.getElementById("run-btn");
|
|
|
|
if (Runner.pollTimer) clearInterval(Runner.pollTimer);
|
|
Runner.pollTimer = setInterval(async () => {
|
|
try {
|
|
const status = await API.taskStatus(taskId);
|
|
logBox.textContent = (status.logs || []).join("\n");
|
|
logBox.scrollTop = logBox.scrollHeight;
|
|
Runner._setStatus(statusBadge, status.status);
|
|
|
|
if (status.status === "completed" || status.status === "failed") {
|
|
clearInterval(Runner.pollTimer);
|
|
runBtn.disabled = false;
|
|
if (status.status === "completed" && status.run_id) {
|
|
Runner.lastRunId = status.run_id;
|
|
reportBtn.hidden = false;
|
|
}
|
|
}
|
|
} catch (err) {
|
|
clearInterval(Runner.pollTimer);
|
|
logBox.textContent += `\n轮询失败:${err.message}`;
|
|
runBtn.disabled = false;
|
|
}
|
|
}, 1200);
|
|
},
|
|
|
|
// 更新状态徽标的文本与配色类。
|
|
_setStatus(badge, status) {
|
|
badge.textContent = status;
|
|
badge.className = "badge " + status;
|
|
},
|
|
};
|