Files
siemens_ragas/webapp/static/js/score_jobs.js
wangwei 4fd515d2d9 feat: async score jobs — POST /api/score/async + 评分记录 page
Each async score job:
- Runs InlineScorer.score() in thread pool
- Writes standard run artifacts (metadata.json, scores.csv, summary.md)
- Runs optimization_advisor => optimization_advice.md
- Result appears in 运行列表 and 报告详情 with full report

New endpoints:
- POST /api/score/async  (202, job_id immediate)
- GET  /api/score/jobs   (list all jobs)
- GET  /api/score/jobs/{id} (single job status)

Frontend:
- 评分记录 nav page with card list
- 5s auto-polling for queued/running jobs
- 查看报告 button navigates to existing 报告详情 page

Dify: change /api/score -> /api/score/async, no response parsing needed

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-24 17:24:22 +08:00

126 lines
4.5 KiB
JavaScript

// score_jobs.js — 评分记录页面(异步 RAGAS 评分任务列表)
// 每条评分完成后自动写入标准 Run 产物,点击「查看报告」复用现有报告详情页。
const ScoreJobs = {
_pollTimers: {}, // job_id -> setInterval handle
async load() {
const list = document.getElementById("scorejobs-list");
const empty = document.getElementById("scorejobs-empty");
list.innerHTML = '<p class="muted">加载中…</p>';
try {
const data = await API.listScoreJobs();
const jobs = data.jobs || [];
list.innerHTML = "";
if (jobs.length === 0) {
empty.hidden = false;
return;
}
empty.hidden = true;
jobs.forEach(job => list.appendChild(ScoreJobs.renderCard(job)));
// Auto-poll any pending jobs
jobs.forEach(job => {
if (job.status === "queued" || job.status === "running") {
ScoreJobs._startPoll(job.job_id);
}
});
} catch (err) {
list.innerHTML = `<p class="muted">加载失败:${App.escape(err.message)}</p>`;
}
},
renderCard(job) {
const card = document.createElement("div");
card.className = "run-card";
card.id = `score-job-${job.job_id}`;
card.innerHTML = ScoreJobs._cardHtml(job);
// Bind report button if already completed
ScoreJobs._bindReportBtn(card, job);
return card;
},
_cardHtml(job) {
const time = App.shortTime(job.created_at);
const question = App.escape((job.request_summary?.question || "—").slice(0, 60));
const metrics = (job.request_summary?.metrics || []).join(", ");
const statusBadge = `<span class="badge ${job.status}">${job.status}</span>`;
let scoreHtml = "";
if (job.status === "completed") {
scoreHtml = Object.entries(job.scores || {})
.map(([k, v]) => {
const cls = App.scoreClass(v);
const text = v === null || v === undefined ? "n/a" : Number(v).toFixed(3);
return `<span class="metric-chip" title="${App.escape(k)}">${App.escape(App.shortMetric(k))} <b class="${cls}">${text}</b></span>`;
})
.join(" ");
if (job.weighted_score !== null && job.weighted_score !== undefined) {
const cls = App.scoreClass(job.weighted_score);
scoreHtml += ` <span class="metric-chip">综合 <b class="${cls}">${Number(job.weighted_score).toFixed(3)}</b></span>`;
}
} else if (job.status === "failed") {
scoreHtml = `<span style="color:var(--bad);font-size:12px">${App.escape((job.error || "").slice(0, 80))}</span>`;
} else {
scoreHtml = `<span class="muted">评分中,请稍候…</span>`;
}
const reportBtn = job.status === "completed" && job.run_id
? `<button class="btn btn-sm btn-primary score-job-report-btn" data-run-id="${App.escape(job.run_id)}">查看报告</button>`
: "";
return `
<div class="run-card-head">
<div class="run-card-title">${question}</div>
<div style="display:flex;gap:8px;align-items:center">${statusBadge}${reportBtn}</div>
</div>
<div class="run-card-meta">
<div>指标:${App.escape(metrics)} · ${time} · ${job.latency_ms}ms</div>
</div>
<div class="run-card-metrics">${scoreHtml}</div>
`;
},
_bindReportBtn(card, job) {
const btn = card.querySelector(".score-job-report-btn");
if (!btn) return;
btn.addEventListener("click", () => {
const runId = btn.dataset.runId;
if (runId) {
App.enableReportNav();
App.navigate("report", runId);
}
});
},
_startPoll(jobId) {
if (ScoreJobs._pollTimers[jobId]) return;
ScoreJobs._pollTimers[jobId] = setInterval(async () => {
try {
const job = await API.getScoreJob(jobId);
const card = document.getElementById(`score-job-${jobId}`);
if (card) {
card.innerHTML = ScoreJobs._cardHtml(job);
ScoreJobs._bindReportBtn(card, job);
}
if (job.status === "completed" || job.status === "failed") {
clearInterval(ScoreJobs._pollTimers[jobId]);
delete ScoreJobs._pollTimers[jobId];
// If completed, pre-enable report nav
if (job.status === "completed" && job.run_id) {
App.enableReportNav();
}
}
} catch (_e) {
clearInterval(ScoreJobs._pollTimers[jobId]);
delete ScoreJobs._pollTimers[jobId];
}
}, 5000);
},
stopAllPolls() {
Object.values(ScoreJobs._pollTimers).forEach(t => clearInterval(t));
ScoreJobs._pollTimers = {};
},
};