diff --git a/pyproject.toml b/pyproject.toml
index 36185bb..c895964 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,7 +1,7 @@
[project]
name = "ultimate-auto-sampler-config-grid-testing-suite"
description = "ComfyUI Sampler testing & benchmarking tool for testing Stable Diffusion samplers, schedulers, CFG scales, prompts, img2img denoise values, and LoRA combinations. Features infinite-canvas dashboard image grids with virtual scrolling that can handle thousands of images, multi-model comparison, automatic resume on interrupt, and real-time visualization. Test entire checkpoint folders, stack multiple LoRAs, generate random seed variations, and export optimized configs. Includes fullscreen mode, keyboard navigation, smart filtering by parameters, and one-click regeneration workflow. Perfect for sampler output optimization and hyperparameter tuning."
-version = "1.2.0"
+version = "1.3.0"
license = {file = "LICENSE"}
classifiers = [
# # For OS-independent nodes (works on all operating systems)
diff --git a/web/config_builder.js b/web/config_builder.js
index 4de2cd4..ccd8a02 100644
--- a/web/config_builder.js
+++ b/web/config_builder.js
@@ -46,12 +46,12 @@ async function getAvailableModels() {
// 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;
- }
+ 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"];
@@ -118,8 +118,8 @@ function parseLoraString(loraStr) {
function buildLoraString(name, modelStr, clipStr) {
if (!name || name === "None") return "None";
- if (name.endsWith("/")) return name;
- if (modelStr === 1.0 && clipStr === 1.0) return name;
+ // if (name.endsWith("/")) return name;
+ // if (modelStr === 1.0 && clipStr === 1.0) return name;
return `${name}:${modelStr.toFixed(2)}:${clipStr.toFixed(2)}`;
}
@@ -179,9 +179,9 @@ app.registerExtension({
} else if (existing.lora_config) {
this.state = this.migrateOldFormat(existing);
}
- } catch (e) {}
+ } catch (e) { }
- this.migrateOldFormat = function(oldState) {
+ this.migrateOldFormat = function (oldState) {
const arrays = oldState.lora_config?.arrays || [];
return {
session_name: oldState.session_name || "my_test_session",
@@ -199,7 +199,7 @@ app.registerExtension({
};
};
- this.saveState = function() {
+ this.saveState = function () {
configWidget.value = JSON.stringify(this.state, null, 2);
this.updatePreview();
};
@@ -209,20 +209,32 @@ app.registerExtension({
this.addDOMWidget("config_ui", "div", this.htmlContainer, { serialize: false, hideOnZoom: false });
// --- UI RENDER ---
- this.renderUI = function() {
+ // --- 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 = `
`;
const root = this.htmlContainer.querySelector("#cb-root");
-
- // Top Section
+
const topRow = document.createElement("div");
topRow.className = "cb-sections-row";
this.renderSessionSection(topRow);
- // this.renderOptionsSection(topRow);
+ this.renderOptionsSection(topRow);
root.appendChild(topRow);
- // Config Arrays
const configSection = document.createElement("div");
configSection.className = "cb-section full-width";
configSection.innerHTML = '⚙️ Config Arrays
';
-
+
const headerBar = document.createElement("div");
headerBar.style.marginBottom = "10px";
const addConfigBtn = document.createElement("button");
@@ -282,7 +289,7 @@ app.registerExtension({
};
headerBar.appendChild(addConfigBtn);
configSection.appendChild(headerBar);
-
+
const arraysContainer = document.createElement("div");
arraysContainer.className = "cb-arrays-container";
this.state.config_arrays.forEach((configArray, arrayIdx) => {
@@ -293,9 +300,15 @@ app.registerExtension({
this.renderPreviewSection(root);
this.updatePreview();
- };
- this.createInputGroup = function(labelText, inputElement) {
+ // 2. RESTORE SCROLL POSITION
+ const newScrollContainer = this.htmlContainer.querySelector(".cb-container");
+ if (newScrollContainer) {
+ newScrollContainer.scrollTop = savedScrollTop;
+ }
+ };;
+
+ this.createInputGroup = function (labelText, inputElement) {
const group = document.createElement("div");
group.className = "cb-input-group";
const label = document.createElement("label");
@@ -306,7 +319,7 @@ app.registerExtension({
return group;
};
- this.renderSessionSection = function(container) {
+ this.renderSessionSection = function (container) {
const section = document.createElement("div");
section.className = "cb-section";
section.innerHTML = '📁 Session Management
';
@@ -329,7 +342,7 @@ app.registerExtension({
container.appendChild(section);
};
- this.renderOptionsSection = function(container) {
+ this.renderOptionsSection = function (container) {
const section = document.createElement("div");
section.className = "cb-section";
section.innerHTML = '🎯 Options
';
@@ -344,13 +357,13 @@ app.registerExtension({
toggleInput.onchange = () => { this.state.include_none = toggleInput.checked; this.saveState(); };
toggleLabel.appendChild(toggleInput);
toggleLabel.appendChild(document.createTextNode(" Include 'None' in Lists"));
-
+
grid.appendChild(toggleLabel);
section.appendChild(grid);
container.appendChild(section);
};
- this.createConfigArrayElement = function(configArray, arrayIdx) {
+ this.createConfigArrayElement = function (configArray, arrayIdx) {
const div = document.createElement("div");
div.className = "cb-array";
@@ -413,7 +426,7 @@ app.registerExtension({
addModelBtn.style.borderLeft = "4px solid #cc6600"; // Orange accent
addModelBtn.textContent = `➕ Add Model`;
addModelBtn.onclick = () => {
- if(!this.state.config_arrays[arrayIdx].models) this.state.config_arrays[arrayIdx].models = [];
+ 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();
@@ -483,7 +496,7 @@ app.registerExtension({
};
// Create Model Element
- this.createModelElement = function(modelStr, arrayIdx, modelIdx) {
+ this.createModelElement = function (modelStr, arrayIdx, modelIdx) {
const div = document.createElement("div");
div.className = "cb-item-card model-card";
const isFolder = modelStr.endsWith("/");
@@ -541,6 +554,7 @@ app.registerExtension({
return div;
};
+ // Create LoRA Element
// Create LoRA Element
this.createLoraElement = function(loraStr, arrayIdx, loraIdx) {
const div = document.createElement("div");
@@ -548,6 +562,7 @@ app.registerExtension({
const parsed = parseLoraString(loraStr);
const isFolder = parsed.name.endsWith("/");
+ // Header
const header = document.createElement("div");
header.className = "cb-header-bar";
const label = document.createElement("span");
@@ -568,6 +583,7 @@ app.registerExtension({
header.appendChild(deleteBtn);
div.appendChild(header);
+ // Type Select
const typeSelect = document.createElement("select");
typeSelect.className = "cb-select";
typeSelect.innerHTML = `
@@ -581,50 +597,85 @@ app.registerExtension({
};
div.appendChild(typeSelect);
+ // Name Select
const nameSelect = document.createElement("select");
nameSelect.className = "cb-select";
const options = isFolder ? loraFolders : availableLoras;
const optionsList = (options.includes(parsed.name) || parsed.name === "None") ? options : [parsed.name, ...options];
nameSelect.innerHTML = optionsList.map(opt => ``).join('');
+
+ // --- FIX IS HERE ---
nameSelect.onchange = () => {
- if (isFolder) this.state.config_arrays[arrayIdx].loras[loraIdx] = nameSelect.value;
- else this.state.config_arrays[arrayIdx].loras[loraIdx] = buildLoraString(nameSelect.value, parsed.model_str, parsed.clip_str);
+ // ALWAYS use buildLoraString to preserve sliders, even for folders
+ this.state.config_arrays[arrayIdx].loras[loraIdx] = buildLoraString(
+ nameSelect.value,
+ parsed.model_str,
+ parsed.clip_str
+ );
this.saveState();
+ this.renderUI();
};
div.appendChild(nameSelect);
- if (!isFolder && parsed.name !== "None") {
- // Sliders logic...
- const makeSlider = (label, val, onChange) => {
- const c = document.createElement("div");
- c.className = "cb-slider-container";
- const l = document.createElement("span");
- l.style.fontSize = "10px"; l.style.color = "#aaa"; l.textContent = label;
- c.appendChild(l);
- const s = document.createElement("input");
- s.type = "range"; s.className = "cb-slider"; s.min = "-10"; s.max = "10"; s.step = "0.01"; s.value = val;
- const v = document.createElement("span");
- v.className = "cb-slider-value"; v.textContent = val.toFixed(2);
- s.oninput = () => v.textContent = parseFloat(s.value).toFixed(2);
- s.onchange = () => onChange(parseFloat(s.value));
- c.appendChild(s); c.appendChild(v);
- return c;
+ // Expand Button (Folders only)
+ if (isFolder && parsed.name !== "None" && parsed.name !== "/") {
+ const expandBtn = document.createElement("button");
+ expandBtn.className = "cb-button primary";
+ expandBtn.style.width = "100%";
+ expandBtn.style.fontSize = "11px";
+ expandBtn.style.marginTop = "4px";
+ expandBtn.textContent = "📂 Add all individually";
+ expandBtn.onclick = () => {
+ const folderPrefix = parsed.name;
+ const matchingLoras = availableLoras.filter(l => l.startsWith(folderPrefix));
+ if (matchingLoras.length > 0) {
+ const newEntries = matchingLoras.map(name =>
+ buildLoraString(name, parsed.model_str, parsed.clip_str)
+ );
+ this.state.config_arrays[arrayIdx].loras.splice(loraIdx, 1, ...newEntries);
+ this.saveState();
+ this.renderUI();
+ } else {
+ alert("No LoRAs found in this folder!");
+ }
};
-
- div.appendChild(makeSlider("M", parsed.model_str, (v) => {
- this.state.config_arrays[arrayIdx].loras[loraIdx] = buildLoraString(parsed.name, v, parsed.clip_str);
- this.saveState();
- }));
- div.appendChild(makeSlider("C", parsed.clip_str, (v) => {
- this.state.config_arrays[arrayIdx].loras[loraIdx] = buildLoraString(parsed.name, parsed.model_str, v);
- this.saveState();
- }));
+ div.appendChild(expandBtn);
}
- return div;
- };
- this.renderPreviewSection = function(root) {
+ // Sliders (Always Visible)
+ const makeSlider = (label, val, onChange) => {
+ const c = document.createElement("div");
+ c.className = "cb-slider-container";
+ const l = document.createElement("span");
+ l.style.fontSize = "10px"; l.style.color = "#aaa"; l.textContent = label;
+ c.appendChild(l);
+ const s = document.createElement("input");
+ s.type = "range"; s.className = "cb-slider"; s.min = "-10"; s.max = "10"; s.step = "0.01"; s.value = val;
+
+ if (parsed.name === "None") s.disabled = true;
+
+ const v = document.createElement("span");
+ v.className = "cb-slider-value"; v.textContent = val.toFixed(2);
+ s.oninput = () => v.textContent = parseFloat(s.value).toFixed(2);
+ s.onchange = () => onChange(parseFloat(s.value));
+ c.appendChild(s); c.appendChild(v);
+ return c;
+ };
+
+ div.appendChild(makeSlider("M", parsed.model_str, (v) => {
+ this.state.config_arrays[arrayIdx].loras[loraIdx] = buildLoraString(parsed.name, v, parsed.clip_str);
+ this.saveState();
+ }));
+ div.appendChild(makeSlider("C", parsed.clip_str, (v) => {
+ this.state.config_arrays[arrayIdx].loras[loraIdx] = buildLoraString(parsed.name, parsed.model_str, v);
+ this.saveState();
+ }));
+
+ return div;
+ };;;
+
+ this.renderPreviewSection = function (root) {
const section = document.createElement("div");
section.className = "cb-section full-width";
section.innerHTML = '👁️ Config Preview (Final Output)
';
@@ -635,18 +686,18 @@ app.registerExtension({
root.appendChild(section);
};
- this.updatePreview = function() {
+ this.updatePreview = function () {
const preview = this.htmlContainer.querySelector("#cb-preview");
if (!preview) return;
- try { preview.textContent = JSON.stringify(this.generateOutput(), null, 2); }
+ try { preview.textContent = JSON.stringify(this.generateOutput(), null, 2); }
catch (e) { preview.textContent = `Error: ${e.message}`; }
};
- this.generateOutput = function() {
+ this.generateOutput = function () {
const configs = [];
this.state.config_arrays.forEach(configArray => {
const split = (str) => str.split(",").map(s => s.trim()).filter(s => s);
-
+
// Process LoRAs
let loras = [...configArray.loras];
const nonNoneLoras = loras.filter(l => l !== "None");
@@ -679,44 +730,44 @@ app.registerExtension({
return configs;
};
- this.loadSession = async function(sessionName) {
+ this.loadSession = async function (sessionName) {
// (Session loading logic mostly same, just updating models parsing)
// Simplified for brevity - assumes previous robust logic handles this
// just ensure we map meta.model to array if single string
try {
const manifestUrl = `/view?filename=manifest.json&type=output&subfolder=benchmarks/${sessionName}&t=${Date.now()}`;
const resp = await fetch(manifestUrl);
- if(!resp.ok) return;
+ if (!resp.ok) return;
const manifest = await resp.json();
const meta = manifest.meta || {};
-
- if(meta.configs_json) {
+
+ if (meta.configs_json) {
const configs = JSON.parse(meta.configs_json);
// Simple conversion
- this.state.config_arrays = configs.map((c, i) => {
- // Handle Model Array conversion
- let m = c.model;
- if(!Array.isArray(m)) m = m ? [m] : ["None"];
-
- // Handle Lora logic (same as before)
- let l = Array.isArray(c.lora) ? c.lora : [c.lora || "None"];
-
- return {
- name: `Loaded Config ${i+1}`,
+ this.state.config_arrays = configs.map((c, i) => {
+ // Handle Model Array conversion
+ let m = c.model;
+ if (!Array.isArray(m)) m = m ? [m] : ["None"];
+
+ // Handle Lora logic (same as before)
+ let l = Array.isArray(c.lora) ? c.lora : [c.lora || "None"];
+
+ return {
+ name: `Loaded Config ${i + 1}`,
samplers: Array.isArray(c.sampler) ? c.sampler.join(", ") : c.sampler,
schedulers: Array.isArray(c.scheduler) ? c.scheduler.join(", ") : c.scheduler,
steps: Array.isArray(c.steps) ? c.steps.join(", ") : c.steps,
cfg: Array.isArray(c.cfg) ? c.cfg.join(", ") : c.cfg,
models: m,
loras: l,
- combine: false
- };
- });
+ combine: false
+ };
+ });
}
this.state.session_name = sessionName;
this.saveState();
this.renderUI();
- } catch(e) { console.error(e); }
+ } catch (e) { console.error(e); }
};
this.renderUI();