2026-06-18 17:28:15 +08:00
|
|
|
|
// runner.js — 新建评估视图:列出场景、LLM角色配置、权重配置、触发评估、轮询任务状态。
|
2026-06-15 15:53:57 +08:00
|
|
|
|
|
|
|
|
|
|
const Runner = {
|
|
|
|
|
|
selectedScenario: null,
|
2026-06-18 17:28:15 +08:00
|
|
|
|
selectedScenarioInfo: null,
|
2026-06-15 15:53:57 +08:00
|
|
|
|
pollTimer: null,
|
2026-06-16 16:27:00 +08:00
|
|
|
|
lastRunId: null,
|
2026-06-15 15:53:57 +08:00
|
|
|
|
|
|
|
|
|
|
init() {
|
|
|
|
|
|
document.getElementById("run-btn").addEventListener("click", () => Runner.trigger());
|
|
|
|
|
|
document.getElementById("view-report-btn").addEventListener("click", () => {
|
|
|
|
|
|
if (Runner.lastRunId) {
|
|
|
|
|
|
App.enableReportNav();
|
2026-06-16 17:55:07 +08:00
|
|
|
|
App.navigate("report", Runner.lastRunId);
|
2026-06-15 15:53:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
});
|
2026-06-18 17:28:15 +08:00
|
|
|
|
document.getElementById("add-doc-weight-btn").addEventListener("click", () => Runner._addDocWeightRow());
|
2026-06-15 15:53:57 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
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>`;
|
|
|
|
|
|
}
|
2026-06-16 16:27:00 +08:00
|
|
|
|
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);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
2026-06-15 15:53:57 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
2026-06-18 17:28:15 +08:00
|
|
|
|
Runner.selectedScenarioInfo = sc;
|
2026-06-15 15:53:57 +08:00
|
|
|
|
document.getElementById("selected-scenario").textContent = sc.path;
|
|
|
|
|
|
document.getElementById("run-btn").disabled = false;
|
2026-06-16 16:27:00 +08:00
|
|
|
|
document.getElementById("llm-assignment-panel").hidden = false;
|
2026-06-18 17:28:15 +08:00
|
|
|
|
Runner._renderWeightPanel(sc);
|
|
|
|
|
|
document.getElementById("weight-config-panel").hidden = false;
|
2026-06-15 15:53:57 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
return item;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-06-18 17:28:15 +08:00
|
|
|
|
// 根据选中场景渲染指标权重行(动态生成,按场景 metrics 列表)
|
|
|
|
|
|
_renderWeightPanel(sc) {
|
|
|
|
|
|
const metricRows = document.getElementById("metric-weight-rows");
|
|
|
|
|
|
metricRows.innerHTML = "";
|
|
|
|
|
|
const metrics = sc.metrics || [];
|
|
|
|
|
|
const existingWeights = sc.metric_weights || {};
|
|
|
|
|
|
metrics.forEach(metric => {
|
|
|
|
|
|
const row = document.createElement("div");
|
|
|
|
|
|
row.className = "weight-row";
|
|
|
|
|
|
const currentVal = existingWeights[metric] != null ? existingWeights[metric] : 1.0;
|
|
|
|
|
|
row.innerHTML = `
|
|
|
|
|
|
<span class="weight-row-label">${App.escape(metric)}</span>
|
|
|
|
|
|
<input class="weight-row-input" type="number" min="0" step="0.1"
|
|
|
|
|
|
data-metric="${App.escape(metric)}" value="${currentVal}" />
|
|
|
|
|
|
`;
|
|
|
|
|
|
metricRows.appendChild(row);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 填充已有文档权重
|
|
|
|
|
|
const docRows = document.getElementById("doc-weight-rows");
|
|
|
|
|
|
docRows.innerHTML = "";
|
|
|
|
|
|
const existingDocWeights = sc.doc_weights || {};
|
|
|
|
|
|
Object.entries(existingDocWeights).forEach(([docName, w]) => {
|
|
|
|
|
|
Runner._addDocWeightRow(docName, w);
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 添加一行文档权重输入
|
|
|
|
|
|
_addDocWeightRow(docName, weight) {
|
|
|
|
|
|
const name = docName !== undefined ? docName : "";
|
|
|
|
|
|
const w = weight !== undefined ? weight : 1.0;
|
|
|
|
|
|
const container = document.getElementById("doc-weight-rows");
|
|
|
|
|
|
const row = document.createElement("div");
|
|
|
|
|
|
row.className = "weight-row";
|
|
|
|
|
|
row.innerHTML = `
|
|
|
|
|
|
<input class="doc-weight-name" type="text" placeholder="PDF 文件名(如 322_双源CT.pdf)" value="${App.escape(String(name))}" />
|
|
|
|
|
|
<input class="weight-row-input" type="number" min="0" step="0.1" value="${w}" />
|
|
|
|
|
|
<button class="weight-row-remove" title="删除">✕</button>
|
|
|
|
|
|
`;
|
|
|
|
|
|
row.querySelector(".weight-row-remove").addEventListener("click", () => row.remove());
|
|
|
|
|
|
container.appendChild(row);
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 收集权重面板当前值;全等权时返回 null(不发送)
|
|
|
|
|
|
_collectWeights() {
|
|
|
|
|
|
const metricWeights = {};
|
|
|
|
|
|
document.querySelectorAll("#metric-weight-rows .weight-row-input").forEach(input => {
|
|
|
|
|
|
const metric = input.dataset.metric;
|
|
|
|
|
|
const val = parseFloat(input.value);
|
|
|
|
|
|
if (metric && !isNaN(val)) metricWeights[metric] = val;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const docWeights = {};
|
|
|
|
|
|
document.querySelectorAll("#doc-weight-rows .weight-row").forEach(row => {
|
|
|
|
|
|
const nameInput = row.querySelector(".doc-weight-name");
|
|
|
|
|
|
const valInput = row.querySelector(".weight-row-input");
|
|
|
|
|
|
if (!nameInput || !valInput) return;
|
|
|
|
|
|
const name = nameInput.value.trim();
|
|
|
|
|
|
const val = parseFloat(valInput.value);
|
|
|
|
|
|
if (name && !isNaN(val)) docWeights[name] = val;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const allMetricDefault = Object.values(metricWeights).every(v => Math.abs(v - 1.0) < 1e-9);
|
|
|
|
|
|
const noDocWeights = Object.keys(docWeights).length === 0;
|
|
|
|
|
|
if (allMetricDefault && noDocWeights) return { metricWeights: null, docWeights: null };
|
|
|
|
|
|
return { metricWeights, docWeights };
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-06-15 15:53:57 +08:00
|
|
|
|
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 {
|
2026-06-16 16:27:00 +08:00
|
|
|
|
await Runner._applyProfilesIfNeeded(logBox);
|
2026-06-15 15:53:57 +08:00
|
|
|
|
const resp = await API.triggerEvaluation(Runner.selectedScenario);
|
|
|
|
|
|
Runner.poll(resp.task_id);
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
Runner._setStatus(statusBadge, "failed");
|
2026-06-16 16:27:00 +08:00
|
|
|
|
logBox.textContent = (logBox.textContent ? logBox.textContent + "\n" : "") + `触发失败:${err.message}`;
|
2026-06-15 15:53:57 +08:00
|
|
|
|
runBtn.disabled = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-06-16 16:27:00 +08:00
|
|
|
|
async _applyProfilesIfNeeded(logBox) {
|
|
|
|
|
|
const judgeId = document.getElementById("role-judge").value;
|
|
|
|
|
|
const answerId = document.getElementById("role-answer").value;
|
|
|
|
|
|
const datasetId = document.getElementById("role-dataset").value;
|
2026-06-18 17:28:15 +08:00
|
|
|
|
const { metricWeights, docWeights } = Runner._collectWeights();
|
2026-06-16 16:27:00 +08:00
|
|
|
|
|
2026-06-18 17:28:15 +08:00
|
|
|
|
if (!judgeId && !answerId && !datasetId && !metricWeights && !docWeights) return;
|
2026-06-16 16:27:00 +08:00
|
|
|
|
|
2026-06-18 17:28:15 +08:00
|
|
|
|
logBox.textContent = "正在将 LLM 配置和权重写入场景文件…\n";
|
2026-06-16 16:27:00 +08:00
|
|
|
|
const body = {
|
|
|
|
|
|
scenario_path: Runner.selectedScenario,
|
|
|
|
|
|
judge_profile_id: judgeId || null,
|
|
|
|
|
|
answer_profile_id: answerId || null,
|
|
|
|
|
|
dataset_profile_id: datasetId || null,
|
2026-06-18 17:28:15 +08:00
|
|
|
|
metric_weights: metricWeights,
|
|
|
|
|
|
doc_weights: docWeights,
|
2026-06-16 16:27:00 +08:00
|
|
|
|
};
|
|
|
|
|
|
const result = await API.applyProfiles(body);
|
|
|
|
|
|
const fields = (result.patched_fields || []).join(", ");
|
|
|
|
|
|
logBox.textContent += fields
|
|
|
|
|
|
? `✓ 已更新字段:${fields}\n`
|
|
|
|
|
|
: "(未找到可更新的字段,继续运行)\n";
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-06-15 15:53:57 +08:00
|
|
|
|
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;
|
2026-06-16 17:55:07 +08:00
|
|
|
|
sessionStorage.setItem("rag_run_id", status.run_id);
|
2026-06-15 15:53:57 +08:00
|
|
|
|
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;
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|