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) <noreply@anthropic.com>
This commit is contained in:
Jason Hoku
2026-03-16 19:51:27 -07:00
co-authored by Claude Opus 4.6
parent 36719ade3f
commit c2fadff0e1
2 changed files with 72 additions and 0 deletions
@@ -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) {
@@ -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) {