Files
HenkDz-nd-super-nodes/web/extension.js
T
Nour d0527baae2 feat: enhance node settings management and auto-enhancement functionality
- 🔧 Introduced settings synchronization for enabling/disabling the extension.
- 🚀 Improved auto-enhancement logic to respect the global enable setting.
- 🛠️ Added methods for handling setting changes and updating UI accordingly.
2025-09-27 18:52:35 +01:00

5625 lines
193 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 as app$1 } from "/scripts/app.js";
class LoraService {
constructor() {
this.availableLoras = [];
this.loraCache = /* @__PURE__ */ new Map();
}
static getInstance() {
if (!LoraService.instance) {
LoraService.instance = new LoraService();
}
return LoraService.instance;
}
/**
* Initialize the service and load available LoRAs
*/
async initialize() {
try {
await this.refreshLoraList();
} catch (error) {
console.error("Super LoRA Loader: Failed to initialize LoRA service:", error);
}
}
/**
* Get list of available LoRA files
*/
async getAvailableLoras() {
if (this.availableLoras.length === 0) {
await this.refreshLoraList();
}
return this.availableLoras;
}
/**
* Refresh the list of available LoRAs from the backend
*/
async refreshLoraList() {
try {
const response = await fetch("/object_info");
const data = await response.json();
const loraLoader = data.LoraLoader;
if (loraLoader && loraLoader.input && loraLoader.input.required && loraLoader.input.required.lora_name) {
this.availableLoras = loraLoader.input.required.lora_name[0] || [];
} else {
const folderResponse = await fetch("/api/v1/folder_paths");
if (folderResponse.ok) {
const folderData = await folderResponse.json();
this.availableLoras = folderData.loras || [];
}
}
console.log(`Super LoRA Loader: Found ${this.availableLoras.length} LoRAs`);
} catch (error) {
console.error("Super LoRA Loader: Failed to refresh LoRA list:", error);
this.availableLoras = [];
}
}
/**
* Create a new LoRA configuration with defaults
*/
createLoraConfig(loraName = "None") {
return {
lora: loraName,
enabled: true,
strength_model: 1,
strength_clip: 1,
trigger_word: "",
tag: "General",
auto_populated: false
};
}
/**
* Validate a LoRA configuration
*/
validateLoraConfig(config) {
if (!config || typeof config !== "object") {
return false;
}
if (typeof config.lora !== "string" || typeof config.enabled !== "boolean") {
return false;
}
const strengthModel = Number(config.strength_model);
const strengthClip = Number(config.strength_clip);
if (isNaN(strengthModel) || isNaN(strengthClip)) {
return false;
}
if (strengthModel < 0 || strengthModel > 2 || strengthClip < 0 || strengthClip > 2) {
return false;
}
return true;
}
/**
* Check if a LoRA is already in the configuration list
*/
isDuplicateLora(configs, loraName) {
return configs.some((config) => config.lora === loraName && loraName !== "None");
}
/**
* Sort LoRA configurations by tag and name
*/
sortLoraConfigs(configs) {
return [...configs].sort((a, b) => {
if (a.tag !== b.tag) {
if (a.tag === "General") return -1;
if (b.tag === "General") return 1;
return a.tag.localeCompare(b.tag);
}
return a.lora.localeCompare(b.lora);
});
}
/**
* Group LoRA configurations by tag
*/
groupLorasByTag(configs) {
const groups = /* @__PURE__ */ new Map();
for (const config of configs) {
const tag = config.tag || "General";
if (!groups.has(tag)) {
groups.set(tag, []);
}
groups.get(tag).push(config);
}
return groups;
}
/**
* Get available tags from a list of LoRA configurations
*/
getAvailableTags(configs) {
const tags = /* @__PURE__ */ new Set();
for (const config of configs) {
tags.add(config.tag || "General");
}
return Array.from(tags).sort((a, b) => {
if (a === "General") return -1;
if (b === "General") return 1;
return a.localeCompare(b);
});
}
/**
* Get common tag suggestions
*/
getCommonTags() {
return [
"General",
"Character",
"Style",
"Quality",
"Effect",
"Background",
"Clothing",
"Pose",
"Lighting"
];
}
/**
* Convert LoRA configs to backend format
*/
convertToBackendFormat(configs) {
const result = {};
configs.forEach((config, index) => {
if (config.lora && config.lora !== "None") {
const key = `lora_${index + 1}`;
result[key] = {
lora: config.lora,
enabled: config.enabled,
strength_model: config.strength_model,
strength_clip: config.strength_clip,
trigger_word: config.trigger_word || "",
tag: config.tag || "General"
};
}
});
return result;
}
/**
* Extract trigger words from all enabled LoRAs
*/
extractTriggerWords(configs) {
const triggerWords = [];
for (const config of configs) {
if (config.enabled && config.trigger_word && config.trigger_word.trim()) {
triggerWords.push(config.trigger_word.trim());
}
}
return triggerWords.join(", ");
}
}
class TemplateService {
constructor() {
this.templates = [];
this.isLoaded = false;
}
static getInstance() {
if (!TemplateService.instance) {
TemplateService.instance = new TemplateService();
}
return TemplateService.instance;
}
/**
* Initialize the template service
*/
async initialize() {
if (!this.isLoaded) {
await this.loadTemplates();
}
}
/**
* Load templates from backend
*/
async loadTemplates() {
try {
const response = await fetch("/super_lora/templates", {
method: "GET"
});
if (response.ok) {
const data = await response.json();
const templates = data.templates ?? data ?? [];
this.templates = Array.isArray(templates) ? templates : [];
this.isLoaded = true;
console.log(`Super LoRA Loader: Loaded ${this.templates.length} templates`);
} else {
console.warn("Super LoRA Loader: Failed to load templates:", response.statusText);
this.templates = [];
this.isLoaded = true;
}
} catch (error) {
console.error("Super LoRA Loader: Error loading templates:", error);
this.templates = [];
this.isLoaded = true;
}
}
/**
* Get all available templates
*/
async getTemplates() {
if (!this.isLoaded) {
await this.loadTemplates();
}
return [...this.templates];
}
/**
* Save a new template
*/
async saveTemplate(name, loraConfigs) {
try {
const validConfigs = loraConfigs.filter(
(config) => config.lora && config.lora !== "None"
);
if (validConfigs.length === 0) {
throw new Error("No valid LoRA configurations to save");
}
const normalized = validConfigs.map((cfg) => ({
lora: cfg.lora,
enabled: !!cfg.enabled,
strength_model: Number(cfg.strength_model ?? 1),
strength_clip: Number(cfg.strength_clip ?? cfg.strength_model ?? 1),
trigger_word: cfg.trigger_word ?? "",
tag: cfg.tag ?? "General",
auto_populated: !!cfg.auto_populated
}));
const template = {
name,
version: "1.0",
lora_configs: normalized
};
const response = await fetch("/super_lora/templates", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(template)
});
if (response.ok) {
await this.loadTemplates();
console.log(`Super LoRA Loader: Template "${name}" saved successfully`);
return true;
} else {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`);
}
} catch (error) {
console.error(`Super LoRA Loader: Failed to save template "${name}":`, error);
return false;
}
}
/**
* Load a template by name
*/
async loadTemplate(name) {
try {
const templates = await this.getTemplates();
const template = templates.find((t) => t && (t.name === name || t.id === name || t.title === name || t === name));
const extractList = (tpl) => {
if (!tpl) return null;
if (Array.isArray(tpl.loras)) return tpl.loras;
if (Array.isArray(tpl.items)) return tpl.items;
if (tpl.template) return extractList(tpl.template);
if (typeof tpl === "string") {
try {
const parsed = JSON.parse(tpl);
return extractList(parsed);
} catch {
}
}
return null;
};
const fetchByName = async () => {
const tryParse = (data) => {
const list2 = extractList(data);
if (!list2) return null;
const valid = list2.filter((cfg) => this.validateLoraConfig(cfg));
return valid;
};
try {
let resp = await fetch(`/super_lora/templates?name=${encodeURIComponent(name)}`);
if (resp.ok) {
const data = await resp.json();
const out = tryParse(data);
if (out) return out;
}
} catch {
}
try {
const resp2 = await fetch(`/super_lora/templates/${encodeURIComponent(name)}`);
if (resp2.ok) {
const data2 = await resp2.json();
const out2 = tryParse(data2);
if (out2) return out2;
}
} catch {
}
return null;
};
if (!template) {
console.warn(`Super LoRA Loader: Template "${name}" not found in cache, trying GET by name`);
return await fetchByName();
}
let list = extractList(template);
if (!list) {
const fetched = await fetchByName();
if (!fetched) {
console.warn(`Super LoRA Loader: Template "${name}" has no loras/items array`);
}
return fetched;
}
const normalize = (cfg) => ({
lora: cfg.lora ?? cfg.file ?? cfg.name ?? "",
enabled: cfg.enabled !== void 0 ? !!cfg.enabled : cfg.on === void 0 ? true : !!cfg.on,
strength_model: cfg.strength_model !== void 0 ? Number(cfg.strength_model) : Number(cfg.strength ?? cfg.value ?? 1),
strength_clip: cfg.strength_clip !== void 0 ? Number(cfg.strength_clip) : Number(cfg.strengthTwo ?? cfg.clip_strength ?? cfg.strength_model ?? cfg.strength ?? 1),
trigger_word: cfg.trigger_word ?? cfg.triggerWord ?? cfg.trigger ?? "",
tag: cfg.tag ?? "General",
auto_populated: cfg.auto_populated ?? cfg._autoPopulatedTriggerWord ?? false
});
const normalized = list.map(normalize);
const validConfigs = normalized.filter((config) => this.validateLoraConfig(config));
if (validConfigs.length !== list.length) {
console.warn(`Super LoRA Loader: Some LoRA configs in template "${name}" are invalid`);
}
console.log(`Super LoRA Loader: Loaded template "${name}" with ${validConfigs.length} LoRAs`);
return validConfigs;
} catch (error) {
console.error(`Super LoRA Loader: Failed to load template "${name}":`, error);
return null;
}
}
/**
* Delete a template
*/
async deleteTemplate(name) {
try {
let response = await fetch(`/super_lora/templates/${encodeURIComponent(name)}`, {
method: "DELETE"
});
if (!response.ok) {
response = await fetch("/super_lora/templates", {
method: "DELETE",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name })
});
}
if (!response.ok && response.status === 405) {
response = await fetch("/super_lora/templates/delete", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name })
});
}
if (!response.ok) {
response = await fetch("/super_lora/templates", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action: "delete", name })
});
}
if (response.ok) {
await this.loadTemplates();
const stillExists = (this.templates || []).some((t) => t && t.name ? t.name === name : t === name);
if (stillExists) {
console.warn(`Super LoRA Loader: Server responded OK but template still present: ${name}`);
return false;
}
console.log(`Super LoRA Loader: Template "${name}" deleted successfully`);
return true;
} else {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`);
}
} catch (error) {
console.error(`Super LoRA Loader: Failed to delete template "${name}":`, error);
return false;
}
}
/**
* Check if a template name already exists
*/
async templateExists(name) {
const templates = await this.getTemplates();
return templates.some((t) => t.name === name);
}
/**
* Get template names for UI selection
*/
async getTemplateNames() {
const templates = await this.getTemplates();
return templates.map((t) => t && t.name ? t.name : String(t)).sort();
}
/**
* Validate a LoRA configuration
*/
validateLoraConfig(config) {
if (!config || typeof config !== "object") {
return false;
}
if (!config.lora || typeof config.enabled !== "boolean") {
return false;
}
const strengthModel = Number(config.strength_model);
const strengthClip = Number(config.strength_clip);
if (isNaN(strengthModel) || isNaN(strengthClip)) {
return false;
}
return true;
}
/**
* Export template to JSON string
*/
async exportTemplate(name) {
try {
const templates = await this.getTemplates();
const template = templates.find((t) => t.name === name);
if (!template) {
return null;
}
return JSON.stringify(template, null, 2);
} catch (error) {
console.error(`Super LoRA Loader: Failed to export template "${name}":`, error);
return null;
}
}
/**
* Import template from JSON string
*/
async importTemplate(jsonString) {
try {
const template = JSON.parse(jsonString);
if (!template.name || !template.loras || !Array.isArray(template.loras)) {
throw new Error("Invalid template format");
}
if (await this.templateExists(template.name)) {
throw new Error(`Template "${template.name}" already exists`);
}
return await this.saveTemplate(template.name, template.loras);
} catch (error) {
console.error("Super LoRA Loader: Failed to import template:", error);
return false;
}
}
/**
* Rename an existing template
*/
async renameTemplate(oldName, newName) {
try {
const src = (oldName || "").trim();
const dst = (newName || "").trim();
if (!src || !dst) return false;
if (src === dst) return true;
if (await this.templateExists(dst)) {
throw new Error(`Template "${dst}" already exists`);
}
const configs = await this.loadTemplate(src);
if (!configs || configs.length === 0) {
throw new Error(`Template "${src}" not found or empty`);
}
const saved = await this.saveTemplate(dst, configs);
if (!saved) return false;
await this.deleteTemplate(src);
await this.loadTemplates();
return true;
} catch (error) {
console.error("Super LoRA Loader: Failed to rename template:", error);
return false;
}
}
}
class CivitAiService {
constructor() {
this.cache = /* @__PURE__ */ new Map();
this.pendingRequests = /* @__PURE__ */ new Map();
}
static getInstance() {
if (!CivitAiService.instance) {
CivitAiService.instance = new CivitAiService();
}
return CivitAiService.instance;
}
/**
* Get trigger words for a LoRA file
*/
async getTriggerWords(loraFileName) {
try {
const modelInfo = await this.getModelInfo(loraFileName);
if (!modelInfo) {
return [];
}
return this.extractTriggerWordsFromModelInfo(modelInfo);
} catch (error) {
console.warn(`Super LoRA Loader: Failed to get trigger words for ${loraFileName}:`, error);
return [];
}
}
/**
* Get model information from CivitAI
*/
async getModelInfo(loraFileName) {
if (this.cache.has(loraFileName)) {
return this.cache.get(loraFileName);
}
if (this.pendingRequests.has(loraFileName)) {
return await this.pendingRequests.get(loraFileName);
}
const requestPromise = this.fetchModelInfo(loraFileName);
this.pendingRequests.set(loraFileName, requestPromise);
try {
const result = await requestPromise;
this.pendingRequests.delete(loraFileName);
if (result) {
this.cache.set(loraFileName, result);
}
return result;
} catch (error) {
this.pendingRequests.delete(loraFileName);
throw error;
}
}
/**
* Fetch model info from backend API
*/
async fetchModelInfo(loraFileName) {
try {
const response = await fetch("/super_lora/civitai_info", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
lora_filename: loraFileName
})
});
if (!response.ok) {
if (response.status === 404) {
return null;
}
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
if (data.error) {
console.warn(`Super LoRA Loader: CivitAI API error for ${loraFileName}:`, data.error);
return null;
}
return data;
} catch (error) {
console.warn(`Super LoRA Loader: Failed to fetch model info for ${loraFileName}:`, error);
return null;
}
}
/**
* Extract trigger words from CivitAI model info
*/
extractTriggerWordsFromModelInfo(modelInfo) {
const triggerWords = [];
if (modelInfo.trainedWords && Array.isArray(modelInfo.trainedWords)) {
for (const item of modelInfo.trainedWords) {
if (typeof item === "string") {
triggerWords.push(item);
} else if (item && typeof item === "object" && "word" in item) {
triggerWords.push(item.word);
}
}
}
return triggerWords.slice(0, 3);
}
/**
* Clear the cache
*/
clearCache() {
this.cache.clear();
}
/**
* Get cache size for debugging
*/
getCacheSize() {
return this.cache.size;
}
/**
* Check if auto-fetch is enabled in settings
*/
isAutoFetchEnabled() {
try {
const app2 = window.app;
if (app2 && app2.ui && app2.ui.settings) {
return app2.ui.settings.getSettingValue("superLora.autoFetchTriggerWords", true);
}
} catch (error) {
console.warn("Super LoRA Loader: Failed to check auto-fetch setting:", error);
}
return true;
}
/**
* Auto-populate trigger words for a LoRA if enabled
*/
async autoPopulateTriggerWords(loraFileName) {
if (!this.isAutoFetchEnabled() || !loraFileName || loraFileName === "None") {
return "";
}
try {
const triggerWords = await this.getTriggerWords(loraFileName);
return triggerWords.length > 0 ? triggerWords[0] : "";
} catch (error) {
console.warn(`Super LoRA Loader: Auto-populate failed for ${loraFileName}:`, error);
return "";
}
}
}
const _TagSetService = class _TagSetService {
static getInstance() {
if (!_TagSetService.instance) {
_TagSetService.instance = new _TagSetService();
}
return _TagSetService.instance;
}
read() {
try {
const raw = localStorage.getItem(_TagSetService.STORAGE_KEY);
if (raw) {
const arr = JSON.parse(raw);
if (Array.isArray(arr)) return this.normalize(arr);
}
} catch {
}
this.write(_TagSetService.DEFAULT_TAGS);
return [..._TagSetService.DEFAULT_TAGS];
}
write(tags) {
try {
const unique = this.normalize(tags);
localStorage.setItem(_TagSetService.STORAGE_KEY, JSON.stringify(unique));
} catch {
}
}
normalize(tags) {
const set = /* @__PURE__ */ new Set();
for (const t of tags) {
const name = String(t || "").trim();
if (!name) continue;
set.add(name);
}
return Array.from(set).sort((a, b) => a.localeCompare(b));
}
getAll() {
return this.read();
}
addTag(name) {
const n = String(name || "").trim();
if (!n) return false;
const tags = this.read();
if (tags.includes(n)) return false;
tags.push(n);
this.write(tags);
return true;
}
renameTag(oldName, newName) {
const src = String(oldName || "").trim();
const dst = String(newName || "").trim();
if (!src || !dst || src === dst) return false;
const tags = this.read();
const idx = tags.indexOf(src);
if (idx === -1) return false;
if (!tags.includes(dst)) {
tags[idx] = dst;
} else {
tags.splice(idx, 1);
}
this.write(tags);
return true;
}
deleteTag(name) {
const n = String(name || "").trim();
if (!n) return false;
const tags = this.read();
const idx = tags.indexOf(n);
if (idx === -1) return false;
tags.splice(idx, 1);
if (!tags.includes("General")) tags.push("General");
this.write(tags);
return true;
}
};
_TagSetService.instance = null;
_TagSetService.STORAGE_KEY = "superlora_tagset_v1";
_TagSetService.DEFAULT_TAGS = ["General", "Character", "Style", "Quality", "Effect"];
let TagSetService = _TagSetService;
class OverlayService {
constructor() {
this.stylesInjected = false;
}
static getInstance() {
if (!OverlayService.instance) {
OverlayService.instance = new OverlayService();
}
return OverlayService.instance;
}
showSearchOverlay(options) {
this.ensureOverlayStyles();
const baseFolderKey = options.baseFolderName || "files";
const restoredState = this.restoreOverlayState(baseFolderKey);
this.removeExistingOverlays();
const items = options.items ?? [];
const overlay = document.createElement("div");
overlay.setAttribute("data-super-lora-overlay", "1");
overlay.style.cssText = `
position: fixed;
inset: 0;
background: rgba(0,0,0,0.55);
z-index: 2147483600;
display: flex;
align-items: center;
justify-content: center;
backdrop-filter: blur(2px);
`;
const panel = document.createElement("div");
panel.style.cssText = `
width: 560px;
max-height: 70vh;
background: #222;
border: 1px solid #444;
border-radius: 8px;
box-shadow: 0 12px 30px rgba(0,0,0,0.4);
color: #fff;
font-family: 'Segoe UI', Arial, sans-serif;
display: flex;
flex-direction: column;
overflow: hidden;
`;
const header = document.createElement("div");
header.textContent = options.title;
header.style.cssText = `
padding: 12px 14px;
font-weight: 600;
border-bottom: 1px solid #444;
background: #2a2a2a;
`;
const search = document.createElement("input");
search.type = "text";
search.placeholder = options.placeholder;
search.style.cssText = `
margin: 10px 12px;
padding: 10px 12px;
border-radius: 6px;
border: 1px solid #555;
background: #1a1a1a;
color: #fff;
outline: none;
`;
const multiEnabled = !!options.enableMultiToggle;
let multiMode = false;
const selectedIds = /* @__PURE__ */ new Set();
const ROOT_KEY = "__ROOT__";
const controls = document.createElement("div");
controls.style.cssText = `
display: ${multiEnabled ? "flex" : "none"};
align-items: center;
justify-content: flex-end;
gap: 10px;
padding: 0 12px 6px 12px;
color: #ddd;
font-size: 12px;
`;
const multiLabel = document.createElement("label");
multiLabel.className = "nd-overlay-multi-toggle";
const multiToggle = document.createElement("input");
multiToggle.type = "checkbox";
multiToggle.className = "nd-overlay-multi-toggle-checkbox";
multiToggle.addEventListener("change", () => {
multiMode = !!multiToggle.checked;
render(search.value);
renderFooter();
});
const multiText = document.createElement("span");
multiText.textContent = "Multi-select";
multiText.className = "nd-overlay-multi-toggle-label";
multiLabel.appendChild(multiToggle);
multiLabel.appendChild(multiText);
controls.appendChild(multiLabel);
const chipWrap = document.createElement("div");
chipWrap.className = "nd-overlay-chips-container";
const subChipWrap = document.createElement("div");
subChipWrap.className = "nd-overlay-subchips-container";
subChipWrap.style.display = "none";
let folderFeatureEnabled = false;
const activeFolders = new Set(restoredState.activeFolders);
const activeSubfolders = new Set(restoredState.activeSubfolders);
const listWrap = document.createElement("div");
listWrap.style.cssText = "overflow: auto; padding: 6px 4px 10px 4px;";
const list = document.createElement("div");
list.style.cssText = "display: flex; flex-direction: column; gap: 4px; padding: 0 8px 8px 8px;";
const empty = document.createElement("div");
empty.textContent = "No results";
empty.style.cssText = "padding: 12px; color: #aaa; display: none;";
const handleKeyDown = (event) => {
if (event.key === "Escape") {
closeOverlay();
}
};
const closeOverlay = () => {
try {
overlay.remove();
} catch {
}
document.removeEventListener("keydown", handleKeyDown);
this.persistOverlayState(baseFolderKey, activeFolders, activeSubfolders);
};
document.addEventListener("keydown", handleKeyDown);
overlay.addEventListener("click", (event) => {
if (event.target === overlay) closeOverlay();
});
const renderChips = (folderCounts) => {
chipWrap.innerHTML = "";
const header2 = document.createElement("div");
header2.className = "nd-overlay-chip-header";
header2.textContent = "Folders";
chipWrap.appendChild(header2);
const chipList = document.createElement("div");
chipList.className = "nd-overlay-chip-list";
chipWrap.appendChild(chipList);
const allFolderNames = Object.keys(folderCounts).sort((a, b) => {
if (a === ROOT_KEY) return -1;
if (b === ROOT_KEY) return 1;
return a.localeCompare(b);
});
const baseFolderName = options.baseFolderName || "files";
allFolderNames.forEach((name) => {
const chip = document.createElement("button");
chip.type = "button";
chip.className = "nd-overlay-chip";
const isActive = activeFolders.has(name);
if (isActive) chip.classList.add("is-active");
const icon = document.createElement("span");
icon.className = "nd-overlay-chip-icon";
icon.textContent = name === ROOT_KEY ? "🏠" : "📁";
const label = document.createElement("span");
label.className = "nd-overlay-chip-label";
label.textContent = name === ROOT_KEY ? `${baseFolderName} (root)` : name;
const count = document.createElement("span");
count.className = "nd-overlay-chip-count";
count.textContent = String(folderCounts[name] ?? 0);
chip.appendChild(icon);
chip.appendChild(label);
chip.appendChild(count);
chip.title = name === ROOT_KEY ? `${baseFolderName} root` : name;
chip.addEventListener("click", () => {
if (activeFolders.has(name)) {
activeFolders.delete(name);
} else {
activeFolders.add(name);
}
this.persistOverlayState(baseFolderKey, activeFolders, activeSubfolders);
render(search.value);
renderSubChips();
});
chipList.appendChild(chip);
});
};
const renderSubChips = () => {
subChipWrap.innerHTML = "";
const show = activeFolders.size > 0;
subChipWrap.style.display = show ? "flex" : "none";
if (!show) return;
const subCountsByKey = {};
const subToTops = {};
items.forEach((item) => {
const parts = item.id.split(/[\\/]/);
const top = parts.length > 1 ? parts[0] : ROOT_KEY;
if (top === ROOT_KEY) return;
if (!activeFolders.has(top)) return;
const sub = parts.length > 2 ? parts[1] : ROOT_KEY;
if (sub === ROOT_KEY) {
const key2 = `${top}/${ROOT_KEY}`;
subCountsByKey[key2] = (subCountsByKey[key2] || 0) + 1;
if (!subToTops["(root)"]) subToTops["(root)"] = /* @__PURE__ */ new Set();
subToTops["(root)"].add(top);
return;
}
const key = `${top}/${sub}`;
subCountsByKey[key] = (subCountsByKey[key] || 0) + 1;
if (!subToTops[sub]) subToTops[sub] = /* @__PURE__ */ new Set();
subToTops[sub].add(top);
});
const header2 = document.createElement("div");
header2.className = "nd-overlay-chip-header";
header2.textContent = activeFolders.size === 1 ? "Subfolders" : "Subfolders (selected)";
subChipWrap.appendChild(header2);
const chipList = document.createElement("div");
chipList.className = "nd-overlay-chip-list";
subChipWrap.appendChild(chipList);
Object.keys(subCountsByKey).sort().forEach((key) => {
const [top, sub] = key.split("/");
const isRootSub = sub === ROOT_KEY;
const duplicate = subToTops[sub]?.size && subToTops[sub].size > 1;
const labelText = isRootSub ? `${top} (root)` : duplicate ? `${top} / ${sub}` : sub;
const countVal = subCountsByKey[key] ?? 0;
const chip = document.createElement("button");
chip.type = "button";
chip.className = "nd-overlay-chip nd-overlay-chip--sub";
const isActive = activeSubfolders.has(key);
if (isActive) chip.classList.add("is-active");
const icon = document.createElement("span");
icon.className = "nd-overlay-chip-icon";
icon.textContent = isRootSub ? "🗂️" : "↳";
const label = document.createElement("span");
label.className = "nd-overlay-chip-label";
label.textContent = labelText;
const count = document.createElement("span");
count.className = "nd-overlay-chip-count";
count.textContent = String(countVal);
chip.appendChild(icon);
chip.appendChild(label);
chip.appendChild(count);
chip.title = `${top}/${isRootSub ? "(root)" : sub}`;
chip.addEventListener("click", () => {
if (activeSubfolders.has(key)) {
activeSubfolders.delete(key);
} else {
activeSubfolders.add(key);
}
this.persistOverlayState(baseFolderKey, activeFolders, activeSubfolders);
render(search.value);
renderSubChips();
});
chipList.appendChild(chip);
});
};
const render = (term) => {
list.innerHTML = "";
const query = (term || "").trim().toLowerCase();
const termFiltered = query ? items.filter((item) => item.label.toLowerCase().includes(query)) : items;
if (folderFeatureEnabled) {
const folderCounts = {};
termFiltered.forEach((item) => {
const parts = item.id.split(/[\\/]/);
const top = parts.length > 1 ? parts[0] : ROOT_KEY;
folderCounts[top] = (folderCounts[top] || 0) + 1;
});
renderChips(folderCounts);
}
let filtered = termFiltered;
if (folderFeatureEnabled && activeFolders.size > 0) {
filtered = termFiltered.filter((item) => {
const parts = item.id.split(/[\\/]/);
const top = parts.length > 1 ? parts[0] : ROOT_KEY;
return activeFolders.has(top);
});
if (activeSubfolders.size > 0) {
filtered = filtered.filter((item) => {
const parts = item.id.split(/[\\/]/);
const top = parts.length > 1 ? parts[0] : "";
if (!top) return false;
const sub = parts.length > 2 ? parts[1] : ROOT_KEY;
const key = `${top}/${sub}`;
return activeSubfolders.has(key);
});
}
}
if (options.allowCreate && query) {
const exact = items.some((item) => item.label.toLowerCase() === query);
if (!exact) {
filtered = [{ id: term, label: `Create "${term}"` }, ...filtered];
}
}
empty.style.display = filtered.length ? "none" : "block";
try {
header.textContent = `${options.title} (${filtered.length}/${items.length})`;
} catch {
}
const maxToShow = Math.min(2e3, filtered.length);
filtered.slice(0, maxToShow).forEach((item) => {
const row = document.createElement("div");
row.style.cssText = "display: flex; align-items: center; gap: 8px; padding: 0;";
const leftBtn = document.createElement("button");
leftBtn.type = "button";
leftBtn.className = "nd-overlay-item-button";
leftBtn.disabled = !!item.disabled;
const rawParts = item.id.split(/[\\/]/);
const parts = rawParts.filter(Boolean);
const folderPath = parts.slice(0, -1);
const lastSegment = parts[parts.length - 1] || item.label;
const pathDisplay = folderPath.length ? folderPath.join(" / ") : "(root)";
const isFolder = !/\.[^./\\]{2,}$/i.test(lastSegment);
const iconGlyph = isFolder ? "📁" : "📄";
leftBtn.title = item.id;
const checkboxSpan = document.createElement("span");
checkboxSpan.className = "nd-overlay-item-checkbox";
checkboxSpan.dataset.state = "hidden";
const iconSpan = document.createElement("span");
iconSpan.className = "nd-overlay-item-icon";
iconSpan.textContent = iconGlyph;
iconSpan.setAttribute("aria-hidden", "true");
const textWrap = document.createElement("div");
textWrap.className = "nd-overlay-item-text";
const primaryLine = document.createElement("div");
primaryLine.className = "nd-overlay-item-title";
primaryLine.textContent = item.label;
if (item.disabled) {
const badge = document.createElement("span");
badge.className = "nd-overlay-item-badge";
badge.textContent = "added";
primaryLine.appendChild(badge);
}
const secondaryLine = document.createElement("div");
secondaryLine.className = "nd-overlay-item-path";
secondaryLine.textContent = pathDisplay;
secondaryLine.style.display = folderPath.length ? "block" : "none";
textWrap.appendChild(primaryLine);
textWrap.appendChild(secondaryLine);
leftBtn.appendChild(checkboxSpan);
leftBtn.appendChild(iconSpan);
leftBtn.appendChild(textWrap);
const applyState = () => {
const isSelected = selectedIds.has(item.id);
if (!multiMode) {
checkboxSpan.dataset.state = "hidden";
} else {
checkboxSpan.dataset.state = isSelected ? "checked" : "unchecked";
}
leftBtn.classList.toggle("is-disabled", !!item.disabled);
leftBtn.classList.toggle("is-selected", multiMode && isSelected);
};
applyState();
leftBtn.addEventListener("click", () => {
if (item.disabled) return;
if (!multiMode) {
options.onChoose(item.id);
closeOverlay();
return;
}
if (selectedIds.has(item.id)) selectedIds.delete(item.id);
else selectedIds.add(item.id);
applyState();
renderFooter();
});
row.appendChild(leftBtn);
const actions = [];
if (options.rightActions && options.rightActions.length) {
actions.push(...options.rightActions);
} else if (options.onRightAction) {
actions.push({ icon: options.rightActionIcon || "🗑", title: options.rightActionTitle, onClick: options.onRightAction });
}
if (actions.length && !item.disabled) {
actions.forEach((action) => {
const rightBtn = document.createElement("button");
rightBtn.type = "button";
rightBtn.textContent = action.icon;
if (action.title) rightBtn.title = action.title;
rightBtn.style.cssText = `
margin-left: 8px;
padding: 10px 12px;
background: #3a2a2a;
color: #fff;
border: 1px solid #5a3a3a;
border-radius: 6px;
cursor: pointer;
`;
rightBtn.addEventListener("click", (event) => {
event.stopPropagation();
event.preventDefault();
action.onClick(item.id);
});
row.appendChild(rightBtn);
});
}
list.appendChild(row);
});
renderFooter();
};
const footer = document.createElement("div");
footer.style.cssText = `
display: none;
padding: 10px 12px;
border-top: 1px solid #444;
background: #1e1e1e;
gap: 8px;
justify-content: flex-end;
`;
const clearBtn = document.createElement("button");
clearBtn.type = "button";
clearBtn.textContent = "Clear";
clearBtn.style.cssText = "padding: 8px 12px; border-radius: 6px; background: #333; color: #fff; border: 1px solid #555; cursor: pointer;";
clearBtn.addEventListener("click", () => {
selectedIds.clear();
render(search.value);
});
const cancelBtn = document.createElement("button");
cancelBtn.type = "button";
cancelBtn.textContent = "Cancel";
cancelBtn.style.cssText = "padding: 8px 12px; border-radius: 6px; background: #444; color: #fff; border: 1px solid #555; cursor: pointer;";
cancelBtn.addEventListener("click", () => closeOverlay());
const addBtn = document.createElement("button");
addBtn.type = "button";
addBtn.textContent = "Add Selected (0)";
addBtn.style.cssText = "padding: 8px 12px; border-radius: 6px; background: #1976d2; color: #fff; border: 1px solid #0d47a1; cursor: pointer; opacity: 0.6;";
addBtn.disabled = true;
addBtn.addEventListener("click", () => {
if (!multiMode) return;
const ids = Array.from(selectedIds);
if (!ids.length) return;
if (typeof options.onChooseMany === "function") {
options.onChooseMany(ids);
} else {
ids.forEach((id) => options.onChoose(id));
}
closeOverlay();
});
footer.appendChild(clearBtn);
footer.appendChild(cancelBtn);
footer.appendChild(addBtn);
const renderFooter = () => {
const count = selectedIds.size;
addBtn.textContent = `Add Selected (${count})`;
addBtn.disabled = count === 0;
addBtn.style.opacity = count === 0 ? "0.6" : "1";
footer.style.display = multiEnabled && multiMode ? "flex" : "none";
};
listWrap.appendChild(empty);
listWrap.appendChild(list);
panel.appendChild(header);
panel.appendChild(search);
panel.appendChild(controls);
const allowFolderChips = Array.isArray(options.folderChips) && options.folderChips.length > 0 || items.some((item) => /[\\/]/.test(item.id));
if (allowFolderChips) {
const initialCounts = {};
items.forEach((item) => {
const parts = item.id.split(/[\\/]/);
const top = parts.length > 1 ? parts[0] : ROOT_KEY;
initialCounts[top] = (initialCounts[top] || 0) + 1;
});
if (Object.keys(initialCounts).length) {
folderFeatureEnabled = true;
renderChips(initialCounts);
panel.appendChild(chipWrap);
panel.appendChild(subChipWrap);
renderSubChips();
}
}
panel.appendChild(listWrap);
panel.appendChild(footer);
overlay.appendChild(panel);
document.body.appendChild(overlay);
search.addEventListener("input", () => render(search.value));
setTimeout(() => {
try {
search.focus();
} catch {
}
render("");
renderFooter();
}, 0);
}
showToast(message, type = "info") {
console.log(`Super LoRA Loader [${type}]: ${message}`);
const colors = {
success: "#28a745",
warning: "#ffc107",
error: "#dc3545",
info: "#17a2b8"
};
const toast = document.createElement("div");
toast.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: ${colors[type]};
color: white;
padding: 14px 20px;
border-radius: 6px;
z-index: 10000;
font-family: 'Segoe UI', Arial, sans-serif;
font-size: 14px;
font-weight: 500;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
opacity: 0;
transition: all 0.3s ease;
max-width: 400px;
word-wrap: break-word;
border-left: 4px solid rgba(255,255,255,0.3);
`;
toast.textContent = message;
document.body.appendChild(toast);
requestAnimationFrame(() => {
toast.style.opacity = "1";
toast.style.transform = "translateY(0)";
});
const timeout = type === "error" ? 5e3 : 3e3;
setTimeout(() => {
toast.style.opacity = "0";
toast.style.transform = "translateY(-10px)";
setTimeout(() => {
try {
toast.remove();
} catch {
}
}, 300);
}, timeout);
}
ensureOverlayStyles() {
if (this.stylesInjected) return;
try {
if (typeof document === "undefined") return;
const existing = document.querySelector('style[data-super-lora-overlay-style="1"]');
if (existing) {
this.stylesInjected = true;
return;
}
const style = document.createElement("style");
style.setAttribute("data-super-lora-overlay-style", "1");
style.textContent = `
[data-super-lora-overlay="1"] .nd-overlay-item-button {
position: relative;
width: 100%;
display: flex;
align-items: flex-start;
gap: 10px;
padding: 8px 10px;
background: #252525;
border: 1px solid #3a3a3a;
border-radius: 6px;
color: #fff;
cursor: pointer;
text-align: left;
transition: background 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;
}
[data-super-lora-overlay="1"] .nd-overlay-item-button:hover:not(.is-disabled) {
background: #2c2c2c;
border-color: #4a4a4a;
}
[data-super-lora-overlay="1"] .nd-overlay-item-button.is-disabled {
background: #2a2a2a;
color: #888;
cursor: not-allowed;
border-color: #343434;
}
[data-super-lora-overlay="1"] .nd-overlay-item-button.is-selected {
background: #263238;
border-color: #4a90e2;
box-shadow: inset 0 0 0 1px rgba(74, 144, 226, 0.35);
}
[data-super-lora-overlay="1"] .nd-overlay-item-checkbox {
width: 16px;
height: 16px;
min-width: 16px;
display: inline-flex;
align-items: center;
justify-content: center;
border: 1px solid #4a4a4a;
border-radius: 4px;
background: #1d1d1d;
color: #8ab4f8;
font-size: 12px;
line-height: 1;
transition: border-color 0.15s ease, background 0.15s ease;
}
[data-super-lora-overlay="1"] .nd-overlay-item-checkbox[data-state="hidden"] {
opacity: 0;
visibility: hidden;
}
[data-super-lora-overlay="1"] .nd-overlay-item-checkbox[data-state="unchecked"]::after {
content: '';
}
[data-super-lora-overlay="1"] .nd-overlay-item-checkbox[data-state="checked"] {
border-color: #4a90e2;
background: rgba(74, 144, 226, 0.18);
}
[data-super-lora-overlay="1"] .nd-overlay-item-checkbox[data-state="checked"]::after {
content: '✔';
}
[data-super-lora-overlay="1"] .nd-overlay-item-checkbox::after {
font-size: 10px;
color: #8ab4f8;
}
[data-super-lora-overlay="1"] .nd-overlay-item-icon {
font-size: 16px;
line-height: 1.5;
opacity: 0.85;
min-width: 18px;
}
[data-super-lora-overlay="1"] .nd-overlay-item-text {
flex: 1;
display: flex;
flex-direction: column;
gap: 2px;
overflow: hidden;
}
[data-super-lora-overlay="1"] .nd-overlay-item-title {
font-size: 14px;
font-weight: 500;
display: flex;
align-items: center;
gap: 6px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
[data-super-lora-overlay="1"] .nd-overlay-item-path {
font-size: 12px;
color: #9aa4b4;
opacity: 0.85;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
[data-super-lora-overlay="1"] .nd-overlay-item-badge {
font-size: 11px;
color: #9aa4b4;
border: 1px solid #4a4f55;
border-radius: 999px;
padding: 1px 6px;
text-transform: uppercase;
letter-spacing: 0.02em;
}
[data-super-lora-overlay="1"] .nd-overlay-chips-container,
[data-super-lora-overlay="1"] .nd-overlay-subchips-container {
display: flex;
flex-direction: column;
gap: 6px;
padding: 0 12px 6px 12px;
}
[data-super-lora-overlay="1"] .nd-overlay-chip-header {
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #9aa4b4;
}
[data-super-lora-overlay="1"] .nd-overlay-chip-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
[data-super-lora-overlay="1"] .nd-overlay-chip {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
border-radius: 999px;
border: 1px solid #3a3a3a;
background: #252525;
color: #fff;
cursor: pointer;
font-size: 12px;
transition: background 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;
}
[data-super-lora-overlay="1"] .nd-overlay-chip.is-active {
border-color: #4a90e2;
background: rgba(74, 144, 226, 0.18);
box-shadow: inset 0 0 0 1px rgba(74, 144, 226, 0.35);
}
[data-super-lora-overlay="1"] .nd-overlay-chip-icon {
font-size: 14px;
opacity: 0.85;
}
[data-super-lora-overlay="1"] .nd-overlay-chip-label {
font-weight: 500;
}
[data-super-lora-overlay="1"] .nd-overlay-chip-count {
font-size: 11px;
background: rgba(255, 255, 255, 0.08);
padding: 0 6px;
border-radius: 999px;
}
[data-super-lora-overlay="1"] .nd-overlay-chip--sub {
font-size: 11px;
padding: 5px 9px;
background: #1f1f1f;
}
[data-super-lora-overlay="1"] .nd-overlay-chip--sub.is-active {
background: rgba(255, 255, 255, 0.06);
border-color: #66aaff;
}
[data-super-lora-overlay="1"] .nd-overlay-multi-toggle {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 4px 8px;
border-radius: 999px;
border: 1px solid #3a3a3a;
background: #1f1f1f;
color: #fff;
font-size: 12px;
cursor: pointer;
user-select: none;
}
[data-super-lora-overlay="1"] .nd-overlay-multi-toggle-checkbox {
appearance: none;
width: 16px;
height: 16px;
border-radius: 4px;
border: 1px solid #4a4a4a;
background: #111;
position: relative;
cursor: pointer;
transition: border-color 0.15s ease, background 0.15s ease;
}
[data-super-lora-overlay="1"] .nd-overlay-multi-toggle-checkbox:checked {
border-color: #4a90e2;
background: rgba(74, 144, 226, 0.2);
}
[data-super-lora-overlay="1"] .nd-overlay-multi-toggle-checkbox:checked::after {
content: '✔';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -55%);
font-size: 10px;
color: #8ab4f8;
}
[data-super-lora-overlay="1"] .nd-overlay-multi-toggle-label {
font-weight: 500;
letter-spacing: 0.01em;
}
`;
document.head.appendChild(style);
this.stylesInjected = true;
} catch {
}
}
removeExistingOverlays() {
try {
document.querySelectorAll('[data-super-lora-overlay="1"]').forEach((el) => el.remove());
} catch {
}
}
restoreOverlayState(baseFolderKey) {
try {
const folderRaw = sessionStorage.getItem(this.getFolderStorageKey(baseFolderKey));
const subRaw = sessionStorage.getItem(this.getSubfolderStorageKey(baseFolderKey));
const activeFolders = folderRaw ? JSON.parse(folderRaw) : [];
const activeSubfolders = subRaw ? JSON.parse(subRaw) : [];
return {
activeFolders: Array.isArray(activeFolders) ? activeFolders : [],
activeSubfolders: Array.isArray(activeSubfolders) ? activeSubfolders : []
};
} catch {
return { activeFolders: [], activeSubfolders: [] };
}
}
persistOverlayState(baseFolderKey, folders, subfolders) {
try {
sessionStorage.setItem(this.getFolderStorageKey(baseFolderKey), JSON.stringify(Array.from(folders)));
sessionStorage.setItem(this.getSubfolderStorageKey(baseFolderKey), JSON.stringify(Array.from(subfolders)));
} catch {
}
}
getFolderStorageKey(baseFolderKey) {
const suffix = baseFolderKey ? `_${baseFolderKey}` : "";
return `superlora_folder_filters${suffix}`;
}
getSubfolderStorageKey(baseFolderKey) {
const suffix = baseFolderKey ? `_${baseFolderKey}` : "";
return `superlora_subfolder_filters${suffix}`;
}
}
class UpdateService {
constructor() {
this.status = null;
this.checkingPromise = null;
this.lastNotifiedVersion = null;
}
static getInstance() {
if (!UpdateService.instance) {
UpdateService.instance = new UpdateService();
}
return UpdateService.instance;
}
getStatus() {
return this.status;
}
async initialize() {
try {
const status = await this.checkForUpdates({ silent: true });
this.maybeNotify(status);
} catch (error) {
console.warn("ND Super Nodes: update check failed to initialize", error);
}
}
async checkForUpdates(options = {}) {
const { force = false, silent = false } = options;
if (this.checkingPromise) {
return this.checkingPromise;
}
if (!force && this.status) {
if (!silent) {
this.maybeNotify(this.status, { showUpToDate: true });
}
return this.status;
}
this.checkingPromise = this.fetchStatus(force).then((status) => {
this.status = status;
if (!silent) {
this.maybeNotify(status, { showUpToDate: true });
} else {
this.maybeNotify(status);
}
return status;
}).catch((error) => {
console.warn("ND Super Nodes: update check failed", error);
if (!silent) {
OverlayService.getInstance().showToast("ND Super Nodes update check failed. See console for details.", "warning");
}
throw error;
}).finally(() => {
this.checkingPromise = null;
});
return this.checkingPromise;
}
openReleasePage() {
const url = this.status?.releaseUrl || "https://github.com/HenkDz/nd-super-nodes/releases/latest";
try {
window.open(url, "_blank", "noopener,noreferrer");
} catch (error) {
OverlayService.getInstance().showToast("Unable to open release page. Copy URL from console.", "warning");
console.warn("ND Super Nodes release URL:", url, error);
}
}
async fetchStatus(force) {
const url = force ? "/super_lora/version?force=1" : "/super_lora/version";
const response = await fetch(url, { cache: force ? "reload" : "no-store" });
if (!response.ok) {
const text = await response.text();
throw new Error(`HTTP ${response.status}: ${text}`);
}
const payload = await response.json();
return payload;
}
maybeNotify(status, options = {}) {
if (!status) {
return;
}
const latestVersion = status.latestVersion || status.localVersion?.version;
const alreadyNotified = latestVersion && latestVersion === this.lastNotifiedVersion;
if (status.hasUpdate) {
if (alreadyNotified) {
return;
}
this.lastNotifiedVersion = latestVersion || null;
const messageParts = [
`🚀 ND Super Nodes v${latestVersion} available`,
"Run update.ps1 / update.sh in your node folder to upgrade."
];
OverlayService.getInstance().showToast(messageParts.join("\n"), "info");
console.info("ND Super Nodes: Update available", status);
return;
}
if (options.showUpToDate && !alreadyNotified) {
this.lastNotifiedVersion = latestVersion || null;
OverlayService.getInstance().showToast("ND Super Nodes is up to date.", "success");
}
}
}
const _FilePickerService = class _FilePickerService {
constructor() {
this.fileCache = /* @__PURE__ */ new Map();
this.cacheTimestamps = /* @__PURE__ */ new Map();
this.lastRefreshTimestamp = 0;
this.pendingLoraRefresh = null;
this.handleGlobalKeyDown = (event) => {
if (!this.isRefreshHotkey(event)) {
return;
}
this.onExternalRefresh("hotkey:R");
};
this.handleManualRefreshEvent = () => {
this.onExternalRefresh("event:nd-super-nodes:refresh-files");
};
if (typeof window === "undefined") {
return;
}
this.setupGlobalRefreshListeners();
}
static getInstance() {
if (!_FilePickerService.instance) {
_FilePickerService.instance = new _FilePickerService();
}
return _FilePickerService.instance;
}
setupGlobalRefreshListeners() {
try {
if (typeof document !== "undefined" && document.addEventListener) {
document.addEventListener("keydown", this.handleGlobalKeyDown, true);
}
window.addEventListener("nd-super-nodes:refresh-files", this.handleManualRefreshEvent);
} catch (error) {
console.warn("ND Super Nodes: failed to attach refresh listeners", error);
}
this.tryHookComfyRefreshFunctions();
}
tryHookComfyRefreshFunctions(attempt = 0) {
const hooked = this.patchRefreshFunctions(window?.app, "app") || this.patchRefreshFunctions(window?.api, "api") || this.patchRefreshFunctions(window?.ui, "ui");
if (hooked || attempt >= 20) {
return;
}
setTimeout(() => this.tryHookComfyRefreshFunctions(attempt + 1), 250 * Math.max(1, attempt + 1));
}
patchRefreshFunctions(source, sourceName) {
if (!source || typeof source !== "object") {
return false;
}
const marker = "__ndSuperNodesRefreshWrapped";
const service = this;
const seen = /* @__PURE__ */ new Set();
let hookedAny = false;
let cursor = source;
while (cursor && cursor !== Object.prototype && cursor !== Function.prototype) {
const keys = Object.getOwnPropertyNames(cursor);
for (const key of keys) {
if (seen.has(key)) {
continue;
}
seen.add(key);
if (!/refresh/i.test(key) || key === marker) {
continue;
}
const original = source[key];
if (typeof original !== "function" || original[marker]) {
continue;
}
const wrapped = function(...args) {
try {
service.onExternalRefresh(`${sourceName}.${key}`);
} catch (error) {
console.warn("ND Super Nodes: refresh hook error", error);
}
return original.apply(this, args);
};
wrapped[marker] = true;
const descriptor = Object.getOwnPropertyDescriptor(source, key);
if (descriptor && "value" in descriptor) {
Object.defineProperty(source, key, {
...descriptor,
value: wrapped
});
} else {
source[key] = wrapped;
}
hookedAny = true;
}
cursor = Object.getPrototypeOf(cursor);
}
return hookedAny;
}
isRefreshHotkey(event) {
if (!event || event.repeat) {
return false;
}
if (event.ctrlKey || event.metaKey || event.altKey) {
return false;
}
const key = event.key?.toLowerCase();
if (key !== "r") {
return false;
}
const target = event.target;
if (this.isInteractiveElement(target)) {
return false;
}
return true;
}
isInteractiveElement(target) {
if (!target || typeof window === "undefined") {
return false;
}
if (target instanceof HTMLElement) {
if (target.isContentEditable) {
return true;
}
const interactiveTags = ["INPUT", "TEXTAREA", "SELECT"];
if (interactiveTags.includes(target.tagName)) {
return true;
}
const role = target.getAttribute?.("role");
if (role && ["textbox", "combobox", "searchbox"].includes(role)) {
return true;
}
}
return false;
}
onExternalRefresh(trigger) {
const now = Date.now();
if (now - this.lastRefreshTimestamp < _FilePickerService.REFRESH_DEBOUNCE_MS) {
return;
}
this.lastRefreshTimestamp = now;
this.clearCache();
this.triggerLinkedServiceRefresh();
try {
window.dispatchEvent(new CustomEvent("nd-super-nodes:files-refreshed", { detail: { trigger } }));
} catch (error) {
}
}
triggerLinkedServiceRefresh() {
try {
const loraService = LoraService.getInstance();
if (!loraService || typeof loraService.refreshLoraList !== "function") {
return;
}
if (this.pendingLoraRefresh) {
return;
}
this.pendingLoraRefresh = Promise.resolve(loraService.refreshLoraList()).catch((error) => {
console.warn("ND Super Nodes: Failed to refresh LoRA list after global refresh", error);
}).finally(() => {
this.pendingLoraRefresh = null;
});
} catch (error) {
console.warn("ND Super Nodes: LoRA refresh hook failed", error);
}
}
static getSupportedFileTypes() {
return this.FILE_TYPES;
}
/**
* Get files for a specific file type
*/
async getFilesForType(fileType) {
const config = _FilePickerService.FILE_TYPES[fileType];
if (!config) {
throw new Error(`Unknown file type: ${fileType}`);
}
const cacheKey = `files_${fileType}`;
const cached = this.getCachedFiles(cacheKey);
if (cached) {
return cached;
}
try {
const params = new URLSearchParams();
params.set("folder_name", config.folderName);
params.set("extensions", config.fileExtensions.join(","));
let response = await fetch(`/super_lora/files?${params.toString()}`, { method: "GET" });
if (!response.ok) {
response = await fetch(`/superlora/files?${params.toString()}`, { method: "GET" });
}
if (!response.ok) {
throw new Error(`Failed to fetch files: ${response.statusText}`);
}
const data = await response.json();
const files = data.files?.map((file) => {
const relativePath = file.relative_path || file.path;
return {
id: relativePath,
label: file.name.replace(/\.(ckpt|pt|pt2|bin|pth|safetensors|pkl|sft|gguf)$/i, ""),
path: relativePath,
fullPath: file.path,
filename: file.name,
extension: file.extension || "",
size: file.size,
modified: file.modified
};
}) || [];
this.setCachedFiles(cacheKey, files);
return files;
} catch (error) {
console.error(`Error fetching ${fileType} files:`, error);
return [];
}
}
/**
* Show enhanced file picker overlay
*/
showFilePicker(fileType, onSelect, options = {}) {
const config = _FilePickerService.FILE_TYPES[fileType];
if (!config) {
throw new Error(`Unknown file type: ${fileType}`);
}
const {
title = `Select ${config.displayName}`,
multiSelect = false,
onMultiSelect,
currentValue
} = options;
this.getFilesForType(fileType).then((files) => {
const items = files.map((file) => ({
id: file.id,
label: file.label,
disabled: currentValue === file.id
}));
const overlay = OverlayService.getInstance();
const topFolders = Array.from(new Set(
files.map((file) => {
const rel = file.path || "";
const parts = rel.split(/[\\/]/);
return parts.length > 1 ? parts[0] : "__ROOT__";
})
));
overlay.showSearchOverlay({
title,
placeholder: config.placeholder || `Search ${config.displayName.toLowerCase()}...`,
items,
allowCreate: false,
enableMultiToggle: multiSelect,
onChoose: (id) => {
const file = files.find((f) => f.id === id);
if (file) {
onSelect(file);
}
},
onChooseMany: onMultiSelect ? (ids) => {
const selectedFiles = ids.map((id) => files.find((f) => f.id === id)).filter(Boolean);
if (onMultiSelect && selectedFiles.length > 0) {
onMultiSelect(selectedFiles);
}
} : void 0,
folderChips: topFolders,
baseFolderName: config.folderName,
currentValue,
rightActions: []
});
}).catch((error) => {
console.error("Failed to load file picker:", error);
OverlayService.getInstance().showToast(`Failed to load ${config.displayName.toLowerCase()}`, "error");
});
}
/**
* Cache management
*/
getCachedFiles(key) {
const cacheTime = this.cacheTimestamps.get(key);
if (!cacheTime) return null;
const fiveMinutesAgo = Date.now() - 5 * 60 * 1e3;
if (cacheTime < fiveMinutesAgo) {
this.fileCache.delete(key);
this.cacheTimestamps.delete(key);
return null;
}
return this.fileCache.get(key) || null;
}
setCachedFiles(key, files) {
this.fileCache.set(key, files);
this.cacheTimestamps.set(key, Date.now());
}
/**
* Clear all caches
*/
clearCache() {
this.fileCache.clear();
this.cacheTimestamps.clear();
}
/**
* Refresh cache for specific file type
*/
async refreshFileType(fileType) {
const cacheKey = `files_${fileType}`;
this.fileCache.delete(cacheKey);
this.cacheTimestamps.delete(cacheKey);
await this.getFilesForType(fileType);
}
};
_FilePickerService.REFRESH_DEBOUNCE_MS = 400;
_FilePickerService.FILE_TYPES = {
models: {
folderName: "checkpoints",
displayName: "Models",
fileExtensions: [".ckpt", ".pt", ".pt2", ".bin", ".pth", ".safetensors", ".pkl", ".sft"],
icon: "🏗️",
placeholder: "Search models..."
},
vae: {
folderName: "vae",
displayName: "VAEs",
fileExtensions: [".ckpt", ".pt", ".pt2", ".bin", ".pth", ".safetensors", ".pkl", ".sft"],
icon: "🎨",
placeholder: "Search VAEs..."
},
loras: {
folderName: "loras",
displayName: "LoRAs",
fileExtensions: [".ckpt", ".pt", ".pt2", ".bin", ".pth", ".safetensors", ".pkl", ".sft"],
icon: "🏷️",
placeholder: "Search LoRAs..."
},
text_encoders: {
folderName: "text_encoders",
displayName: "Text Encoders",
fileExtensions: [".ckpt", ".pt", ".pt2", ".bin", ".pth", ".safetensors", ".pkl", ".sft"],
icon: "📝",
placeholder: "Search text encoders..."
},
diffusion_models: {
folderName: "diffusion_models",
displayName: "Diffusion Models",
fileExtensions: [".ckpt", ".pt", ".pt2", ".bin", ".pth", ".safetensors", ".pkl", ".sft"],
icon: "🧠",
placeholder: "Search diffusion models..."
},
gguf_unet_models: {
folderName: "unet",
displayName: "UNET GGUF Models",
fileExtensions: [".gguf"],
icon: "🧠",
placeholder: "Search GGUF UNET models..."
},
controlnet: {
folderName: "controlnet",
displayName: "ControlNets",
fileExtensions: [".ckpt", ".pt", ".pt2", ".bin", ".pth", ".safetensors", ".pkl", ".sft"],
icon: "🎛️",
placeholder: "Search ControlNets..."
},
upscale_models: {
folderName: "upscale_models",
displayName: "Upscale Models",
fileExtensions: [".ckpt", ".pt", ".pt2", ".bin", ".pth", ".safetensors", ".pkl", ".sft"],
icon: "🔍",
placeholder: "Search upscale models..."
}
};
let FilePickerService = _FilePickerService;
class SuperLoraBaseWidget {
constructor(name) {
this.name = name;
this.type = "custom";
this.value = {};
this.hitAreas = {};
}
draw(_ctx, _node, _w, _posY, _height) {
}
onMouseDown(event, pos, node) {
return this.handleHitAreas(event, pos, node, "onDown");
}
onClick(event, pos, node) {
return this.handleHitAreas(event, pos, node, "onClick");
}
handleHitAreas(event, pos, node, handler) {
const entries = Object.entries(this.hitAreas);
entries.sort((a, b) => {
const pa = a[1]?.priority || 0;
const pb = b[1]?.priority || 0;
return pb - pa;
});
for (const [key, area] of entries) {
const bounds = area.bounds;
if (bounds && bounds.length >= 4 && this.isInBounds(pos, bounds)) {
const fn = (handler === "onDown" ? area.onDown : area.onClick) || (handler === "onDown" ? area.onClick : area.onDown);
if (fn) {
return fn.call(this, event, pos, node);
}
}
}
return false;
}
isInBounds(pos, bounds) {
if (bounds.length < 4) return false;
const [x, y, width, height] = bounds;
return pos[0] >= x && pos[0] <= x + width && pos[1] >= y && pos[1] <= y + height;
}
computeSize() {
return [200, 25];
}
}
const WidgetAPI = {
showLoraSelector: () => {
throw new Error("WidgetAPI.showLoraSelector not initialized");
},
showTagSelector: () => {
throw new Error("WidgetAPI.showTagSelector not initialized");
},
showSettingsDialog: () => {
throw new Error("WidgetAPI.showSettingsDialog not initialized");
},
showLoadTemplateDialog: () => {
throw new Error("WidgetAPI.showLoadTemplateDialog not initialized");
},
showNameOverlay: () => {
throw new Error("WidgetAPI.showNameOverlay not initialized");
},
showInlineText: () => {
throw new Error("WidgetAPI.showInlineText not initialized");
},
showToast: () => {
throw new Error("WidgetAPI.showToast not initialized");
},
calculateNodeSize: () => {
throw new Error("WidgetAPI.calculateNodeSize not initialized");
},
organizeByTags: () => {
throw new Error("WidgetAPI.organizeByTags not initialized");
},
addLoraWidget: () => {
throw new Error("WidgetAPI.addLoraWidget not initialized");
},
removeLoraWidget: () => {
throw new Error("WidgetAPI.removeLoraWidget not initialized");
},
getLoraConfigs: () => {
throw new Error("WidgetAPI.getLoraConfigs not initialized");
},
syncExecutionWidgets: () => {
throw new Error("WidgetAPI.syncExecutionWidgets not initialized");
},
templateService: null,
civitaiService: null
};
function setWidgetAPI(api) {
Object.assign(WidgetAPI, api);
}
const STORAGE_KEY = "super_lora_manual_triggers_v1";
function readStore() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return {};
const data = JSON.parse(raw);
return data && typeof data === "object" ? data : {};
} catch {
return {};
}
}
function writeStore(map) {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(map));
} catch {
}
}
const TriggerWordStore = {
get(loraFileName) {
if (!loraFileName) return null;
const map = readStore();
const val = map[loraFileName];
return typeof val === "string" && val.trim().length ? val : null;
},
set(loraFileName, triggerWords) {
if (!loraFileName) return;
const map = readStore();
if (typeof triggerWords === "string" && triggerWords.trim().length) {
map[loraFileName] = triggerWords.trim();
} else {
delete map[loraFileName];
}
writeStore(map);
},
remove(loraFileName) {
if (!loraFileName) return;
const map = readStore();
if (map[loraFileName] !== void 0) {
delete map[loraFileName];
writeStore(map);
}
}
};
class SuperLoraTagWidget extends SuperLoraBaseWidget {
constructor(tag) {
super(`tag_${tag}`);
this.tag = tag;
this.onCollapseDown = (_event, _pos, node) => {
this.value.collapsed = !this.value.collapsed;
WidgetAPI.calculateNodeSize(node);
node.setDirtyCanvas(true, false);
return true;
};
this.onToggleDown = (_event, _pos, node) => {
const lorasInTag = this.getLorasInTag(node);
const allEnabled = lorasInTag.every((w) => w.value.enabled);
lorasInTag.forEach((w) => w.value.enabled = !allEnabled);
node.setDirtyCanvas(true, false);
return true;
};
this.value = { type: "SuperLoraTagWidget", tag, collapsed: false };
this.hitAreas = {
toggle: { bounds: [0, 0], onDown: this.onToggleDown, priority: 10 },
collapse: { bounds: [0, 0], onDown: this.onCollapseDown, priority: 0 }
};
}
draw(ctx, node, w, posY, height) {
const margin = 10;
let posX = margin;
ctx.save();
ctx.fillStyle = "#2a2a2a";
ctx.fillRect(0, posY, w, height);
ctx.beginPath();
ctx.moveTo(0, posY + 0.5);
ctx.lineTo(w, posY + 0.5);
ctx.moveTo(0, posY + height - 0.5);
ctx.lineTo(w, posY + height - 0.5);
const midY = height / 2;
const lorasInTag = this.getLorasInTag(node);
lorasInTag.length > 0 && lorasInTag.every((w2) => w2.value.enabled);
ctx.fillStyle = "#fff";
ctx.font = "12px Arial";
ctx.textAlign = "left";
ctx.textBaseline = "middle";
ctx.fillText(this.value.collapsed ? "▶" : "▼", posX, posY + midY);
this.hitAreas.collapse.bounds = [0, 0, w, height];
posX += 20;
this.hitAreas.toggle.bounds = [0, 0, 0, 0];
ctx.fillStyle = "#fff";
ctx.fillText(`${this.tag} (${lorasInTag.length})`, posX, posY + midY);
ctx.restore();
}
getLorasInTag(node) {
if (!node.customWidgets) return [];
return node.customWidgets.filter(
(w) => w instanceof SuperLoraWidget && w.value.tag === this.tag
);
}
computeSize() {
return [400, 25];
}
isCollapsed() {
return this.value.collapsed;
}
}
class SuperLoraWidget extends SuperLoraBaseWidget {
constructor(name) {
super(name);
this.onEnabledDown = (_event, _pos, node) => {
this.value.enabled = !this.value.enabled;
node.setDirtyCanvas(true, false);
try {
WidgetAPI.syncExecutionWidgets(node);
} catch {
}
return true;
};
this.onLoraClick = (event, _pos, node) => {
WidgetAPI.showLoraSelector(node, this, event);
return true;
};
this.onStrengthClick = (event, _pos, node) => {
try {
const app2 = window?.app;
const canvas = app2?.canvas;
if (canvas?.prompt) {
canvas.prompt("Model Strength", this.value.strength ?? 1, (v) => {
const val = parseFloat(v);
if (!Number.isNaN(val)) {
this.value.strength = Math.max(-10, Math.min(10, val));
node.setDirtyCanvas(true, true);
}
}, event);
return true;
}
} catch {
}
return false;
};
this.onStrengthDownClick = (_event, _pos, node) => {
this.value.strength = Math.max(-10, this.value.strength - 0.1);
node.setDirtyCanvas(true, false);
try {
WidgetAPI.syncExecutionWidgets(node);
} catch {
}
return true;
};
this.onStrengthUpClick = (_event, _pos, node) => {
this.value.strength = Math.min(10, this.value.strength + 0.1);
node.setDirtyCanvas(true, false);
try {
WidgetAPI.syncExecutionWidgets(node);
} catch {
}
return true;
};
this.onStrengthClipClick = (event, _pos, node) => {
try {
const app2 = window?.app;
const canvas = app2?.canvas;
if (canvas?.prompt) {
canvas.prompt("CLIP Strength", this.value.strengthClip ?? this.value.strength ?? 1, (v) => {
const val = parseFloat(v);
if (!Number.isNaN(val)) {
this.value.strengthClip = Math.max(-10, Math.min(10, val));
node.setDirtyCanvas(true, true);
try {
WidgetAPI.syncExecutionWidgets(node);
} catch {
}
}
}, event);
return true;
}
} catch {
}
return false;
};
this.onStrengthClipDownClick = (_event, _pos, node) => {
this.value.strengthClip = Math.max(-10, (this.value.strengthClip ?? this.value.strength ?? 1) - 0.1);
node.setDirtyCanvas(true, false);
try {
WidgetAPI.syncExecutionWidgets(node);
} catch {
}
return true;
};
this.onStrengthClipUpClick = (_event, _pos, node) => {
this.value.strengthClip = Math.min(10, (this.value.strengthClip ?? this.value.strength ?? 1) + 0.1);
node.setDirtyCanvas(true, false);
try {
WidgetAPI.syncExecutionWidgets(node);
} catch {
}
return true;
};
this.onMoveUpClick = (_event, _pos, node) => {
const idx = node.customWidgets.indexOf(this);
if (idx <= 1) return true;
if (node?.properties?.enableTags) {
for (let j = idx - 1; j >= 0; j--) {
const w = node.customWidgets[j];
if (w instanceof SuperLoraWidget) {
if (w.value?.tag === this.value?.tag) {
const tmp = node.customWidgets[idx];
node.customWidgets[idx] = node.customWidgets[j];
node.customWidgets[j] = tmp;
break;
} else if (!(w instanceof SuperLoraWidget)) {
break;
}
}
if (w instanceof SuperLoraTagWidget) break;
}
} else {
const temp = node.customWidgets[idx];
node.customWidgets[idx] = node.customWidgets[idx - 1];
node.customWidgets[idx - 1] = temp;
}
WidgetAPI.calculateNodeSize(node);
node.setDirtyCanvas(true, false);
return true;
};
this.onMoveDownClick = (_event, _pos, node) => {
const idx = node.customWidgets.indexOf(this);
if (idx >= node.customWidgets.length - 1) return true;
if (node?.properties?.enableTags) {
for (let j = idx + 1; j < node.customWidgets.length; j++) {
const w = node.customWidgets[j];
if (w instanceof SuperLoraWidget) {
if (w.value?.tag === this.value?.tag) {
const tmp = node.customWidgets[idx];
node.customWidgets[idx] = node.customWidgets[j];
node.customWidgets[j] = tmp;
break;
} else if (!(w instanceof SuperLoraWidget)) {
break;
}
}
if (w instanceof SuperLoraTagWidget) break;
}
} else {
const temp = node.customWidgets[idx];
node.customWidgets[idx] = node.customWidgets[idx + 1];
node.customWidgets[idx + 1] = temp;
}
WidgetAPI.calculateNodeSize(node);
node.setDirtyCanvas(true, false);
return true;
};
this.onTriggerWordsClick = (event, _pos, node) => {
try {
try {
event?.stopPropagation?.();
event?.preventDefault?.();
} catch {
}
if (WidgetAPI && typeof WidgetAPI.showInlineText === "function") {
const rect = this._triggerRect;
const place = rect ? { rect, node, widget: this } : void 0;
WidgetAPI.showInlineText(event, this.value.triggerWords || "", async (v) => {
const newVal = String(v ?? "");
this.value.triggerWords = newVal;
this.value.autoFetched = false;
this.value = { ...this.value, fetchAttempted: false };
try {
TriggerWordStore.set(this.value.lora, newVal);
} catch {
}
this.value = { ...this.value };
node.setDirtyCanvas(true, true);
try {
WidgetAPI.syncExecutionWidgets(node);
} catch {
}
}, place);
return true;
}
} catch {
}
try {
const app2 = window?.app;
const canvas = app2?.canvas;
if (canvas?.prompt) {
canvas.prompt("Trigger Words", this.value.triggerWords || "", async (v) => {
const newVal = String(v ?? "");
this.value.triggerWords = newVal;
this.value.autoFetched = false;
this.value = { ...this.value, fetchAttempted: false };
try {
TriggerWordStore.set(this.value.lora, newVal);
} catch {
}
node.setDirtyCanvas(true, true);
try {
WidgetAPI.syncExecutionWidgets(node);
} catch {
}
}, event);
return true;
}
} catch {
}
return false;
};
this.onRefreshClick = async (_event, _pos, node) => {
try {
const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
this._refreshSpinActive = true;
this._refreshSpinStarted = now;
this._refreshSpinEnd = now + 650;
this._refreshSpinPeriod = 800;
try {
node.setDirtyCanvas(true, false);
} catch {
}
} catch {
}
try {
try {
TriggerWordStore.remove(this.value.lora);
} catch {
}
this.value.triggerWords = "";
this.value.autoFetched = false;
this.value = { ...this.value, fetchAttempted: false };
await this.fetchTriggerWords();
node.setDirtyCanvas(true, true);
try {
WidgetAPI.syncExecutionWidgets(node);
} catch {
}
return true;
} catch {
return false;
} finally {
try {
const now2 = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
const end = Math.max(this._refreshSpinEnd || now2, now2 + 200);
this._refreshSpinEnd = end;
const timeoutMs = Math.max(0, end - now2);
setTimeout(() => {
this._refreshSpinActive = false;
try {
node.setDirtyCanvas(true, false);
} catch {
}
;
}, timeoutMs);
} catch {
}
}
};
this.onTagClick = (_event, _pos, node) => {
WidgetAPI.showTagSelector(node, this);
return true;
};
this.onRemoveClick = (_event, _pos, node) => {
WidgetAPI.removeLoraWidget(node, this);
return true;
};
this.value = {
lora: "None",
enabled: true,
strength: 1,
strengthClip: 1,
triggerWords: "",
tag: "General",
autoFetched: false,
fetchAttempted: false
};
this.hitAreas = {
enabled: { bounds: [0, 0], onDown: this.onEnabledDown, priority: 60 },
lora: { bounds: [0, 0], onClick: this.onLoraClick, priority: 10 },
tag: { bounds: [0, 0], onClick: this.onTagClick, priority: 20 },
strength: { bounds: [0, 0], onClick: this.onStrengthClick, priority: 80 },
strengthDown: { bounds: [0, 0], onClick: this.onStrengthDownClick, priority: 90 },
strengthUp: { bounds: [0, 0], onClick: this.onStrengthUpClick, priority: 90 },
strengthClip: { bounds: [0, 0], onClick: this.onStrengthClipClick, priority: 80 },
strengthClipDown: { bounds: [0, 0], onClick: this.onStrengthClipDownClick, priority: 90 },
strengthClipUp: { bounds: [0, 0], onClick: this.onStrengthClipUpClick, priority: 90 },
triggerWords: { bounds: [0, 0], onClick: this.onTriggerWordsClick, priority: 85 },
refresh: { bounds: [0, 0], onClick: this.onRefreshClick, priority: 95 },
remove: { bounds: [0, 0], onClick: this.onRemoveClick, priority: 100 },
moveUp: { bounds: [0, 0], onClick: this.onMoveUpClick, priority: 70 },
moveDown: { bounds: [0, 0], onClick: this.onMoveDownClick, priority: 70 }
};
}
draw(ctx, node, w, posY, height) {
const margin = 8;
const rowHeight = 28;
ctx.save();
const innerWidth = Math.max(0, w - margin * 2);
const clampedHeight = Math.max(0, height);
ctx.beginPath();
ctx.rect(margin, posY, innerWidth, clampedHeight);
ctx.clip();
const bodyHeight = Math.max(4, height - 4);
const bodyY = posY + (height >= bodyHeight ? Math.floor((height - bodyHeight) / 2) : 0);
const cornerRadius = Math.min(6, bodyHeight / 2);
ctx.fillStyle = "#2a2a2a";
ctx.beginPath();
ctx.roundRect(margin, bodyY, innerWidth, bodyHeight, cornerRadius || 0);
ctx.fill();
ctx.strokeStyle = "#3a3a3a";
ctx.lineWidth = 1;
ctx.stroke();
ctx.clip();
if (!this.value.enabled) {
ctx.fillStyle = "rgba(0,0,0,0.4)";
ctx.fill();
}
ctx.font = "11px 'Segoe UI', Arial, sans-serif";
ctx.textBaseline = "middle";
const topPad = node.properties?.showTriggerWords ? 4 : Math.max(4, Math.floor((height - rowHeight) / 2));
let currentY = posY + topPad;
this.drawFirstRow(ctx, node, w, currentY, rowHeight, height);
ctx.restore();
}
drawFirstRow(ctx, node, w, posY, rowHeight, fullHeight) {
const margin = 8;
let posX = margin + 6;
const midY = rowHeight / 2;
const toggleSize = 20;
const toggleY = (rowHeight - toggleSize) / 2;
ctx.fillStyle = "#2a2a2a";
ctx.beginPath();
ctx.roundRect(posX, posY + toggleY, toggleSize, toggleSize, 2);
ctx.fill();
ctx.strokeStyle = this.value.enabled ? "#1b5e20" : "#3a3a3a";
ctx.lineWidth = 1;
ctx.stroke();
ctx.fillStyle = this.value.enabled ? "#2e7d32" : "";
ctx.textAlign = "center";
ctx.font = "12px Arial";
if (this.value.enabled) {
ctx.fillText("●", posX + toggleSize / 2, posY + midY);
}
this.hitAreas.enabled.bounds = [posX, 0, toggleSize, fullHeight];
posX += toggleSize + 8;
const loraWidgets = node.customWidgets?.filter((w2) => w2 instanceof SuperLoraWidget) || [];
const indexInLoras = loraWidgets.indexOf(this);
const lastIndex = loraWidgets.length - 1;
const showMoveArrows = loraWidgets.length > 1 && node?.properties?.showMoveArrows !== false;
const showStrength = node?.properties?.showStrengthControls !== false;
const showRemove = node?.properties?.showRemoveButton !== false;
const arrowSize = 20;
const strengthWidth = 50;
const btnSize = 20;
const removeSize = 20;
const gapSmall = 2;
const gap = 8;
const rightEdge = node.size[0] - margin;
let cursorX = rightEdge;
let removeX = -9999;
let plusX = -9999;
let minusX = -9999;
let strengthX = -9999;
let plusClipX = -9999;
let minusClipX = -9999;
let strengthClipX = -9999;
let upX = -9999;
let downX = -9999;
if (showRemove) {
cursorX -= removeSize;
removeX = cursorX - gap;
cursorX -= gap;
}
if (showStrength) {
cursorX -= btnSize;
plusX = cursorX - gap;
cursorX -= gapSmall;
cursorX -= strengthWidth;
strengthX = cursorX - gap;
cursorX -= gapSmall;
cursorX -= btnSize;
minusX = cursorX - gap;
cursorX -= gap;
if (node?.properties?.showSeparateStrengths) {
cursorX -= btnSize;
plusClipX = cursorX - gap;
cursorX -= gapSmall;
cursorX -= strengthWidth;
strengthClipX = cursorX - gap;
cursorX -= gapSmall;
cursorX -= btnSize;
minusClipX = cursorX - gap;
cursorX -= gap;
}
}
if (showMoveArrows) {
const leftMostMinus = showStrength && node?.properties?.showSeparateStrengths ? Math.min(minusX, minusClipX) : minusX;
const arrowRightStart = showStrength ? leftMostMinus - gap : showRemove ? removeX - gap : rightEdge - gap;
upX = arrowRightStart - arrowSize - 4;
downX = upX - (arrowSize + 2);
cursorX -= gap;
}
if (node?.properties?.enableTags && node?.properties?.showTagChip !== false) {
const iconSize = 20;
const iconY = posY + Math.floor((rowHeight - iconSize) / 2);
ctx.fillStyle = this.value.enabled ? "#333" : "#2a2a2a";
ctx.beginPath();
ctx.roundRect(posX, iconY, iconSize, iconSize, 2);
ctx.fill();
ctx.strokeStyle = "#444";
ctx.lineWidth = 1;
ctx.stroke();
ctx.fillStyle = "#FFD700";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.font = "12px Arial";
ctx.save();
if (!this.value.enabled) {
ctx.globalAlpha *= 0.55;
}
ctx.fillText("🏷", posX + iconSize / 2, posY + midY);
ctx.restore();
this.hitAreas.tag.bounds = [posX, 0, iconSize, fullHeight];
posX += iconSize + 6;
ctx.font = "12px 'Segoe UI', Arial, sans-serif";
} else {
this.hitAreas.tag.bounds = [0, 0, 0, 0];
}
const loraLeft = posX;
const rightMost = [
showMoveArrows ? downX : null,
showStrength ? minusX : null,
showStrength && node?.properties?.showSeparateStrengths ? minusClipX : null,
showRemove ? removeX : null
].filter((v) => typeof v === "number");
const loraMaxRight = (rightMost.length ? Math.min(...rightMost) : rightEdge) - gap;
const loraWidth = Math.max(100, loraMaxRight - loraLeft);
const showTriggers = !!(node.properties && node.properties.showTriggerWords);
const nameWidth = showTriggers ? Math.max(80, Math.floor(loraWidth * 0.6)) : loraWidth;
const trigWidth = showTriggers ? loraWidth - nameWidth : 0;
ctx.textAlign = "left";
ctx.font = "12px 'Segoe UI', Arial, sans-serif";
ctx.fillStyle = this.value.enabled ? "#fff" : "#888";
const loraText = this.value.lora === "None" ? "Click to select LoRA..." : this.value.lora;
const loraDisplay = this.truncateText(ctx, loraText, nameWidth);
ctx.fillText(loraDisplay, loraLeft, posY + midY);
this.hitAreas.lora.bounds = [loraLeft, 0, nameWidth, fullHeight];
const controlsAlpha = this.value.enabled ? 1 : 0.55;
ctx.save();
ctx.globalAlpha *= controlsAlpha;
const triggerLeft = loraLeft + nameWidth;
if (showTriggers && trigWidth > 0) {
const hasTrigger = !!(this.value.triggerWords && String(this.value.triggerWords).trim());
const pillH = 20;
const pillY = posY + Math.floor((rowHeight - pillH) / 2);
this._triggerRect = { x: triggerLeft, y: pillY, w: trigWidth, h: pillH };
ctx.fillStyle = "#2f2f2f";
ctx.beginPath();
ctx.roundRect(triggerLeft, pillY, trigWidth, pillH, 3);
ctx.fill();
const padX = 6;
ctx.textAlign = "left";
ctx.font = "10px 'Segoe UI', Arial, sans-serif";
if (hasTrigger) {
ctx.fillStyle = this.value.enabled ? "#fff" : "#aaa";
const trigDisplay = this.truncateText(ctx, String(this.value.triggerWords), trigWidth - padX * 2);
ctx.fillText(trigDisplay, triggerLeft + padX, posY + midY);
} else {
ctx.fillStyle = "#888";
const placeholder = "Click to add trigger words...";
const phDisplay = this.truncateText(ctx, placeholder, trigWidth - padX * 2);
ctx.fillText(phDisplay, triggerLeft + padX, posY + midY);
}
this.hitAreas.triggerWords.bounds = [triggerLeft, 0, trigWidth, fullHeight];
try {
const dotRadius = 7;
const dotCx = triggerLeft + trigWidth - 10;
const dotCy = posY + midY;
let showDot = true;
let color = "rgba(74, 158, 255, 0.85)";
const has = hasTrigger;
const auto = !!this.value.autoFetched;
const attempted = !!this.value?.fetchAttempted;
if (has && auto) {
color = "rgba(40, 167, 69, 0.85)";
} else if (has && !auto) {
color = "rgba(74, 158, 255, 0.85)";
} else if (!has && attempted) {
color = "rgba(253, 126, 20, 0.9)";
} else {
color = "rgba(160, 160, 160, 0.7)";
}
if (showDot) {
ctx.save();
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(dotCx, dotCy, dotRadius, 0, Math.PI * 2);
ctx.fill();
const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
const spinActive = !!this._refreshSpinActive;
const spinEnd = this._refreshSpinEnd || 0;
const spinStart = this._refreshSpinStarted || 0;
const isSpinning = spinActive && now < spinEnd;
if (isSpinning) {
try {
if (!this._spinRafScheduled) {
this._spinRafScheduled = true;
(window.requestAnimationFrame || ((cb) => setTimeout(cb, 16)))(() => {
this._spinRafScheduled = false;
try {
node.setDirtyCanvas(true, false);
} catch {
}
});
}
} catch {
}
}
ctx.fillStyle = "#111";
ctx.font = "10px Arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
if (isSpinning) {
const period = Math.max(500, this._refreshSpinPeriod || 800);
const progress = (now - spinStart) % period / period;
const angle = progress * Math.PI * 2;
ctx.save();
ctx.translate(dotCx, dotCy);
ctx.rotate(angle);
ctx.fillText("↻", 0, 0);
ctx.restore();
} else {
ctx.fillText("↻", dotCx, dotCy);
}
ctx.restore();
const size = dotRadius * 2 + 2;
this.hitAreas.refresh.bounds = [dotCx - dotRadius, 0, size, fullHeight];
}
} catch {
}
} else {
this.hitAreas.triggerWords.bounds = [0, 0, 0, 0];
}
if (showMoveArrows && node?.properties?.showMoveArrows !== false) {
const arrowY = (rowHeight - arrowSize) / 2;
let disableDown;
let disableUp;
if (node?.properties?.enableTags) {
const groupWidgets = (node.customWidgets || []).filter((w2) => w2 instanceof SuperLoraWidget && w2.value?.tag === this.value.tag);
const groupIndex = groupWidgets.indexOf(this);
const groupLastIndex = groupWidgets.length - 1;
disableDown = groupIndex === groupLastIndex;
disableUp = groupIndex === 0;
} else {
disableDown = indexInLoras === lastIndex;
disableUp = indexInLoras === 0;
}
ctx.globalAlpha = controlsAlpha * (disableDown ? 0.35 : 1);
ctx.fillStyle = "#555";
ctx.beginPath();
ctx.roundRect(downX, posY + arrowY, arrowSize, arrowSize, 2);
ctx.fill();
ctx.fillStyle = "#fff";
ctx.textAlign = "center";
ctx.font = "12px Arial";
ctx.fillText("▼", downX + arrowSize / 2, posY + midY);
this.hitAreas.moveDown.bounds = disableDown ? [0, 0, 0, 0] : [downX, 0, arrowSize, fullHeight];
ctx.globalAlpha = controlsAlpha * (disableUp ? 0.35 : 1);
ctx.fillStyle = "#555";
ctx.beginPath();
ctx.roundRect(upX, posY + arrowY, arrowSize, arrowSize, 2);
ctx.fill();
ctx.fillStyle = "#fff";
ctx.textAlign = "center";
ctx.font = "12px Arial";
ctx.fillText("▲", upX + arrowSize / 2, posY + midY);
this.hitAreas.moveUp.bounds = disableUp ? [0, 0, 0, 0] : [upX, 0, arrowSize, fullHeight];
ctx.globalAlpha = controlsAlpha;
} else {
this.hitAreas.moveUp.bounds = [0, 0, 0, 0];
this.hitAreas.moveDown.bounds = [0, 0, 0, 0];
}
const btnY = (rowHeight - btnSize) / 2;
if (showStrength) {
ctx.fillStyle = "#666";
ctx.beginPath();
ctx.roundRect(minusX, posY + btnY, btnSize, btnSize, 2);
ctx.fill();
ctx.fillStyle = "#fff";
ctx.textAlign = "center";
ctx.font = "12px Arial";
ctx.fillText("-", minusX + btnSize / 2, posY + midY);
this.hitAreas.strengthDown.bounds = [minusX, 0, btnSize, fullHeight];
} else {
this.hitAreas.strengthDown.bounds = [0, 0, 0, 0];
}
if (node?.properties?.showStrengthControls !== false) {
const strengthY = (rowHeight - 20) / 2;
ctx.fillStyle = this.value.enabled ? "#3b2a4a" : "#2a2a2a";
ctx.beginPath();
ctx.roundRect(strengthX, posY + strengthY, strengthWidth, 20, 3);
ctx.fill();
ctx.strokeStyle = "#4a4a4a";
ctx.lineWidth = 1;
ctx.stroke();
ctx.fillStyle = this.value.enabled ? "#e5e5e5" : "#bdbdbd";
ctx.textAlign = "center";
ctx.font = "12px Arial";
ctx.fillText(this.value.strength.toFixed(2), strengthX + strengthWidth / 2, posY + midY);
this.hitAreas.strength.bounds = [strengthX, 0, strengthWidth, fullHeight];
} else {
this.hitAreas.strength.bounds = [0, 0, 0, 0];
}
if (node?.properties?.showStrengthControls !== false) {
ctx.fillStyle = "#666";
ctx.beginPath();
ctx.roundRect(plusX, posY + btnY, btnSize, btnSize, 2);
ctx.fill();
ctx.fillStyle = "#fff";
ctx.textAlign = "center";
ctx.font = "12px Arial";
ctx.fillText("+", plusX + btnSize / 2, posY + midY);
this.hitAreas.strengthUp.bounds = [plusX, 0, btnSize, fullHeight];
} else {
this.hitAreas.strengthUp.bounds = [0, 0, 0, 0];
}
if (showStrength && node?.properties?.showSeparateStrengths) {
ctx.fillStyle = "#666";
ctx.beginPath();
ctx.roundRect(minusClipX, posY + btnY, btnSize, btnSize, 2);
ctx.fill();
ctx.fillStyle = "#fff";
ctx.textAlign = "center";
ctx.font = "12px Arial";
ctx.fillText("-", minusClipX + btnSize / 2, posY + midY);
this.hitAreas.strengthClipDown.bounds = [minusClipX, 0, btnSize, fullHeight];
const strengthY2 = (rowHeight - 20) / 2;
ctx.fillStyle = this.value.enabled ? "#4a3f1f" : "#2a2a2a";
ctx.beginPath();
ctx.roundRect(strengthClipX, posY + strengthY2, strengthWidth, 20, 3);
ctx.fill();
ctx.strokeStyle = "#4a4a4a";
ctx.lineWidth = 1;
ctx.stroke();
ctx.fillStyle = this.value.enabled ? "#e5e5e5" : "#bdbdbd";
ctx.textAlign = "center";
ctx.font = "12px Arial";
ctx.fillText(this.value.strengthClip.toFixed(2), strengthClipX + strengthWidth / 2, posY + midY);
this.hitAreas.strengthClip.bounds = [strengthClipX, 0, strengthWidth, fullHeight];
ctx.fillStyle = "#666";
ctx.beginPath();
ctx.roundRect(plusClipX, posY + btnY, btnSize, btnSize, 2);
ctx.fill();
ctx.fillStyle = "#fff";
ctx.textAlign = "center";
ctx.font = "12px Arial";
ctx.fillText("+", plusClipX + btnSize / 2, posY + midY);
this.hitAreas.strengthClipUp.bounds = [plusClipX, 0, btnSize, fullHeight];
} else {
this.hitAreas.strengthClipDown.bounds = [0, 0, 0, 0];
this.hitAreas.strengthClip.bounds = [0, 0, 0, 0];
this.hitAreas.strengthClipUp.bounds = [0, 0, 0, 0];
}
ctx.restore();
if (node?.properties?.showRemoveButton !== false) {
const removeY = (rowHeight - removeSize) / 2;
ctx.fillStyle = "#3a2a2a";
ctx.beginPath();
ctx.roundRect(removeX, posY + removeY, removeSize, removeSize, 2);
ctx.fill();
ctx.strokeStyle = "#5a3a3a";
ctx.lineWidth = 1;
ctx.stroke();
ctx.fillStyle = "#fff";
ctx.textAlign = "center";
ctx.font = "12px Arial";
ctx.fillText("🗑", removeX + removeSize / 2, posY + midY);
this.hitAreas.remove.bounds = [removeX, 0, removeSize, fullHeight];
} else {
this.hitAreas.remove.bounds = [0, 0, 0, 0];
}
}
isCollapsedByTag(node) {
if (!node.customWidgets) return false;
const tagWidget = node.customWidgets.find((w) => w instanceof SuperLoraTagWidget && w.tag === this.value.tag);
return tagWidget?.isCollapsed?.() || false;
}
truncateText(ctx, text, maxWidth) {
const metrics = ctx.measureText(text);
if (metrics.width <= maxWidth) return text;
let truncated = text;
while (ctx.measureText(truncated + "...").width > maxWidth && truncated.length > 0) {
truncated = truncated.slice(0, -1);
}
return truncated + "...";
}
computeSize() {
return [450, 50];
}
setLora(lora, node) {
this.value.lora = lora;
this.value.triggerWords = "";
this.value.autoFetched = false;
this.value = { ...this.value, fetchAttempted: false };
if (lora !== "None") {
try {
const manual = TriggerWordStore.get(lora);
if (manual) {
this.value.triggerWords = manual;
this.value.autoFetched = false;
return;
}
} catch {
}
if (node && node?.properties?.autoFetchTriggerWords !== false) {
this.fetchTriggerWords();
}
}
}
async fetchTriggerWords() {
try {
this.value.fetchAttempted = true;
try {
const manual = TriggerWordStore.get(this.value.lora);
if (manual) {
this.value.triggerWords = manual;
this.value.autoFetched = false;
return;
}
} catch {
}
const words = await WidgetAPI.civitaiService.getTriggerWords(this.value.lora);
if (words.length > 0) {
this.value.triggerWords = words.join(", ");
this.value.autoFetched = true;
try {
TriggerWordStore.set(this.value.lora, this.value.triggerWords);
} catch {
}
} else {
this.value = { ...this.value, fetchAttempted: true };
}
} catch (error) {
console.warn("Failed to fetch trigger words:", error);
}
}
}
class SuperLoraHeaderWidget extends SuperLoraBaseWidget {
constructor() {
super("SuperLoraHeaderWidget");
this.onToggleAllDown = (_event, _pos, node) => {
const allState = this.getAllLorasState(node);
if (!node.customWidgets) return true;
const loraWidgets = node.customWidgets.filter((w) => w instanceof SuperLoraWidget);
loraWidgets.forEach((w) => w.value.enabled = !allState);
node.setDirtyCanvas(true, true);
return true;
};
this.onAddLoraDown = (event, _pos, node) => {
WidgetAPI.showLoraSelector(node, void 0, event);
return true;
};
this.onSaveTemplateDown = (_event, _pos, node) => {
WidgetAPI.showNameOverlay({
title: "Save Template",
placeholder: "Template name...",
initial: "My LoRA Set",
submitLabel: "Save",
onCommit: async (templateName) => {
const name = (templateName || "").trim();
if (!name) {
WidgetAPI.showToast("Please enter a template name", "warning");
return;
}
const configs = WidgetAPI.getLoraConfigs(node);
const validConfigs = configs.filter((config) => config.lora && config.lora !== "None");
if (validConfigs.length === 0) {
WidgetAPI.showToast("⚠️ No valid LoRAs to save in template", "warning");
return;
}
try {
const exists = await WidgetAPI.templateService.templateExists(name);
if (exists) {
WidgetAPI.showToast(`⚠️ Template "${name}" already exists. Choose a different name.`, "warning");
return;
}
const success = await WidgetAPI.templateService.saveTemplate(name, validConfigs);
WidgetAPI.showToast(success ? `✅ Template "${name}" saved successfully!` : "❌ Failed to save template. Please try again.", success ? "success" : "error");
} catch (error) {
console.error("Template save error:", error);
WidgetAPI.showToast("❌ Error saving template. Check console for details.", "error");
}
}
});
return true;
};
this.onLoadTemplateDown = (event, _pos, node) => {
WidgetAPI.showLoadTemplateDialog(node, event);
return true;
};
this.onSettingsDown = (event, _pos, node) => {
WidgetAPI.showSettingsDialog(node, event);
return true;
};
this.value = { type: "SuperLoraHeaderWidget" };
this.hitAreas = {
toggleAll: { bounds: [0, 0], onDown: this.onToggleAllDown },
addLora: { bounds: [0, 0], onDown: this.onAddLoraDown },
saveTemplate: { bounds: [0, 0], onDown: this.onSaveTemplateDown },
loadTemplate: { bounds: [0, 0], onDown: this.onLoadTemplateDown },
settings: { bounds: [0, 0], onDown: this.onSettingsDown }
};
}
draw(ctx, node, w, posY, height) {
const margin = 8;
const buttonHeight = 24;
const buttonSpacing = 8;
let posX = margin;
ctx.save();
ctx.beginPath();
ctx.rect(0, posY, w, height);
ctx.clip();
const headerGradient = ctx.createLinearGradient(0, posY, 0, posY + height);
headerGradient.addColorStop(0, "#2f2f2f");
headerGradient.addColorStop(1, "#232323");
ctx.fillStyle = headerGradient;
ctx.fillRect(0, posY, w, height);
ctx.strokeStyle = "#3a3a3a";
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(0, posY + 0.5);
ctx.lineTo(w, posY + 0.5);
ctx.moveTo(0, posY + height - 0.5);
ctx.lineTo(w, posY + height - 0.5);
ctx.stroke();
const midY = posY + height / 2;
ctx.font = "500 11px 'Segoe UI', Arial, sans-serif";
ctx.textBaseline = "middle";
const buttonStyles = {
primary: {
gradient: ["#4f81ff", "#2f60f0"],
border: "#1f3fbf",
text: "#f7f9ff",
innerStroke: "rgba(255, 255, 255, 0.18)",
shadow: { color: "rgba(56, 109, 255, 0.45)", blur: 10, offsetY: 1 },
font: "600 11px 'Segoe UI', Arial, sans-serif",
iconFont: "700 14px 'Segoe UI', Arial, sans-serif"
},
secondary: {
gradient: ["#3a3a3a", "#2c2c2c"],
border: "#4a4a4a",
text: "#dedede",
innerStroke: "rgba(255, 255, 255, 0.08)",
font: "500 11px 'Segoe UI', Arial, sans-serif",
iconFont: "600 13px 'Segoe UI', Arial, sans-serif"
}
};
const modeWidths = {
addLora: { full: 132, short: 92, icon: 44 },
default: { full: 100, short: 64, icon: 36 }
};
const drawButton = (x, width, style, label, mode) => {
const buttonY = posY + (height - buttonHeight) / 2;
ctx.save();
const gradient = ctx.createLinearGradient(x, buttonY, x, buttonY + buttonHeight);
gradient.addColorStop(0, style.gradient[0]);
gradient.addColorStop(1, style.gradient[1]);
ctx.beginPath();
if (style.shadow) {
ctx.shadowColor = style.shadow.color;
ctx.shadowBlur = style.shadow.blur;
ctx.shadowOffsetY = style.shadow.offsetY ?? 0;
}
ctx.fillStyle = gradient;
ctx.roundRect(x, buttonY, width, buttonHeight, 5);
ctx.fill();
ctx.shadowColor = "transparent";
ctx.shadowBlur = 0;
ctx.shadowOffsetY = 0;
ctx.lineWidth = 1;
ctx.strokeStyle = style.border;
ctx.stroke();
if (style.innerStroke) {
ctx.beginPath();
const inset = 0.6;
ctx.roundRect(x + inset, buttonY + inset, width - inset * 2, buttonHeight - inset * 2, 4);
ctx.strokeStyle = style.innerStroke;
ctx.lineWidth = 1;
ctx.stroke();
}
ctx.fillStyle = style.text;
ctx.textAlign = "center";
const font = mode === "icon" ? style.iconFont ?? "600 13px 'Segoe UI', Arial, sans-serif" : style.font ?? "500 11px 'Segoe UI', Arial, sans-serif";
ctx.font = font;
const verticalOffset = mode === "icon" ? -0.5 : 0;
ctx.fillText(label, x + width / 2, midY + verticalOffset);
ctx.restore();
};
const availableWidth = w - margin * 2;
const allEnabled = this.getAllLorasState(node);
const toggleText = allEnabled ? "Disable All" : "Enable All";
const toggleShort = allEnabled ? "Disable" : "Enable";
const buttons = [
{ id: "addLora", text: "Add LoRA", shortText: "Add", icon: "➕", style: buttonStyles.primary, initialMode: "full", combineIcon: true },
{ id: "toggleAll", text: toggleText, shortText: toggleShort, icon: "⏯️", style: buttonStyles.secondary, initialMode: "full" },
{ id: "saveTemplate", text: "Save Set", shortText: "Save", icon: "💾", style: buttonStyles.secondary, initialMode: "full" },
{ id: "loadTemplate", text: "Load Set", shortText: "Load", icon: "📂", style: buttonStyles.secondary, initialMode: "full" },
{ id: "settings", text: "Settings", shortText: "Set", icon: "⚙️", style: buttonStyles.secondary, initialMode: "full" }
];
const totalSpacing = buttonSpacing * (buttons.length - 1);
const getModeWidth = (id, mode) => {
const preset = modeWidths[id] || modeWidths.default;
return preset[mode];
};
const computeTotalWidth = (modes2) => {
return modes2.reduce((sum, mode, idx) => {
const btn = buttons[idx];
return sum + getModeWidth(btn.id, mode);
}, 0);
};
const modes = buttons.map((btn) => btn.initialMode);
const degradeSteps = [
{ indices: [1, 2, 3], from: "full", to: "short" },
{ indices: [4], from: "full", to: "short" },
{ indices: [0], from: "full", to: "short" },
{ indices: [1, 2, 3], from: "short", to: "icon" },
{ indices: [4], from: "short", to: "icon" },
{ indices: [0], from: "short", to: "icon" }
];
let totalWidth = computeTotalWidth(modes);
const availableForButtons = Math.max(60, availableWidth - totalSpacing);
let stepIndex = 0;
while (totalWidth > availableForButtons && stepIndex < degradeSteps.length) {
const step = degradeSteps[stepIndex];
let changed = false;
for (const index of step.indices) {
if (modes[index] === step.from) {
modes[index] = step.to;
changed = true;
}
}
if (changed) {
totalWidth = computeTotalWidth(modes);
} else {
stepIndex++;
}
}
if (totalWidth > availableForButtons) {
const scale = availableForButtons / totalWidth;
const minWidths = buttons.map((btn) => getModeWidth(btn.id, "icon"));
totalWidth = 0;
const scaledWidths = buttons.map((btn, idx) => {
const rawWidth = getModeWidth(btn.id, modes[idx]) * scale;
const clamped = Math.max(minWidths[idx], rawWidth);
totalWidth += clamped;
return clamped;
});
const over = totalWidth - availableForButtons;
if (over > 0.1) {
let remainingOver = over;
for (let i = scaledWidths.length - 1; i >= 0 && remainingOver > 0.1; i--) {
const minWidth = minWidths[i];
const reducible = scaledWidths[i] - minWidth;
if (reducible > 0) {
const delta = Math.min(reducible, remainingOver);
scaledWidths[i] -= delta;
remainingOver -= delta;
}
}
}
buttons.forEach((btn, idx) => {
const btnWidth = scaledWidths[idx];
const mode = modes[idx];
let label;
if (mode === "icon") {
label = btn.icon;
} else if (mode === "short") {
label = btn.combineIcon && btn.icon ? `${btn.icon} ${btn.shortText}` : btn.shortText;
} else {
label = btn.combineIcon && btn.icon ? `${btn.icon} ${btn.text}` : btn.text;
}
drawButton(posX, btnWidth, btn.style, label, mode);
this.hitAreas[btn.id].bounds = [posX, 0, btnWidth, height];
posX += btnWidth + buttonSpacing;
});
} else {
buttons.forEach((btn, idx) => {
const mode = modes[idx];
const btnWidth = getModeWidth(btn.id, mode);
let label;
if (mode === "icon") {
label = btn.icon;
} else if (mode === "short") {
label = btn.combineIcon && btn.icon ? `${btn.icon} ${btn.shortText}` : btn.shortText;
} else {
label = btn.combineIcon && btn.icon ? `${btn.icon} ${btn.text}` : btn.text;
}
drawButton(posX, btnWidth, btn.style, label, mode);
this.hitAreas[btn.id].bounds = [posX, 0, btnWidth, height];
posX += btnWidth + buttonSpacing;
});
}
ctx.restore();
}
getAllLorasState(node) {
if (!node.customWidgets) return false;
const loraWidgets = node.customWidgets.filter((w) => w instanceof SuperLoraWidget);
return loraWidgets.length > 0 && loraWidgets.every((w) => w.value.enabled);
}
computeSize() {
return [450, 35];
}
}
const app = window.app;
const LiteGraph = window.LiteGraph;
const _SuperLoraNode = class _SuperLoraNode {
static async initialize() {
if (this.initialized) {
return;
}
if (this.initializationPromise) {
return this.initializationPromise;
}
this.initializationPromise = (async () => {
this.loraService = LoraService.getInstance();
this.templateService = TemplateService.getInstance();
this.civitaiService = CivitAiService.getInstance();
this.updateService = UpdateService.getInstance();
FilePickerService.getInstance();
setWidgetAPI({
showLoraSelector: (node, widget, e) => _SuperLoraNode.showLoraSelector(node, widget, e),
showTagSelector: (node, widget) => _SuperLoraNode.showTagSelector(node, widget),
showSettingsDialog: (node, e) => _SuperLoraNode.showSettingsDialog(node, e),
showLoadTemplateDialog: (node, e) => _SuperLoraNode.showLoadTemplateDialog(node, e),
showNameOverlay: (opts) => _SuperLoraNode.showNameOverlay(opts),
showInlineText: (e, initial, onCommit, place) => _SuperLoraNode.showInlineText(e, initial, onCommit, place),
showToast: (m, t) => _SuperLoraNode.showToast(m, t),
calculateNodeSize: (node) => _SuperLoraNode.calculateNodeSize(node),
organizeByTags: (node) => _SuperLoraNode.organizeByTags(node),
addLoraWidget: (node, config) => _SuperLoraNode.addLoraWidget(node, config),
removeLoraWidget: (node, widget) => _SuperLoraNode.removeLoraWidget(node, widget),
getLoraConfigs: (node) => _SuperLoraNode.getLoraConfigs(node),
templateService: _SuperLoraNode.templateService,
civitaiService: _SuperLoraNode.civitaiService,
syncExecutionWidgets: (node) => _SuperLoraNode.syncExecutionWidgets(node)
});
await Promise.all([
this.loraService.initialize(),
this.templateService.initialize(),
this.updateService.initialize()
]);
try {
window.NDSuperNodesUpdateStatus = this.updateService.getStatus();
} catch {
}
this.initialized = true;
})();
try {
await this.initializationPromise;
} finally {
this.initializationPromise = null;
}
}
static isNodeBypassed(node) {
if (!node) {
return false;
}
const flags = node.flags || {};
if (flags.bypass || flags.bypassed || flags.skip_processing || flags.skipProcessing) {
return true;
}
if (node.properties && (node.properties.bypass === true || node.properties.skip === true)) {
return true;
}
try {
if (typeof LiteGraph !== "undefined" && LiteGraph && typeof LiteGraph.NEVER === "number") {
if (node.mode === LiteGraph.NEVER) {
return true;
}
}
} catch {
}
return false;
}
/**
* Set up the node type with custom widgets
*/
static setup(nodeType, _nodeData) {
const originalNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function() {
if (originalNodeCreated) {
originalNodeCreated.apply(this, arguments);
}
_SuperLoraNode.setupAdvancedNode(this);
try {
this.widgets = (this.widgets || []).filter((w) => {
const nm = w?.name || "";
return !(nm === "lora_bundle" || nm.startsWith("lora_"));
});
} catch {
}
};
const originalOnDrawForeground = nodeType.prototype.onDrawForeground;
nodeType.prototype.onDrawForeground = function(ctx) {
if (originalOnDrawForeground) {
originalOnDrawForeground.call(this, ctx);
}
_SuperLoraNode.drawCustomWidgets(this, ctx);
};
const originalOnMouseDown = nodeType.prototype.onMouseDown;
nodeType.prototype.onMouseDown = function(event, pos) {
if (_SuperLoraNode.handleMouseDown(this, event, pos)) {
return true;
}
return originalOnMouseDown ? originalOnMouseDown.call(this, event, pos) : false;
};
const originalOnMouseUp = nodeType.prototype.onMouseUp;
nodeType.prototype.onMouseUp = function(event, pos) {
if (_SuperLoraNode.handleMouseUp(this, event, pos)) {
return true;
}
return originalOnMouseUp ? originalOnMouseUp.call(this, event, pos) : false;
};
const originalOnResize = nodeType.prototype.onResize;
nodeType.prototype.onResize = function(size, ...rest) {
const minHeight = _SuperLoraNode.computeContentHeight(this);
if (Array.isArray(size)) {
size[1] = Math.max(size[1], minHeight);
} else if (size && typeof size === "object") {
if (typeof size[1] === "number") {
size[1] = Math.max(size[1], minHeight);
}
if (typeof size.y === "number") {
size.y = Math.max(size.y, minHeight);
}
}
if (this.size) {
this.size[1] = Math.max(this.size[1], minHeight);
}
if (originalOnResize) {
return originalOnResize.apply(this, [size, ...rest]);
}
return void 0;
};
const originalSerialize = nodeType.prototype.serialize;
nodeType.prototype.serialize = function() {
const data = originalSerialize.apply(this, arguments);
try {
const freshBundle = _SuperLoraNode.buildBundle(this);
let bridge = (this.widgets || []).find((w) => w?.name === "lora_bundle");
if (!bridge) {
bridge = this.addWidget("text", "lora_bundle", freshBundle, () => {
}, {});
}
bridge.type = "text";
bridge.hidden = true;
bridge.draw = () => {
};
bridge.computeSize = () => [0, 0];
bridge.value = freshBundle;
bridge.serializeValue = () => freshBundle;
data.inputs = data.inputs || {};
data.inputs.lora_bundle = freshBundle;
} catch {
}
data.customWidgets = _SuperLoraNode.serializeCustomWidgets(this);
return data;
};
const originalConfigure = nodeType.prototype.configure;
nodeType.prototype.configure = function(data) {
if (originalConfigure) {
originalConfigure.call(this, data);
}
if (data.customWidgets) {
_SuperLoraNode.deserializeCustomWidgets(this, data.customWidgets);
} else {
_SuperLoraNode.setupAdvancedNode(this);
}
try {
this.widgets = (this.widgets || []).filter((w) => {
const nm = w?.name || "";
return !(nm.startsWith("lora_") && nm !== "lora_bundle");
});
} catch {
}
_SuperLoraNode.syncExecutionWidgets(this);
};
const originalGetExtraMenuOptions = nodeType.prototype.getExtraMenuOptions;
nodeType.prototype.getExtraMenuOptions = function(_canvas, optionsArr) {
try {
let options = Array.isArray(optionsArr) ? optionsArr : [];
if (originalGetExtraMenuOptions) {
const maybe = originalGetExtraMenuOptions.call(this, _canvas, options);
if (Array.isArray(maybe)) options = maybe;
}
const hasAddLoRA = options.some((opt) => opt && opt.content === "🏷️ Add LoRA");
const hasSettings = options.some((opt) => opt && opt.content === "⚙️ Settings");
if (!hasAddLoRA || !hasSettings) {
if (options.length === 0 || options[options.length - 1] !== null) {
options.push(null);
}
if (!hasAddLoRA) {
options.push({ content: "🏷️ Add LoRA", callback: (_event) => _SuperLoraNode.showLoraSelector(this, void 0, void 0) });
}
if (!hasSettings) {
options.push({ content: "⚙️ Settings", callback: (_event) => _SuperLoraNode.showSettingsDialog(this) });
}
}
return options;
} catch {
const safe = optionsArr && Array.isArray(optionsArr) ? optionsArr : [];
const ensureSeparator = () => {
if (safe.length === 0 || safe[safe.length - 1] !== null) {
safe.push(null);
}
};
if (!safe.some((opt) => opt && opt.content === "🏷️ Add LoRA")) {
ensureSeparator();
safe.push({ content: "🏷️ Add LoRA", callback: (_event) => _SuperLoraNode.showLoraSelector(this, void 0, void 0) });
}
if (!safe.some((opt) => opt && opt.content === "⚙️ Settings")) {
ensureSeparator();
safe.push({ content: "⚙️ Settings", callback: (_event) => _SuperLoraNode.showSettingsDialog(this) });
}
return safe;
}
};
}
/**
* Initialize advanced node with custom widgets
*/
static setupAdvancedNode(node) {
if (node.customWidgets && node.customWidgets.length > 0) {
console.log("Super LoRA Loader: Node already initialized, skipping");
return;
}
node.properties = node.properties || {};
node.properties.enableTags = node.properties.enableTags !== false;
node.properties.showTriggerWords = node.properties.showTriggerWords !== false;
node.properties.showSeparateStrengths = node.properties.showSeparateStrengths || false;
node.properties.autoFetchTriggerWords = node.properties.autoFetchTriggerWords !== false;
node.customWidgets = node.customWidgets || [];
node.customWidgets.push(new SuperLoraHeaderWidget());
const contentHeight = this.computeContentHeight(node);
node.size = [node.size[0], Math.max(node.size[1], contentHeight)];
console.log("Super LoRA Loader: Advanced node setup complete");
}
static computeContentHeight(node) {
const marginDefault = _SuperLoraNode.MARGIN_SMALL;
let currentY = this.NODE_WIDGET_TOP_OFFSET;
if (!node?.customWidgets) {
return Math.max(currentY, 100);
}
const renderable = [];
for (const widget of node.customWidgets) {
const isCollapsed = widget instanceof SuperLoraWidget && widget.isCollapsedByTag(node);
if (isCollapsed) continue;
const size = widget.computeSize();
const height = widget instanceof SuperLoraWidget ? 34 : size[1];
if (height === 0) continue;
renderable.push(widget);
}
renderable.forEach((widget, index) => {
const size = widget.computeSize();
const height = widget instanceof SuperLoraWidget ? 34 : size[1];
let marginAfter = widget instanceof SuperLoraTagWidget && widget.isCollapsed() ? 0 : marginDefault;
const isLast = index === renderable.length - 1;
if (isLast && widget instanceof SuperLoraTagWidget && widget.isCollapsed()) {
marginAfter = Math.max(marginDefault, 8);
}
currentY += height + marginAfter;
});
return Math.max(currentY, 100);
}
/**
* Calculate required node size based on widgets
*/
static calculateNodeSize(node) {
const newHeight = this.computeContentHeight(node);
if (node.size[1] !== newHeight) {
node.size[1] = newHeight;
}
}
/**
* Custom drawing for all widgets
*/
static drawCustomWidgets(node, ctx) {
if (!node.customWidgets) return;
const isBypassed = _SuperLoraNode.isNodeBypassed(node);
const marginDefault = _SuperLoraNode.MARGIN_SMALL;
let currentY = this.NODE_WIDGET_TOP_OFFSET;
if (!isBypassed) {
const renderable = [];
for (const widget of node.customWidgets) {
const size = widget.computeSize();
const isCollapsed = widget instanceof SuperLoraWidget && widget.isCollapsedByTag(node);
const height = widget instanceof SuperLoraWidget ? 34 : size[1];
if (height === 0 || isCollapsed) continue;
renderable.push(widget);
}
renderable.forEach((widget, index) => {
const size = widget.computeSize();
const height = widget instanceof SuperLoraWidget ? 34 : size[1];
widget.draw(ctx, node, node.size[0], currentY, height);
let marginAfter = widget instanceof SuperLoraTagWidget && widget.isCollapsed() ? 0 : marginDefault;
const isLast = index === renderable.length - 1;
if (isLast && widget instanceof SuperLoraTagWidget && widget.isCollapsed()) {
marginAfter = Math.max(marginDefault, 8);
}
currentY += height + marginAfter;
});
}
if (isBypassed) {
try {
ctx.save();
ctx.globalCompositeOperation = "source-over";
ctx.fillStyle = "rgba(142, 88, 255, 0.42)";
const radius = 6;
if (typeof ctx.roundRect === "function") {
ctx.beginPath();
ctx.roundRect(4, 4, Math.max(0, node.size[0] - 8), Math.max(0, node.size[1] - 8), radius);
ctx.fill();
} else {
ctx.fillRect(4, 4, Math.max(0, node.size[0] - 8), Math.max(0, node.size[1] - 8));
}
ctx.restore();
} catch {
}
}
}
/**
* Handle mouse interactions
*/
static handleMouseDown(node, event, pos) {
return this.handleMouseEvent(node, event, pos, "onMouseDown");
}
static handleMouseUp(node, event, pos) {
return this.handleMouseEvent(node, event, pos, "onClick");
}
static handleMouseEvent(node, event, pos, handler) {
if (!node.customWidgets) return false;
if (_SuperLoraNode.isNodeBypassed(node)) {
return true;
}
try {
const rect = app?.canvas?.canvas?.getBoundingClientRect?.();
const ds = app?.canvas?.ds;
let sx = event && (event.clientX ?? event.pageX) || null;
let sy = event && (event.clientY ?? event.pageY) || null;
if ((sx == null || sy == null) && rect && ds) {
sx = rect.left + (pos[0] + (ds.offset?.[0] || 0)) * (ds.scale || 1);
sy = rect.top + (pos[1] + (ds.offset?.[1] || 0)) * (ds.scale || 1);
}
if (sx != null && sy != null) {
_SuperLoraNode._lastPointerScreen = { x: sx, y: sy };
}
} catch {
}
const marginDefault = _SuperLoraNode.MARGIN_SMALL;
let currentY = this.NODE_WIDGET_TOP_OFFSET;
for (const widget of node.customWidgets) {
const size = widget.computeSize();
const isCollapsed = widget instanceof SuperLoraWidget && widget.isCollapsedByTag(node);
if (size[1] === 0 || isCollapsed) {
continue;
}
const height = widget instanceof SuperLoraWidget ? 34 : size[1];
const widgetStartY = currentY;
const widgetEndY = currentY + height;
if (pos[1] >= widgetStartY && pos[1] <= widgetEndY) {
const localPos = [pos[0], pos[1] - widgetStartY];
if (widget[handler]) {
if (widget[handler](event, localPos, node)) {
return true;
}
}
}
const marginAfter = widget instanceof SuperLoraTagWidget && widget.isCollapsed() ? 0 : marginDefault;
currentY += height + marginAfter;
}
return false;
}
/**
* Compute the top Y offset (in node-local coordinates) for a given widget
*/
static computeWidgetTop(node, targetWidget) {
if (!node?.customWidgets) return this.NODE_WIDGET_TOP_OFFSET;
const marginDefault = _SuperLoraNode.MARGIN_SMALL;
let currentY = this.NODE_WIDGET_TOP_OFFSET;
for (const widget of node.customWidgets) {
const size = widget.computeSize?.() || [0, 0];
const isCollapsed = widget instanceof SuperLoraWidget && widget.isCollapsedByTag?.(node);
const height = widget instanceof SuperLoraWidget ? 34 : size[1];
if (height === 0 || isCollapsed) {
continue;
}
if (widget === targetWidget) {
return currentY;
}
const marginAfter = widget instanceof SuperLoraTagWidget && widget.isCollapsed?.() ? 0 : marginDefault;
currentY += height + marginAfter;
}
return currentY;
}
/**
* Show LoRA selector dialog with enhanced search functionality
*/
static async showLoraSelector(node, widget, _event) {
try {
const availableLoras = await _SuperLoraNode.loraService.getAvailableLoras();
const usedLoras = this.getUsedLoras(node);
const items = availableLoras.map((name) => ({
id: name,
label: name.replace(/\.(safetensors|ckpt|pt)$/i, ""),
disabled: usedLoras.has(name)
}));
OverlayService.getInstance().showSearchOverlay({
title: "Add LoRA",
placeholder: "Search LoRAs...",
items,
onChoose: (id) => {
if (this.isDuplicateLora(node, id)) {
this.showToast("⚠️ Already added to the list", "warning");
return;
}
if (widget) {
widget.setLora(id, node);
this.showToast("✅ LoRA updated", "success");
} else {
this.addLoraWidget(node, { lora: id });
this.showToast("✅ LoRA added", "success");
}
node.setDirtyCanvas(true, true);
},
enableMultiToggle: true,
onChooseMany: (ids) => {
const added = [];
const skipped = [];
ids.forEach((id) => {
if (this.isDuplicateLora(node, id)) {
skipped.push(id);
return;
}
if (widget) {
if (added.length === 0) {
widget.setLora(id, node);
added.push(id);
return;
}
}
this.addLoraWidget(node, { lora: id });
added.push(id);
});
if (added.length) this.showToast(`✅ Added ${added.length} LoRA${added.length > 1 ? "s" : ""}`, "success");
if (skipped.length) this.showToast(`⚠️ Skipped ${skipped.length} duplicate${skipped.length > 1 ? "s" : ""}`, "warning");
node.setDirtyCanvas(true, true);
},
// Provide folder chips explicitly (top-level folders)
folderChips: Array.from(new Set(
availableLoras.map((p) => p.split(/[\\/]/)[0]).filter(Boolean)
)).sort(),
// Fix the root chip label for LoRAs
baseFolderName: "loras"
});
} catch (error) {
console.error("Failed to show LoRA selector:", error);
this.showToast("Failed to load LoRAs", "error");
}
}
/**
* Show tag selector dialog
*/
static showTagSelector(node, widget) {
const svc = TagSetService.getInstance();
const existingTags = this.getExistingTags(node);
const fromStore = svc.getAll();
const allTags = Array.from(/* @__PURE__ */ new Set([
...fromStore,
...existingTags
]));
const items = allTags.map((tag) => ({ id: tag, label: tag }));
OverlayService.getInstance().showSearchOverlay({
title: "Select Tag",
placeholder: "Search or create tag...",
items,
allowCreate: true,
onChoose: (tag) => {
try {
svc.addTag(tag);
} catch {
}
widget.value.tag = tag;
this.organizeByTags(node);
this.calculateNodeSize(node);
node.setDirtyCanvas(true, false);
},
rightActions: [
{
icon: "✏️",
title: "Rename tag",
onClick: (name) => {
this.showNameOverlay({
title: "Rename Tag",
placeholder: "New tag name...",
initial: name,
submitLabel: "Rename",
onCommit: (newName) => {
const ok = svc.renameTag(name, newName);
this.showToast(ok ? "✅ Tag renamed" : "❌ Failed to rename tag", ok ? "success" : "error");
this.showTagSelector(node, widget);
}
});
}
},
{
icon: "🗑",
title: "Delete tag",
onClick: (name) => {
const okConfirm = confirm(`Delete tag "${name}"?`);
if (!okConfirm) return;
const ok = svc.deleteTag(name);
this.showToast(ok ? "✅ Tag deleted" : "❌ Failed to delete tag", ok ? "success" : "error");
this.showTagSelector(node, widget);
}
}
]
});
}
/**
* Show settings dialog
*/
static showSettingsDialog(node, event) {
const coreItems = [
{
content: `${node.properties.enableTags ? "✅" : "❌"} Enable Tags`,
callback: () => {
node.properties.enableTags = !node.properties.enableTags;
this.organizeByTags(node);
this.calculateNodeSize(node);
node.setDirtyCanvas(true, false);
this.syncExecutionWidgets(node);
}
},
{
content: `${node.properties.showSeparateStrengths ? "✅" : "❌"} Separate Model/CLIP Strengths`,
callback: () => {
const enabling = !node.properties.showSeparateStrengths;
node.properties.showSeparateStrengths = enabling;
try {
const widgets = (node.customWidgets || []).filter((w) => w instanceof SuperLoraWidget);
if (enabling) {
widgets.forEach((w) => {
const m = parseFloat(w.value?.strength ?? 0) || 0;
w.value.strengthClip = typeof w.value?.strengthClip === "number" ? w.value.strengthClip : m;
});
} else {
widgets.forEach((w) => {
const m = parseFloat(w.value?.strength ?? 0) || 0;
w.value.strength = m;
w.value.strengthClip = m;
});
}
} catch {
}
node.setDirtyCanvas(true, false);
this.syncExecutionWidgets(node);
}
},
{
content: `${node.properties.autoFetchTriggerWords ? "✅" : "❌"} Auto-fetch Trigger Words`,
callback: () => {
node.properties.autoFetchTriggerWords = !node.properties.autoFetchTriggerWords;
this.syncExecutionWidgets(node);
}
}
];
const showItems = [
{
content: `${node.properties.showTriggerWords ? "✅" : "❌"} Show Trigger Words`,
callback: () => {
node.properties.showTriggerWords = !node.properties.showTriggerWords;
node.setDirtyCanvas(true, false);
this.syncExecutionWidgets(node);
}
},
{
content: `${node.properties.showTagChip !== false ? "✅" : "❌"} Show Tag Chip`,
callback: () => {
node.properties.showTagChip = node.properties.showTagChip === false ? true : false;
node.setDirtyCanvas(true, false);
this.syncExecutionWidgets(node);
}
},
{
content: `${node.properties.showMoveArrows !== false ? "✅" : "❌"} Show Move Arrows`,
callback: () => {
node.properties.showMoveArrows = node.properties.showMoveArrows === false ? true : false;
node.setDirtyCanvas(true, false);
this.syncExecutionWidgets(node);
}
},
{
content: `${node.properties.showRemoveButton !== false ? "✅" : "❌"} Show Remove Button`,
callback: () => {
node.properties.showRemoveButton = node.properties.showRemoveButton === false ? true : false;
node.setDirtyCanvas(true, false);
this.syncExecutionWidgets(node);
}
},
{
content: `${node.properties.showStrengthControls !== false ? "✅" : "❌"} Show Strength Controls`,
callback: () => {
node.properties.showStrengthControls = node.properties.showStrengthControls === false ? true : false;
node.setDirtyCanvas(true, false);
this.syncExecutionWidgets(node);
}
}
];
const menuItems = [...coreItems, null, ...showItems];
new LiteGraph.ContextMenu(menuItems, { title: "Settings", event });
}
/**
* Show save template dialog
*/
static showSaveTemplateDialog(node) {
const templateName = prompt("Enter template name:", "My LoRA Set");
if (templateName && templateName.trim()) {
const configs = this.getLoraConfigs(node);
this.templateService.saveTemplate(templateName.trim(), configs).then((success) => {
if (success) {
this.showToast(`Template "${templateName.trim()}" saved successfully!`, "success");
} else {
this.showToast("Failed to save template", "error");
}
});
}
}
/**
* Show load template dialog with enhanced UI
*/
static async showLoadTemplateDialog(node, event) {
try {
const templateNames = await this.templateService.getTemplateNames();
if (templateNames.length === 0) {
this.showToast("📁 No templates available. Create one first!", "info");
return;
}
const items = templateNames.map((name) => ({ id: name, label: name }));
this.showSearchOverlay({
title: "Load Template",
placeholder: "Search templates...",
items,
onChoose: async (name) => {
try {
const template = await this.templateService.loadTemplate(name);
if (template) {
this.loadTemplate(node, template);
this.showToast(`✅ Template "${name}" loaded successfully!`, "success");
} else {
this.showToast(`❌ Failed to load template "${name}". It may be corrupted.`, "error");
}
} catch (error) {
console.error(`Template load error for "${name}":`, error);
this.showToast(`❌ Error loading template. Check console for details.`, "error");
}
},
rightActions: [
{
icon: "✏️",
title: "Rename template",
onClick: async (name) => {
this.showNameOverlay({
title: "Rename Template",
placeholder: "New template name...",
initial: name,
submitLabel: "Rename",
onCommit: async (newName) => {
const src = (name || "").trim();
const dst = (newName || "").trim();
if (!dst || dst === src) return;
const ok = await this.templateService.renameTemplate(src, dst);
this.showToast(ok ? "✅ Template renamed" : "❌ Failed to rename", ok ? "success" : "error");
if (ok) this.showLoadTemplateDialog(node, event);
}
});
}
},
{
icon: "🗑",
title: "Delete template",
onClick: async (name) => {
const ok = confirm(`Delete template "${name}"? This cannot be undone.`);
if (!ok) return;
const deleted = await this.templateService.deleteTemplate(name);
this.showToast(deleted ? "✅ Template deleted" : "❌ Failed to delete template", deleted ? "success" : "error");
if (deleted) this.showLoadTemplateDialog(node, event);
}
}
]
});
} catch (error) {
console.error("Failed to show template selector:", error);
this.showToast("❌ Error loading templates. Check console for details.", "error");
}
}
/**
* Add a new LoRA widget
*/
static addLoraWidget(node, config) {
const widget = new SuperLoraWidget(`lora_${Date.now()}`);
if (config) {
const { lora: cfgLora, ...rest } = config;
if (Object.keys(rest).length) {
Object.assign(widget.value, rest);
}
if (cfgLora && cfgLora !== "None") {
widget.setLora(cfgLora, node);
}
}
if (node?.properties?.enableTags) {
widget.value.tag = widget.value.tag || "General";
}
node.customWidgets = node.customWidgets || [];
node.customWidgets.push(widget);
if (node?.properties?.enableTags) {
this.organizeByTags(node);
}
this.calculateNodeSize(node);
node.setDirtyCanvas(true, false);
this.syncExecutionWidgets(node);
return widget;
}
/**
* Remove a LoRA widget
*/
static removeLoraWidget(node, widget) {
const index = node.customWidgets.indexOf(widget);
if (index >= 0) {
node.customWidgets.splice(index, 1);
this.organizeByTags(node);
this.calculateNodeSize(node);
node.setDirtyCanvas(true, false);
this.syncExecutionWidgets(node);
}
}
/**
* Organize widgets by tags
*/
static organizeByTags(node) {
if (!node.properties.enableTags) {
node.customWidgets = node.customWidgets.filter((w) => !(w instanceof SuperLoraTagWidget));
return;
}
const loraWidgets = node.customWidgets.filter((w) => w instanceof SuperLoraWidget);
const headerWidget = node.customWidgets.find((w) => w instanceof SuperLoraHeaderWidget);
const tagGroups = {};
for (const widget of loraWidgets) {
const tag = widget.value.tag || "General";
if (!tagGroups[tag]) tagGroups[tag] = [];
tagGroups[tag].push(widget);
}
node.customWidgets = [headerWidget].filter(Boolean);
const sortedTags = Object.keys(tagGroups).sort(
(a, b) => a === "General" ? -1 : b === "General" ? 1 : a.localeCompare(b)
);
for (const tag of sortedTags) {
let tagWidget = node.customWidgets.find(
(w) => w instanceof SuperLoraTagWidget && w.tag === tag
);
if (!tagWidget) {
tagWidget = new SuperLoraTagWidget(tag);
}
node.customWidgets.push(tagWidget);
node.customWidgets.push(...tagGroups[tag]);
}
}
/**
* Get used LoRA names
*/
static getUsedLoras(node) {
return new Set(
node.customWidgets.filter((w) => w instanceof SuperLoraWidget).map((w) => w.value.lora).filter((lora) => lora && lora !== "None")
);
}
/**
* Check if a LoRA is already used in the node
*/
static isDuplicateLora(node, loraName) {
const usedLoras = this.getUsedLoras(node);
return usedLoras.has(loraName);
}
/**
* Get existing tags
*/
static getExistingTags(node) {
return Array.from(new Set(
node.customWidgets.filter((w) => w instanceof SuperLoraWidget).map((w) => w.value.tag).filter((tag) => tag)
));
}
/**
* Get LoRA configurations
*/
static getLoraConfigs(node) {
return node.customWidgets.filter((w) => w instanceof SuperLoraWidget).map((w) => ({
lora: w.value.lora,
enabled: w.value.enabled,
strength_model: w.value.strength,
strength_clip: w.value.strengthClip,
trigger_word: w.value.triggerWords,
tag: w.value.tag,
auto_populated: w.value.autoFetched
})).filter((config) => config.lora && config.lora !== "None");
}
/**
* Load template configurations
*/
static loadTemplate(node, configs) {
node.customWidgets = node.customWidgets.filter(
(w) => !(w instanceof SuperLoraWidget) && !(w instanceof SuperLoraTagWidget)
);
for (const config of configs) {
const widget = new SuperLoraWidget(`lora_${Date.now()}_${Math.random()}`);
widget.value = {
lora: config.lora,
enabled: config.enabled !== false,
strength: config.strength_model || 1,
strengthClip: config.strength_clip || config.strength_model || 1,
triggerWords: config.trigger_word || "",
tag: config.tag || "General",
autoFetched: config.auto_populated || false
};
node.customWidgets.push(widget);
}
this.organizeByTags(node);
this.calculateNodeSize(node);
node.setDirtyCanvas(true, false);
this.syncExecutionWidgets(node);
}
/**
* (THE BRIDGE) Syncs data from custom lora widgets to invisible execution widgets.
*/
static syncExecutionWidgets(node) {
node.setDirtyCanvas(true, true);
}
// Build the JSON bundle the backend expects from current custom widgets
static buildBundle(node) {
const loraWidgets = node.customWidgets?.filter((w) => w instanceof SuperLoraWidget) || [];
const bundle = loraWidgets.filter((w) => w?.value?.lora && w.value.lora !== "None").map((w) => ({
lora: w.value.lora,
enabled: w.value.enabled,
strength: w.value.strength,
strengthClip: w.value.strengthClip,
triggerWords: w.value.triggerWords,
tag: w.value.tag,
autoFetched: w.value.autoFetched
}));
try {
return JSON.stringify(bundle);
} catch {
return "[]";
}
}
/**
* Serialize custom widgets for saving
*/
static serializeCustomWidgets(node) {
if (!node.customWidgets) return null;
const cloneProperties = JSON.parse(JSON.stringify(node.properties || {}));
return {
properties: cloneProperties,
widgets: node.customWidgets.map((widget) => ({
name: widget.name,
type: widget.constructor.name,
value: JSON.parse(JSON.stringify(widget.value))
}))
};
}
/**
* Deserialize custom widgets when loading
*/
static deserializeCustomWidgets(node, data) {
if (!data) return;
try {
node.properties = node.properties || {};
if (data.properties) {
Object.assign(node.properties, JSON.parse(JSON.stringify(data.properties)));
}
const restoredWidgets = [];
if (Array.isArray(data.widgets)) {
for (const widgetData of data.widgets) {
let widget;
switch (widgetData.type) {
case "SuperLoraHeaderWidget":
widget = new SuperLoraHeaderWidget();
break;
case "SuperLoraTagWidget":
widget = new SuperLoraTagWidget(widgetData.value?.tag);
break;
case "SuperLoraWidget":
widget = new SuperLoraWidget(widgetData.name);
break;
default:
continue;
}
widget.value = { ...widget.value, ...JSON.parse(JSON.stringify(widgetData.value || {})) };
restoredWidgets.push(widget);
}
}
node.customWidgets = restoredWidgets.length ? restoredWidgets : [new SuperLoraHeaderWidget()];
} catch (error) {
console.warn("SuperLoRA: Failed to restore custom widgets, resetting defaults", error);
node.customWidgets = [new SuperLoraHeaderWidget()];
}
node.setDirtyCanvas(true, true);
}
/**
* Get execution data for backend
*/
static getExecutionData(node) {
const loraWidgets = node.customWidgets?.filter((w) => w instanceof SuperLoraWidget) || [];
const executionData = {};
loraWidgets.forEach((widget, index) => {
if (widget.value.lora && widget.value.lora !== "None") {
executionData[`lora_${index}`] = {
lora: widget.value.lora,
enabled: widget.value.enabled,
strength: widget.value.strength,
strengthClip: widget.value.strengthClip,
triggerWords: widget.value.triggerWords,
tag: widget.value.tag,
autoFetched: widget.value.autoFetched
};
}
});
return executionData;
}
/**
* Show toast notification with enhanced styling
*/
static showToast(message, type = "info") {
OverlayService.getInstance().showToast(message, type);
}
// Inline editors for better UX
static showInlineNumber(event, initial, onCommit) {
const input = document.createElement("input");
input.type = "number";
input.step = "0.05";
input.value = String(initial ?? 0);
let leftPx, topPx;
try {
if (event && typeof event.clientX === "number" && typeof event.clientY === "number") {
leftPx = event.clientX + 8;
topPx = event.clientY - 10;
console.log(`[showInlineNumber] Using event coordinates: ${leftPx}, ${topPx}`);
} else {
const lastPointer = _SuperLoraNode._lastPointerScreen;
leftPx = (lastPointer?.x ?? 100) + 8;
topPx = (lastPointer?.y ?? 100) - 10;
console.log(`[showInlineNumber] Using fallback coordinates: ${leftPx}, ${topPx}`);
}
} catch (error) {
console.warn("[showInlineNumber] Coordinate calculation failed, using fallback:", error);
const lastPointer = _SuperLoraNode._lastPointerScreen;
leftPx = (lastPointer?.x ?? 100) + 8;
topPx = (lastPointer?.y ?? 100) - 10;
}
input.style.cssText = `
position: fixed;
left: ${leftPx}px;
top: ${topPx}px;
width: 80px;
padding: 4px 6px;
font-size: 12px;
z-index: 2147483647;
pointer-events: auto;
border: 1px solid #444;
border-radius: 3px;
background: #2f2f2f;
color: #fff;
box-shadow: 0 0 0 1px rgba(0,0,0,0.2) inset;
`;
let removedNum = false;
const cleanup = () => {
if (removedNum) return;
removedNum = true;
try {
input.remove();
} catch {
}
};
const commit = () => {
const v = parseFloat(input.value);
if (!Number.isNaN(v)) onCommit(v);
cleanup();
};
input.addEventListener("keydown", (e) => {
if (e.key === "Enter") commit();
if (e.key === "Escape") cleanup();
});
input.addEventListener("blur", cleanup);
document.body.appendChild(input);
input.focus();
input.select();
}
static showInlineText(event, initial, onCommit, place) {
const input = document.createElement("input");
input.type = "text";
input.value = initial ?? "";
let leftPx, topPx, widthPx, heightPx;
try {
if (event && typeof event.clientX === "number" && typeof event.clientY === "number") {
leftPx = event.clientX + 8;
topPx = event.clientY - 10;
widthPx = 260;
heightPx = 20;
console.log(`[showInlineText] Using event coordinates: ${leftPx}, ${topPx}`);
} else if (place?.rect && place?.node) {
const rect = place.rect;
const node = place.node;
const canvasEl = app?.canvas?.canvas;
const cRect = canvasEl?.getBoundingClientRect?.();
const ds = app?.canvas?.ds;
const scale = ds?.scale || 1;
const offset = ds?.offset || [0, 0];
if (cRect) {
const nodePos = node.pos || [0, 0];
const worldX = nodePos[0] + rect.x;
const worldY = nodePos[1] + rect.y;
leftPx = cRect.left + (worldX + offset[0]) * scale;
topPx = cRect.top + (worldY + offset[1]) * scale;
widthPx = Math.max(100, rect.w * scale);
heightPx = Math.max(16, rect.h * scale);
console.log(`[showInlineText] Using place rect: ${leftPx}, ${topPx}, rect:`, rect);
} else {
throw new Error("Canvas rect not available");
}
} else {
const lastPointer = _SuperLoraNode._lastPointerScreen;
leftPx = (lastPointer?.x ?? 100) + 8;
topPx = (lastPointer?.y ?? 100) - 10;
widthPx = 260;
heightPx = 20;
console.log(`[showInlineText] Using fallback coordinates: ${leftPx}, ${topPx}`);
}
} catch (error) {
console.warn("[showInlineText] Coordinate calculation failed, using fallback:", error);
const lastPointer = _SuperLoraNode._lastPointerScreen;
leftPx = (lastPointer?.x ?? 100) + 8;
topPx = (lastPointer?.y ?? 100) - 10;
widthPx = 260;
heightPx = 20;
}
input.style.cssText = `
position: fixed;
left: ${leftPx}px;
top: ${topPx}px;
width: ${widthPx}px;
height: ${heightPx}px;
padding: 2px 6px;
font-size: 12px;
z-index: 2147483647;
pointer-events: auto;
border: 1px solid #444;
border-radius: 3px;
background: #2f2f2f;
color: #fff;
box-shadow: 0 0 0 1px rgba(0,0,0,0.2) inset;
`;
let removedTxt = false;
const cleanup = () => {
if (removedTxt) return;
removedTxt = true;
try {
input.remove();
} catch {
}
};
const commit = () => {
onCommit(input.value ?? "");
cleanup();
};
input.addEventListener("keydown", (e) => {
if (e.key === "Enter") commit();
if (e.key === "Escape") cleanup();
});
input.addEventListener("blur", cleanup);
document.body.appendChild(input);
input.focus();
input.select();
}
// Overlay utilities
static showSearchOverlay(opts) {
OverlayService.getInstance().showSearchOverlay(opts);
}
static showNameOverlay(opts) {
const overlay = document.createElement("div");
overlay.style.cssText = `position: fixed; inset: 0; background: rgba(0,0,0,0.55); z-index: 2147483600; display: flex; align-items: center; justify-content: center; backdrop-filter: blur(2px);`;
const panel = document.createElement("div");
panel.style.cssText = `width: 420px; background: #222; border: 1px solid #444; border-radius: 8px; color: #fff; font-family: 'Segoe UI', Arial, sans-serif; box-shadow: 0 12px 30px rgba(0,0,0,0.4); overflow: hidden;`;
const header = document.createElement("div");
header.textContent = opts.title;
header.style.cssText = `padding: 12px 14px; font-weight: 600; border-bottom: 1px solid #444; background: #2a2a2a;`;
const form = document.createElement("form");
form.style.cssText = `display: flex; gap: 8px; padding: 14px;`;
const input = document.createElement("input");
input.type = "text";
input.placeholder = opts.placeholder;
input.value = opts.initial || "";
input.style.cssText = `flex: 1; padding: 10px 12px; border-radius: 6px; border: 1px solid #555; background: #1a1a1a; color: #fff; outline: none;`;
const submit = document.createElement("button");
submit.type = "submit";
submit.textContent = opts.submitLabel || "Save";
submit.style.cssText = `padding: 10px 14px; background: #1976d2; color: #fff; border: 1px solid #0d47a1; border-radius: 6px; cursor: pointer;`;
const close = () => overlay.remove();
form.addEventListener("submit", (e) => {
e.preventDefault();
opts.onCommit(input.value);
close();
});
overlay.addEventListener("click", (e) => {
if (e.target === overlay) close();
});
document.addEventListener("keydown", function onKey(e) {
if (e.key === "Escape") {
close();
document.removeEventListener("keydown", onKey);
}
});
form.appendChild(input);
form.appendChild(submit);
panel.appendChild(header);
panel.appendChild(form);
overlay.appendChild(panel);
document.body.appendChild(overlay);
setTimeout(() => input.focus(), 0);
}
};
_SuperLoraNode.NODE_WIDGET_TOP_OFFSET = 68;
_SuperLoraNode.MARGIN_SMALL = 2;
_SuperLoraNode.loraService = LoraService.getInstance();
_SuperLoraNode.templateService = TemplateService.getInstance();
_SuperLoraNode.initialized = false;
_SuperLoraNode.initializationPromise = null;
let SuperLoraNode = _SuperLoraNode;
const GGUF_CLIP_WIDGET_MAP = {
DualCLIPLoaderGGUF: ["clip_name1", "clip_name2"],
TripleCLIPLoaderGGUF: ["clip_name1", "clip_name2", "clip_name3"],
QuadrupleCLIPLoaderGGUF: ["clip_name1", "clip_name2", "clip_name3", "clip_name4"]
};
const _NodeEnhancerExtension = class _NodeEnhancerExtension {
static debugLog(...args) {
if (this.DEBUG) {
console.debug("[NodeEnhancer]", ...args);
}
}
static isExtensionEnabled() {
try {
return app$1?.ui?.settings?.getSettingValue?.(this.SETTINGS.enabled, true) !== false;
} catch {
return true;
}
}
static isAutoEnhanceEnabled() {
try {
if (!this.isExtensionEnabled()) {
return false;
}
return app$1?.ui?.settings?.getSettingValue?.(this.SETTINGS.autoEnhanceAll, false) === true;
} catch {
return false;
}
}
static normalizeValueForWidget(widget, value) {
if (!widget || typeof value !== "string") {
return value;
}
const sample = (() => {
const opts = widget.options?.values;
if (Array.isArray(opts)) {
return opts.find((entry) => typeof entry === "string");
}
if (opts && typeof opts === "object") {
return Object.keys(opts).find((key) => typeof key === "string");
}
return void 0;
})();
if (typeof sample === "string") {
const prefersBackslash = sample.includes("\\") && !sample.includes("/");
const prefersSlash = sample.includes("/") && !sample.includes("\\");
if (prefersBackslash) {
return value.replace(/\//g, "\\");
}
if (prefersSlash) {
return value.replace(/\\/g, "/");
}
}
return value;
}
static isGloballyEnabled() {
try {
if (!this.isExtensionEnabled()) {
return false;
}
return app$1?.ui?.settings?.getSettingValue?.(this.SETTINGS.contextToggle, true) !== false;
} catch {
return true;
}
}
static setNodeFlag(node, enabled) {
try {
node.properties = node.properties || {};
if (enabled) {
node.properties[_NodeEnhancerExtension.NODE_FLAG_KEY] = true;
node.properties[_NodeEnhancerExtension.LEGACY_NODE_FLAG_KEY] = true;
} else if (node.properties) {
delete node.properties[_NodeEnhancerExtension.NODE_FLAG_KEY];
delete node.properties[_NodeEnhancerExtension.LEGACY_NODE_FLAG_KEY];
}
} catch {
}
}
static shouldRestoreEnabled(node, data) {
if (data) {
if (typeof data.ndSuperSelectorEnabled !== "undefined") {
return !!data.ndSuperSelectorEnabled;
}
if (typeof data.ndPowerEnabled !== "undefined") {
return !!data.ndPowerEnabled;
}
}
try {
const props = node?.properties;
if (props?.[_NodeEnhancerExtension.NODE_FLAG_KEY] === true) {
return true;
}
if (props?.[_NodeEnhancerExtension.LEGACY_NODE_FLAG_KEY] === true) {
return true;
}
return false;
} catch {
return false;
}
}
static createOverlayWidget(node, targetWidget, config) {
const overlayWidget = {
name: `${config.widgetName}__ndOverlay`,
type: _NodeEnhancerExtension.OVERLAY_WIDGET_TYPE,
value: targetWidget.value ?? "",
_ndDisplayValue: targetWidget.value ?? "",
_ndWidgetLabel: targetWidget.label || targetWidget.name || config.widgetName,
_ndPlaceholder: _NodeEnhancerExtension.buildPlaceholder(config, targetWidget),
__ndOverlay: true,
__ndTargetWidgetName: config.widgetName,
serialize: false,
parent: node,
computeSize(width) {
const H = window.LiteGraph?.NODE_WIDGET_HEIGHT || 20;
return [width, H];
},
draw(ctx, nodeRef, widgetWidth, widgetY) {
const H = window.LiteGraph?.NODE_WIDGET_HEIGHT || 20;
const margin = 6;
const gutter = 10;
const inset = 8;
const x = margin + gutter;
const y = widgetY;
const availableWidth = Math.max(60, widgetWidth - margin * 2 - gutter * 2);
const w = Math.max(120, Math.min(availableWidth, (nodeRef?.size?.[0] || widgetWidth) - 24));
const caretX = x + w - inset - 12;
const iconX = caretX + 10;
const valueEnd = caretX - 10;
const labelText = this._ndWidgetLabel || config.widgetName;
const rawValue = this._ndDisplayValue && String(this._ndDisplayValue) || this._ndPlaceholder;
const isPlaceholder = rawValue === this._ndPlaceholder;
ctx.save();
ctx.lineWidth = 1;
ctx.strokeStyle = "#FFD700";
ctx.fillStyle = "#1f1f1f";
if (typeof ctx.roundRect === "function") {
ctx.beginPath();
ctx.roundRect(x, y, w, H, 8);
ctx.fill();
ctx.stroke();
} else {
ctx.fillRect(x, y, w, H);
ctx.strokeRect(x, y, w, H);
}
ctx.font = "12px Arial";
ctx.textBaseline = "middle";
const labelFullWidth = ctx.measureText(labelText).width;
const valueFullWidth = ctx.measureText(rawValue).width;
const labelSpacing = 8;
const totalSpace = valueEnd - (x + inset);
const neededValueWidth = Math.min(valueFullWidth, totalSpace);
const availableForLabel = totalSpace - neededValueWidth;
let showLabel = labelFullWidth > 0;
let labelWidth = 0;
if (showLabel && availableForLabel > labelSpacing) {
labelWidth = Math.min(labelFullWidth, availableForLabel - labelSpacing);
showLabel = labelWidth > 0;
} else {
showLabel = false;
}
const valueStart = showLabel ? x + inset + labelWidth + labelSpacing : x + inset;
const valueMaxWidth = Math.max(12, valueEnd - valueStart);
const displayLabel = showLabel ? _NodeEnhancerExtension.truncateText(ctx, labelText, labelWidth, false) : "";
const displayValue = valueFullWidth <= valueMaxWidth ? rawValue : _NodeEnhancerExtension.truncateText(ctx, rawValue, valueMaxWidth, false);
if (showLabel) {
ctx.textAlign = "left";
ctx.fillStyle = "#888888";
ctx.fillText(displayLabel, x + inset, y + H / 2);
}
ctx.textAlign = "right";
ctx.fillStyle = isPlaceholder ? "#7a7a7a" : "#ffffff";
ctx.fillText(displayValue, valueEnd, y + H / 2);
ctx.textAlign = "center";
ctx.fillStyle = "#ffd54f";
ctx.fillText("⚡", iconX, y + H / 2 + 0.5);
ctx.fillStyle = "#888";
ctx.fillText("▾", caretX, y + H / 2 + 1);
ctx.restore();
this.last_y = widgetY;
this.last_height = H;
},
mouse(event, pos, nodeRef) {
const evtType = event?.type;
const isLeft = event?.button === 0 || event?.which === 1 || event?.button === void 0;
if (evtType === "pointerdown" && isLeft) {
_NodeEnhancerExtension.showEnhancedPicker(nodeRef ?? node, config);
return true;
}
if (evtType === "pointerup" && isLeft) {
return true;
}
return false;
},
serializeValue() {
if (typeof targetWidget.serializeValue === "function") {
try {
return targetWidget.serializeValue();
} catch {
}
}
return targetWidget.value;
}
};
overlayWidget.callback = () => {
_NodeEnhancerExtension.showEnhancedPicker(node, config);
};
overlayWidget.updateDisplay = (value, displayValue) => {
const normalizedValue = typeof value === "string" ? value : "";
const normalizedDisplay = typeof displayValue === "string" ? displayValue : normalizedValue;
overlayWidget.value = normalizedValue;
overlayWidget._ndDisplayValue = normalizedDisplay || "";
};
return overlayWidget;
}
static formatDisplayValue(value) {
if (!value) {
return "";
}
try {
return String(value);
} catch {
return "";
}
}
static buildPlaceholder(config, widget) {
const base = widget?.label || widget?.name || config.label || config.widgetName || "file";
return `Select ${base}`;
}
static ensureWidgetValueSelectable(widget, value) {
if (!widget || typeof value !== "string") {
return;
}
try {
const before = widget.options?.values;
if (!widget.options) {
widget.options = { values: [value] };
_NodeEnhancerExtension.debugLog("Initialized widget options for value", value, { widgetName: widget.name });
return;
}
const options = widget.options;
const { values } = options;
if (Array.isArray(values)) {
if (!values.includes(value)) {
options.values = [...values, value];
_NodeEnhancerExtension.debugLog("Appended value to array options", value, { widgetName: widget.name, before, after: options.values });
}
return;
}
if (values && typeof values === "object") {
if (!(value in values)) {
options.values = { ...values, [value]: value };
_NodeEnhancerExtension.debugLog("Added value to object options", value, { widgetName: widget.name, before, after: options.values });
}
return;
}
options.values = [value];
_NodeEnhancerExtension.debugLog("Reset options.values to single entry", value, { widgetName: widget.name, before });
} catch (error) {
console.warn("Node Enhancer: failed to sync widget values", error);
}
}
static enhanceWidget(node, widget, config) {
node.__ndEnhancedWidgets = node.__ndEnhancedWidgets || {};
const meta = node.__ndEnhancedWidgets[config.widgetName] || { original: {} };
const original = meta.original;
if (!("callback" in original)) original.callback = widget.callback;
if (!("mouse" in original)) original.mouse = widget.mouse;
if (!("draw" in original)) original.draw = widget.draw;
if (!("computeSize" in original)) original.computeSize = widget.computeSize;
if (!("hidden" in original)) original.hidden = widget.hidden;
if (!("options" in original)) original.options = widget.options ? { ...widget.options } : void 0;
if (!("values" in original)) original.values = widget.options?.values ? [...widget.options.values] : void 0;
if (!("serialize" in original)) original.serialize = widget.serialize;
if (!("skipSerialize" in original)) original.skipSerialize = widget.skipSerialize;
widget._ndPlaceholder = _NodeEnhancerExtension.buildPlaceholder(config, widget);
widget.hidden = true;
widget.computeSize = _NodeEnhancerExtension.HIDDEN_WIDGET_SIZE;
if (!meta.overlay) {
const overlay = _NodeEnhancerExtension.createOverlayWidget(node, widget, config);
meta.overlay = overlay;
if (!node.widgets) node.widgets = [];
const idx = node.widgets.indexOf(widget);
if (idx >= 0) node.widgets.splice(idx + 1, 0, overlay);
else node.widgets.push(overlay);
}
widget.callback = function() {
_NodeEnhancerExtension.showEnhancedPicker(node, config);
return true;
};
widget.mouse = function(event, pos, nodeRef) {
const evtType = event?.type;
const isLeft = evtType === "pointerdown" && (event?.button === 0 || event?.which === 1 || event?.button === void 0);
if (isLeft) {
_NodeEnhancerExtension.showEnhancedPicker(nodeRef ?? node, config);
return true;
}
if (typeof meta.original.mouse === "function") {
return meta.original.mouse.call(this, event, pos, nodeRef ?? node);
}
return false;
};
node.__ndEnhancedWidgets[config.widgetName] = meta;
}
static restoreWidget(node, widget, config) {
const meta = node.__ndEnhancedWidgets?.[config.widgetName];
if (!meta) return;
const original = meta.original || {};
if (widget) {
if ("callback" in original) {
widget.callback = original.callback;
} else {
delete widget.callback;
}
if ("mouse" in original) {
widget.mouse = original.mouse;
} else {
delete widget.mouse;
}
if ("draw" in original) {
widget.draw = original.draw;
} else {
delete widget.draw;
}
if ("computeSize" in original) {
if (original.computeSize) {
widget.computeSize = original.computeSize;
} else {
delete widget.computeSize;
}
}
if ("serialize" in original) {
widget.serialize = original.serialize;
} else {
delete widget.serialize;
}
if ("skipSerialize" in original) {
widget.skipSerialize = original.skipSerialize;
} else {
delete widget.skipSerialize;
}
if ("options" in original) {
if (original.options === void 0) {
delete widget.options;
} else {
widget.options = { ...original.options };
if (original.values !== void 0) {
widget.options.values = [...original.values];
}
}
} else if (widget.options && "values" in widget.options) {
delete widget.options.values;
}
if ("hidden" in original) {
if (original.hidden === void 0) {
delete widget.hidden;
} else {
widget.hidden = original.hidden;
}
} else {
delete widget.hidden;
}
delete widget._ndPlaceholder;
delete widget.last_y;
delete widget.last_height;
delete widget._ndDisplayValue;
}
if (meta.overlay) {
const idx = node.widgets?.indexOf(meta.overlay) ?? -1;
if (idx >= 0) node.widgets.splice(idx, 1);
delete meta.overlay;
}
delete node.__ndEnhancedWidgets[config.widgetName];
if (node.__ndEnhancedWidgets && Object.keys(node.__ndEnhancedWidgets).length === 0) {
delete node.__ndEnhancedWidgets;
}
_NodeEnhancerExtension.restoreTitle(node);
}
/**
* Initialize the node enhancer extension
*/
static async initialize() {
console.log("Node Enhancer Extension: Initializing...");
this.loadUserPreferences();
await this.filePickerService;
this.setupSettingsSync();
this.refreshSettingState();
console.log("Node Enhancer Extension: Initialized successfully");
}
/**
* Set up enhancement for a specific node type
*/
static setup(nodeType, nodeData) {
const configs = this.ENHANCED_NODES.filter((c) => c.nodeType === nodeData.name);
if (!configs.length) return;
const originalCreate = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function() {
originalCreate?.apply(this, arguments);
if (!this.__ndPowerEnabled) this.__ndPowerEnabled = false;
if (typeof this.__ndOriginalTitle === "undefined") {
this.__ndOriginalTitle = this.title || this.constructor?.title || "";
}
if (this.__ndPowerEnabled) {
configs.forEach((cfg) => _NodeEnhancerExtension.enableForNode(this, cfg));
_NodeEnhancerExtension.applyTitleBadge(this);
} else {
_NodeEnhancerExtension.restoreTitle(this);
}
};
const originalMenu = nodeType.prototype.getExtraMenuOptions;
nodeType.prototype.getExtraMenuOptions = function(canvas, optionsArr) {
if (!_NodeEnhancerExtension.isGloballyEnabled()) {
return originalMenu ? originalMenu.call(this, canvas, optionsArr) : optionsArr;
}
const providedOptions = Array.isArray(optionsArr) ? optionsArr : [];
const originalResult = originalMenu ? originalMenu.call(this, canvas, providedOptions) : providedOptions;
const targetOptions = Array.isArray(originalResult) ? originalResult : Array.isArray(optionsArr) ? optionsArr : providedOptions;
const options = Array.isArray(targetOptions) ? targetOptions : [];
for (let i = options.length - 1; i >= 0; i--) {
if (options[i]?.__ndSuperSelectorToggle) {
options.splice(i, 1);
}
}
const toggleOption = {
content: this.__ndPowerEnabled ? "➖ Disable ND Super Selector" : "⚡ Enable ND Super Selector",
callback: () => {
try {
if (this.__ndPowerEnabled) {
configs.forEach((cfg) => _NodeEnhancerExtension.disableForNode(this, cfg));
this.__ndPowerEnabled = false;
_NodeEnhancerExtension.restoreTitle(this);
_NodeEnhancerExtension.setNodeFlag(this, false);
this.setDirtyCanvas?.(true, true);
} else {
_NodeEnhancerExtension.enableConfigsForNode(this, configs);
}
} catch (error) {
console.warn("Node Enhancer: toggle failed", error);
}
}
};
toggleOption.__ndSuperSelectorToggle = true;
if (options.length && options[0] !== null) {
options.unshift(null);
}
options.unshift(toggleOption);
if (Array.isArray(optionsArr) && optionsArr !== options) {
optionsArr.length = 0;
optionsArr.push(...options);
}
return options;
};
const originalSerialize = nodeType.prototype.serialize;
nodeType.prototype.serialize = function() {
const data = originalSerialize ? originalSerialize.apply(this, arguments) : {};
try {
const enabled = !!this.__ndPowerEnabled;
data.ndSuperSelectorEnabled = enabled;
data.ndPowerEnabled = enabled;
data.properties = data.properties || {};
if (enabled) {
data.properties[_NodeEnhancerExtension.NODE_FLAG_KEY] = true;
data.properties[_NodeEnhancerExtension.LEGACY_NODE_FLAG_KEY] = true;
} else if (data.properties) {
delete data.properties[_NodeEnhancerExtension.NODE_FLAG_KEY];
delete data.properties[_NodeEnhancerExtension.LEGACY_NODE_FLAG_KEY];
}
if (this.__ndEnhancedWidgets && Array.isArray(this.widgets)) {
const hasOverlayWidgets = this.widgets.some((w) => w?.__ndOverlay);
if (hasOverlayWidgets) {
const serializedValues = [];
for (const widget of this.widgets) {
if (!widget || widget.__ndOverlay || widget.serialize === false) {
continue;
}
if (typeof widget.serializeValue === "function") {
try {
serializedValues.push(widget.serializeValue());
continue;
} catch {
}
}
serializedValues.push(widget.value ?? null);
}
data.widgets_values = serializedValues;
}
}
} catch {
}
return data;
};
const originalConfigure = nodeType.prototype.configure;
nodeType.prototype.configure = function(data) {
if (originalConfigure) {
originalConfigure.call(this, data);
}
try {
if (typeof this.__ndPowerEnabled === "undefined") this.__ndPowerEnabled = false;
const shouldEnable = _NodeEnhancerExtension.shouldRestoreEnabled(this, data);
if (shouldEnable) {
if (!this.__ndPowerEnabled) {
this.__ndPowerEnabled = true;
configs.forEach((cfg) => _NodeEnhancerExtension.enableForNode(this, cfg));
}
_NodeEnhancerExtension.applyTitleBadge(this);
} else if (this.__ndPowerEnabled) {
configs.forEach((cfg) => _NodeEnhancerExtension.disableForNode(this, cfg));
this.__ndPowerEnabled = false;
_NodeEnhancerExtension.restoreTitle(this);
} else {
configs.forEach((cfg) => _NodeEnhancerExtension.disableForNode(this, cfg));
_NodeEnhancerExtension.restoreTitle(this);
}
_NodeEnhancerExtension.setNodeFlag(this, !!this.__ndPowerEnabled);
} catch {
}
};
const originalOnMouseDown = nodeType.prototype.onMouseDown;
nodeType.prototype.onMouseDown = function(event, pos) {
const handled = configs.some((cfg) => _NodeEnhancerExtension.handleEnhancedMouseDown(this, event, pos, cfg));
if (handled) {
return true;
}
return originalOnMouseDown ? originalOnMouseDown.call(this, event, pos) : false;
};
console.log(`Node Enhancer: Successfully enhanced ${nodeData.name}`);
}
/**
* Set up enhanced node with custom widgets
*/
static setupEnhancedNode(node, config) {
if (node.__ndEnhancedWidgets?.[config.widgetName]) {
return;
}
const widget = node.widgets?.find((w) => w.name === config.widgetName);
if (!widget) {
console.warn(`Node Enhancer: Could not find widget "${config.widgetName}" in ${config.nodeType}`);
return;
}
node.__ndPowerEnabled = true;
node.__ndEnhancedWidgets = node.__ndEnhancedWidgets || {};
node.__ndEnhancedWidgets[config.widgetName] = node.__ndEnhancedWidgets[config.widgetName] || { original: {} };
_NodeEnhancerExtension.debugLog("Enhancing widget", {
nodeType: node?.type,
widgetName: widget?.name,
initialValue: widget?.value,
options: widget?.options?.values
});
this.enhanceWidget(node, widget, config);
this.applyTitleBadge(node);
this.setNodeFlag(node, true);
}
/** Enable enhancement for a specific node instance */
static enableForNode(node, config) {
this.setupEnhancedNode(node, config);
this.setNodeFlag(node, true);
}
static enableConfigsForNode(node, configs) {
if (!node || !Array.isArray(configs) || !configs.length) {
return;
}
let anyEnabled = false;
for (const config of configs) {
try {
this.enableForNode(node, config);
anyEnabled = true;
} catch (error) {
console.warn("Node Enhancer: failed to enable config", config, error);
}
}
if (anyEnabled) {
node.__ndPowerEnabled = true;
this.applyTitleBadge(node);
this.setNodeFlag(node, true);
node.setDirtyCanvas?.(true, true);
}
}
static enableEnhancementsForNode(node) {
if (!this.isExtensionEnabled()) {
return;
}
if (!node?.type) {
return;
}
const configs = this.ENHANCED_NODES.filter((cfg) => cfg.nodeType === node.type);
if (!configs.length) {
return;
}
this.enableConfigsForNode(node, configs);
}
/** Disable enhancement for a specific node instance */
static disableForNode(node, config) {
if (!node.__ndEnhancedWidgets || !node.__ndEnhancedWidgets[config.widgetName]) {
return;
}
const widget = node.widgets?.find((w) => w.name === config.widgetName);
_NodeEnhancerExtension.restoreWidget(node, widget, config);
if (!node.__ndEnhancedWidgets) {
node.__ndPowerEnabled = false;
}
_NodeEnhancerExtension.restoreTitle(node);
this.setNodeFlag(node, !!node.__ndPowerEnabled);
}
static showEnhancedPicker(node, config) {
const widget = node.widgets?.find((w) => w.name === config.widgetName);
if (!widget) return;
const currentValue = widget.value;
this.filePickerService.showFilePicker(
config.fileType,
(file) => {
let newValue;
if (file && typeof file.id === "string") {
newValue = file.id;
} else if (file && typeof file.path === "string") {
newValue = file.path;
} else if (file && typeof file.filename === "string") {
newValue = file.filename;
}
_NodeEnhancerExtension.debugLog("File picker selection", {
nodeType: node?.type,
widgetName: config.widgetName,
file,
proposedValue: newValue,
existingValues: widget.options?.values
});
if (typeof newValue === "string") {
try {
const normalizedValue = _NodeEnhancerExtension.normalizeValueForWidget(widget, newValue);
widget.value = normalizedValue;
_NodeEnhancerExtension.ensureWidgetValueSelectable(widget, normalizedValue);
_NodeEnhancerExtension.debugLog("Applied widget value", {
nodeType: node?.type,
widgetName: config.widgetName,
newValue: normalizedValue,
updatedValues: widget.options?.values
});
} catch (error) {
console.warn("Node Enhancer: failed to assign widget value", error);
}
}
const meta = node.__ndEnhancedWidgets?.[config.widgetName];
if (meta?.overlay) {
const displayValue = _NodeEnhancerExtension.formatDisplayValue(
file && typeof file.label === "string" && file.label || file && typeof file.path === "string" && file.path || file && typeof file.filename === "string" && file.filename || widget.value
);
meta.overlay.updateDisplay(widget.value, displayValue);
_NodeEnhancerExtension.debugLog("Updated overlay display", {
nodeType: node?.type,
widgetName: config.widgetName,
displayValue,
widgetValue: widget.value
});
}
node.setDirtyCanvas?.(true, true);
},
{
title: `Select ${config.fileType}`,
multiSelect: false,
currentValue
}
);
}
/**
* Handle mouse events for enhanced widgets
*/
static handleEnhancedMouseDown(node, event, pos, config) {
if (!node.__ndPowerEnabled || !node.__ndEnhancedWidgets || !node.__ndEnhancedWidgets[config.widgetName]) return false;
const widget = node.widgets?.find((w) => w.name === config.widgetName);
if (!widget) return false;
try {
const isLeft = event?.button === 0 || event?.which === 1 || !("button" in (event || {}));
if (!isLeft) return false;
const y = widget?.last_y ?? null;
const H = window.LiteGraph?.NODE_WIDGET_HEIGHT || 20;
if (y == null) return false;
const withinX = pos[0] >= 0 && pos[0] <= (node.size?.[0] || 200);
const withinY = pos[1] >= y && pos[1] <= y + H;
if (withinX && withinY) {
this.showEnhancedPicker(node, config);
return true;
}
} catch {
}
return false;
}
/**
* Draw enhanced widgets (visual indicators)
*/
static drawEnhancedWidgets(node, ctx) {
if (!node.__ndPowerEnabled) return;
}
static applyTitleBadge(node) {
try {
if (typeof node.__ndOriginalTitle === "undefined") {
node.__ndOriginalTitle = node.title || node.constructor?.title || "";
} else {
node.title = node.__ndOriginalTitle;
}
} catch {
}
}
static restoreTitle(node) {
try {
if (typeof node.__ndOriginalTitle !== "undefined") {
node.title = node.__ndOriginalTitle;
}
} catch {
}
}
// Preference helpers retained for future global defaults (no-op currently)
static loadUserPreferences() {
}
static saveUserPreferences() {
}
static getAvailableEnhancements() {
return this.ENHANCED_NODES;
}
static enableEnhancement(nodeTypeName) {
if (!this.isExtensionEnabled()) return;
const configs = this.ENHANCED_NODES.filter((cfg) => cfg.nodeType === nodeTypeName);
if (!configs.length) return;
const graph = window.app?.graph;
if (!graph) return;
(graph._nodes || []).forEach((node) => {
if (node?.type === nodeTypeName) {
try {
this.enableConfigsForNode(node, configs);
} catch (error) {
console.warn("Node Enhancer: failed to enable node", node, error);
}
}
});
}
static setupSettingsSync(attempt = 0) {
if (this.settingsHookInitialized) {
return;
}
const settings = app$1?.ui?.settings;
if (!settings) {
if (attempt > 20) {
return;
}
const delay = Math.min(1e3, 100 * Math.max(1, attempt + 1));
setTimeout(() => this.setupSettingsSync(attempt + 1), delay);
return;
}
this.settingsHookInitialized = true;
try {
const originalSet = settings.setSettingValue?.bind(settings);
if (typeof originalSet === "function") {
const self = this;
settings.setSettingValue = function(key, value) {
const result = originalSet(key, value);
self.handleSettingChanged(key, value);
return result;
};
}
} catch (error) {
console.warn("Node Enhancer: failed to hook settings.setSettingValue", error);
}
try {
const originalLoad = settings.loadSettings?.bind(settings);
if (typeof originalLoad === "function") {
const self = this;
settings.loadSettings = async function(...args) {
const result = await originalLoad(...args);
self.refreshSettingState();
return result;
};
}
} catch (error) {
console.warn("Node Enhancer: failed to hook settings.loadSettings", error);
}
this.refreshSettingState();
}
static handleSettingChanged(key, value) {
if (this.suppressSettingSideEffects) {
return;
}
if (key === this.SETTINGS.enabled) {
this.handleGlobalEnabledChange(value !== false);
}
if (key === this.SETTINGS.autoEnhanceAll) {
if (value) {
this.applyAutoEnhanceAll();
}
}
if (key === this.SETTINGS.contextToggle || key === this.SETTINGS.autoEnhanceAll || key === this.SETTINGS.enabled) {
this.updateSettingVisibility();
}
}
static refreshSettingState() {
this.updateSettingVisibility();
if (!this.isExtensionEnabled()) {
this.disableAllEnhancements();
return;
}
if (this.isAutoEnhanceEnabled()) {
this.applyAutoEnhanceAll();
}
}
static handleGlobalEnabledChange(enabled) {
if (!enabled) {
this.disableAllEnhancements();
} else if (this.isAutoEnhanceEnabled()) {
this.applyAutoEnhanceAll();
}
}
static updateSettingVisibility() {
if (typeof document === "undefined") {
return;
}
this.ensureSettingsStyles();
const enabled = this.isExtensionEnabled();
const targets = [this.SETTINGS.autoEnhanceAll, this.SETTINGS.contextToggle];
for (const id of targets) {
const element = this.findSettingElement(id);
this.setSettingDisabled(element, !enabled);
}
}
static ensureSettingsStyles() {
if (this.settingsStyleInjected || typeof document === "undefined") {
return;
}
try {
const style = document.createElement("style");
style.id = "nd-super-nodes-settings-styles";
style.textContent = `
.nd-super-nodes-setting-disabled {
opacity: 0.5;
pointer-events: none;
}
.nd-super-nodes-setting-disabled input,
.nd-super-nodes-setting-disabled button,
.nd-super-nodes-setting-disabled select {
pointer-events: none;
}
`;
document.head?.appendChild(style);
this.settingsStyleInjected = true;
} catch (error) {
console.warn("Node Enhancer: failed to inject settings styles", error);
}
}
static findSettingElement(id) {
if (typeof document === "undefined") {
return null;
}
const settings = app$1?.ui?.settings;
const candidates = [];
try {
const stored = settings?.settings?.get?.(id)?.element ?? settings?.settings?.[id]?.element;
if (stored instanceof HTMLElement) {
candidates.push(stored);
}
} catch {
}
const selectorCandidates = [
`[data-setting-id="${id}"]`,
`[data-id="${id}"]`,
`[data-setting="${id}"]`,
`[data-settingname="${id}"]`,
(() => {
try {
if (typeof window.CSS?.escape === "function") {
return `#${window.CSS.escape(id)}`;
}
} catch {
}
return null;
})()
].filter(Boolean);
for (const selector of selectorCandidates) {
try {
const el = document.querySelector(selector);
if (el instanceof HTMLElement) {
candidates.push(el);
}
} catch {
}
}
for (const candidate of candidates) {
if (candidate instanceof HTMLElement) {
return candidate;
}
}
try {
const label = Array.from(document.querySelectorAll("label")).find((node) => node.textContent?.includes(id));
if (label && label instanceof HTMLElement) {
return label.closest(".setting-item");
}
} catch {
}
return null;
}
static setSettingDisabled(element, disabled) {
if (!element) {
return;
}
const className = "nd-super-nodes-setting-disabled";
if (disabled) {
element.classList.add(className);
element.setAttribute("aria-disabled", "true");
} else {
element.classList.remove(className);
element.removeAttribute("aria-disabled");
}
const inputs = element.querySelectorAll("input, button, select");
inputs.forEach((input) => {
if (input instanceof HTMLInputElement || input instanceof HTMLButtonElement || input instanceof HTMLSelectElement) {
input.disabled = disabled;
}
});
}
static applyAutoEnhanceAll() {
if (!this.isAutoEnhanceEnabled()) {
return;
}
const graph = window.app?.graph;
if (!graph) {
return;
}
const nodes = Array.isArray(graph._nodes) ? graph._nodes : [];
nodes.forEach((node) => {
if (!node?.type) {
return;
}
const configs = this.ENHANCED_NODES.filter((cfg) => cfg.nodeType === node.type);
if (!configs.length) {
return;
}
this.enableConfigsForNode(node, configs);
});
}
static disableAllEnhancements() {
const graph = window.app?.graph;
if (!graph) {
return;
}
const nodes = Array.isArray(graph._nodes) ? graph._nodes : [];
nodes.forEach((node) => {
if (!node?.type) {
return;
}
const configs = this.ENHANCED_NODES.filter((cfg) => cfg.nodeType === node.type);
if (!configs.length) {
return;
}
this.disableConfigsForNode(node, configs);
});
}
static disableConfigsForNode(node, configs) {
if (!node || !Array.isArray(configs) || !configs.length) {
return;
}
let changed = false;
for (const config of configs) {
if (node.__ndEnhancedWidgets?.[config.widgetName]) {
try {
this.disableForNode(node, config);
changed = true;
} catch (error) {
console.warn("Node Enhancer: failed to disable config", config, error);
}
}
}
if (changed) {
this.setNodeFlag(node, false);
node.setDirtyCanvas?.(true, true);
}
}
static onGraphConfigured() {
if (!this.isExtensionEnabled()) {
this.disableAllEnhancements();
return;
}
if (this.isAutoEnhanceEnabled()) {
this.applyAutoEnhanceAll();
}
}
static loadSet(key, sessionFirst = false) {
try {
if (sessionFirst) {
const fromSession = sessionStorage.getItem(key);
if (fromSession) return new Set(JSON.parse(fromSession));
}
const fromLocal = localStorage.getItem(key);
if (fromLocal) return new Set(JSON.parse(fromLocal));
if (!sessionFirst) {
const fromSession = sessionStorage.getItem(key);
if (fromSession) return new Set(JSON.parse(fromSession));
}
} catch {
}
return /* @__PURE__ */ new Set();
}
static persistSet(key, value) {
try {
const data = JSON.stringify(Array.from(value));
sessionStorage.setItem(key, data);
localStorage.setItem(key, data);
} catch {
}
}
static truncateText(ctx, text, maxWidth, keepEnd = false) {
if (!text) return "";
if (ctx.measureText(text).width <= maxWidth) {
return text;
}
const ellipsis = "…";
if (keepEnd) {
let length2 = text.length;
while (length2 > 0) {
const candidate = ellipsis + text.slice(Math.max(0, text.length - length2));
if (ctx.measureText(candidate).width <= maxWidth) {
return candidate;
}
length2--;
}
return text.slice(-1);
}
let length = text.length;
while (length > 0) {
const candidate = text.slice(0, length) + ellipsis;
if (ctx.measureText(candidate).width <= maxWidth) {
return candidate;
}
length--;
}
return ellipsis;
}
};
_NodeEnhancerExtension.ENHANCED_NODES = [
{
nodeType: "CheckpointLoader",
fileType: "models",
widgetName: "ckpt_name",
label: "ND Super Selector"
},
{
nodeType: "CheckpointLoaderSimple",
fileType: "models",
widgetName: "ckpt_name",
label: "ND Super Selector"
},
{
nodeType: "VAELoader",
fileType: "vae",
widgetName: "vae_name",
label: "ND Super Selector"
},
{
nodeType: "LoraLoader",
fileType: "loras",
widgetName: "lora_name",
label: "ND Super Selector"
},
{
nodeType: "UNETLoader",
fileType: "diffusion_models",
widgetName: "unet_name",
label: "ND Super Selector"
},
{
nodeType: "UnetLoaderGGUF",
fileType: "gguf_unet_models",
widgetName: "unet_name",
label: "ND Super Selector"
},
{
nodeType: "UnetLoaderGGUFAdvanced",
fileType: "gguf_unet_models",
widgetName: "unet_name",
label: "ND Super Selector"
},
{
nodeType: "CLIPLoader",
fileType: "text_encoders",
widgetName: "clip_name",
label: "ND Super Selector"
},
{
nodeType: "CLIPLoaderGGUF",
fileType: "text_encoders",
widgetName: "clip_name",
label: "ND Super Selector"
},
...Object.entries(GGUF_CLIP_WIDGET_MAP).flatMap(
([nodeType, widgetNames]) => widgetNames.map((widgetName, index) => ({
nodeType,
fileType: "text_encoders",
widgetName,
label: `ND Super Selector (${index + 1})`
}))
),
{
nodeType: "ControlNetLoader",
fileType: "controlnet",
widgetName: "control_net_name",
label: "ND Super Selector"
},
{
nodeType: "UpscaleModelLoader",
fileType: "upscale_models",
widgetName: "model_name",
label: "ND Super Selector"
}
];
_NodeEnhancerExtension.filePickerService = FilePickerService.getInstance();
_NodeEnhancerExtension.HIDDEN_WIDGET_SIZE = (_width) => [0, -4];
_NodeEnhancerExtension.DEBUG = true;
_NodeEnhancerExtension.NODE_FLAG_KEY = "ndSuperSelectorEnabled";
_NodeEnhancerExtension.LEGACY_NODE_FLAG_KEY = "ndPowerEnabled";
_NodeEnhancerExtension.OVERLAY_WIDGET_TYPE = "ndSuperSelectorOverlay";
_NodeEnhancerExtension.SETTINGS = {
enabled: "nodeEnhancer.enabled",
autoEnhanceAll: "nodeEnhancer.autoEnhanceAll",
contextToggle: "nodeEnhancer.enableContextToggle"
};
_NodeEnhancerExtension.settingsHookInitialized = false;
_NodeEnhancerExtension.suppressSettingSideEffects = false;
_NodeEnhancerExtension.settingsStyleInjected = false;
let NodeEnhancerExtension = _NodeEnhancerExtension;
const EXTENSION_NAME$1 = "NodeEnhancer";
const EXTENSION_VERSION = "1.0.0";
const nodeEnhancerExtension = {
name: EXTENSION_NAME$1,
version: EXTENSION_VERSION,
// Extension settings
settings: [
{
id: "nodeEnhancer.enabled",
name: "Enable Node Enhancement",
type: "boolean",
defaultValue: true
},
{
id: "nodeEnhancer.autoEnhanceAll",
name: "Auto-enhance All Nodes",
type: "boolean",
defaultValue: false
},
{
id: "nodeEnhancer.enableContextToggle",
name: "Show ND Super Selector Toggle in Node Menu",
type: "boolean",
defaultValue: true
}
],
// Extension commands (minimal; per-node toggle lives in right-click menu)
commands: [
{
id: "nodeEnhancer.clearCache",
label: "ND Super Selector: Clear File Cache",
function: () => {
try {
NodeEnhancerExtension["filePickerService"]?.clearCache?.();
console.log("ND Super Selector: File cache cleared");
} catch {
}
}
}
],
/**
* Called when the extension is loaded
*/
async init() {
console.log(`${EXTENSION_NAME$1} v${EXTENSION_VERSION}: Initializing...`);
try {
await NodeEnhancerExtension.initialize();
console.log(`${EXTENSION_NAME$1}: Initialization successful`);
} catch (error) {
console.error(`${EXTENSION_NAME$1}: Initialization failed:`, error);
}
},
/**
* Called before a node type is registered
* This is where we inject our enhancements
*/
async beforeRegisterNodeDef(nodeType, nodeData) {
try {
NodeEnhancerExtension.setup(nodeType, nodeData);
} catch (error) {
console.error("Node Enhancer: Error in beforeRegisterNodeDef:", error);
}
},
/**
* Called when a node is created
*/
nodeCreated(node) {
const enabled = app$1.ui.settings.getSettingValue("nodeEnhancer.enabled", true);
const autoEnhanceAll = enabled && app$1.ui.settings.getSettingValue("nodeEnhancer.autoEnhanceAll", false);
if (autoEnhanceAll && node?.type) {
const availableEnhancements = NodeEnhancerExtension.getAvailableEnhancements();
const hasEnhancement = availableEnhancements.some((e) => e.nodeType === node.type);
if (hasEnhancement) {
NodeEnhancerExtension.enableEnhancementsForNode(node);
console.log(`Node Enhancer: Auto-enhanced ${node.type}`);
}
}
},
/**
* Called before the graph is configured
*/
beforeConfigureGraph(_graphData) {
console.log("Node Enhancer: Configuring graph");
},
/**
* Called after the graph is configured
*/
afterConfigureGraph(_graphData) {
console.log("Node Enhancer: Graph configured");
NodeEnhancerExtension.onGraphConfigured();
}
};
console.log(`${EXTENSION_NAME$1}: Registering extension with ComfyUI`);
app$1.registerExtension(nodeEnhancerExtension);
console.log(`${EXTENSION_NAME$1}: Extension registered successfully`);
const EXTENSION_NAME = "SuperLoraLoader";
const NODE_TYPE = "NdSuperLoraLoader";
const superLoraExtension = {
name: EXTENSION_NAME,
// Extension settings
settings: [
{
id: "superLora.autoFetchTriggerWords",
name: "Auto-fetch Trigger Words",
type: "boolean",
defaultValue: true
},
{
id: "superLora.enableTags",
name: "Enable Tag System",
type: "boolean",
defaultValue: false
},
{
id: "superLora.showSeparateStrengths",
name: "Show Separate Model/CLIP Strengths",
type: "boolean",
defaultValue: false
},
{
id: "superLora.enableTemplates",
name: "Enable Template System",
type: "boolean",
defaultValue: true
},
{
id: "superLora.enableDeletion",
name: "Enable LoRA Deletion",
type: "boolean",
defaultValue: true
},
{
id: "superLora.enableSorting",
name: "Enable LoRA Sorting",
type: "boolean",
defaultValue: true
}
],
// Extension commands
commands: [
{
id: "superLora.addLora",
label: "Add LoRA to Super LoRA Loader",
function: () => {
console.log("Super LoRA Loader: Add LoRA command triggered");
}
},
{
id: "superLora.showTriggerWords",
label: "Show All Trigger Words",
function: () => {
console.log("Super LoRA Loader: Show trigger words command triggered");
}
},
{
id: "superLora.checkUpdates",
label: "Check ND Super Nodes Updates",
function: () => {
UpdateService.getInstance().checkForUpdates({ force: true, silent: false }).catch(() => {
});
}
}
],
/**
* Called before a node type is registered
*/
beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData.name === NODE_TYPE) {
console.log("Super LoRA Loader: Registering node type");
SuperLoraNode.initialize().then(() => {
console.log("Super LoRA Loader: Services initialized");
}).catch((err) => {
console.error("Super LoRA Loader: Initialization error", err);
});
try {
SuperLoraNode.setup(nodeType, nodeData);
console.log("Super LoRA Loader: Node type registered successfully");
} catch (err) {
console.error("Super LoRA Loader: Error during node setup; continuing with default registration", err);
}
}
},
/**
* Called when a node is created
*/
nodeCreated(node) {
if (node.type === NODE_TYPE) {
console.log("Super LoRA Loader: Node created", node.id);
this.setupNodeEventHandlers?.(node);
}
},
/**
* Called before the graph is configured
*/
beforeConfigureGraph(_graphData) {
console.log("Super LoRA Loader: Configuring graph");
},
/**
* Set up additional event handlers for the node
*/
setupNodeEventHandlers(node) {
const originalOnRemoved = node.onRemoved;
node.onRemoved = function() {
console.log("Super LoRA Loader: Node removed", this.id);
if (originalOnRemoved) {
originalOnRemoved.apply(this, arguments);
}
};
const originalClone = node.clone;
node.clone = function() {
const clonedNode = originalClone ? originalClone.apply(this, arguments) : this;
console.log("Super LoRA Loader: Node cloned", this.id, "->", clonedNode.id);
return clonedNode;
};
const originalOnPropertyChanged = node.onPropertyChanged;
node.onPropertyChanged = function(name, value) {
console.log("Super LoRA Loader: Property changed", name, value);
if (name.startsWith("@")) {
const settingName = name.substring(1);
this.onSettingChanged?.(settingName, value);
}
if (originalOnPropertyChanged) {
originalOnPropertyChanged.apply(this, arguments);
}
};
}
};
console.log("Super LoRA Loader: Registering extension with ComfyUI");
app$1.registerExtension(superLoraExtension);
console.log("Super LoRA Loader: Extension registered successfully");
try {
window.SuperLoraNode = SuperLoraNode;
} catch {
}
//# sourceMappingURL=extension.js.map