From c2fadff0e11520ac2d01d812771d2bcb114ed63e Mon Sep 17 00:00:00 2001 From: Jason Hoku Date: Mon, 16 Mar 2026 19:51:27 -0700 Subject: [PATCH] feat: add LoRA weight arrays UI for comparing strengths New "+ Compare Strengths" button next to each LoRA's Model Strength slider. Enter comma-separated values (e.g., 0.5, 0.8, 1.0) to grid search different LoRA strengths. Outputs bracket notation in the config JSON (e.g., lora.safetensors:[0.5, 0.8, 1.0]:1.0) which the existing Python expansion code handles as Cartesian products. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../conf-builder-config-management.js | 55 +++++++++++++++++++ web/conf_builder/conf-builder-utilities.js | 17 ++++++ 2 files changed, 72 insertions(+) diff --git a/web/conf_builder/conf-builder-config-management.js b/web/conf_builder/conf-builder-config-management.js index e44c4af..34e0ba5 100644 --- a/web/conf_builder/conf-builder-config-management.js +++ b/web/conf_builder/conf-builder-config-management.js @@ -1583,6 +1583,14 @@ export function createLoraElement(node, loraStr, arrayIdx, loraIdx, availableLor ); contentDiv.appendChild(nameSearchable); + // Check if this LoRA has weight arrays stored + if (!node.state.config_arrays[arrayIdx].lora_weight_arrays) { + node.state.config_arrays[arrayIdx].lora_weight_arrays = {}; + } + const weightArrays = node.state.config_arrays[arrayIdx].lora_weight_arrays; + const hasModelArray = weightArrays[parsed.name + "_model"] && weightArrays[parsed.name + "_model"].length > 1; + const hasClipArray = weightArrays[parsed.name + "_clip"] && weightArrays[parsed.name + "_clip"].length > 1; + const modelSlider = createSlider("Model Strength", currentModelStr, 0, 2, 0.05, (val) => { currentModelStr = val; const currentName = isCombined ? cleanName + "*" : parsed.name; @@ -1605,6 +1613,53 @@ export function createLoraElement(node, loraStr, arrayIdx, loraIdx, availableLor }); contentDiv.appendChild(modelSlider); + // Weight Array "+" button and editor for Model Strength + const modelArrayDiv = document.createElement("div"); + modelArrayDiv.style.cssText = "margin: -4px 0 6px 0; display: flex; align-items: center; gap: 4px;"; + const modelArrayBtn = document.createElement("button"); + modelArrayBtn.textContent = hasModelArray ? "✕ Array" : "+ Compare Strengths"; + modelArrayBtn.className = "cb-btn"; + modelArrayBtn.style.cssText = "font-size: 9px; padding: 1px 6px; color: " + (hasModelArray ? "#f80" : "#0af") + ";"; + + const modelArrayInput = document.createElement("input"); + modelArrayInput.type = "text"; + modelArrayInput.placeholder = "e.g. 0.5, 0.8, 1.0"; + modelArrayInput.style.cssText = "flex: 1; background: #1a1a1a; color: #ccc; border: 1px solid #444; border-radius: 4px; padding: 2px 6px; font-size: 10px; display: " + (hasModelArray ? "block" : "none") + ";"; + modelArrayInput.value = hasModelArray ? weightArrays[parsed.name + "_model"].join(", ") : ""; + + modelArrayBtn.onclick = () => { + if (hasModelArray || modelArrayInput.style.display !== "none") { + // Remove array + delete weightArrays[parsed.name + "_model"]; + if (isStrengthLocked) delete weightArrays[parsed.name + "_clip"]; + node.saveState(); + updatePreview(node); + debouncedRenderUI(node); + } else { + // Show array input + modelArrayInput.style.display = "block"; + modelArrayInput.value = currentModelStr.toFixed(2); + modelArrayInput.focus(); + } + }; + + modelArrayInput.onchange = () => { + const vals = modelArrayInput.value.split(",").map(v => parseFloat(v.trim())).filter(v => !isNaN(v)); + if (vals.length > 1) { + weightArrays[parsed.name + "_model"] = vals; + if (isStrengthLocked) weightArrays[parsed.name + "_clip"] = vals; + } else { + delete weightArrays[parsed.name + "_model"]; + if (isStrengthLocked) delete weightArrays[parsed.name + "_clip"]; + } + node.saveState(); + updatePreview(node); + }; + + modelArrayDiv.appendChild(modelArrayBtn); + modelArrayDiv.appendChild(modelArrayInput); + contentDiv.appendChild(modelArrayDiv); + // CLIP Slider - conditionally visible based on lock state let clipSliderContainer = null; if (!isStrengthLocked) { diff --git a/web/conf_builder/conf-builder-utilities.js b/web/conf_builder/conf-builder-utilities.js index d743863..a013848 100644 --- a/web/conf_builder/conf-builder-utilities.js +++ b/web/conf_builder/conf-builder-utilities.js @@ -513,6 +513,23 @@ export function convertStateToConfigs(state) { return !configArray.lora_bypass_states?.[parsed.name]; }); + // Apply weight arrays (bracket notation) for LoRAs with multiple strengths + const wa = configArray.lora_weight_arrays || {}; + loras = loras.map(l => { + const parsed = parseLoraString(l); + const modelArr = wa[parsed.name + "_model"]; + const clipArr = wa[parsed.name + "_clip"]; + if (modelArr && modelArr.length > 1) { + const modelPart = "[" + modelArr.join(", ") + "]"; + const clipPart = (clipArr && clipArr.length > 1) ? "[" + clipArr.join(", ") + "]" : parsed.clip_str.toFixed(2); + return parsed.name + ":" + modelPart + ":" + clipPart; + } + if (clipArr && clipArr.length > 1) { + return parsed.name + ":" + parsed.model_str.toFixed(2) + ":[" + clipArr.join(", ") + "]"; + } + return l; + }); + // Convert loras array to proper format let loraValue; if (loras.length === 0) {