fix: Inspector keeps a pressed button through its click; not-taught reason trusts the server's label; no reload flicker at the end; keyboard arrows commit like typing
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
05ade5db82
commit
ec0021892d
@@ -84,9 +84,16 @@ define("number", "md", ({ value = 0, min, max, step = 1, precision, unit, onChan
|
||||
};
|
||||
// "change" is the browser's commit: each spinner-arrow click, the wheel, and leaving a typed edit. Blur alone missed
|
||||
// the arrows, so a redraw put the old value back. Typing does not fire it, so mid-typing values stay typeable.
|
||||
input.addEventListener("change", commit);
|
||||
input.addEventListener("blur", commit);
|
||||
input.addEventListener("keydown", (e) => { if (e.key === "Enter") { e.preventDefault(); commit(); } });
|
||||
// Keyboard arrows count as typing (committed on Enter or leaving): committed per press, a panel that redraws on a change
|
||||
// would rebuild the field under the caret and the second press would go nowhere.
|
||||
let keyed = false;
|
||||
input.addEventListener("change", () => { if (keyed) { keyed = false; return; } commit(); });
|
||||
input.addEventListener("blur", () => { keyed = false; commit(); });
|
||||
input.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter") { e.preventDefault(); keyed = false; commit(); }
|
||||
else if (/^(ArrowUp|ArrowDown|PageUp|PageDown)$/.test(e.key)) keyed = true;
|
||||
});
|
||||
input.addEventListener("pointerdown", () => { keyed = false; });
|
||||
|
||||
const node = unit
|
||||
? el("span", { cls: "cx-number-wrap", children: [input, el("span", { cls: "cx-unit", text: unit })] })
|
||||
|
||||
@@ -353,6 +353,12 @@ test("a spinner-arrow click commits at once; leaving afterwards does not save it
|
||||
assert.deepEqual(seen, [4]);
|
||||
fire(input, "blur");
|
||||
assert.deepEqual(seen, [4]);
|
||||
input.dispatchEvent(new window.KeyboardEvent("keydown", { key: "ArrowUp", bubbles: true }));
|
||||
input.value = "5";
|
||||
fire(input, "change");
|
||||
assert.deepEqual(seen, [4], "a keyboard arrow is typing: not committed per press");
|
||||
fire(input, "blur");
|
||||
assert.deepEqual(seen, [4, 5]);
|
||||
});
|
||||
|
||||
test("a number field ignores unparseable text on commit", () => {
|
||||
|
||||
@@ -27,3 +27,22 @@ test("a scene's text changed elsewhere (the Story box) shows in the Prompt box,
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
assert.equal(box().value, "while typing"); // caught up once the focus left
|
||||
});
|
||||
|
||||
test("typing in the Prompt box, then pressing a button in the panel, does not rebuild the button out from under the click", async () => {
|
||||
let fire;
|
||||
const doc = { id: "P", frame_rate: 25, num_frames_per_scene: 49, scenes: [{ id: "a", text: "old" }], scene_renders: {} };
|
||||
const p = { project: doc, get scenes() { return doc.scenes; }, get selected() { return doc.scenes[0]; }, selectedId: "a",
|
||||
setText: () => {}, setScene: () => {}, edit: (fn) => fn(doc) };
|
||||
const host = document.createElement("div");
|
||||
document.body.append(host);
|
||||
hub.setup({ host, app: { project: p, on: (fn) => { fire = fn; return () => {}; }, api: {}, say: () => {} } });
|
||||
const box = host.querySelector("textarea");
|
||||
box.focus();
|
||||
doc.scenes[0].text = "typed here"; fire("change"); // the box's own edit, as setText would announce it
|
||||
const button = box.closest("[aria-label=Properties]") ? [...box.closest("[aria-label=Properties]").querySelectorAll("button, input[type=checkbox]")].find((b) => b !== box) : null;
|
||||
assert.ok(button, "a control inside the panel body");
|
||||
button.focus(); // the mouse-down moves focus to it
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
assert.ok(button.isConnected, "the pressed button is still there for its mouse-up");
|
||||
host.remove();
|
||||
});
|
||||
|
||||
@@ -28,7 +28,9 @@ export default {
|
||||
// own box is being typed in: that would rebuild it under the caret. It catches up when the focus leaves.
|
||||
let shownText;
|
||||
const typing = () => body.node.contains(document.activeElement) && /^(input|textarea)$/i.test(document.activeElement.tagName);
|
||||
body.node.addEventListener("focusout", () => setTimeout(() => { if (!typing()) draw(); }));
|
||||
// Catch up once focus leaves the panel. Focus moving to a button or select inside it is a click under way: rebuilding
|
||||
// between its mouse-down and mouse-up dropped the click; the change it makes redraws afterwards anyway.
|
||||
body.node.addEventListener("focusout", (e) => { if (!(e.relatedTarget && body.node.contains(e.relatedTarget))) setTimeout(() => { if (!typing()) draw(); }); });
|
||||
function draw(force) { // not on every keystroke: that would rebuild the box being typed in
|
||||
const sc = p.selected, open = p.project;
|
||||
const next = `${tab}|${open && open.id}|${p.selectedId}|${tab === "project" && open ? [open.postfix_enabled !== false, open.generation_mode, open.export_size_from, open.width, open.height, open.scenes.map((s) => s.frames_mode === "custom"), Object.keys(open.scene_renders || {}), open.scenes.length] : ""}|${tab === "scene" && sc && open ? [effFrames(sc, open), effFps(sc, open), p.scenes.indexOf(sc), sc.frames_mode, sc.fps_mode, p.scenes.length, sc.source_image, (sc.references || []).join(), sc.source_in, sc.source_dur, sc.removed_from_plan, (sc.source || {}).type, sc.excluded, typing() ? shownText : (shownText = sc.text), ((open.scene_renders || {})[sc.id] || {}).durationSec, Boolean(((open.scene_renders || {})[sc.id] || {}).media), bin.version, ...sections.map((s) => { try { return s.key(sc, open); } catch { return ""; } })] : ""}`;
|
||||
|
||||
@@ -24,7 +24,7 @@ export default {
|
||||
const paint = () => { root.classList.toggle("fp-peek", on); place(); const z = zone(); if (sizer && z) sizer.observe(z); };
|
||||
paint();
|
||||
const tab = c.button.sm({ label: "Auto-hide", tone: "neutral", pressed: on, title: "Show the timeline only while the pointer is on it", onClick: () => {
|
||||
on = !on; paint(); tab.setPressed && tab.setPressed(on);
|
||||
on = !on; clearTimeout(timer); timer = null; root.classList.remove("fp-peek-open"); paint(); tab.setPressed && tab.setPressed(on);
|
||||
try { on ? localStorage.setItem(KEY, "1") : localStorage.removeItem(KEY); } catch { /* lasts until reload */ }
|
||||
} });
|
||||
host.append(tab.node);
|
||||
|
||||
@@ -106,7 +106,7 @@ export default {
|
||||
const v = viewer.element;
|
||||
if (seg && v && !v.paused) onTime(v); // timeupdate is too slow to catch a short clip's end
|
||||
else head.set(head.at + 0.04);
|
||||
if (head.at >= total()) { pause(); head.set(Math.min(head.at, total())); }
|
||||
if (head.at >= total()) { head.set(Math.min(head.at, total())); pause(); } // back on the end first: past it there is no clip, and the picture would drop and reload
|
||||
}, 40);
|
||||
function start() { if (!total()) return; if (head.at >= total() - 0.02) head.set(0); playing = true; sync(); }
|
||||
function pause() { playing = false; sync(); }
|
||||
|
||||
@@ -4,6 +4,15 @@ import { inPlace } from "../../shell/place.js";
|
||||
import { genUnitId, isGenerative, unitRoot } from "../../shell/scenes.js";
|
||||
import { CHOICES, FORGET, nameOf, tasteOf } from "./choices.js";
|
||||
|
||||
/** What to say when a rating taught nothing, or "" when it did. The run captured nothing, a learner is on and no key is
|
||||
* set: the missing key is why. Any other reason is the server's own (the key on screen now may not be the run's). */
|
||||
export function notTaught(r, ps) {
|
||||
if (!(r && r.why)) return "";
|
||||
const taste = ps.modulesById && ps.modulesById().taste;
|
||||
const keyless = r.reason === "nothing" && taste && ps.useful(taste) && !String((ps.currentValues().taste || {}).key || "").trim();
|
||||
return keyless ? "Taste not taught: no Taste key is set, so nothing learns from ratings. Name one in Settings ▸ Engine ▸ System ▸ Taste key." : `Taste not taught: ${r.why}.`;
|
||||
}
|
||||
|
||||
export default {
|
||||
id: "rating",
|
||||
mount: "timeline.actions",
|
||||
@@ -27,9 +36,8 @@ export default {
|
||||
const body = taste === "clear" ? { rating: null, axis: null } : taste;
|
||||
try {
|
||||
const r = await app.api.rateTaste(t.render.promptId, body.rating, body.axis);
|
||||
// with no key nothing is ever captured: that, not the server's list of other causes, is the reason
|
||||
const keyless = !String((app.pipeline.currentValues().taste || {}).key || "").trim();
|
||||
if (r && r.why) c.toast.warn({ text: keyless ? "Taste not taught: no Taste key is set, so nothing learns from ratings. Name one in Settings ▸ Engine ▸ System ▸ Taste key." : `Taste not taught: ${r.why}.` });
|
||||
const text = notTaught(r, app.pipeline);
|
||||
if (text) c.toast.warn({ text });
|
||||
} catch { /* no taste module here: the rating stays a label */ }
|
||||
};
|
||||
const draw = () => {
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { notTaught } from "./index.js";
|
||||
|
||||
const ps = (key, learning = true) => { const taste = { id: "taste" }; return { modulesById: () => ({ taste }), useful: () => learning, currentValues: () => ({ taste: { key } }) }; };
|
||||
|
||||
test("the not-taught reason blames a missing key only when the run captured nothing, a learner is on, and no key is set", () => {
|
||||
const nothing = { why: "this run captured nothing", reason: "nothing" }, forgotten = { why: "a new Generate started", reason: "forgotten" };
|
||||
assert.match(notTaught(nothing, ps("")), /no Taste key is set/);
|
||||
assert.match(notTaught(forgotten, ps("")), /new Generate started/, "the key cleared after the run is not why");
|
||||
assert.match(notTaught(nothing, ps("", false)), /captured nothing/, "nothing learns anyway: a key would not have helped");
|
||||
assert.match(notTaught(nothing, ps("portraits")), /captured nothing/);
|
||||
assert.equal(notTaught({ recorded: ["vel"], why: null }, ps("")), "");
|
||||
});
|
||||
@@ -262,8 +262,9 @@ def rate(prompt_id, rating, axis=None):
|
||||
return out
|
||||
|
||||
if not entry:
|
||||
out["reason"] = "forgotten" if prompt_id in state.get("forgot", []) else "nothing" # a label the app can act on; `why` is for people
|
||||
out["why"] = ("a new Generate started before this clip was rated, and unrated clips are forgotten then"
|
||||
if prompt_id in state.get("forgot", []) else
|
||||
if out["reason"] == "forgotten" else
|
||||
"this run captured nothing: no learning feature acted on it, or ComfyUI reused an "
|
||||
"earlier result instead of sampling again")
|
||||
return out
|
||||
|
||||
@@ -200,8 +200,9 @@ def test_a_clip_that_captured_nothing_is_not_blamed_on_a_later_generate():
|
||||
store.capture("k", "vel", {"a": torch.zeros(2)}, prompt_id="p1")
|
||||
store.new_generation()
|
||||
store.capture("k", "vel", {"a": torch.zeros(2)}, prompt_id="p2") # p2 ran and kept p1's forgetting on record
|
||||
assert "new Generate" in store.rate("p1", "liked")["why"]
|
||||
assert "captured nothing" in store.rate("p3", "liked")["why"]
|
||||
first, third = store.rate("p1", "liked"), store.rate("p3", "liked")
|
||||
assert "new Generate" in first["why"] and first["reason"] == "forgotten"
|
||||
assert "captured nothing" in third["why"] and third["reason"] == "nothing"
|
||||
|
||||
|
||||
def test_deleting_a_key_leaves_other_keys_runs_ratable_and_one_run_can_wait_under_two_keys():
|
||||
|
||||
Reference in New Issue
Block a user