fix(ui): Chat reads the shared last-run record, refreshes after a run, undo keeps the chosen pipeline, a throwing input hook is a note
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
74cbd3c3bb
commit
b3717973fe
+1
-1
@@ -29,7 +29,7 @@ linkPipeline({ project, pipeline, onOpen: (fn) => on((what) => { if (what === "o
|
||||
const frame = build(document.getElementById("app"));
|
||||
const selection = createSelection({ project });
|
||||
selection.on(() => say("select"));
|
||||
const app = { editorSettings: [], inputHooks: [], project, pipeline, generate, api, on, say, title: frame.setZoneTitle, selection, playhead: createPlayhead(), theme: window.ComposerTheme, mode: createMode(), panel: frame.togglePanel, closePanels: frame.closePanels, dock: createDock(), maintenance: createMaintenance({ api, flush: async () => { await project.flush(); if (project.unsaved) throw new Error("Your latest edits could not be saved, so nothing was changed. Try again in a moment."); } }), keys: createKeys(), resetLayout: () => { frame.resetLayout(); app.dock.reset(); } };
|
||||
const app = { editorSettings: [], inputHooks: [], lastRun: { typed: undefined, n: 0 }, project, pipeline, generate, api, on, say, title: frame.setZoneTitle, selection, playhead: createPlayhead(), theme: window.ComposerTheme, mode: createMode(), panel: frame.togglePanel, closePanels: frame.closePanels, dock: createDock(), maintenance: createMaintenance({ api, flush: async () => { await project.flush(); if (project.unsaved) throw new Error("Your latest edits could not be saved, so nothing was changed. Try again in a moment."); } }), keys: createKeys(), resetLayout: () => { frame.resetLayout(); app.dock.reset(); } };
|
||||
const settings = createSettings({ menubar: hostFor("menubar.menus") });
|
||||
const result = await loadFeatures(paths, { app });
|
||||
settings.addButton();
|
||||
|
||||
@@ -15,7 +15,7 @@ export function chatHook({ project, scene, slots }) {
|
||||
|
||||
export function chat(app, own) {
|
||||
const p = app.project, page = c.region.stack({ gap: "sm" });
|
||||
let last = null, alive = true, key = "", draft = "";
|
||||
let last = null, alive = true, key = "", draft = "", runs = app.lastRun.n;
|
||||
own(() => { alive = false; });
|
||||
const rounds = () => { const r = p.pref("enhance_chat", []); return Array.isArray(r) ? r : []; };
|
||||
const typed = () => { // the prompt a comment is about: the picked scene's own text, as typed
|
||||
@@ -27,7 +27,7 @@ export function chat(app, own) {
|
||||
function draw(force) {
|
||||
if (!alive) return;
|
||||
const list = rounds(), ok = last && last.ok, next = JSON.stringify([p.project && p.project.id, list, last && last.after]);
|
||||
if (!force && next === key) return;
|
||||
if (!force && (next === key || (page.node.contains(document.activeElement) && document.activeElement.tagName === "TEXTAREA"))) return; // not under the caret
|
||||
key = next;
|
||||
if (!p.project) return page.set([c.hint.default({ text: "Open a project first." })]);
|
||||
const bubbles = [...list.flatMap((r) => [first(r) ? c.text.sm({ text: `Model: ${first(r)}` }) : null, c.text.sm({ text: `You: ${r.comment}` })]),
|
||||
@@ -38,7 +38,7 @@ export function chat(app, own) {
|
||||
const comment = draft.trim(), now = rounds();
|
||||
if (!comment) return;
|
||||
const prev = now.length ? first(now[now.length - 1]) : null, here = typed();
|
||||
const fresh = ok && last.after !== prev && app.enhanceTyped !== undefined && app.enhanceTyped === here; // a rewrite of THIS prompt that nobody has commented on yet
|
||||
const fresh = ok && last.after !== prev && app.lastRun.typed !== undefined && app.lastRun.typed === here; // a rewrite of THIS prompt that nobody has commented on yet
|
||||
draft = "";
|
||||
p.setPref("enhance_chat", [...now, { rewrites: fresh ? { whole: last.after } : {}, comment, ...(here === undefined ? {} : { original: here }) }]);
|
||||
draw(true);
|
||||
@@ -48,7 +48,7 @@ export function chat(app, own) {
|
||||
c.toolbar.default({ items: [c.button.sm({ label: "Send", tone: "primary", onClick: send })], trailing: [c.button.sm({ label: "Reset", tone: "ghost", disabled: !list.length, onClick: () => { p.setPref("enhance_chat", []); draw(true); } })] })]);
|
||||
}
|
||||
const look = () => app.api.enhancerRuns().then((r) => { last = (r.runs || []).slice(-1)[0] || null; draw(); }).catch(() => {});
|
||||
own(app.on((what) => { if (what === "open") look(); else draw(); }));
|
||||
own(app.on((what) => { if (what === "open" || app.lastRun.n !== runs) { runs = app.lastRun.n; look(); } else draw(); }));
|
||||
draw(true);
|
||||
look();
|
||||
return page;
|
||||
|
||||
@@ -56,7 +56,6 @@ export default {
|
||||
needs: ["project", "api", "pipeline"],
|
||||
setup({ host, app }) {
|
||||
let win = null, owned = [];
|
||||
app.inputHooks.push(chatHook);
|
||||
const cleanup = () => { owned.forEach((f) => f()); owned = []; };
|
||||
const open = () => {
|
||||
if (win) return;
|
||||
@@ -67,6 +66,7 @@ export default {
|
||||
onClose: () => { cleanup(); win = null; } });
|
||||
};
|
||||
host.append(c.button.sm({ label: "◆ Composer", tone: "neutral", onClick: open }).node);
|
||||
app.inputHooks.push(chatHook); // last, so a setup that failed above leaves nothing behind
|
||||
return () => { cleanup(); app.inputHooks.splice(app.inputHooks.indexOf(chatHook) >>> 0, 1); };
|
||||
},
|
||||
};
|
||||
|
||||
@@ -14,7 +14,7 @@ const rig = (phase, extra = {}) => {
|
||||
waitForTerminal: () => Object.assign(Promise.resolve("done"), { cancel() {} }), ...extra };
|
||||
const project = { project: doc, get scenes() { return doc.scenes; }, selected: null,
|
||||
edit: (fn) => fn(doc), editFor: async (_id, fn) => fn(doc) };
|
||||
const app = { project, pipeline: { slots: () => [] }, generate: g, api: { newTasteGeneration: () => Promise.resolve({}) }, on: () => () => {} };
|
||||
const app = { project, lastRun: { n: 0 }, pipeline: { slots: () => [] }, generate: g, api: { newTasteGeneration: () => Promise.resolve({}) }, on: () => () => {} };
|
||||
const host = document.createElement("div");
|
||||
hub.setup({ host, app });
|
||||
return { handlers, doc, host };
|
||||
|
||||
@@ -24,7 +24,7 @@ export default {
|
||||
g.on("hold", () => { held = true; draw(); }); g.on("release", () => { held = false; draw(); }); // while the page asks ComfyUI whether a run is already going // the pipeline check's refusals: said where the person is looking
|
||||
let busy = false, stopped = false, held = false;
|
||||
|
||||
const record = (pid, unit, image, frames, promptId) => p.editFor(pid, (pr) => {
|
||||
const record = (pid, unit, image, frames, promptId) => { app.lastRun.n += 1; return p.editFor(pid, (pr) => {
|
||||
const group = pr.scenes.filter((s) => genUnitId(s) === unit);
|
||||
if (!group.length) return false;
|
||||
const secs = (frames || group.reduce((t, s) => t + effFrames(s, pr), 0) - (group.length - 1)) / (pr.frame_rate || 25); // as queued, not as the project reads now
|
||||
@@ -35,7 +35,7 @@ export default {
|
||||
if (!(s.cut_offset_frames > 0)) s.rating = ""; // a new render has not been rated yet // a fresh render was made at this length: an earlier trim's window no longer applies
|
||||
});
|
||||
return true;
|
||||
}).catch(() => tell("The result could not be saved to its project."));
|
||||
}).catch(() => tell("The result could not be saved to its project.")); };
|
||||
|
||||
async function runUnits(units) {
|
||||
if (busy) return;
|
||||
@@ -53,7 +53,7 @@ export default {
|
||||
// The unit is made once at the length of its clips together (each cut adds one shared frame).
|
||||
const frames = group.reduce((t, s) => t + effFrames(s, p.project), 0) - (group.length - 1);
|
||||
const { inputs, unwired, noPrompt, notes } = await buildInputs({ project: p.project, scene: root, slots: app.pipeline.slots(), expand, frames, hooks: app.inputHooks });
|
||||
app.enhanceTyped = (root.text || "").trim(); // what a Chat comment made now would be about
|
||||
app.lastRun.typed = (root.text || "").trim(); // what a Chat comment made now would be about
|
||||
if (stopped) break;
|
||||
if (noPrompt && !made) tell("This pipeline has no prompt input, so the scene text is not sent.");
|
||||
notes.forEach(tell);
|
||||
|
||||
@@ -30,7 +30,8 @@ export async function buildInputs({ project, scene, slots, expand, frames, hooks
|
||||
// Features that add inputs of their own (a Chat comment for the enhancer, say) say so through a hook: { inputs: {slot: {name: value}}, notes: [text] }.
|
||||
const notes = [];
|
||||
for (const hook of hooks) {
|
||||
const out = hook({ project, scene, slots }) || {};
|
||||
let out;
|
||||
try { out = hook({ project, scene, slots }) || {}; } catch (err) { notes.push(`An add-on's input could not be added to this run: ${err.message}`); continue; }
|
||||
for (const [id, fields] of Object.entries(out.inputs || {})) raw[id] = { ...raw[id], ...fields };
|
||||
notes.push(...(out.notes || []));
|
||||
}
|
||||
|
||||
@@ -48,3 +48,9 @@ test("a hook's inputs are merged and its notes come back", async () => {
|
||||
assert.equal(r.inputs.k.extra, "x");
|
||||
assert.deepEqual(r.notes, ["said"]);
|
||||
});
|
||||
|
||||
test("a hook that throws does not stop the run: its problem comes back as a note", async () => {
|
||||
const r = await buildInputs({ project: {}, scene: { text: "x" }, slots, hooks: [() => { throw new Error("boom"); }], expand: async () => null });
|
||||
assert.equal(r.inputs.p.text, "x");
|
||||
assert.match(r.notes[0], /boom/);
|
||||
});
|
||||
|
||||
@@ -53,6 +53,8 @@ const put = (project) => json("PUT", `${BASE}/${encodeURIComponent(project.id)}`
|
||||
* `onChange` fires for anything that alters what is on screen, including the
|
||||
* selection. Nothing here draws; the timeline subscribes.
|
||||
*/
|
||||
const KEPT = ["editor_settings", "models"];
|
||||
|
||||
export function createProject({ onChange, onError, onOpen, keepConsistent } = {}) {
|
||||
let project = null;
|
||||
let selected = null;
|
||||
@@ -107,9 +109,9 @@ export function createProject({ onChange, onError, onOpen, keepConsistent } = {}
|
||||
// before this must not still be allowed to land on top of it once its
|
||||
// own fetch catches up.
|
||||
generation += 1;
|
||||
const prefs = project.editor_settings; // preferences are not edits: stepping back never takes them with it
|
||||
const kept = KEPT.map((k) => project[k]); // preferences and the chosen pipeline are not edits: stepping back never takes them with it
|
||||
project = was.project;
|
||||
if (prefs !== undefined) project.editor_settings = prefs;
|
||||
KEPT.forEach((k, i) => { if (kept[i] !== undefined) project[k] = kept[i]; });
|
||||
selected = was.selected;
|
||||
// The scene that was selected may be gone -- stepping FORWARD into a state
|
||||
// where it had been removed. Fall back rather than leaving a selection that
|
||||
|
||||
@@ -651,3 +651,13 @@ test("an undo takes edits back but never an editor preference", async () => {
|
||||
p.redo();
|
||||
assert.equal(p.pref("upscaleMode", "never"), "always");
|
||||
});
|
||||
|
||||
test("an undo keeps the chosen pipeline too", async () => {
|
||||
server();
|
||||
const p = createProject({});
|
||||
await p.start();
|
||||
p.setVideo("width", 832);
|
||||
p.setField("models", { slots: ["B"] }, { quiet: true });
|
||||
p.undo();
|
||||
assert.deepEqual(p.project.models, { slots: ["B"] });
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user