Files

229 lines
9.5 KiB
JavaScript

/**
* Standalone Universal Trigger Toggle
* REVISION V1.3.1 - FIX [] BUTTON + REMOVE STRENGTH TOGGLE
* Updated: 2025-12-26 02:00:00
*/
import { app } from "../../scripts/app.js";
// --- CSS INJECTION (Clean Pill UI) ---
const style = document.createElement('style');
style.textContent = `
.universal-tags-container {
display: flex;
flex-wrap: wrap;
gap: 6px;
padding: 10px;
background: rgba(15, 15, 20, 0.9);
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.1);
width: 100%;
height: 100%;
min-height: 60px;
overflow-y: auto;
box-sizing: border-box;
margin: 5px 0;
align-items: flex-start;
align-content: flex-start;
}
.universal-tag {
padding: 0 14px;
height: 26px;
line-height: 26px;
border-radius: 13px;
font-size: 11px;
font-weight: 500;
cursor: pointer;
user-select: none;
transition: all 0.1s ease-in-out;
background: rgba(255, 255, 255, 0.08);
color: rgba(255, 255, 255, 0.6);
border: 1px solid rgba(255, 255, 255, 0.12);
display: flex;
align-items: center;
gap: 4px;
white-space: nowrap;
}
.universal-tag.active {
background: rgba(59, 130, 246, 0.9);
color: white;
border-color: #3b82f6;
box-shadow: 0 0 8px rgba(59, 130, 246, 0.3);
}
`;
document.head.appendChild(style);
function addTagsWidget(node, name, opts, callback) {
const container = document.createElement("div");
container.className = "universal-tags-container";
let widgetValue = opts?.defaultVal || [];
const renderTags = (tagsData, widget) => {
container.innerHTML = "";
if (!tagsData || tagsData.length === 0) {
container.innerHTML = '<div style="width:100%; text-align:center; opacity:0.5; padding:20px; font-size:11px; font-style:italic;">No trigger words detected</div>';
return;
}
tagsData.forEach((tag, index) => {
const tagEl = document.createElement("div");
tagEl.className = `universal-tag ${tag.active ? 'active' : ''}`;
tagEl.textContent = tag.text;
tagEl.onclick = (e) => {
e.stopPropagation();
widget.value[index].active = !widget.value[index].active;
renderTags(widget.value, widget);
if (widget.callback) widget.callback(widget.value);
};
container.appendChild(tagEl);
});
};
const widget = node.addDOMWidget(name, "custom", container, {
getValue: () => widgetValue,
setValue: (v) => { widgetValue = v; renderTags(widgetValue, widget); },
hideOnZoom: true
});
widget.value = widgetValue;
widget.callback = callback;
widget.serializeValue = () => widgetValue;
return widget;
}
app.registerExtension({
name: "UniversalTriggerToggle.Standalone",
async nodeCreated(node) {
if (node.comfyClass === "Universal Trigger Toggle" || node.comfyClass === "Universal Trigger Toggle (LoraManager)") {
const self = this;
node.serialize_widgets = true;
// 1. ADD THE INPUT SOCKET MANUALLY
node.addInput("trigger_words", "STRING", { "shape": 7 });
// 2. ADD A HIDDEN WIDGET for data sync
const hiddenWidget = node.addWidget("text", "trigger_words", "", (v) => {});
hiddenWidget.hidden = true;
hiddenWidget.computeSize = () => [0, -4];
node.triggerWordsWidget = hiddenWidget;
setTimeout(() => {
const groupModeWidget = node.widgets.find(w => w.name === "group_mode");
const tagWidget = addTagsWidget(node, "toggle_trigger_words", { defaultVal: [] }, () => {
node.setDirtyCanvas(true);
});
node.tagWidget = tagWidget;
const syncFromInput = async () => {
const input = node.inputs?.find(i => i.name === "trigger_words");
if (!input?.link) return;
const link = app.graph.links[input.link];
const originNode = app.graph.getNodeById(link.origin_id);
if (!originNode || !originNode.widgets) return;
let raw = "";
for (const w of originNode.widgets) {
if (typeof w.value === "string" && w.value.trim()) {
// Check if value is a lora path or trigger words
if (w.value.includes(".safetensors") || w.value.startsWith("[") || w.name === "trigger_words") {
raw = w.value;
break;
}
}
}
if (!raw) return;
let processed = raw;
try {
const trimmedRaw = raw.trim();
if (trimmedRaw.startsWith('[') || trimmedRaw.startsWith('{')) {
// BUG FIX: If it's just an empty array "[]", treat as empty
if (trimmedRaw === "[]" || trimmedRaw === "{}") {
processed = "";
} else {
const parsed = JSON.parse(trimmedRaw);
const paths = [];
if (Array.isArray(parsed)) {
parsed.forEach(i => {
if (i.lora) paths.push(i.lora);
else if (typeof i === 'string' && i.endsWith('.safetensors')) paths.push(i);
});
} else if (parsed.lora) {
paths.push(parsed.lora);
}
if (paths.length > 0) {
const all = [];
for (const p of paths) {
const n = p.split(/[\\\/]/).pop().replace(/\.safetensors$/i, "");
const r = await fetch(`/api/lm/loras/get-trigger-words?name=${encodeURIComponent(n)}`);
const d = await r.json();
if (d.success && d.trigger_words) all.push(...d.trigger_words);
}
processed = all.length > 0 ? all.join(", ") : "";
} else {
processed = ""; // Nothing found in JSON
}
}
} else if (raw.toLowerCase().endsWith(".safetensors")) {
const n = raw.split(/[\\\/]/).pop().replace(/\.safetensors$/i, "");
const r = await fetch(`/api/lm/loras/get-trigger-words?name=${encodeURIComponent(n)}`);
const d = await r.json();
if (d.success && d.trigger_words) processed = d.trigger_words.join(", ");
}
} catch(e) {
console.error("Error syncing input:", e);
}
if (node.triggerWordsWidget.value !== processed) {
node.triggerWordsWidget.value = processed;
self.updateTagsFromMessage(node, processed);
}
};
const interval = setInterval(() => {
if (node.graph) syncFromInput();
else clearInterval(interval);
}, 800);
if (groupModeWidget) {
const orig = groupModeWidget.callback;
groupModeWidget.callback = function() {
if (orig) orig.apply(this, arguments);
self.updateTagsFromMessage(node, node.triggerWordsWidget.value);
};
}
if (node.triggerWordsWidget.value) {
self.updateTagsFromMessage(node, node.triggerWordsWidget.value);
}
node.onRemoved = () => clearInterval(interval);
node.setDirtyCanvas(true, true);
}, 150);
}
},
updateTagsFromMessage(node, message) {
if (!node.tagWidget) return;
const existing = {};
(node.tagWidget.value || []).forEach(t => existing[t.text] = t);
const groupMode = node.widgets.find(w => w.name === "group_mode")?.value ?? false;
const defActive = node.widgets.find(w => w.name === "default_active")?.value ?? true;
let words = [];
if (groupMode) {
words = message.includes(',,') ? message.split(/,{2,}/) : (message ? [message] : []);
} else {
words = message.replace(/,{2,}/g, ',').split(',').map(w => w.trim()).filter(x => x);
}
node.tagWidget.value = words.map(w => ({
text: w,
active: existing[w] ? existing[w].active : defActive
}));
}
});