From ffb447104e15c0411d0df9bb8d2e6686f756f78c Mon Sep 17 00:00:00 2001 From: digital-garbage <133010426+digital-garbage@users.noreply.github.com> Date: Sat, 16 May 2026 17:13:42 +0300 Subject: [PATCH] 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 --- web/funpack_conditioning_adjust.js | 442 +++++++++++++++++++++++++++++ 1 file changed, 442 insertions(+) create mode 100644 web/funpack_conditioning_adjust.js diff --git a/web/funpack_conditioning_adjust.js b/web/funpack_conditioning_adjust.js new file mode 100644 index 00000000..34f463a0 --- /dev/null +++ b/web/funpack_conditioning_adjust.js @@ -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); + }; + }, +});