Files
siemens_ragas/webapp/static/js/runner.js
wangwei ca01e44ad2 feat(webapp): add session persistence via URL hash routing + sessionStorage
- app.js: hash-based router (#runs / #new / #profiles / #report/{runId})
  - navigate() pushes history entries for back/forward support
  - _restoreSession() reads hash on load and popstate
  - sessionStorage fallback for same-tab refreshes
  - run-card highlights selected run (.run-card.selected)
- runner.js: use App.navigate() for report redirect; persist lastRunId to sessionStorage
- index.html: report nav button starts disabled (enabled on run select/restore)
- app.css: .run-card.selected with petrol border + ring

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-16 17:55:07 +08:00

184 lines
6.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.

// runner.js — 新建评估视图列出场景、LLM角色配置、触发评估、轮询任务状态与日志。
const Runner = {
selectedScenario: null,
pollTimer: null,
lastRunId: null,
// 绑定运行按钮。
init() {
document.getElementById("run-btn").addEventListener("click", () => Runner.trigger());
document.getElementById("view-report-btn").addEventListener("click", () => {
if (Runner.lastRunId) {
App.enableReportNav();
App.navigate("report", Runner.lastRunId);
}
});
},
// 加载并渲染可触发的场景列表。
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>`;
}
// 同时加载 profiles 供角色选择
Runner._populateProfileSelects();
},
// 填充三个角色下拉框
async _populateProfileSelects() {
const cached = Profiles.getAll();
const profiles = cached.length > 0
? cached
: (await API.profiles().catch(() => ({ profiles: [] }))).profiles;
["role-judge", "role-answer", "role-dataset"].forEach(id => {
const sel = document.getElementById(id);
sel.innerHTML = '<option value="">— 使用场景原始配置 —</option>';
profiles.forEach(p => {
const opt = document.createElement("option");
opt.value = p.profile_id;
opt.textContent = `${p.name} (${p.model})`;
sel.appendChild(opt);
});
});
},
// 构造单个场景条目。
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;
// 显示 LLM 角色面板
document.getElementById("llm-assignment-panel").hidden = false;
});
}
return item;
},
// 触发评估:先 apply profiles若选了再触发任务。
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 {
// Step 1: apply LLM profiles to YAML if any selected
await Runner._applyProfilesIfNeeded(logBox);
// Step 2: trigger evaluation
const resp = await API.triggerEvaluation(Runner.selectedScenario);
Runner.poll(resp.task_id);
} catch (err) {
Runner._setStatus(statusBadge, "failed");
logBox.textContent = (logBox.textContent ? logBox.textContent + "\n" : "") + `触发失败:${err.message}`;
runBtn.disabled = false;
}
},
// 如果用户选了 profile就先 apply 写回 YAML
async _applyProfilesIfNeeded(logBox) {
const judgeId = document.getElementById("role-judge").value;
const answerId = document.getElementById("role-answer").value;
const datasetId = document.getElementById("role-dataset").value;
if (!judgeId && !answerId && !datasetId) return; // 全空,跳过
logBox.textContent = "正在将 LLM 配置写入场景文件…\n";
const body = {
scenario_path: Runner.selectedScenario,
judge_profile_id: judgeId || null,
answer_profile_id: answerId || null,
dataset_profile_id: datasetId || null,
};
const result = await API.applyProfiles(body);
const fields = (result.patched_fields || []).join(", ");
logBox.textContent += fields
? `✓ 已更新字段:${fields}\n`
: "(未找到可更新的字段,继续运行)\n";
},
// 周期性轮询任务状态,刷新日志与徽标。
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;
sessionStorage.setItem("rag_run_id", 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;
},
};