Merge feat/textgate-run-from-here: Text Gate run-from-here + sticky edit
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,55 @@
|
|||||||
|
# Text Gate — "Run from here" + sticky edit (design)
|
||||||
|
|
||||||
|
**Goal:** Bring the Text Gate to parity with the Image Gate's "Run from here"
|
||||||
|
affordance, plus a text-specific touch: keep the user's edited text across
|
||||||
|
re-runs ("start from there").
|
||||||
|
|
||||||
|
**Scope:** Frontend only — `web/text_gate.js`. No changes to `gates/textgate.py`,
|
||||||
|
`gates/gate_bus.py`, or `gates/gate_server.py`. The gate already re-arms and
|
||||||
|
re-pauses on every run (`GateBus.arm` → `wait_payload`) and `IS_CHANGED` returns
|
||||||
|
`NaN`, so re-queuing the prompt is enough to "resume": cached upstream means the
|
||||||
|
gate re-pauses near-instantly.
|
||||||
|
|
||||||
|
## State machine
|
||||||
|
|
||||||
|
The node currently has no explicit state. Add three:
|
||||||
|
|
||||||
|
- **idle** — before the first run. Pass shown, Run-from-here hidden.
|
||||||
|
- **paused** — socket `datasete-textgate-show` arrived. Textarea editable &
|
||||||
|
populated, **▶ Pass** shown, **Run from here** hidden, status `edit, then Pass`.
|
||||||
|
- **passed** — after Pass click. Textarea keeps the edited text, **Pass** hidden,
|
||||||
|
**▶ Run from here** shown, status `passed — Run from here to re-run`.
|
||||||
|
|
||||||
|
**Run from here** click → `app.queuePrompt(0, 1)` with `app.queuePrompt(0)`
|
||||||
|
fallback — copied verbatim from the Image Gate's `queueFromHere`.
|
||||||
|
|
||||||
|
## Sticky edited text
|
||||||
|
|
||||||
|
The Image Gate keeps its mask sticky; the Text Gate keeps its text. The live
|
||||||
|
textarea IS the sticky store, gated by the last-seen input:
|
||||||
|
|
||||||
|
- Track `node._tgInput` = the last incoming text the server pushed.
|
||||||
|
- On each re-pause with `incoming`:
|
||||||
|
- if `incoming === node._tgInput` (upstream unchanged — the Run-from-here
|
||||||
|
case) → **keep** the current textarea value, so the gate re-runs *your*
|
||||||
|
edited version (including any edits made after Pass).
|
||||||
|
- else (a genuine upstream recompute) → overwrite the textarea with `incoming`.
|
||||||
|
- always set `node._tgInput = incoming`.
|
||||||
|
|
||||||
|
Net: "Run from here" re-runs your version, but a real upstream change still
|
||||||
|
surfaces instead of hiding behind a stale edit. `_tgInput` is per-session
|
||||||
|
(not serialized) — a page reload starts fresh, which is fine.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- `node --check web/text_gate.js` (no JS test harness in the repo — consistent
|
||||||
|
with the other `web/*.js`).
|
||||||
|
- Manual: pause → edit → Pass → button appears → Run-from-here re-pauses showing
|
||||||
|
your edited text → downstream re-runs; change something upstream → new input
|
||||||
|
shows.
|
||||||
|
|
||||||
|
## Dropped (YAGNI)
|
||||||
|
|
||||||
|
- A separate "↺ reset to input" button — the upstream-change detection covers the
|
||||||
|
stale-edit footgun.
|
||||||
|
- Any backend auto-pass / bypass mode — not requested.
|
||||||
+60
-7
@@ -7,6 +7,12 @@ import { api } from "../../scripts/api.js";
|
|||||||
// the server pushes via the "datasete-textgate-show" socket event and POSTs the
|
// the server pushes via the "datasete-textgate-show" socket event and POSTs the
|
||||||
// edited text back. Outputs are static (text, signal) — no dynamic slots.
|
// edited text back. Outputs are static (text, signal) — no dynamic slots.
|
||||||
//
|
//
|
||||||
|
// After Pass, a "▶ Run from here" button re-queues the prompt (Image Gate
|
||||||
|
// parity): the gate re-arms every run and IS_CHANGED is NaN, so cached upstream
|
||||||
|
// means it re-pauses near-instantly. The edited text is sticky — kept across
|
||||||
|
// re-runs while the upstream input is unchanged, so Run-from-here re-runs YOUR
|
||||||
|
// version; a genuine upstream change still surfaces the new input.
|
||||||
|
//
|
||||||
// Sizing follows the Image Pool node: the editor is always present and FILLS the
|
// Sizing follows the Image Pool node: the editor is always present and FILLS the
|
||||||
// node, with only a min-height floor (no max) so the node stays freely resizable
|
// node, with only a min-height floor (no max) so the node stays freely resizable
|
||||||
// and the textarea grows with it.
|
// and the textarea grows with it.
|
||||||
@@ -28,6 +34,30 @@ async function postPass(node, text) {
|
|||||||
await api.fetchApi(`${R}/pass`, { method: "POST", body: fd });
|
await api.fetchApi(`${R}/pass`, { method: "POST", body: fd });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- run-from-here + state --------------------------------------------------
|
||||||
|
// States: "idle" (pre-run), "paused" (waiting for Pass), "passed" (Run-from-here
|
||||||
|
// shown). Re-queuing the whole prompt is enough to "resume" — cached upstream
|
||||||
|
// re-pauses the gate, matching the Image Gate's queueFromHere.
|
||||||
|
|
||||||
|
async function queueFromHere(node) {
|
||||||
|
try {
|
||||||
|
await app.queuePrompt(0, 1);
|
||||||
|
} catch (e) {
|
||||||
|
try { await app.queuePrompt(0); } catch (e2) { console.error("[tgate] queue failed", e2); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setState(node, s) {
|
||||||
|
node._tgState = s;
|
||||||
|
const tg = node._tg;
|
||||||
|
if (!tg) return;
|
||||||
|
tg.pass.style.display = s === "passed" ? "none" : "";
|
||||||
|
tg.runHere.style.display = s === "passed" ? "" : "none";
|
||||||
|
if (s === "paused") tg.status.textContent = "edit, then Pass";
|
||||||
|
else if (s === "passed") tg.status.textContent = "passed — Run from here to re-run";
|
||||||
|
node.setDirtyCanvas?.(true, true);
|
||||||
|
}
|
||||||
|
|
||||||
// ---- sizing (Image Pool pattern) --------------------------------------------
|
// ---- sizing (Image Pool pattern) --------------------------------------------
|
||||||
|
|
||||||
// Only a min-height FLOOR — no max — so the DOM widget fills the node and grows
|
// Only a min-height FLOOR — no max — so the DOM widget fills the node and grows
|
||||||
@@ -59,6 +89,8 @@ function injectStyles() {
|
|||||||
border:1px solid #555; color:#fff; }
|
border:1px solid #555; color:#fff; }
|
||||||
.tgate-pass { background:rgba(40,130,70,0.95); }
|
.tgate-pass { background:rgba(40,130,70,0.95); }
|
||||||
.tgate-pass:hover { background:rgba(55,160,90,0.98); }
|
.tgate-pass:hover { background:rgba(55,160,90,0.98); }
|
||||||
|
.tgate-run { background:rgba(40,90,140,0.95); }
|
||||||
|
.tgate-run:hover { background:rgba(60,120,180,0.98); }
|
||||||
.tgate-status { font-size:11px; opacity:0.6; margin-left:auto; }
|
.tgate-status { font-size:11px; opacity:0.6; margin-left:auto; }
|
||||||
`;
|
`;
|
||||||
const style = document.createElement("style");
|
const style = document.createElement("style");
|
||||||
@@ -81,21 +113,37 @@ function setupTextGateNode(node) {
|
|||||||
|
|
||||||
const btns = document.createElement("div");
|
const btns = document.createElement("div");
|
||||||
btns.className = "tgate-btns";
|
btns.className = "tgate-btns";
|
||||||
|
|
||||||
const pass = document.createElement("button");
|
const pass = document.createElement("button");
|
||||||
pass.className = "tgate-pass";
|
pass.className = "tgate-pass";
|
||||||
pass.textContent = "▶ Pass";
|
pass.textContent = "▶ Pass";
|
||||||
const status = document.createElement("span");
|
|
||||||
status.className = "tgate-status";
|
|
||||||
pass.onclick = async () => {
|
pass.onclick = async () => {
|
||||||
await postPass(node, area.value);
|
await postPass(node, area.value);
|
||||||
status.textContent = "passed";
|
setState(node, "passed");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Re-queue the prompt; cached upstream re-pauses the gate so you can run your
|
||||||
|
// edited text downstream again without recomputing the graph above it.
|
||||||
|
const runHere = document.createElement("button");
|
||||||
|
runHere.className = "tgate-run";
|
||||||
|
runHere.textContent = "▶ Run from here";
|
||||||
|
runHere.style.display = "none";
|
||||||
|
runHere.onclick = async () => {
|
||||||
|
node._tg.status.textContent = "re-running…";
|
||||||
|
await queueFromHere(node);
|
||||||
|
};
|
||||||
|
|
||||||
|
const status = document.createElement("span");
|
||||||
|
status.className = "tgate-status";
|
||||||
|
|
||||||
btns.appendChild(pass);
|
btns.appendChild(pass);
|
||||||
|
btns.appendChild(runHere);
|
||||||
btns.appendChild(status);
|
btns.appendChild(status);
|
||||||
|
|
||||||
wrap.appendChild(area);
|
wrap.appendChild(area);
|
||||||
wrap.appendChild(btns);
|
wrap.appendChild(btns);
|
||||||
node._tg = { wrap, area, status };
|
node._tg = { wrap, area, status, pass, runHere };
|
||||||
|
node._tgState = "idle";
|
||||||
|
|
||||||
// FILLS the node: floor-only min height, no max (Image Pool pattern).
|
// FILLS the node: floor-only min height, no max (Image Pool pattern).
|
||||||
node._tgWidget = node.addDOMWidget("textgate_editor", "div", wrap, {
|
node._tgWidget = node.addDOMWidget("textgate_editor", "div", wrap, {
|
||||||
@@ -125,10 +173,15 @@ app.registerExtension({
|
|||||||
const d = e.detail || {};
|
const d = e.detail || {};
|
||||||
const node = app.graph?.getNodeById?.(parseInt(d.id, 10));
|
const node = app.graph?.getNodeById?.(parseInt(d.id, 10));
|
||||||
if (!node || node.type !== NODE || !node._tg) return;
|
if (!node || node.type !== NODE || !node._tg) return;
|
||||||
node._tg.area.value = d.text || "";
|
const incoming = d.text || "";
|
||||||
node._tg.status.textContent = "edit, then Pass";
|
// Sticky edit: keep the current editor text when the upstream input is
|
||||||
|
// unchanged (the Run-from-here case, upstream cached), so the gate re-runs
|
||||||
|
// YOUR version. Only overwrite on a genuine upstream change.
|
||||||
|
const unchanged = node._tgInput !== undefined && incoming === node._tgInput;
|
||||||
|
if (!unchanged) node._tg.area.value = incoming;
|
||||||
|
node._tgInput = incoming;
|
||||||
|
setState(node, "paused");
|
||||||
try { node._tg.area.focus(); } catch (err) { /* ignore */ }
|
try { node._tg.area.focus(); } catch (err) { /* ignore */ }
|
||||||
node.setDirtyCanvas?.(true, true);
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user