Fix FunPackConditioningAdjust UI: use native button widget instead of custom widget
addCustomWidget failed silently in this ComfyUI version. Switch to
node.addWidget("button",...) which is natively supported. Also add the
same hideWidget retry timing pattern Scene Builder uses (rAF + two
setTimeout calls) so the adjustments text widget reliably disappears
after the node is created or reconfigured.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
9c06051a74
commit
ffb447104e
@@ -0,0 +1,442 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { api } from "../../scripts/api.js";
|
||||
|
||||
const NODE_NAME = "FunPackConditioningAdjust";
|
||||
const HIDDEN_WIDGETS = new Set(["adjustments"]);
|
||||
const CATEGORY_ORDER = ["action", "camera", "subject", "appearance", "environment", "style", "quality", "details"];
|
||||
|
||||
let activePanel = null;
|
||||
|
||||
function widgetByName(node, name) {
|
||||
return (node.widgets || []).find((w) => w.name === name);
|
||||
}
|
||||
|
||||
function getAdjustments(node) {
|
||||
const widget = widgetByName(node, "adjustments");
|
||||
try {
|
||||
const parsed = JSON.parse(String(widget?.value || "[]"));
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function setAdjustments(node, items) {
|
||||
const widget = widgetByName(node, "adjustments");
|
||||
if (widget) widget.value = JSON.stringify(items);
|
||||
node.setDirtyCanvas?.(true, true);
|
||||
app.graph?.setDirtyCanvas?.(true, true);
|
||||
}
|
||||
|
||||
function hideWidget(widget) {
|
||||
if (!widget) return;
|
||||
widget.__funpackHidden = true;
|
||||
widget.hidden = true;
|
||||
widget.options = widget.options || {};
|
||||
widget.options.hidden = true;
|
||||
widget.computeSize = () => [0, -4];
|
||||
widget.computedHeight = 0;
|
||||
widget.type = "hidden";
|
||||
for (const key of ["element", "inputEl", "textElement", "parentEl"]) {
|
||||
const el = widget[key];
|
||||
if (el?.style) {
|
||||
el.style.display = "none";
|
||||
el.style.visibility = "hidden";
|
||||
el.style.pointerEvents = "none";
|
||||
}
|
||||
if (el) el.hidden = true;
|
||||
}
|
||||
}
|
||||
|
||||
function linkedRefinementKey(node) {
|
||||
const input = (node.inputs || []).find((i) => i.name === "refinement_key_input");
|
||||
const linkId = Array.isArray(input?.link) ? input.link[0] : input?.link;
|
||||
if (linkId == null) return "";
|
||||
const link = app.graph?.links?.[linkId];
|
||||
const source = link ? app.graph?.getNodeById?.(link.origin_id) : null;
|
||||
if (!source) return "";
|
||||
const selected = String(widgetByName(source, "refinement_key")?.value || "").trim();
|
||||
const typed = String(widgetByName(source, "key_name")?.value || "").trim();
|
||||
const key = selected && selected !== "-None-" ? selected : typed;
|
||||
return key || "";
|
||||
}
|
||||
|
||||
async function fetchPhraseMemory(key) {
|
||||
if (!key) return [];
|
||||
try {
|
||||
const res = await api.fetchApi(`/funpack/phrase_memory?key=${encodeURIComponent(key)}`, { cache: "no-store" });
|
||||
if (!res.ok) return [];
|
||||
const data = await res.json();
|
||||
return Array.isArray(data.phrases) ? data.phrases : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function closePanel() {
|
||||
activePanel?.remove();
|
||||
activePanel = null;
|
||||
}
|
||||
|
||||
function panelButton(label, cls = "") {
|
||||
const btn = document.createElement("button");
|
||||
btn.type = "button";
|
||||
btn.textContent = label;
|
||||
btn.className = `funpack-adj-button ${cls}`.trim();
|
||||
return btn;
|
||||
}
|
||||
|
||||
function renderPanel(node) {
|
||||
closePanel();
|
||||
injectStyles();
|
||||
|
||||
const root = document.createElement("div");
|
||||
root.className = "funpack-adj-panel";
|
||||
root.style.cssText = "position:fixed;left:50%;top:50%;transform:translate(-50%,-50%);z-index:10000;";
|
||||
document.body.append(root);
|
||||
activePanel = root;
|
||||
|
||||
const items = getAdjustments(node).map((item) => ({ ...item }));
|
||||
|
||||
// Header
|
||||
const header = document.createElement("div");
|
||||
header.className = "funpack-adj-header";
|
||||
const title = document.createElement("span");
|
||||
title.textContent = "Conditioning Adjustments";
|
||||
const closeBtn = panelButton("Close");
|
||||
closeBtn.addEventListener("click", () => {
|
||||
setAdjustments(node, items.filter((i) => String(i.phrase || "").trim()));
|
||||
closePanel();
|
||||
});
|
||||
header.append(title, closeBtn);
|
||||
root.append(header);
|
||||
|
||||
const hint = document.createElement("div");
|
||||
hint.className = "funpack-adj-hint";
|
||||
hint.textContent = "Each phrase is encoded by the connected CLIP. Positive strength pushes conditioning toward that phrase, negative away. Range: -1.0 to +1.0 (typical useful range: 0.05 - 0.30).";
|
||||
root.append(hint);
|
||||
|
||||
// Row list
|
||||
const list = document.createElement("div");
|
||||
list.className = "funpack-adj-list";
|
||||
root.append(list);
|
||||
|
||||
function renderRows() {
|
||||
list.replaceChildren();
|
||||
if (!items.length) {
|
||||
const empty = document.createElement("div");
|
||||
empty.className = "funpack-adj-empty";
|
||||
empty.textContent = "No adjustments yet. Add a phrase below or click a chip from the session bank.";
|
||||
list.append(empty);
|
||||
return;
|
||||
}
|
||||
for (let idx = 0; idx < items.length; idx++) {
|
||||
const item = items[idx];
|
||||
const row = document.createElement("div");
|
||||
row.className = "funpack-adj-row";
|
||||
|
||||
const phraseInput = document.createElement("input");
|
||||
phraseInput.type = "text";
|
||||
phraseInput.value = String(item.phrase || "");
|
||||
phraseInput.placeholder = "phrase or word";
|
||||
phraseInput.className = "funpack-adj-phrase";
|
||||
phraseInput.addEventListener("input", () => { item.phrase = phraseInput.value; });
|
||||
|
||||
const strengthInput = document.createElement("input");
|
||||
strengthInput.type = "number";
|
||||
strengthInput.value = String(Number.isFinite(item.strength) ? item.strength : 0.0);
|
||||
strengthInput.min = "-1";
|
||||
strengthInput.max = "1";
|
||||
strengthInput.step = "0.05";
|
||||
strengthInput.className = "funpack-adj-strength";
|
||||
strengthInput.addEventListener("input", () => {
|
||||
const v = parseFloat(strengthInput.value);
|
||||
item.strength = Number.isFinite(v) ? Math.max(-1, Math.min(1, v)) : 0.0;
|
||||
});
|
||||
|
||||
const del = panelButton("×", "danger compact");
|
||||
del.title = "Remove";
|
||||
del.addEventListener("click", () => { items.splice(idx, 1); renderRows(); });
|
||||
|
||||
row.append(phraseInput, strengthInput, del);
|
||||
list.append(row);
|
||||
}
|
||||
}
|
||||
renderRows();
|
||||
|
||||
// Footer
|
||||
const footer = document.createElement("div");
|
||||
footer.className = "funpack-adj-footer";
|
||||
const addBtn = panelButton("+ Add phrase", "primary");
|
||||
addBtn.addEventListener("click", () => {
|
||||
items.push({ phrase: "", strength: 0.1 });
|
||||
renderRows();
|
||||
list.querySelectorAll(".funpack-adj-phrase")[items.length - 1]?.focus();
|
||||
});
|
||||
const clearBtn = panelButton("Clear all", "danger");
|
||||
clearBtn.addEventListener("click", () => { items.length = 0; renderRows(); });
|
||||
footer.append(addBtn, clearBtn);
|
||||
root.append(footer);
|
||||
|
||||
// Session phrase bank
|
||||
const key = linkedRefinementKey(node);
|
||||
if (key) {
|
||||
const bankSection = document.createElement("div");
|
||||
bankSection.className = "funpack-adj-bank-section";
|
||||
const bankTitle = document.createElement("div");
|
||||
bankTitle.className = "funpack-adj-bank-title";
|
||||
bankTitle.textContent = `Session phrases (${key})`;
|
||||
bankSection.append(bankTitle);
|
||||
|
||||
const search = document.createElement("input");
|
||||
search.type = "search";
|
||||
search.placeholder = "Search learned phrases";
|
||||
search.className = "funpack-adj-search";
|
||||
bankSection.append(search);
|
||||
|
||||
const bank = document.createElement("div");
|
||||
bank.className = "funpack-adj-bank";
|
||||
const loading = document.createElement("div");
|
||||
loading.className = "funpack-adj-empty";
|
||||
loading.textContent = "Loading...";
|
||||
bank.append(loading);
|
||||
bankSection.append(bank);
|
||||
root.append(bankSection);
|
||||
|
||||
fetchPhraseMemory(key).then((phrases) => {
|
||||
const renderBank = () => {
|
||||
const q = search.value.toLowerCase().trim();
|
||||
const filtered = q ? phrases.filter((p) => p.text.toLowerCase().includes(q)) : phrases;
|
||||
bank.replaceChildren();
|
||||
if (!filtered.length) {
|
||||
const empty = document.createElement("div");
|
||||
empty.className = "funpack-adj-empty";
|
||||
empty.textContent = phrases.length ? "No matches." : "No learned phrases in this session yet.";
|
||||
bank.append(empty);
|
||||
return;
|
||||
}
|
||||
const byCategory = new Map();
|
||||
for (const p of filtered) {
|
||||
const cat = CATEGORY_ORDER.includes(p.category) ? p.category : "details";
|
||||
if (!byCategory.has(cat)) byCategory.set(cat, []);
|
||||
byCategory.get(cat).push(p);
|
||||
}
|
||||
for (const cat of CATEGORY_ORDER) {
|
||||
const group = byCategory.get(cat);
|
||||
if (!group?.length) continue;
|
||||
const label = document.createElement("div");
|
||||
label.className = "funpack-adj-cat-label";
|
||||
label.textContent = cat;
|
||||
bank.append(label);
|
||||
const chipRow = document.createElement("div");
|
||||
chipRow.className = "funpack-adj-chip-row";
|
||||
for (const p of group) {
|
||||
const chip = document.createElement("button");
|
||||
chip.type = "button";
|
||||
chip.className = "funpack-adj-chip";
|
||||
chip.textContent = p.text;
|
||||
chip.title = `${p.text} (seen ${p.evidence}x)`;
|
||||
chip.addEventListener("click", () => {
|
||||
if (!items.find((i) => i.phrase.trim().toLowerCase() === p.text.toLowerCase())) {
|
||||
items.push({ phrase: p.text, strength: 0.1 });
|
||||
renderRows();
|
||||
}
|
||||
});
|
||||
chipRow.append(chip);
|
||||
}
|
||||
bank.append(chipRow);
|
||||
}
|
||||
};
|
||||
renderBank();
|
||||
search.addEventListener("input", renderBank);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let stylesInjected = false;
|
||||
function injectStyles() {
|
||||
if (stylesInjected) return;
|
||||
stylesInjected = true;
|
||||
const style = document.createElement("style");
|
||||
style.textContent = `
|
||||
.funpack-adj-panel {
|
||||
width: min(440px, calc(100vw - 24px));
|
||||
max-height: min(580px, calc(100vh - 24px));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 10px;
|
||||
border: 1px solid rgba(180,190,200,0.35);
|
||||
border-radius: 8px;
|
||||
background: rgba(30,32,36,0.98);
|
||||
box-shadow: 0 18px 48px rgba(0,0,0,0.42);
|
||||
color: #ddd;
|
||||
font: 12px sans-serif;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
.funpack-adj-panel * { box-sizing: border-box; }
|
||||
.funpack-adj-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
padding-bottom: 8px;
|
||||
margin-bottom: 4px;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.08);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.funpack-adj-hint {
|
||||
color: #9da6b0;
|
||||
font-size: 11px;
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.4;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.funpack-adj-list {
|
||||
overflow-y: auto;
|
||||
min-height: 40px;
|
||||
max-height: 200px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
padding-right: 3px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.funpack-adj-empty { color: #9da6b0; padding: 6px 0; }
|
||||
.funpack-adj-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0,1fr) 74px 28px;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
.funpack-adj-phrase,
|
||||
.funpack-adj-strength {
|
||||
min-height: 28px;
|
||||
padding: 5px 7px;
|
||||
border: 1px solid rgba(180,190,200,0.28);
|
||||
border-radius: 5px;
|
||||
background: #17191d;
|
||||
color: #f2f2f2;
|
||||
outline: none;
|
||||
width: 100%;
|
||||
}
|
||||
.funpack-adj-strength { text-align: right; }
|
||||
.funpack-adj-footer {
|
||||
display: flex;
|
||||
gap: 7px;
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid rgba(255,255,255,0.08);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.funpack-adj-button {
|
||||
min-height: 26px;
|
||||
padding: 4px 10px;
|
||||
border: 1px solid rgba(180,190,200,0.35);
|
||||
border-radius: 5px;
|
||||
background: #22252a;
|
||||
color: #eee;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.funpack-adj-button:hover { background: #2b3037; }
|
||||
.funpack-adj-button.primary { border-color: rgba(100,210,140,0.6); background: #244832; }
|
||||
.funpack-adj-button.danger { border-color: rgba(255,130,130,0.45); background: #472626; }
|
||||
.funpack-adj-button.compact { min-height: 24px; padding: 2px 7px; }
|
||||
.funpack-adj-bank-section {
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid rgba(255,255,255,0.08);
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
.funpack-adj-bank-title {
|
||||
font-weight: 700;
|
||||
color: #58a6d6;
|
||||
margin-bottom: 5px;
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.funpack-adj-search {
|
||||
width: 100%;
|
||||
min-height: 28px;
|
||||
padding: 5px 7px;
|
||||
border: 1px solid rgba(180,190,200,0.28);
|
||||
border-radius: 5px;
|
||||
background: #17191d;
|
||||
color: #f2f2f2;
|
||||
outline: none;
|
||||
margin-bottom: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.funpack-adj-bank {
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
.funpack-adj-cat-label {
|
||||
color: #58a6d6;
|
||||
font-weight: 700;
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
margin: 6px 0 3px;
|
||||
}
|
||||
.funpack-adj-chip-row { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 2px; }
|
||||
.funpack-adj-chip {
|
||||
padding: 3px 8px;
|
||||
border: 1px solid rgba(255,255,255,0.14);
|
||||
border-radius: 5px;
|
||||
background: rgba(255,255,255,0.07);
|
||||
color: #eee;
|
||||
cursor: pointer;
|
||||
font: 11px sans-serif;
|
||||
}
|
||||
.funpack-adj-chip:hover { background: rgba(100,210,140,0.25); border-color: rgba(100,210,140,0.5); }
|
||||
`;
|
||||
document.head.append(style);
|
||||
}
|
||||
|
||||
function setupNode(node) {
|
||||
const hideInternal = () => {
|
||||
for (const w of node.widgets || []) {
|
||||
if (HIDDEN_WIDGETS.has(w.name)) hideWidget(w);
|
||||
}
|
||||
node.setDirtyCanvas?.(true, true);
|
||||
app.graph?.setDirtyCanvas?.(true, true);
|
||||
};
|
||||
hideInternal();
|
||||
window.requestAnimationFrame?.(hideInternal);
|
||||
window.setTimeout(hideInternal, 0);
|
||||
window.setTimeout(hideInternal, 250);
|
||||
|
||||
// Remove any previously added button to avoid duplicates on reconfigure
|
||||
node.widgets = (node.widgets || []).filter((w) => w.name !== "funpack_adj_open");
|
||||
|
||||
node.addWidget("button", "Edit Adjustments", "funpack_adj_open", () => {
|
||||
renderPanel(node);
|
||||
}, { serialize: false });
|
||||
|
||||
node.setDirtyCanvas?.(true, true);
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: "funpack.conditioningAdjust",
|
||||
beforeRegisterNodeDef(nodeType, nodeData) {
|
||||
if (nodeData.name !== NODE_NAME) return;
|
||||
const orig = nodeType.prototype.onNodeCreated;
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
orig?.apply(this, arguments);
|
||||
setupNode(this);
|
||||
};
|
||||
const origCfg = nodeType.prototype.configure;
|
||||
nodeType.prototype.configure = function (info) {
|
||||
origCfg?.apply(this, arguments);
|
||||
setupNode(this);
|
||||
};
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user