Add cancellable job execution

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
Lihatoo 2026-06-12 17:30:59 +08:00
parent 11dc85d7ad
commit 048d4b0c07
2 changed files with 292 additions and 59 deletions

View file

@ -451,6 +451,12 @@
border: 1px solid rgba(21, 32, 27, 0.08);
}
button.danger {
background: linear-gradient(135deg, #c73528 0%, #8f1f18 100%);
color: #fff;
border: 1px solid rgba(143, 31, 24, 0.22);
}
.pill {
display: inline-flex;
align-items: center;
@ -2093,8 +2099,11 @@ A+B</textarea>
strand_sequence_design_placeholder: "a b ~c 或 N10A5",
run_btn: "开始分析",
run_design_btn: "开始设计",
run_waiting_btn: "等待结果返回...",
submission_locked: "已有任务在等待结果返回,请等待完成;如需强制重新提交请刷新页面。",
run_waiting_btn: "终止任务",
status_canceling: "正在终止任务...",
status_canceled: "任务已终止",
cancel_job_failed: "终止任务失败",
submission_locked: "已有任务在等待结果返回,请终止或等待。",
example_btn: "载入示例",
history_title: "最近任务",
clear_history: "清空历史",
@ -2401,8 +2410,11 @@ A+B</textarea>
strand_sequence_design_placeholder: "a b ~c or N10A5",
run_btn: "Run Analysis",
run_design_btn: "Run Design",
run_waiting_btn: "Waiting for result...",
submission_locked: "A job is already waiting for results. Wait for it to finish, or refresh the page to submit again.",
run_waiting_btn: "Terminate Job",
status_canceling: "Terminating job...",
status_canceled: "Job canceled",
cancel_job_failed: "Cancel job failed",
submission_locked: "A job is already waiting for results. Cancel or wait.",
example_btn: "Load Example",
history_title: "Recent Jobs",
clear_history: "Clear History",
@ -2660,6 +2672,7 @@ A+B</textarea>
let lastResult = null;
let currentJobId = null;
let activeSubmission = false;
let cancellationRequested = false;
let probabilitySvgCache = new Map();
let styledSvgCache = new Map();
let viewerScale = 1;
@ -2686,8 +2699,11 @@ A+B</textarea>
}
function updateRunButtonState() {
runBtn.disabled = activeSubmission;
runBtn.disabled = activeSubmission && (!currentJobId || cancellationRequested);
runBtn.setAttribute("aria-busy", activeSubmission ? "true" : "false");
runBtn.classList.toggle("primary", !activeSubmission);
runBtn.classList.toggle("secondary", false);
runBtn.classList.toggle("danger", activeSubmission);
runBtn.textContent = activeSubmission ? t("run_waiting_btn") : runButtonBaseLabel();
}
@ -2696,6 +2712,27 @@ A+B</textarea>
updateRunButtonState();
}
async function cancelCurrentJob() {
if (!currentJobId || cancellationRequested) return;
cancellationRequested = true;
setStatus(formatStatus(t("status_canceling")));
updateRunButtonState();
try {
const { response, data } = await fetchJsonOrThrow(`/api/jobs/${encodeURIComponent(currentJobId)}/cancel`, {
method: "POST",
});
if (!response.ok || data.status !== "success") {
throw new Error(data?.error || `Cancel failed with HTTP ${response.status}`);
}
refreshHealth();
} catch (error) {
cancellationRequested = false;
setStatus(`${t("cancel_job_failed")}: ${error.message}`, true);
} finally {
updateRunButtonState();
}
}
function setQueueMetric(node, value) {
node.textContent = value === undefined || value === null ? "-" : String(value);
}
@ -5370,7 +5407,6 @@ A+B</textarea>
}
async function waitForJob(jobId) {
const startedAt = Date.now();
for (;;) {
const { response, data: job } = await fetchJsonOrThrow(`/api/jobs/${jobId}`);
if (!response.ok) {
@ -5380,6 +5416,11 @@ A+B</textarea>
setStatus(formatStatus(t("status_queued")));
} else if (job.status === "running") {
setStatus(formatStatus(t("status_polling")));
} else if (job.status === "cancel_requested") {
setStatus(formatStatus(t("status_canceling")));
} else if (job.status === "canceled") {
refreshHealth();
throw new Error(t("status_canceled"));
} else if (job.status === "success") {
refreshHealth();
if (job.result && typeof job.elapsed_seconds === "number") {
@ -5390,9 +5431,6 @@ A+B</textarea>
refreshHealth();
throw new Error(job.error?.message || "Job failed");
}
if (Date.now() - startedAt > 10 * 60 * 1000) {
throw new Error(t("request_timeout"));
}
await new Promise((resolve) => setTimeout(resolve, 1200));
}
}
@ -5412,8 +5450,9 @@ A+B</textarea>
return;
}
setSubmissionLocked(true);
currentJobId = null;
cancellationRequested = false;
setSubmissionLocked(true);
setStatus(formatStatus(t("status_running")));
results.innerHTML = "";
@ -5428,6 +5467,7 @@ A+B</textarea>
throw new Error(data?.error || `Analysis failed with HTTP ${response.status}`);
}
currentJobId = data.job_id;
updateRunButtonState();
setStatus(formatStatus(t("status_waiting_job")));
const result = await waitForJob(data.job_id);
result.job_id = data.job_id;
@ -5438,11 +5478,19 @@ A+B</textarea>
renderHistory();
setStatus(formatStatus(t("status_done")));
} catch (error) {
results.innerHTML = `<div class="result-card"><h3>${t("request_failed")}</h3><pre>${String(error)}</pre></div>`;
setStatus(t("status_error"), true);
if (cancellationRequested) {
results.innerHTML = `<div class="result-card"><h3>${t("status_canceled")}</h3><pre>${String(error)}</pre></div>`;
setStatus(formatStatus(t("status_canceled")));
} else {
results.innerHTML = `<div class="result-card"><h3>${t("request_failed")}</h3><pre>${String(error)}</pre></div>`;
setStatus(t("status_error"), true);
}
refreshHealth();
} finally {
setSubmissionLocked(false);
currentJobId = null;
cancellationRequested = false;
updateRunButtonState();
}
}
@ -5459,8 +5507,9 @@ A+B</textarea>
return;
}
setSubmissionLocked(true);
currentJobId = null;
cancellationRequested = false;
setSubmissionLocked(true);
setStatus(formatStatus(t("status_running")));
results.innerHTML = "";
@ -5475,6 +5524,7 @@ A+B</textarea>
throw new Error(data?.error || `Analysis failed with HTTP ${response.status}`);
}
currentJobId = data.job_id;
updateRunButtonState();
setStatus(formatStatus(t("status_waiting_job")));
const result = await waitForJob(data.job_id);
result.job_id = data.job_id;
@ -5485,11 +5535,19 @@ A+B</textarea>
renderHistory();
setStatus(formatStatus(t("status_done")));
} catch (error) {
results.innerHTML = `<div class="result-card"><h3>${t("request_failed")}</h3><pre>${String(error)}</pre></div>`;
setStatus(t("status_error"), true);
if (cancellationRequested) {
results.innerHTML = `<div class="result-card"><h3>${t("status_canceled")}</h3><pre>${String(error)}</pre></div>`;
setStatus(formatStatus(t("status_canceled")));
} else {
results.innerHTML = `<div class="result-card"><h3>${t("request_failed")}</h3><pre>${String(error)}</pre></div>`;
setStatus(t("status_error"), true);
}
refreshHealth();
} finally {
setSubmissionLocked(false);
currentJobId = null;
cancellationRequested = false;
updateRunButtonState();
}
}
@ -5727,7 +5785,13 @@ A+B</textarea>
historyImportInput.value = "";
}
});
runBtn.addEventListener("click", runAnalysis);
runBtn.addEventListener("click", () => {
if (activeSubmission) {
cancelCurrentJob();
return;
}
runAnalysis();
});
document.getElementById("exampleBtn").addEventListener("click", loadExample);
workflowSelect.addEventListener("change", () => {
syncWorkflow();