feat(models): missing requirements are a compact list above the grid
One line each with a + on the right, dismissible (persisted, returns as a one-line link). Satisfied, optional and the guided-wiring rules fold away.
This commit is contained in:
@@ -1183,71 +1183,114 @@
|
||||
return slotProducesType(req.type, null);
|
||||
}
|
||||
|
||||
// Dismiss state for the requirements list. Persisted, because a pipeline someone is
|
||||
// deliberately leaving incomplete stays incomplete across reloads, and re-nagging every
|
||||
// time the panel opens is exactly what the dismiss is for.
|
||||
const REQ_HIDE_KEY = "funpack_models_req_hidden";
|
||||
function reqPanelHidden(next) {
|
||||
try {
|
||||
if (next === undefined) return localStorage.getItem(REQ_HIDE_KEY) === "1";
|
||||
if (next) localStorage.setItem(REQ_HIDE_KEY, "1");
|
||||
else localStorage.removeItem(REQ_HIDE_KEY);
|
||||
} catch (_) { /* private mode: the list just stays visible */ }
|
||||
return !!next;
|
||||
}
|
||||
|
||||
function requirementsPanel() {
|
||||
const sec = el("div", "req-panel");
|
||||
if (config.disable_core) {
|
||||
sec.append(el("div", "req-title", "Custom workflow"));
|
||||
// No FunPack core means no FunPack requirements — say so once, briefly, and get out
|
||||
// of the way. Everything else in this panel still works on the user's own nodes.
|
||||
sec.append(el("div", "req-empty",
|
||||
"Built-in FunPack pipeline is off. Wire your workflow nodes below; FunPack loader requirements do not apply."));
|
||||
"Custom workflow — the built-in pipeline is off, so FunPack's loader requirements "
|
||||
+ "do not apply. Wire your own nodes below."));
|
||||
return sec;
|
||||
}
|
||||
if (pipelineLocked()) {
|
||||
if (reqPanelHidden()) {
|
||||
const missingNow = requirements.filter((r) => r.required && !requirementSatisfied(r));
|
||||
if (!missingNow.length) return sec; // nothing to say; stay quiet
|
||||
const line = el("div", "req-collapsed");
|
||||
line.append(el("span", "req-dot", "✕"));
|
||||
line.append(el("span", null, `${missingNow.length} missing in pipeline`));
|
||||
const show = el("button", "btn ghost tiny", "Show");
|
||||
show.onclick = () => { reqPanelHidden(false); render(); };
|
||||
line.append(show);
|
||||
sec.append(line);
|
||||
return sec;
|
||||
}
|
||||
// The wiring rules are reference material, not a warning — folded away so the first
|
||||
// thing in this panel is the short list of what is actually missing.
|
||||
function guidedRulesFold() {
|
||||
// The latent and audio path differ by family — H3 makes both streams in one node
|
||||
// that feeds the sampler directly, so pointing at Studio · latent here would send
|
||||
// the user to a port its graph does not even have.
|
||||
const h3 = familyKey() === "minimax_h3";
|
||||
sec.append(el("div", "req-hint guided-hint",
|
||||
const det = el("details", "req-fold");
|
||||
det.append(el("summary", "req-fold-sum", "Guided wiring rules"));
|
||||
det.append(el("div", "req-hint guided-hint",
|
||||
(h3
|
||||
? "Guided wiring: the AV LATENT → Chain Sampler · latent_template (Empty MiniMax H3 "
|
||||
? "The AV LATENT → Chain Sampler · latent_template (Empty MiniMax H3 "
|
||||
+ "AV Latent makes both streams). Audio VAE → VAE Decode Audio · vae, and optionally "
|
||||
+ "also Chain Sampler · audio_vae to encode audio references. "
|
||||
: "Guided wiring: LATENT → Studio · latent (core forwards to Concat · video_latent). "
|
||||
: "LATENT → Studio · latent (core forwards to Concat · video_latent). "
|
||||
+ "Audio latent → Concat · audio_latent only. ")
|
||||
+ "IMAGE → Studio · source_image (Input Image Processing defaults to Timeline scene image). "
|
||||
+ "MODEL/CLIP may chain through LoRA. "
|
||||
+ "Enable Full control for manual rewiring."));
|
||||
return det;
|
||||
}
|
||||
sec.append(el("div", "req-title", "Pipeline requirements"));
|
||||
|
||||
if (!requirements.length) {
|
||||
sec.append(el("div", "req-title", "Pipeline requirements"));
|
||||
sec.append(el("div", "req-empty", "Requirements not loaded — open Models to refresh."));
|
||||
return sec;
|
||||
}
|
||||
|
||||
const required = requirements.filter((r) => r.required);
|
||||
const optional = requirements.filter((r) => !r.required);
|
||||
const allOk = required.every(requirementSatisfied);
|
||||
const missing = required.filter((r) => !requirementSatisfied(r));
|
||||
const allOk = !missing.length;
|
||||
|
||||
required.forEach((req) => {
|
||||
// One line per requirement: dot, name, type, and — only when it is missing and we know
|
||||
// which role fills it — a + on the right. The hint used to be a paragraph inside the
|
||||
// row, which made every missing item a block three times the height of a satisfied one.
|
||||
function reqRow(req, cls) {
|
||||
const ok = requirementSatisfied(req);
|
||||
const row = el("div", "req-row" + (ok ? " ok" : " missing"));
|
||||
row.append(el("span", "req-dot", ok ? "✓" : "✕"));
|
||||
const lbl = el("span", "req-label", req.label);
|
||||
const badge = el("span", "req-type", req.type);
|
||||
row.append(lbl); row.append(badge);
|
||||
if (!ok) {
|
||||
const hint = el("div", "req-hint", req.hint);
|
||||
row.append(hint);
|
||||
if (req.role_hint) {
|
||||
const addBtn = el("button", "btn ghost tiny req-add", "+ Add");
|
||||
addBtn.onclick = () => openNodeSetup(req.role_hint);
|
||||
row.append(addBtn);
|
||||
}
|
||||
const row = el("div", "req-row" + cls + (ok ? " ok" : (req.required ? " missing" : "")));
|
||||
row.append(el("span", "req-dot", ok ? "✓" : (req.required ? "✕" : "·")));
|
||||
row.append(el("span", "req-label", req.label));
|
||||
row.append(el("span", "req-type", req.type));
|
||||
if (req.hint) row.title = req.hint;
|
||||
if (!ok && req.required && req.role_hint) {
|
||||
const addBtn = el("button", "btn ghost tiny req-add", "+");
|
||||
addBtn.title = `Add a ${req.label} — ${req.hint || "picks the node for you"}`;
|
||||
addBtn.onclick = () => openNodeSetup(req.role_hint);
|
||||
row.append(addBtn);
|
||||
}
|
||||
sec.append(row);
|
||||
});
|
||||
return row;
|
||||
}
|
||||
|
||||
if (optional.length) {
|
||||
const optHead = el("div", "req-opt-head", "Optional");
|
||||
sec.append(optHead);
|
||||
optional.forEach((req) => {
|
||||
const ok = requirementSatisfied(req);
|
||||
const row = el("div", "req-row opt" + (ok ? " ok" : ""));
|
||||
row.append(el("span", "req-dot", ok ? "✓" : "·"));
|
||||
row.append(el("span", "req-label", req.label));
|
||||
row.append(el("span", "req-type", req.type));
|
||||
sec.append(row);
|
||||
});
|
||||
const head = el("div", "req-head");
|
||||
head.append(el("span", "req-title", missing.length
|
||||
? `Missing in pipeline · ${missing.length}` : "Pipeline requirements"));
|
||||
// Dismissable, because a half-built pipeline the user is deliberately leaving half-built
|
||||
// should not keep shouting. It comes back as a one-line link, never silently.
|
||||
const hide = el("button", "req-dismiss", "✕");
|
||||
hide.title = "Hide this list";
|
||||
hide.onclick = () => { reqPanelHidden(true); render(); };
|
||||
head.append(hide);
|
||||
sec.append(head);
|
||||
|
||||
// Missing first: it is the only part that needs doing.
|
||||
missing.forEach((req) => sec.append(reqRow(req, "")));
|
||||
const satisfied = required.filter(requirementSatisfied);
|
||||
if (satisfied.length || optional.length) {
|
||||
const det = el("details", "req-fold");
|
||||
det.open = false;
|
||||
det.append(el("summary", "req-fold-sum",
|
||||
`Satisfied${optional.length ? " and optional" : ""} · ${satisfied.length + optional.length}`));
|
||||
satisfied.forEach((req) => det.append(reqRow(req, "")));
|
||||
optional.forEach((req) => det.append(reqRow(req, " opt")));
|
||||
sec.append(det);
|
||||
}
|
||||
|
||||
if (allOk && config.slots.length) {
|
||||
@@ -1262,6 +1305,7 @@
|
||||
sec.append(bar);
|
||||
}
|
||||
|
||||
if (pipelineLocked()) sec.append(guidedRulesFold());
|
||||
return sec;
|
||||
}
|
||||
|
||||
@@ -1750,8 +1794,10 @@
|
||||
pane.append(banner);
|
||||
}
|
||||
pane.append(familySection());
|
||||
pane.append(nodeGrid(v));
|
||||
// Above the node grid: what is missing is what the user came here to fix, and it used
|
||||
// to sit below a grid tall enough to push it off screen.
|
||||
pane.append(requirementsPanel());
|
||||
pane.append(nodeGrid(v));
|
||||
pane.append(coreSection());
|
||||
return pane;
|
||||
}
|
||||
|
||||
@@ -1479,9 +1479,19 @@ body.tl-spacing { cursor: grabbing; user-select: none; }
|
||||
/* config validation banner */
|
||||
/* Pipeline requirements panel */
|
||||
.req-panel { margin-bottom: 18px; }
|
||||
.req-title { font-size: 11px; color: var(--teal); margin-bottom: 8px; font-weight: 600;}
|
||||
.req-head { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
|
||||
.req-title { font-size: 11px; color: var(--teal); font-weight: 600; }
|
||||
.req-dismiss { margin-left: auto; background: none; border: 0; cursor: pointer;
|
||||
color: var(--faint); font-size: 12px; line-height: 1; padding: 2px 4px; border-radius: 4px; }
|
||||
.req-dismiss:hover { color: var(--fg); background: var(--line); }
|
||||
.req-collapsed { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--muted); }
|
||||
.req-collapsed .req-dot { color: var(--danger); }
|
||||
.req-fold { margin-top: 6px; }
|
||||
.req-fold-sum { font-size: 11px; color: var(--faint); cursor: pointer; padding: 4px 0; }
|
||||
.req-opt-head { font-size: 11px; color: var(--faint); margin: 8px 0 4px; font-weight: 600;}
|
||||
.req-row { display: flex; align-items: flex-start; gap: 8px; padding: 6px 10px; border-radius: 6px; margin-bottom: 4px; font-size: 12px; flex-wrap: wrap; }
|
||||
/* One line, never wrapping: the row is a status, not a paragraph. */
|
||||
.req-row { display: flex; align-items: center; gap: 8px; padding: 5px 10px; border-radius: 6px; margin-bottom: 3px; font-size: 12px; }
|
||||
.req-row .req-label { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.req-row.ok { background: color-mix(in srgb, var(--teal) 8%, transparent); }
|
||||
.req-row.missing { background: color-mix(in srgb, var(--danger) 8%, transparent); border: 1px solid color-mix(in srgb, var(--danger) 30%, transparent); }
|
||||
.req-row.opt { background: transparent; opacity: 0.7; }
|
||||
@@ -1491,7 +1501,9 @@ body.tl-spacing { cursor: grabbing; user-select: none; }
|
||||
.req-label { flex: 1; }
|
||||
.req-type { font-family: var(--font-mono); font-size: 10px; color: var(--faint); flex: 0 0 auto; padding: 1px 5px; border: 1px solid var(--line); border-radius: 3px; }
|
||||
.req-hint { width: 100%; margin-top: 3px; margin-left: 22px; font-size: 11px; color: var(--muted); }
|
||||
.req-add { flex: 0 0 auto; }
|
||||
/* Square, so it reads as an action on the row rather than a second label. */
|
||||
.req-add { flex: 0 0 auto; width: 20px; height: 20px; padding: 0; line-height: 1;
|
||||
display: inline-flex; align-items: center; justify-content: center; font-size: 14px; }
|
||||
.req-empty { color: var(--faint); font-size: 12px; padding: 8px 0; }
|
||||
|
||||
.valid-banner { display: flex; align-items: center; gap: 9px; padding: 9px 12px; border-radius: 8px; margin-bottom: 14px; font-size: 12px; border: 1px solid var(--line); }
|
||||
|
||||
Reference in New Issue
Block a user