Files
JasonHoku-ComfyUI-Ultimate-…/web/config_builder.js
T

1316 lines
63 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.
/**
* Ultimate Config Builder - COMPLETE HTML UI
* - ALL inputs in HTML/JS
* - Session loading updates everything
* - Live JSON preview
* - FLEX WRAP LAYOUT
* - DYNAMIC MODEL SELECTION (Multiple Models & Folders)
* - TRIGGER WORD LOOKUP FEATURE
*/
import { app } from "../../scripts/app.js";
// Cache for available resources
let availableLoras = null;
let loraFolders = null;
let availableModels = null;
let modelFolders = null;
let availableSessions = ["None"];
// Fetch available LoRAs
async function getAvailableLoras() {
if (availableLoras) return availableLoras;
try {
const resp = await fetch("/object_info");
const objectInfo = await resp.json();
for (const nodeType in objectInfo) {
const nodeDef = objectInfo[nodeType];
if (nodeDef.input?.required?.lora_name) {
const loraInput = nodeDef.input.required.lora_name;
if (Array.isArray(loraInput) && Array.isArray(loraInput[0])) {
availableLoras = loraInput[0];
return availableLoras;
}
}
}
} catch (e) { console.error("[ConfigBuilder] Error fetching LoRAs:", e); }
availableLoras = ["None"];
return availableLoras;
}
// Fetch available Models (Checkpoints)
async function getAvailableModels() {
if (availableModels) return availableModels;
try {
const resp = await fetch("/object_info");
const objectInfo = await resp.json();
// Look for standard CheckpointLoaderSimple or similar
const loaderNode = objectInfo["CheckpointLoaderSimple"] || objectInfo["CheckpointLoader"];
if (loaderNode?.input?.required?.ckpt_name) {
const modelInput = loaderNode.input.required.ckpt_name;
if (Array.isArray(modelInput) && Array.isArray(modelInput[0])) {
availableModels = modelInput[0];
console.log(`[ConfigBuilder] Found ${availableModels.length} Models`);
return availableModels;
}
}
} catch (e) { console.error("[ConfigBuilder] Error fetching Models:", e); }
availableModels = ["None"];
return availableModels;
}
// Extract folders from paths (Generic)
function extractFolders(itemList) {
const folders = new Set(["/"]);
itemList.forEach(item => {
const parts = item.split(/[\/\\]/);
if (parts.length > 1) {
let currentPath = "";
for (let i = 0; i < parts.length - 1; i++) {
currentPath += parts[i] + "/";
folders.add(currentPath);
}
}
});
return Array.from(folders).sort();
}
async function getLoraFolders() {
if (loraFolders) return loraFolders;
const loras = await getAvailableLoras();
loraFolders = extractFolders(loras);
return loraFolders;
}
async function getModelFolders() {
if (modelFolders) return modelFolders;
const models = await getAvailableModels();
modelFolders = extractFolders(models);
return modelFolders;
}
// Fetch available sessions
async function getAvailableSessions() {
try {
const resp = await fetch("/object_info");
const objectInfo = await resp.json();
for (const nodeType in objectInfo) {
const nodeDef = objectInfo[nodeType];
if (nodeType === "UltimateConfigBuilder" && nodeDef.input?.required?.load_session) {
availableSessions = nodeDef.input.required.load_session[0];
return availableSessions;
}
}
} catch (e) { console.error("[ConfigBuilder] Error fetching sessions:", e); }
return availableSessions;
}
// Parse LoRA string
function parseLoraString(loraStr) {
if (!loraStr || loraStr === "None") return { name: "None", model_str: 1.0, clip_str: 1.0 };
if (loraStr.endsWith("/")) return { name: loraStr, model_str: 1.0, clip_str: 1.0 };
const parts = loraStr.split(":");
return {
name: parts[0] || "None",
model_str: parts.length > 1 ? parseFloat(parts[1]) : 1.0,
clip_str: parts.length > 2 ? parseFloat(parts[2]) : 1.0
};
}
function buildLoraString(name, modelStr, clipStr) {
if (!name || name === "None") return "None";
return `${name}:${modelStr.toFixed(2)}:${clipStr.toFixed(2)}`;
}
// Create a searchable/filterable select element
function createSearchableSelect(options, currentValue, onChange, placeholder = "Search...") {
const container = document.createElement("div");
container.style.position = "relative";
container.style.width = "100%";
const input = document.createElement("input");
input.className = "cb-input";
input.type = "text";
input.placeholder = placeholder;
input.value = currentValue || "";
input.autocomplete = "off";
const dropdown = document.createElement("div");
dropdown.style.cssText = `
position: absolute;
top: 100%;
left: 0;
right: 0;
max-height: 300px;
overflow-y: auto;
background: #1a1a1a;
border: 1px solid #0066cc;
border-top: none;
border-radius: 0 0 4px 4px;
z-index: 1000;
display: none;
`;
let filteredOptions = options;
const renderOptions = () => {
dropdown.innerHTML = "";
filteredOptions.forEach(opt => {
const optDiv = document.createElement("div");
optDiv.textContent = opt;
optDiv.style.cssText = `
padding: 8px 10px;
cursor: pointer;
color: white;
font-size: 13px;
font-family: monospace;
`;
optDiv.onmouseover = () => optDiv.style.background = "#2a2a2a";
optDiv.onmouseout = () => optDiv.style.background = "transparent";
optDiv.onclick = () => {
input.value = opt;
dropdown.style.display = "none";
onChange(opt);
};
dropdown.appendChild(optDiv);
});
if (filteredOptions.length === 0) {
const noResults = document.createElement("div");
noResults.textContent = "No matches found";
noResults.style.cssText = "padding: 8px 10px; color: #666; font-style: italic;";
dropdown.appendChild(noResults);
}
};
input.onfocus = () => {
filteredOptions = options;
renderOptions();
dropdown.style.display = "block";
};
input.oninput = () => {
const searchTerm = input.value.toLowerCase();
filteredOptions = options.filter(opt =>
opt.toLowerCase().includes(searchTerm)
);
renderOptions();
dropdown.style.display = "block";
};
input.onkeydown = (e) => {
if (e.key === "Enter" && filteredOptions.length > 0) {
input.value = filteredOptions[0];
dropdown.style.display = "none";
onChange(filteredOptions[0]);
} else if (e.key === "Escape") {
dropdown.style.display = "none";
}
};
// Close dropdown when clicking outside
document.addEventListener("click", (e) => {
if (!container.contains(e.target)) {
dropdown.style.display = "none";
}
});
container.appendChild(input);
container.appendChild(dropdown);
return container;
}
app.registerExtension({
name: "UltimateConfigBuilder.CompleteHTML",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "UltimateConfigBuilder") {
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = async function () {
const result = onNodeCreated?.apply(this, arguments);
const configWidget = this.widgets?.find(w => w.name === "lora_config");
if (!configWidget) return result;
this.widgets.forEach(w => {
w.type = "converted-widget";
w.computeSize = () => [0, -4];
});
// Pre-fetch everything
await getAvailableLoras();
await getLoraFolders();
await getAvailableModels();
await getModelFolders();
await getAvailableSessions();
this.configWidget = configWidget;
this.state = {
session_name: "my_test_session",
include_none: false,
config_arrays: [{
name: "Config 1",
samplers: "euler, dpmpp_2m",
schedulers: "normal, karras",
steps: "20, 30",
cfg: "7.0",
models: ["None"],
loras: ["None"],
lora_omit_triggers: [],
combine: false
}]
};
// Load state
try {
const existing = JSON.parse(configWidget.value);
if (existing.config_arrays) {
this.state = existing;
// Migration check: ensure models is an array
this.state.config_arrays.forEach(arr => {
if (arr.model && !arr.models) {
arr.models = [arr.model];
delete arr.model;
}
if (!arr.models) arr.models = ["None"];
if (!arr.lora_omit_triggers) arr.lora_omit_triggers = [];
});
} else if (existing.lora_config) {
this.state = this.migrateOldFormat(existing);
}
} catch (e) { }
this.migrateOldFormat = function (oldState) {
const arrays = oldState.lora_config?.arrays || [];
return {
session_name: oldState.session_name || "my_test_session",
include_none: oldState.include_none !== undefined ? oldState.include_none : false,
config_arrays: arrays.map(arr => ({
name: arr.name,
samplers: oldState.samplers || "euler",
schedulers: oldState.schedulers || "normal",
steps: oldState.steps || "20",
cfg: oldState.cfg || "7.0",
models: oldState.model ? [oldState.model] : ["None"],
loras: arr.loras || ["None"],
lora_omit_triggers: [],
combine: arr.combine || false
}))
};
};
this.saveState = function () {
configWidget.value = JSON.stringify(this.state, null, 2);
this.updatePreview();
};
this.htmlContainer = document.createElement("div");
this.htmlContainer.style.cssText = `width: 100%; height: 100%; background: #1a1a1a; display: flex; flex-direction: column;`;
this.addDOMWidget("config_ui", "div", this.htmlContainer, { serialize: false, hideOnZoom: false });
// --- TRIGGER LOOKUP MODAL ---
this.showTriggerLookupModal = async function(arrayIdx) {
const configArray = this.state.config_arrays[arrayIdx];
// Create modal overlay
const overlay = document.createElement("div");
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
`;
const modal = document.createElement("div");
modal.style.cssText = `
background: #2a2a2a;
border: 2px solid #0066cc;
border-radius: 8px;
padding: 20px;
max-width: 600px;
max-height: 80vh;
overflow-y: auto;
color: white;
`;
const title = document.createElement("h3");
title.textContent = "🔎 LoRA Trigger Words Lookup";
title.style.cssText = "margin: 0 0 15px 0; color: #0066cc;";
modal.appendChild(title);
const status = document.createElement("div");
status.textContent = "🔄 Fetching trigger words from CivitAI...";
status.style.cssText = "margin-bottom: 15px; color: #aaa;";
modal.appendChild(status);
const content = document.createElement("div");
modal.appendChild(content);
const buttonBar = document.createElement("div");
buttonBar.style.cssText = "display: flex; gap: 10px; margin-top: 15px; justify-content: flex-end;";
const addAllBtn = document.createElement("button");
addAllBtn.className = "cb-button primary";
addAllBtn.textContent = "➕ Add All Selected to Omit List";
addAllBtn.disabled = true;
const closeBtn = document.createElement("button");
closeBtn.className = "cb-button";
closeBtn.textContent = "Close";
closeBtn.onclick = () => document.body.removeChild(overlay);
buttonBar.appendChild(addAllBtn);
buttonBar.appendChild(closeBtn);
modal.appendChild(buttonBar);
overlay.appendChild(modal);
document.body.appendChild(overlay);
// Fetch triggers
try {
const loras = configArray.loras.filter(l => l && l !== "None");
const response = await fetch("/configbuilder/lookup_triggers", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ loras })
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
const triggers = data.triggers || {};
status.textContent = `✅ Found triggers for ${Object.keys(triggers).length} LoRAs`;
// Display results
const selectedTriggers = new Set();
Object.entries(triggers).forEach(([loraName, triggerList]) => {
const loraSection = document.createElement("div");
loraSection.style.cssText = `
background: #333;
border-left: 3px solid #0066cc;
padding: 10px;
margin-bottom: 10px;
border-radius: 4px;
`;
const loraTitle = document.createElement("div");
loraTitle.textContent = loraName.split('/').pop().replace('.safetensors', '');
loraTitle.style.cssText = "font-weight: bold; margin-bottom: 8px; color: #0066cc;";
loraSection.appendChild(loraTitle);
if (!triggerList || triggerList.length === 0) {
const noTriggers = document.createElement("div");
noTriggers.textContent = "No triggers found";
noTriggers.style.cssText = "color: #888; font-style: italic;";
loraSection.appendChild(noTriggers);
} else {
triggerList.forEach(trigger => {
const triggerRow = document.createElement("label");
triggerRow.style.cssText = `
display: flex;
align-items: center;
gap: 8px;
padding: 4px;
cursor: pointer;
border-radius: 3px;
`;
triggerRow.onmouseover = () => triggerRow.style.background = "#444";
triggerRow.onmouseout = () => triggerRow.style.background = "transparent";
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.checked = false;
checkbox.onchange = () => {
if (checkbox.checked) {
selectedTriggers.add(trigger);
} else {
selectedTriggers.delete(trigger);
}
addAllBtn.disabled = selectedTriggers.size === 0;
};
const triggerText = document.createElement("span");
triggerText.textContent = trigger;
triggerText.style.cssText = "color: #ddd;";
triggerRow.appendChild(checkbox);
triggerRow.appendChild(triggerText);
loraSection.appendChild(triggerRow);
});
}
content.appendChild(loraSection);
});
// Add All button handler
addAllBtn.onclick = () => {
const existing = new Set(configArray.lora_omit_triggers);
selectedTriggers.forEach(t => existing.add(t));
this.state.config_arrays[arrayIdx].lora_omit_triggers = Array.from(existing);
this.saveState();
this.renderUI();
document.body.removeChild(overlay);
};
} catch (error) {
status.textContent = `❌ Error: ${error.message}`;
console.error("[ConfigBuilder] Trigger lookup error:", error);
}
};
// --- UI RENDERING ---
this.renderUI = function () {
// 1. SAVE SCROLL POSITION
const scrollContainer = this.htmlContainer.querySelector(".cb-container");
const savedScrollTop = scrollContainer ? scrollContainer.scrollTop : 0;
this.htmlContainer.innerHTML = `
<style>
.cb-container {
padding: 12px;
height: 100%;
overflow-y: auto;
box-sizing: border-box;
}
.cb-sections-row { display: flex; flex-wrap: wrap; gap: 12px; margin-bottom: 12px; }
.cb-section { background: #2a2a2a; border-radius: 4px; padding: 12px; border: 1px solid #3a3a3a; box-sizing: border-box; flex: 1 1 300px; }
.cb-section.full-width { flex: 1 1 100%; width: 100%; }
.cb-section-title { color: #0066cc; font-size: 14px; font-weight: bold; margin-bottom: 10px; padding-bottom: 6px; border-bottom: 1px solid #3a3a3a; }
.cb-flex-grid { display: flex; flex-wrap: wrap; gap: 10px; align-items: flex-end; }
.cb-input-group { flex: 1 1 180px; min-width: 140px; display: flex; flex-direction: column; }
.cb-input-group.wide { flex: 2 1 300px; }
.cb-input, .cb-select { background: #1a1a1a; border: 1px solid #4a4a4a; color: white; padding: 8px 10px; border-radius: 4px; width: 100%; box-sizing: border-box; font-size: 13px; font-family: monospace; }
.cb-input:focus, .cb-select:focus { outline: none; border-color: #0066cc; }
.cb-label { color: #aaa; font-size: 12px; margin-bottom: 4px; display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.cb-button { background: #4a4a4a; border: none; color: white; padding: 8px 12px; border-radius: 4px; cursor: pointer; font-size: 13px; margin: 4px 2px; white-space: nowrap; }
.cb-button:hover { background: #5a5a5a; }
.cb-button.primary { background: #0066cc; }
.cb-button.primary:hover { background: #0077ee; }
.cb-button.danger { background: #cc3333; }
.cb-button.danger:hover { background: #dd4444; }
.cb-array { background: #333; border-radius: 4px; padding: 12px; margin: 8px 0; border: 1px solid #3a3a3a; width: 100%; }
.cb-arrays-container { display: flex; flex-direction: column; gap: 12px; }
.cb-list-grid { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; padding-top: 10px; border-top: 1px dashed #444; }
.cb-item-card { background: #2a2a2a; border-radius: 4px; padding: 10px; border-left: 3px solid #0066cc; flex: 1 1 300px; min-width: 250px; display: flex; flex-direction: column; gap: 6px; }
.cb-item-card.model-card { border-left-color: #cc6600; }
.cb-slider-container { display: flex; align-items: center; gap: 10px; }
.cb-slider { flex: 1; height: 6px; background: #1a1a1a; border-radius: 3px; outline: none; }
.cb-slider-value { color: #0066cc; font-weight: bold; min-width: 45px; text-align: right; font-size: 12px; }
.cb-toggle { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; padding: 6px 0; }
.cb-preview { background: #0a0a0a; border: 1px solid #3a3a3a; border-radius: 4px; padding: 10px; margin: 10px 0; max-height: 400px; overflow-y: auto; font-family: monospace; font-size: 11px; color: #0cc; }
.cb-controls-bar { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin-top: 10px; padding: 8px; background: #252525; border-radius: 4px; }
.cb-header-bar { display: flex; justify-content: space-between; margin-bottom: 4px; }
</style>
<div class="cb-container" id="cb-root"></div>
`;
const root = this.htmlContainer.querySelector("#cb-root");
const topRow = document.createElement("div");
topRow.className = "cb-sections-row";
this.renderSessionSection(topRow);
root.appendChild(topRow);
const configSection = document.createElement("div");
configSection.className = "cb-section full-width";
configSection.innerHTML = '<div class="cb-section-title">⚙️ Config Arrays</div>';
const headerBar = document.createElement("div");
headerBar.style.marginBottom = "10px";
const addConfigBtn = document.createElement("button");
addConfigBtn.className = "cb-button primary";
addConfigBtn.textContent = "➕ Add Config Array";
addConfigBtn.onclick = () => {
this.state.config_arrays.push({
name: `Config ${this.state.config_arrays.length + 1}`,
samplers: "euler",
schedulers: "normal",
steps: "20",
cfg: "7.0",
models: ["None"],
loras: ["None"],
lora_omit_triggers: [],
combine: false
});
this.saveState();
this.renderUI();
};
headerBar.appendChild(addConfigBtn);
configSection.appendChild(headerBar);
const arraysContainer = document.createElement("div");
arraysContainer.className = "cb-arrays-container";
this.state.config_arrays.forEach((configArray, arrayIdx) => {
arraysContainer.appendChild(this.createConfigArrayElement(configArray, arrayIdx));
});
configSection.appendChild(arraysContainer);
root.appendChild(configSection);
this.renderPreviewSection(root);
this.updatePreview();
// 2. RESTORE SCROLL POSITION
const newScrollContainer = this.htmlContainer.querySelector(".cb-container");
if (newScrollContainer) {
newScrollContainer.scrollTop = savedScrollTop;
}
};
this.renderSessionSection = function (container) {
const section = document.createElement("div");
section.className = "cb-section";
section.innerHTML = '<div class="cb-section-title">📁 Session Management</div>';
const grid = document.createElement("div");
grid.className = "cb-flex-grid";
const nameInput = document.createElement("input");
nameInput.className = "cb-input";
nameInput.value = this.state.session_name;
nameInput.onchange = () => { this.state.session_name = nameInput.value; this.saveState(); };
grid.appendChild(this.createInputGroup("Session Name", nameInput));
const loadSearchable = createSearchableSelect(
availableSessions,
"",
async (value) => {
if (value && value !== "None") {
await this.loadSession(value);
}
},
"Search sessions..."
);
grid.appendChild(this.createInputGroup("Load Session", loadSearchable));
section.appendChild(grid);
container.appendChild(section);
};
this.createConfigArrayElement = function (configArray, arrayIdx) {
const div = document.createElement("div");
div.className = "cb-array";
// Settings Grid
const settingsGrid = document.createElement("div");
settingsGrid.className = "cb-flex-grid";
const nameInput = document.createElement("input");
nameInput.className = "cb-input";
nameInput.value = configArray.name;
nameInput.onchange = () => { this.state.config_arrays[arrayIdx].name = nameInput.value; this.saveState(); };
settingsGrid.appendChild(this.createInputGroup("Config Name", nameInput));
const samplersInput = document.createElement("input");
samplersInput.className = "cb-input";
samplersInput.value = configArray.samplers;
samplersInput.onchange = () => { this.state.config_arrays[arrayIdx].samplers = samplersInput.value; this.saveState(); };
settingsGrid.appendChild(this.createInputGroup("Samplers", samplersInput));
const schedulersInput = document.createElement("input");
schedulersInput.className = "cb-input";
schedulersInput.value = configArray.schedulers;
schedulersInput.onchange = () => { this.state.config_arrays[arrayIdx].schedulers = schedulersInput.value; this.saveState(); };
settingsGrid.appendChild(this.createInputGroup("Schedulers", schedulersInput));
const stepsInput = document.createElement("input");
stepsInput.className = "cb-input";
stepsInput.value = configArray.steps;
stepsInput.onchange = () => { this.state.config_arrays[arrayIdx].steps = stepsInput.value; this.saveState(); };
settingsGrid.appendChild(this.createInputGroup("Steps", stepsInput));
const cfgInput = document.createElement("input");
cfgInput.className = "cb-input";
cfgInput.value = configArray.cfg;
cfgInput.onchange = () => { this.state.config_arrays[arrayIdx].cfg = cfgInput.value; this.saveState(); };
settingsGrid.appendChild(this.createInputGroup("CFG", cfgInput));
div.appendChild(settingsGrid);
// --- CONTROLS BAR (Buttons) ---
const controlsBar = document.createElement("div");
controlsBar.className = "cb-controls-bar";
const toggleLabel = document.createElement("label");
toggleLabel.className = "cb-toggle";
const toggleInput = document.createElement("input");
toggleInput.type = "checkbox";
toggleInput.checked = configArray.combine;
toggleInput.onchange = () => { this.state.config_arrays[arrayIdx].combine = toggleInput.checked; this.saveState(); };
toggleLabel.appendChild(toggleInput);
toggleLabel.appendChild(document.createTextNode(" Stack All LoRAs"));
controlsBar.appendChild(toggleLabel);
const spacer = document.createElement("div");
spacer.style.flex = "1";
controlsBar.appendChild(spacer);
const addModelBtn = document.createElement("button");
addModelBtn.className = "cb-button";
addModelBtn.style.borderLeft = "4px solid #cc6600";
addModelBtn.textContent = `➕ Add Model`;
addModelBtn.onclick = () => {
if (!this.state.config_arrays[arrayIdx].models) this.state.config_arrays[arrayIdx].models = [];
this.state.config_arrays[arrayIdx].models.push("None");
this.saveState();
this.renderUI();
};
controlsBar.appendChild(addModelBtn);
const addLoraBtn = document.createElement("button");
addLoraBtn.className = "cb-button";
addLoraBtn.style.borderLeft = "4px solid #0066cc";
addLoraBtn.textContent = `➕ Add LoRA`;
addLoraBtn.onclick = () => {
this.state.config_arrays[arrayIdx].loras.push("None");
this.saveState();
this.renderUI();
};
controlsBar.appendChild(addLoraBtn);
const deleteBtn = document.createElement("button");
deleteBtn.className = "cb-button danger";
deleteBtn.textContent = `🗑️ Delete Config`;
deleteBtn.onclick = () => {
this.state.config_arrays.splice(arrayIdx, 1);
this.saveState();
this.renderUI();
};
controlsBar.appendChild(deleteBtn);
div.appendChild(controlsBar);
// --- MODELS GRID ---
if (configArray.models && configArray.models.length > 0) {
const modelGrid = document.createElement("div");
modelGrid.className = "cb-list-grid";
const modelTitle = document.createElement("div");
modelTitle.style.width = "100%";
modelTitle.style.color = "#cc6600";
modelTitle.style.fontWeight = "bold";
modelTitle.style.fontSize = "12px";
modelTitle.textContent = "Models / Checkpoints";
modelGrid.appendChild(modelTitle);
configArray.models.forEach((model, modelIdx) => {
modelGrid.appendChild(this.createModelElement(model, arrayIdx, modelIdx));
});
div.appendChild(modelGrid);
}
// --- LORAS GRID ---
if (configArray.loras && configArray.loras.length > 0) {
const loraGrid = document.createElement("div");
loraGrid.className = "cb-list-grid";
const loraTitle = document.createElement("div");
loraTitle.style.width = "100%";
loraTitle.style.color = "#0066cc";
loraTitle.style.fontWeight = "bold";
loraTitle.style.fontSize = "12px";
loraTitle.textContent = "LoRAs";
loraGrid.appendChild(loraTitle);
// --- OMIT TRIGGERS SECTION (NEW) ---
const omitSection = document.createElement("div");
omitSection.style.cssText = `
width: 100%;
background: #252525;
border-radius: 4px;
padding: 10px;
margin-top: 10px;
border-left: 3px solid #cc6600;
`;
const omitTitle = document.createElement("div");
omitTitle.textContent = "🚫 Omit Trigger Words";
omitTitle.style.cssText = "font-weight: bold; margin-bottom: 8px; color: #cc6600; font-size: 12px;";
omitSection.appendChild(omitTitle);
// Ensure lora_omit_triggers exists
if (!configArray.lora_omit_triggers) {
configArray.lora_omit_triggers = [];
}
// Chips display
const chipsContainer = document.createElement("div");
chipsContainer.style.cssText = `
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 8px;
min-height: 30px;
`;
const renderChips = () => {
chipsContainer.innerHTML = "";
if (configArray.lora_omit_triggers.length === 0) {
const placeholder = document.createElement("div");
placeholder.textContent = "No triggers omitted";
placeholder.style.cssText = "color: #666; font-style: italic; padding: 4px;";
chipsContainer.appendChild(placeholder);
return;
}
configArray.lora_omit_triggers.forEach((trigger, tIdx) => {
const chip = document.createElement("div");
chip.style.cssText = `
display: flex;
align-items: center;
background: #444;
color: #fff;
border-radius: 12px;
padding: 2px 8px;
font-size: 11px;
`;
const text = document.createElement("span");
text.textContent = trigger;
chip.appendChild(text);
const closeBtn = document.createElement("span");
closeBtn.textContent = "×";
closeBtn.style.cssText = "margin-left: 6px; cursor: pointer; color: #ff8888; font-weight: bold;";
closeBtn.onclick = () => {
this.state.config_arrays[arrayIdx].lora_omit_triggers.splice(tIdx, 1);
this.saveState();
renderChips();
};
chip.appendChild(closeBtn);
chipsContainer.appendChild(chip);
});
};
renderChips();
omitSection.appendChild(chipsContainer);
// Input & Buttons Row
const inputRow = document.createElement("div");
inputRow.style.cssText = "display: flex; gap: 8px; margin-bottom: 8px;";
const triggerInput = document.createElement("input");
triggerInput.className = "cb-input";
triggerInput.placeholder = "Enter trigger to omit...";
triggerInput.style.flex = "1";
// Add on Enter key
triggerInput.onkeydown = (e) => {
if (e.key === "Enter" && triggerInput.value.trim()) {
addTrigger();
}
};
const addTriggerBtn = document.createElement("button");
addTriggerBtn.className = "cb-button primary";
addTriggerBtn.textContent = "Add";
addTriggerBtn.style.padding = "4px 12px";
const addTrigger = () => {
const val = triggerInput.value.trim();
if (val && !configArray.lora_omit_triggers.includes(val)) {
this.state.config_arrays[arrayIdx].lora_omit_triggers.push(val);
this.saveState();
renderChips();
triggerInput.value = "";
}
};
addTriggerBtn.onclick = addTrigger;
inputRow.appendChild(triggerInput);
inputRow.appendChild(addTriggerBtn);
omitSection.appendChild(inputRow);
// --- NEW: LOOKUP BUTTON ---
const lookupBtn = document.createElement("button");
lookupBtn.className = "cb-button";
lookupBtn.style.cssText = `
width: 100%;
background: linear-gradient(135deg, #0066cc, #0088ff);
border-left: 4px solid #00aaff;
margin-top: 4px;
`;
lookupBtn.textContent = "🔎 Lookup Current LoRA Triggerwords For Review";
lookupBtn.onclick = async () => {
await this.showTriggerLookupModal(arrayIdx);
};
omitSection.appendChild(lookupBtn);
configArray.loras.forEach((lora, loraIdx) => {
loraGrid.appendChild(this.createLoraElement(lora, arrayIdx, loraIdx));
});
div.appendChild(loraGrid);
div.appendChild(omitSection);
}
return div;
};
// Create Model Element
this.createModelElement = function (modelStr, arrayIdx, modelIdx) {
const div = document.createElement("div");
div.className = "cb-item-card model-card";
const isFolder = modelStr.endsWith("/");
// Header
const header = document.createElement("div");
header.className = "cb-header-bar";
const label = document.createElement("span");
label.textContent = `Model #${modelIdx + 1}`;
label.style.color = "#aaa";
header.appendChild(label);
const deleteBtn = document.createElement("button");
deleteBtn.className = "cb-button danger";
deleteBtn.style.padding = "2px 6px";
deleteBtn.style.fontSize = "10px";
deleteBtn.textContent = "✖";
deleteBtn.onclick = () => {
this.state.config_arrays[arrayIdx].models.splice(modelIdx, 1);
this.saveState();
this.renderUI();
};
header.appendChild(deleteBtn);
div.appendChild(header);
// Type Select
const typeSelect = document.createElement("select");
typeSelect.className = "cb-select";
typeSelect.innerHTML = `
<option value="file" ${!isFolder ? 'selected' : ''}>Checkpoint File</option>
<option value="folder" ${isFolder ? 'selected' : ''}>Folder</option>
`;
typeSelect.onchange = () => {
this.state.config_arrays[arrayIdx].models[modelIdx] = typeSelect.value === "folder" ? "/" : "None";
this.saveState();
this.renderUI();
};
div.appendChild(typeSelect);
// Name Select (Searchable)
const options = isFolder ? modelFolders : availableModels;
const currentVal = modelStr;
const optionsList = (options.includes(currentVal) || currentVal === "None" || currentVal === "/") ? options : [currentVal, ...options];
const nameSearchable = createSearchableSelect(
optionsList,
currentVal,
(value) => {
this.state.config_arrays[arrayIdx].models[modelIdx] = value;
this.saveState();
this.renderUI();
},
isFolder ? "Search folders..." : "Search models..."
);
div.appendChild(nameSearchable);
// --- EXPAND BUTTON (Fixed for Windows Slashes) ---
if (isFolder && modelStr !== "None" && modelStr !== "/") {
const expandBtn = document.createElement("button");
expandBtn.className = "cb-button";
expandBtn.style.borderLeft = "3px solid #cc6600";
expandBtn.style.width = "100%";
expandBtn.style.fontSize = "11px";
expandBtn.style.marginTop = "4px";
expandBtn.textContent = "📂 Add all individually";
expandBtn.onclick = () => {
const normalize = (str) => str.replace(/\\/g, "/");
const folderPrefix = normalize(modelStr);
const matchingModels = availableModels.filter(m => normalize(m).startsWith(folderPrefix));
if (matchingModels.length > 0) {
this.state.config_arrays[arrayIdx].models.splice(modelIdx, 1, ...matchingModels);
this.saveState();
this.renderUI();
} else {
alert(`No Checkpoints found in folder: ${folderPrefix}\n(Checked against ${availableModels.length} available models)`);
}
};
div.appendChild(expandBtn);
}
return div;
};
// Create LoRA Element
this.createLoraElement = function (loraStr, arrayIdx, loraIdx) {
const div = document.createElement("div");
div.className = "cb-item-card";
const parsed = parseLoraString(loraStr);
const isFolder = parsed.name.endsWith("/") || parsed.name.endsWith("/*");
const isCombined = parsed.name.endsWith("/*");
const cleanName = isCombined ? parsed.name.slice(0, -1) : parsed.name;
// --- Header ---
const header = document.createElement("div");
header.className = "cb-header-bar";
const label = document.createElement("span");
label.textContent = `LoRA #${loraIdx + 1}`;
label.style.color = "#aaa";
header.appendChild(label);
const deleteBtn = document.createElement("button");
deleteBtn.className = "cb-button danger";
deleteBtn.style.padding = "2px 6px";
deleteBtn.style.fontSize = "10px";
deleteBtn.textContent = "✖";
deleteBtn.onclick = () => {
this.state.config_arrays[arrayIdx].loras.splice(loraIdx, 1);
this.saveState();
this.renderUI();
};
header.appendChild(deleteBtn);
div.appendChild(header);
// --- Type Selector ---
const typeSelect = document.createElement("select");
typeSelect.className = "cb-select";
typeSelect.innerHTML = `
<option value="file" ${!isFolder ? 'selected' : ''}>LoRA File</option>
<option value="folder" ${isFolder && !isCombined ? 'selected' : ''}>Folder (Separate)</option>
<option value="combined" ${isCombined ? 'selected' : ''}>Folder (Combined /*)</option>
`;
typeSelect.onchange = () => {
let newName = "None";
if (typeSelect.value === "file") newName = "None";
else if (typeSelect.value === "folder") newName = "/";
else if (typeSelect.value === "combined") newName = "/*";
this.state.config_arrays[arrayIdx].loras[loraIdx] = buildLoraString(newName, 1.0, 1.0);
this.saveState();
this.renderUI();
};
div.appendChild(typeSelect);
// --- Name Selector (Searchable) ---
const options = isFolder ? loraFolders : availableLoras;
const currentVal = cleanName;
const optionsList = (options.includes(currentVal) || currentVal === "None" || currentVal === "/" || currentVal === "/*")
? options
: [currentVal, ...options];
const nameSearchable = createSearchableSelect(
optionsList,
currentVal,
(selectedName) => {
const finalName = isCombined && !selectedName.endsWith("*") && selectedName !== "None"
? selectedName + "*"
: selectedName;
this.state.config_arrays[arrayIdx].loras[loraIdx] = buildLoraString(finalName, parsed.model_str, parsed.clip_str);
this.saveState();
this.renderUI();
},
isFolder ? "Search folders..." : "Search LoRAs..."
);
div.appendChild(nameSearchable);
// --- Sliders (Show for all types including folders) ---
const modelSlider = this.createSlider("Model Strength", parsed.model_str, 0, 2, 0.05, (val) => {
const currentName = isCombined ? cleanName + "*" : parsed.name;
this.state.config_arrays[arrayIdx].loras[loraIdx] = buildLoraString(currentName, val, parsed.clip_str);
this.saveState();
});
div.appendChild(modelSlider);
const clipSlider = this.createSlider("CLIP Strength", parsed.clip_str, 0, 2, 0.05, (val) => {
const currentName = isCombined ? cleanName + "*" : parsed.name;
this.state.config_arrays[arrayIdx].loras[loraIdx] = buildLoraString(currentName, parsed.model_str, val);
this.saveState();
});
div.appendChild(clipSlider);
// --- Expand Button (Show for all folders including combined and root /) ---
if (isFolder && parsed.name !== "None") {
const expandBtn = document.createElement("button");
expandBtn.className = "cb-button";
expandBtn.style.borderLeft = "3px solid #0066cc";
expandBtn.style.width = "100%";
expandBtn.style.fontSize = "11px";
expandBtn.style.marginTop = "4px";
expandBtn.textContent = "📂 Add all individually";
expandBtn.onclick = () => {
const normalize = (str) => str.replace(/\\/g, "/");
let matchingLoras;
if (cleanName === "/" || cleanName === "") {
// Root folder - get all LoRAs
matchingLoras = availableLoras;
} else {
// Specific folder
const folderPrefix = normalize(cleanName);
matchingLoras = availableLoras.filter(l => normalize(l).startsWith(folderPrefix));
}
if (matchingLoras.length > 0) {
const withStrengths = matchingLoras.map(l => buildLoraString(l, parsed.model_str, parsed.clip_str));
this.state.config_arrays[arrayIdx].loras.splice(loraIdx, 1, ...withStrengths);
this.saveState();
this.renderUI();
} else {
alert(`No LoRAs found in folder: ${cleanName || "root"}\n(Checked against ${availableLoras.length} available LoRAs)`);
}
};
div.appendChild(expandBtn);
}
return div;
};
this.createSlider = function (label, value, min, max, step, onChange) {
const container = document.createElement("div");
container.className = "cb-slider-container";
const labelElem = document.createElement("span");
labelElem.className = "cb-label";
labelElem.textContent = label;
labelElem.style.flex = "1";
container.appendChild(labelElem);
const slider = document.createElement("input");
slider.type = "range";
slider.className = "cb-slider";
slider.min = min;
slider.max = max;
slider.step = step;
slider.value = value;
container.appendChild(slider);
const valueDisplay = document.createElement("span");
valueDisplay.className = "cb-slider-value";
valueDisplay.textContent = value.toFixed(2);
container.appendChild(valueDisplay);
slider.oninput = () => {
const val = parseFloat(slider.value);
valueDisplay.textContent = val.toFixed(2);
onChange(val);
};
return container;
};
this.createInputGroup = function (label, element) {
const group = document.createElement("div");
group.className = "cb-input-group";
const labelElem = document.createElement("label");
labelElem.className = "cb-label";
labelElem.textContent = label;
group.appendChild(labelElem);
group.appendChild(element);
return group;
};
this.renderPreviewSection = function (container) {
const section = document.createElement("div");
section.className = "cb-section full-width";
section.innerHTML = '<div class="cb-section-title">📄 JSON Preview</div>';
const preview = document.createElement("pre");
preview.className = "cb-preview";
preview.id = "json-preview";
section.appendChild(preview);
container.appendChild(section);
};
this.updatePreview = function () {
const preview = this.htmlContainer.querySelector("#json-preview");
if (!preview) return;
const configs = this.convertStateToConfigs();
preview.textContent = JSON.stringify(configs, null, 2);
};
this.convertStateToConfigs = function () {
const configs = [];
const split = (str) => str.split(",").map(s => s.trim()).filter(s => s);
this.state.config_arrays.forEach(configArray => {
// Process LoRAs
let loras = configArray.loras.filter(l => l && l !== "None");
if (this.state.include_none || loras.length === 0) {
loras = [loras];
}
// Process Models
let finalModels = configArray.models?.filter(m => m && m !== "None") || [];
const config = {
sampler: split(configArray.samplers),
scheduler: split(configArray.schedulers),
steps: configArray.steps.split(",").map(s => parseInt(s)),
cfg: configArray.cfg.split(",").map(s => parseFloat(s)),
lora: loras.length > 1 ? loras : loras[0] || "None",
model: finalModels.length > 1 ? finalModels : finalModels[0] || "None",
lora_omit_triggers: configArray.lora_omit_triggers || []
};
configs.push(config);
});
return configs;
};
this.convertConfigsToConfigArrays = function (configs) {
if (!configs || !Array.isArray(configs)) {
return [{
name: "Config 1",
samplers: "euler",
schedulers: "normal",
steps: "20",
cfg: "7.0",
models: ["None"],
loras: ["None"],
lora_omit_triggers: [],
combine: false
}];
}
const configArrays = [];
const toString = (val) => {
if (Array.isArray(val)) return val.join(", ");
return String(val !== undefined && val !== null ? val : "");
};
configs.forEach((config, idx) => {
const loraValue = config.lora;
const loras = [];
let hasCombined = false;
let loraList = [];
if (typeof loraValue === 'string') loraList = [loraValue];
else if (Array.isArray(loraValue)) loraList = loraValue;
else loraList = ["None"];
loraList.forEach(loraStr => {
if (!loraStr || loraStr === "None") {
loras.push("None");
} else if (loraStr.includes(" + ")) {
hasCombined = true;
const parts = loraStr.split(" + ");
parts.forEach(part => loras.push(part.trim()));
} else {
loras.push(loraStr);
}
});
let models = config.model;
if (!Array.isArray(models)) models = models ? [models] : ["None"];
let omitTriggers = config.lora_omit_triggers;
if (!Array.isArray(omitTriggers)) omitTriggers = [];
configArrays.push({
name: `Loaded Config ${idx + 1}`,
samplers: toString(config.sampler || "euler"),
schedulers: toString(config.scheduler || "normal"),
steps: toString(config.steps || "20"),
cfg: toString(config.cfg || "7.0"),
models: models,
loras: loras,
lora_omit_triggers: omitTriggers,
combine: hasCombined
});
});
return configArrays.length > 0 ? configArrays : [{
name: "Config 1",
samplers: "euler",
schedulers: "normal",
steps: "20",
cfg: "7.0",
models: ["None"],
loras: ["None"],
lora_omit_triggers: [],
combine: false
}];
};
this.loadSession = async function (sessionName) {
console.log(`[ConfigBuilder] Loading session: ${sessionName}`);
if (!availableLoras) await getAvailableLoras();
if (!loraFolders) await getLoraFolders();
if (!availableModels) await getAvailableModels();
if (!modelFolders) await getModelFolders();
try {
const manifestUrl = `/view?filename=manifest.json&type=output&subfolder=benchmarks/${sessionName}&t=${Date.now()}`;
const resp = await fetch(manifestUrl);
if (!resp.ok) return;
const manifest = await resp.json();
const meta = manifest.meta || {};
if (meta.configs_json) {
try {
const configs = JSON.parse(meta.configs_json);
let loadedArrays = this.convertConfigsToConfigArrays(configs);
const normalize = (str) => str.replace(/\\/g, "/");
loadedArrays.forEach(arr => {
arr.loras = arr.loras.map(loraStr => {
const parsed = parseLoraString(loraStr);
const normName = normalize(parsed.name);
if (normName.endsWith("/") || normName.endsWith("/*")) {
return buildLoraString(normName, parsed.model_str, parsed.clip_str);
}
const potentialFolder = normName + "/";
if (loraFolders.includes(potentialFolder)) {
return buildLoraString(potentialFolder, parsed.model_str, parsed.clip_str);
}
if (loraFolders.includes(normName)) {
return buildLoraString(normName, parsed.model_str, parsed.clip_str);
}
return loraStr;
});
arr.models = arr.models.map(modelStr => {
const normModel = normalize(modelStr);
if (normModel.endsWith("/")) return normModel;
if (modelFolders.includes(normModel + "/")) {
return normModel + "/";
}
return normModel;
});
});
this.state.config_arrays = loadedArrays;
} catch (e) {
console.error("[ConfigBuilder] Error parsing configs_json:", e);
}
}
this.state.session_name = sessionName;
this.saveState();
this.renderUI();
} catch (e) {
console.error("[ConfigBuilder] Error loading session:", e);
}
};
this.renderUI();
return result;
};
}
}
});