feat(ui): Engine and Modules settings sections
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
4aa8796ab7
commit
e1d8e3da16
@@ -0,0 +1,32 @@
|
||||
// Engine: the settings every installed module has volunteered, by category.
|
||||
import { composer as c } from "../../composer/composer.js";
|
||||
import { settingRow, whenSatisfied } from "./field.js";
|
||||
|
||||
const CATEGORIES = [["continuity", "Continuity"], ["guidance", "Guidance"], ["conditioning", "Conditioning"], ["sampling", "Sampling"], ["post", "Post"], ["system", "System"], ["", "Other"]];
|
||||
|
||||
export const engine = (app) => function mount() {
|
||||
const ps = app.pipeline;
|
||||
const page = c.region.stack({ gap: "md" });
|
||||
let category = null;
|
||||
|
||||
function draw() {
|
||||
if (ps.loading()) return page.set([c.hint.default({ text: "Loading…" })]);
|
||||
if (ps.loadError()) return page.set([c.banner.warn({ text: `Could not load Engine settings: ${ps.loadError()}` })]);
|
||||
const withSettings = ps.activeModules().filter((m) => Object.keys(m.settings || {}).length);
|
||||
const cats = CATEGORIES.filter(([k]) => withSettings.some((m) => (m.category || "") === k));
|
||||
if (!cats.length) return page.set([c.emptyState.default({ icon: "⚡", title: "Nothing to set", hint: "No installed module exposes a setting for this pipeline." })]);
|
||||
if (!cats.some(([k]) => k === category)) category = cats[0][0];
|
||||
const values = ps.currentValues(), notes = ps.saveNotes();
|
||||
const change = async (id, name, value) => { await ps.setModuleValue(id, name, value); draw(); };
|
||||
page.set([
|
||||
c.segmented.sm({ label: "Category", value: category, options: cats.map(([value, label]) => ({ value, label })), onChange: (v) => { category = v; draw(); } }),
|
||||
notes.length ? c.banner.info({ text: notes.join(" ") }) : null,
|
||||
...withSettings.filter((m) => (m.category || "") === category).flatMap((m) => [c.label.section({ text: m.title || m.id }),
|
||||
...Object.entries(m.settings).filter(([, spec]) => whenSatisfied(values, m.id, spec.when)).map(([name, spec]) => settingRow(m.id, name, spec, values, change))]),
|
||||
].filter(Boolean));
|
||||
}
|
||||
draw();
|
||||
ps.ensureLoaded().then(draw);
|
||||
const off = ps.subscribe(draw);
|
||||
return { node: page.node, destroy: () => { off(); page.node.remove(); } };
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
// One control for one module setting ({type, min, max, step, options, ui, hint, unit, default, when}). Engine settings
|
||||
// and anything else that edits the module-values tree draw their rows through here.
|
||||
import { composer as c } from "../../composer/composer.js";
|
||||
|
||||
/** A `when` clause names sibling settings on the same module that must hold a value for the row to show. */
|
||||
export const whenSatisfied = (values, id, when) => !when || Object.entries(when).every(([k, v]) => (values[id] || {})[k] === v);
|
||||
|
||||
export function settingRow(id, name, spec, values, onChange) {
|
||||
const label = spec.label || name;
|
||||
const hint = spec.hint ? spec.hint + (spec.unit ? ` (${spec.unit})` : "") : spec.unit || "";
|
||||
const now = values[id] && values[id][name];
|
||||
const value = now !== undefined ? now : spec.default;
|
||||
const set = (v) => onChange(id, name, v);
|
||||
let control;
|
||||
if (spec.type === "bool") return c.toggle.default({ label, hint, checked: Boolean(value), onChange: set }); // a toggle carries its own label and hint
|
||||
if (spec.type === "int" || spec.type === "float") {
|
||||
const slider = (spec.ui || "slider") === "slider" && spec.min != null && spec.max != null;
|
||||
const step = spec.step ?? (spec.type === "int" ? 1 : 0.01);
|
||||
control = slider ? c.slider.readout({ label, value, min: spec.min, max: spec.max, step, precision: spec.type === "int" ? 0 : 2, onCommit: set })
|
||||
: c.number.md({ label, value, min: spec.min, max: spec.max, step, precision: spec.type === "int" ? 0 : undefined, onChange: set });
|
||||
} else if (spec.type === "enum") control = c.select.md({ label, value, options: (spec.options || []).map((o) => ({ value: o.value, label: o.label || o.value })), onChange: set });
|
||||
else if (spec.type === "color") control = c.color.swatch({ label, value: value || "#000000", onChange: set });
|
||||
else control = (spec.type === "multiline" ? c.textarea.md({ label, value: value || "", rows: 3, onCommit: set }) : c.input.md({ label, value: value || "", onCommit: set }));
|
||||
return c.settingsRow.default({ label, hint, control });
|
||||
}
|
||||
@@ -3,6 +3,8 @@ import { composer as c } from "../../composer/composer.js";
|
||||
import { about } from "./about.js";
|
||||
import { appearance } from "./appearance.js";
|
||||
import { models } from "./models.js";
|
||||
import { engine } from "./engine.js";
|
||||
import { modules } from "./modules.js";
|
||||
|
||||
const later = (what) => function mount() {
|
||||
return c.emptyState.default({ icon: "◌", title: "Not built yet", hint: what });
|
||||
@@ -19,11 +21,11 @@ export default {
|
||||
add("editor", "Editor", "How the editor behaves. The open project remembers these, so they follow it to another machine.", "☰", "neutral",
|
||||
later("Upscale model, prompt autocomplete, shortcut ideas, i2v options."), "", "upscale autocomplete shortcuts ideas preferences");
|
||||
add("engine", "Engine", "Settings every installed module has volunteered, grouped by what they're for.", "⚡", "warn",
|
||||
later("Continuity, guidance, conditioning, sampling and post settings of the installed modules."), "Generation", "engine module settings");
|
||||
engine(app), "Generation", "engine module settings");
|
||||
add("models", "Models & Pipeline", "Loaders and nodes wired into the live pipeline.", "⬡", "accent",
|
||||
models(app), "Generation", "models loaders unet vae clip lora");
|
||||
add("modules", "Modules", "Switch a module off for this project; see and re-enable ones that failed.", "☷", "neutral",
|
||||
later("Per-module on/off for this project, and the ones that failed to load."), "Generation", "modules enable disable quarantine");
|
||||
modules(app), "Generation", "modules enable disable quarantine");
|
||||
add("refinement", "Refinement & Taste", "Learned-taste state: refinement keys and the Absolute global-taste store.", "✦", "danger",
|
||||
later("Refinement keys: export, delete; the taste store."), "Learning", "refinement taste keys rating");
|
||||
add("system", "Updates & ComfyUI", "Server connection, FunPack code updates, pipeline health.", "⟳", "good",
|
||||
|
||||
@@ -4,7 +4,6 @@ import { composer as c } from "../../composer/composer.js";
|
||||
|
||||
const widgetControl = (w, current, set) => {
|
||||
const label = w.name;
|
||||
if (w.type === "BOOLEAN") return c.toggle.default({ label, checked: Boolean(current), onChange: set });
|
||||
if (w.type === "COMBO") {
|
||||
const choices = [...(w.choices || [])];
|
||||
if (current != null && !choices.includes(current)) choices.unshift(current); // a file gone from disk still shows, so saving something else cannot swap it out
|
||||
@@ -41,7 +40,7 @@ export const models = (app) => function mount() {
|
||||
const spec = specs[slot.node];
|
||||
if (!spec) return [c.hint.default({ text: `${slot.node} is not installed — this slot can't be edited or run.` })];
|
||||
const rows = (spec.widgets || []).filter((w) => !Array.isArray(slot.inputs && slot.inputs[w.name])) // fed by another node: edited there
|
||||
.map((w) => c.settingsRow.default({ label: w.name, hint: w.tooltip || "", control: widgetControl(w, slot.inputs && slot.inputs[w.name] !== undefined ? slot.inputs[w.name] : w.default, set(slot, w.name)) }));
|
||||
.map((w) => w.type === "BOOLEAN" ? c.toggle.default({ label: w.name, hint: w.tooltip || "", checked: Boolean(slot.inputs && slot.inputs[w.name] !== undefined ? slot.inputs[w.name] : w.default), onChange: set(slot, w.name) }) : c.settingsRow.default({ label: w.name, hint: w.tooltip || "", control: widgetControl(w, slot.inputs && slot.inputs[w.name] !== undefined ? slot.inputs[w.name] : w.default, set(slot, w.name)) }));
|
||||
return [c.header.sm({ text: spec.title || slot.node }), ...rows];
|
||||
})]),
|
||||
].filter(Boolean));
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
// Modules: switch a module off for this project, and bring back the ones the server turned off after a failure.
|
||||
import { composer as c } from "../../composer/composer.js";
|
||||
|
||||
const CATEGORIES = [["continuity", "Continuity"], ["guidance", "Guidance"], ["conditioning", "Conditioning"], ["sampling", "Sampling"], ["post", "Post"], ["system", "System"], ["", "Other"]];
|
||||
|
||||
export const modules = (app) => function mount() {
|
||||
const ps = app.pipeline;
|
||||
const page = c.region.stack({ gap: "md" });
|
||||
|
||||
function draw() {
|
||||
if (ps.loading()) return page.set([c.hint.default({ text: "Loading…" })]);
|
||||
if (ps.loadError()) return page.set([c.banner.warn({ text: `Could not load: ${ps.loadError()}` })]);
|
||||
const control = ps.control();
|
||||
const mine = Object.values(ps.modulesById()).filter((m) => control[m.id] && control[m.id].controllable);
|
||||
if (!mine.length) return page.set([c.emptyState.default({ icon: "☷", title: "Nothing to switch", hint: "No installed module can be switched off." })]);
|
||||
const name = (m) => m.title || m.id;
|
||||
const row = (m) => {
|
||||
const held = control[m.id].quarantine;
|
||||
if (held) {
|
||||
return c.settingsRow.default({ label: `${name(m)} — failed`, hint: `Off since ${held.when || "a failure"}: ${held.reason || "an error"}`,
|
||||
control: c.button.sm({ label: "Turn back on", tone: "ghost", onClick: async () => {
|
||||
try { await app.api.releaseModule(m.id); } catch (err) { c.toast.warn({ text: `Could not turn ${name(m)} back on: ${err.message}` }); }
|
||||
await ps.refreshControl(); draw();
|
||||
} }) });
|
||||
}
|
||||
return c.toggle.default({ label: name(m), hint: ps.isOff(m.id) ? "Off for this project" : undefined, checked: !ps.isOff(m.id),
|
||||
onChange: (on) => ps.setOff(m.id, !on).then(draw) });
|
||||
};
|
||||
page.set([c.hint.default({ text: "A module you turn off here is off for this project only, and disappears from Engine settings. One that failed while generating is off for every project until its code is repaired or you turn it back on." }),
|
||||
...CATEGORIES.flatMap(([key, label]) => { const list = mine.filter((m) => (m.category || "") === key); return list.length ? [c.label.section({ text: label }), ...list.map(row)] : []; })]);
|
||||
}
|
||||
draw();
|
||||
ps.ensureLoaded().then(() => ps.refreshControl()).then(draw);
|
||||
const off = ps.subscribe(draw);
|
||||
return { node: page.node, destroy: () => { off(); page.node.remove(); } };
|
||||
};
|
||||
@@ -22,6 +22,7 @@ export const api = {
|
||||
pipeline: () => call("GET", "/api/pipeline"),
|
||||
editPipeline: (body) => call("POST", "/api/pipeline", body),
|
||||
modules: (traits) => call("GET", `/api/modules${traits ? `?traits=${encodeURIComponent(traits)}` : ""}`),
|
||||
releaseModule: (id) => call("POST", "/api/control/release", { id }),
|
||||
describeNodes: (classes) => call("GET", `/api/nodes?classes=${encodeURIComponent(classes.join(","))}`),
|
||||
system: () => call("GET", "/api/system"),
|
||||
gitStatus: () => call("GET", "/api/git/status?remote=0"),
|
||||
|
||||
Reference in New Issue
Block a user