Files
olivv-cs-ComfyUI-FunPack/web/funpack_conditioning_adjust.js
T
digital-garbageandClaude Sonnet 4.6 ffb447104e 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>
2026-05-16 17:13:42 +03:00

443 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
};
},
});