diff --git a/CHANGELOG.md b/CHANGELOG.md index 7950fa07..c2f63b07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,12 @@ still happens inside Studio dead-last — after shortcut expansion and after the scene split — which is what makes a `$var` work *inside* a shortcut's replacement while a value containing a comma or a trigger word can never move a scene cut. +- **An Interrupt button in Easy Gen.** The progress panel now carries the same + **■ Interrupt** control the Cutting Room's player has — Easy Gen previously had no way to + stop a run short of the ComfyUI queue. A stopped run reports "Generation stopped." instead + of a failure (an interrupted job is recorded as an error with no media, which is + indistinguishable from a real crash at the API level), and any partial media it did write + is still shown. ### Changed - **Variable values are now auto-growing text boxes** (both UIs) instead of one-line inputs — diff --git a/easy_gen/frontend/easy.css b/easy_gen/frontend/easy.css index 3a6bf735..b3adc30b 100644 --- a/easy_gen/frontend/easy.css +++ b/easy_gen/frontend/easy.css @@ -58,7 +58,10 @@ a.brand:hover .brand-sub { color: var(--amber-2); } height: 6px; border-radius: 4px; background: linear-gradient(90deg, var(--amber), var(--amber-2)); width: 0%; transition: width .2s ease; margin-bottom: 6px; } -.easy-progress-label { font-size: 12px; color: var(--muted); font-family: var(--font-mono); } +.easy-progress-row { display: flex; align-items: center; gap: 10px; } +.easy-progress-label { flex: 1; min-width: 0; font-size: 12px; color: var(--muted); font-family: var(--font-mono); + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.easy-progress-stop:disabled { opacity: .6; cursor: default; } .easy-bar { flex: none; display: flex; align-items: flex-end; gap: 12px; diff --git a/easy_gen/frontend/preview.js b/easy_gen/frontend/preview.js index 9902058d..3dd97fa6 100644 --- a/easy_gen/frontend/preview.js +++ b/easy_gen/frontend/preview.js @@ -22,7 +22,14 @@ progressWrap.hidden = true; const bar = el("div", "easy-progress-bar"); const label = el("div", "easy-progress-label"); - progressWrap.append(bar, label); + // Same "■ Interrupt" affordance as the Cutting Room's player readout, and it lives in the + // progress panel so it only exists while there's something to stop. + const stopBtn = el("button", "gen-interrupt easy-progress-stop", "■ Interrupt"); + stopBtn.type = "button"; + stopBtn.title = "Stop the current generation"; + const progressRow = el("div", "easy-progress-row"); + progressRow.append(label, stopBtn); + progressWrap.append(bar, progressRow); root.append(inner, closeBtn, badge, progressWrap); let pollTimer = null; @@ -138,8 +145,29 @@ progressWrap.hidden = false; label.textContent = "Starting…"; bar.style.width = "0%"; + // An interrupted job lands in ComfyUI's history as status_str "error" with no media — + // indistinguishable from a real failure at the API level. Remember that the user asked + // for it so the poll below reports "stopped", not "Generation failed". + let interrupted = false; + stopBtn.disabled = false; + stopBtn.textContent = "■ Interrupt"; + stopBtn.onclick = async () => { + interrupted = true; + stopBtn.disabled = true; + stopBtn.textContent = "Interrupting…"; + label.textContent = "Interrupting…"; + try { await API.interrupt(); } + catch (e) { + // The run may well still be going — say so instead of leaving a dead button. + interrupted = false; + stopBtn.disabled = false; + stopBtn.textContent = "■ Interrupt"; + label.textContent = "Could not interrupt: " + (e.message || e); + } + }; progressTimer = setInterval(async () => { + if (interrupted) return; // don't paint "Sampling…" over "Interrupting…" try { const pr = await API.progress(); if (pr.max > 0) { @@ -165,14 +193,19 @@ stopPolling(); progressWrap.hidden = true; const media = res.media && res.media.length ? res.media[res.media.length - 1] : null; - if (media) setGenerated(media, projectId); else showEmpty("Generation finished but produced no media."); - if (onDone) onDone(true, res); + // A run stopped mid-sampling can still have written partial media — show it if so, + // it's the whole point of interrupting a long run you can already tell is wrong. + if (media) setGenerated(media, projectId); + else if (interrupted) showEmpty("Generation stopped."); + else showEmpty("Generation finished but produced no media."); + if (onDone) onDone(!interrupted, res); return; } if (res.state === "error") { stopPolling(); progressWrap.hidden = true; - showError(res.error || "Generation failed."); + if (interrupted) showEmpty("Generation stopped."); + else showError(res.error || "Generation failed."); if (onDone) onDone(false, res); return; }