From f5b8044bad881b4e77c4b3b2f1f2bf3569495cb5 Mon Sep 17 00:00:00 2001
From: cmdr2 <secondary.cmdr2@gmail.com>
Date: Sat, 15 Jul 2023 19:33:49 +0530
Subject: [PATCH] UI changes for multiple LoRA files

---
 ui/index.html             | 12 ++---
 ui/media/css/main.css     |  4 ++
 ui/media/js/auto-save.js  | 23 ++++++----
 ui/media/js/dnd.js        | 66 ++++++++++++++++++++-------
 ui/media/js/main.js       | 96 +++++++++++++++++++++++++--------------
 ui/media/js/parameters.js |  2 -
 6 files changed, 134 insertions(+), 69 deletions(-)

diff --git a/ui/index.html b/ui/index.html
index 81ac3558..af7f1fde 100644
--- a/ui/index.html
+++ b/ui/index.html
@@ -225,18 +225,14 @@
                         <label for="height"><small>(height)</small></label>
                         <div id="small_image_warning" class="displayNone">Small image sizes can cause bad image quality</div>
                     </td></tr>
-                    <tr class="pl-5"><td><label for="num_inference_steps">Inference Steps:</label></td><td> <input id="num_inference_steps" name="num_inference_steps" size="4" value="25" onkeypress="preventNonNumericalInput(event)"></td></tr>
+                    <tr class="pl-5"><td><label for="num_inference_steps">Inference Steps:</label></td><td> <input id="num_inference_steps" name="num_inference_steps" type="number" min="1" step="1" style="width: 42pt" value="25" onkeypress="preventNonNumericalInput(event)"></td></tr>
                     <tr class="pl-5"><td><label for="guidance_scale_slider">Guidance Scale:</label></td><td> <input id="guidance_scale_slider" name="guidance_scale_slider" class="editor-slider" value="75" type="range" min="11" max="500"> <input id="guidance_scale" name="guidance_scale" size="4" pattern="^[0-9\.]+$" onkeypress="preventNonNumericalInput(event)"></td></tr>
                     <tr id="prompt_strength_container" class="pl-5"><td><label for="prompt_strength_slider">Prompt Strength:</label></td><td> <input id="prompt_strength_slider" name="prompt_strength_slider" class="editor-slider" value="80" type="range" min="0" max="99"> <input id="prompt_strength" name="prompt_strength" size="4" pattern="^[0-9\.]+$" onkeypress="preventNonNumericalInput(event)"><br/></td></tr>
-                    <tr id="lora_model_container" class="pl-5"><td><label for="lora_model">LoRA:</label></td><td>
-                        <input id="lora_model" type="text" spellcheck="false" autocomplete="off" class="model-filter" data-path="" />
-                    </td></tr>
-                    <tr id="lora_alpha_container" class="pl-5">
-                        <td><label for="lora_alpha_slider">LoRA Strength:</label></td>
+                    <tr id="lora_model_container" class="pl-5">
                         <td>
-                            <small>-2</small> <input id="lora_alpha_slider" name="lora_alpha_slider" class="editor-slider" value="50" type="range" min="-200" max="200"> <small>2</small> &nbsp;
-                            <input id="lora_alpha" name="lora_alpha" size="4" pattern="^-?[0-9]*\.?[0-9]*$" onkeypress="preventNonNumericalInput(event)"><br/>
+                            <label for="lora_model">LoRA:</label>
                         </td>
+                        <td class="model_entries"></td>
                     </tr>
                     <tr class="pl-5"><td><label for="hypernetwork_model">Hypernetwork:</label></td><td>
                         <input id="hypernetwork_model" type="text" spellcheck="false" autocomplete="off" class="model-filter" data-path="" />
diff --git a/ui/media/css/main.css b/ui/media/css/main.css
index f806e026..0297228f 100644
--- a/ui/media/css/main.css
+++ b/ui/media/css/main.css
@@ -1678,3 +1678,7 @@ body.wait-pause {
 #embeddings-list::-webkit-scrollbar-thumb {
   background: var(--background-color3);
 }
+
+.model_entry .model_name {
+    width: 70%;
+}
\ No newline at end of file
diff --git a/ui/media/js/auto-save.js b/ui/media/js/auto-save.js
index bbcbf9a5..1ff51e2e 100644
--- a/ui/media/js/auto-save.js
+++ b/ui/media/js/auto-save.js
@@ -16,7 +16,9 @@ const SETTINGS_IDS_LIST = [
     "clip_skip",
     "vae_model",
     "hypernetwork_model",
-    "lora_model",
+    "lora_model_0",
+    "lora_model_1",
+    "lora_model_2",
     "sampler_name",
     "width",
     "height",
@@ -24,7 +26,9 @@ const SETTINGS_IDS_LIST = [
     "guidance_scale",
     "prompt_strength",
     "hypernetwork_strength",
-    "lora_alpha",
+    "lora_alpha_0",
+    "lora_alpha_1",
+    "lora_alpha_2",
     "tiling",
     "output_format",
     "output_quality",
@@ -176,13 +180,14 @@ function loadSettings() {
         // So this is likely the first time Easy Diffusion is running.
         // Initialize vram_usage_level based on the available VRAM
         function initGPUProfile(event) {
-            if (    "detail" in event 
-                && "active" in event.detail
-                && "cuda:0" in event.detail.active
-                && event.detail.active["cuda:0"].mem_total <4.5 )
-            {
-               vramUsageLevelField.value = "low"
-               vramUsageLevelField.dispatchEvent(new Event("change"))
+            if (
+                "detail" in event &&
+                "active" in event.detail &&
+                "cuda:0" in event.detail.active &&
+                event.detail.active["cuda:0"].mem_total < 4.5
+            ) {
+                vramUsageLevelField.value = "low"
+                vramUsageLevelField.dispatchEvent(new Event("change"))
             }
             document.removeEventListener("system_info_update", initGPUProfile)
         }
diff --git a/ui/media/js/dnd.js b/ui/media/js/dnd.js
index 4e50b638..7128bc69 100644
--- a/ui/media/js/dnd.js
+++ b/ui/media/js/dnd.js
@@ -292,29 +292,58 @@ const TASK_MAPPING = {
     use_lora_model: {
         name: "LoRA model",
         setUI: (use_lora_model) => {
-            const oldVal = loraModelField.value
-            use_lora_model =
-                use_lora_model === undefined || use_lora_model === null || use_lora_model === "None"
-                    ? ""
-                    : use_lora_model
+            use_lora_model.forEach((model_name, i) => {
+                let field = loraModels[i][0]
+                const oldVal = field.value
 
-            if (use_lora_model !== "") {
-                use_lora_model = getModelPath(use_lora_model, [".ckpt", ".safetensors"])
-                use_lora_model = use_lora_model !== "" ? use_lora_model : oldVal
+                if (model_name !== "") {
+                    model_name = getModelPath(model_name, [".ckpt", ".safetensors"])
+                    model_name = model_name !== "" ? model_name : oldVal
+                }
+                field.value = model_name
+            })
+
+            // clear the remaining entries
+            for (let i = use_lora_model.length; i < loraModels.length; i++) {
+                loraModels[i][0].value = ""
             }
-            loraModelField.value = use_lora_model
         },
-        readUI: () => loraModelField.value,
-        parse: (val) => val,
+        readUI: () => {
+            let values = loraModels.map((e) => e[0].value)
+            values = values.filter((e) => e.trim() !== "")
+            values = values.length > 0 ? values : "None"
+            return values
+        },
+        parse: (val) => {
+            val = !val || val === "None" ? "" : val
+            val = Array.isArray(val) ? val : [val]
+            return val
+        },
     },
     lora_alpha: {
         name: "LoRA Strength",
         setUI: (lora_alpha) => {
-            loraAlphaField.value = lora_alpha
-            updateLoraAlphaSlider()
+            lora_alpha.forEach((model_strength, i) => {
+                let field = loraModels[i][1]
+                field.value = model_strength
+            })
+
+            // clear the remaining entries
+            for (let i = lora_alpha.length; i < loraModels.length; i++) {
+                loraModels[i][1].value = 0
+            }
+        },
+        readUI: () => {
+            let models = loraModels.filter((e) => e[0].value.trim() !== "")
+            let values = models.map((e) => e[1].value)
+            values = values.length > 0 ? values : 0
+            return values
+        },
+        parse: (val) => {
+            val = Array.isArray(val) ? val : [val]
+            val = val.map((e) => parseFloat(e))
+            return val
         },
-        readUI: () => parseFloat(loraAlphaField.value),
-        parse: (val) => parseFloat(val),
     },
     use_hypernetwork_model: {
         name: "Hypernetwork model",
@@ -426,8 +455,11 @@ function restoreTaskToUI(task, fieldsToSkip) {
     }
 
     if (!("use_lora_model" in task.reqBody)) {
-        loraModelField.value = ""
-        loraModelField.dispatchEvent(new Event("change"))
+        loraModels.forEach((e) => {
+            e[0].value = ""
+            e[1].value = 0
+            e[0].dispatchEvent(new Event("change"))
+        })
     }
 
     // restore the original prompt if provided (e.g. use settings), fallback to prompt as needed (e.g. copy/paste or d&d)
diff --git a/ui/media/js/main.js b/ui/media/js/main.js
index b2ebdbed..e31bdd10 100644
--- a/ui/media/js/main.js
+++ b/ui/media/js/main.js
@@ -103,9 +103,6 @@ let vaeModelField = new ModelDropdown(document.querySelector("#vae_model"), "vae
 let hypernetworkModelField = new ModelDropdown(document.querySelector("#hypernetwork_model"), "hypernetwork", "None")
 let hypernetworkStrengthSlider = document.querySelector("#hypernetwork_strength_slider")
 let hypernetworkStrengthField = document.querySelector("#hypernetwork_strength")
-let loraModelField = new ModelDropdown(document.querySelector("#lora_model"), "lora", "None")
-let loraAlphaSlider = document.querySelector("#lora_alpha_slider")
-let loraAlphaField = document.querySelector("#lora_alpha")
 let outputFormatField = document.querySelector("#output_format")
 let outputLosslessField = document.querySelector("#output_lossless")
 let outputLosslessContainer = document.querySelector("#output_lossless_container")
@@ -159,6 +156,8 @@ let undoButton = document.querySelector("#undo")
 let undoBuffer = []
 const UNDO_LIMIT = 20
 
+let loraModels = []
+
 imagePreview.addEventListener("drop", function(ev) {
     const data = ev.dataTransfer?.getData("text/plain")
     if (!data) {
@@ -1292,13 +1291,31 @@ function getCurrentUserRequest() {
         newTask.reqBody.use_hypernetwork_model = hypernetworkModelField.value
         newTask.reqBody.hypernetwork_strength = parseFloat(hypernetworkStrengthField.value)
     }
-    if (testDiffusers.checked && loraModelField.value) {
-        newTask.reqBody.use_lora_model = loraModelField.value
-        newTask.reqBody.lora_alpha = parseFloat(loraAlphaField.value)
+    if (testDiffusers.checked) {
+        let [modelNames, modelStrengths] = getModelInfo(loraModels)
+
+        if (modelNames.length > 0) {
+            modelNames = modelNames.length == 1 ? modelNames[0] : modelNames
+            modelStrengths = modelStrengths.length == 1 ? modelStrengths[0] : modelStrengths
+
+            newTask.reqBody.use_lora_model = modelNames
+            newTask.reqBody.lora_alpha = modelStrengths
+        }
     }
     return newTask
 }
 
+function getModelInfo(models) {
+    let modelInfo = models.map((e) => [e[0].value, e[1].value])
+    modelInfo = modelInfo.filter((e) => e[0].trim() !== "")
+    modelInfo = modelInfo.map((e) => [e[0], parseFloat(e[1])])
+
+    let modelNames = modelInfo.map((e) => e[0])
+    let modelStrengths = modelInfo.map((e) => e[1])
+
+    return [modelNames, modelStrengths]
+}
+
 function getPrompts(prompts) {
     if (typeof prompts === "undefined") {
         prompts = promptField.value
@@ -1836,33 +1853,6 @@ function updateHypernetworkStrengthContainer() {
 hypernetworkModelField.addEventListener("change", updateHypernetworkStrengthContainer)
 updateHypernetworkStrengthContainer()
 
-/********************* LoRA alpha **********************/
-function updateLoraAlpha() {
-    loraAlphaField.value = loraAlphaSlider.value / 100
-    loraAlphaField.dispatchEvent(new Event("change"))
-}
-
-function updateLoraAlphaSlider() {
-    if (loraAlphaField.value < -2) {
-        loraAlphaField.value = -2
-    } else if (loraAlphaField.value > 2) {
-        loraAlphaField.value = 2
-    }
-
-    loraAlphaSlider.value = loraAlphaField.value * 100
-    loraAlphaSlider.dispatchEvent(new Event("change"))
-}
-
-loraAlphaSlider.addEventListener("input", updateLoraAlpha)
-loraAlphaField.addEventListener("input", updateLoraAlphaSlider)
-updateLoraAlpha()
-
-function updateLoraAlphaContainer() {
-    document.querySelector("#lora_alpha_container").style.display = loraModelField.value === "" ? "none" : ""
-}
-loraModelField.addEventListener("change", updateLoraAlphaContainer)
-updateLoraAlphaContainer()
-
 /********************* JPEG/WEBP Quality **********************/
 function updateOutputQuality() {
     outputQualityField.value = 0 | outputQualitySlider.value
@@ -2250,3 +2240,43 @@ prettifyInputs(document)
 // set the textbox as focused on start
 promptField.focus()
 promptField.selectionStart = promptField.value.length
+
+// multi-models
+function addModelEntry(i, modelContainer, modelsList, modelType, defaultValue, minStrength, maxStrength, strengthStep) {
+    let nameId = modelType + "_model_" + i
+    let strengthId = modelType + "_alpha_" + i
+
+    const modelEntry = document.createElement("div")
+    modelEntry.className = "model_entry"
+    modelEntry.innerHTML = `
+        <input id="${nameId}" class="model_name" type="text" spellcheck="false" autocomplete="off" class="model-filter" data-path="" />
+        <input id="${strengthId}" class="model_strength" type="number" max="${maxStrength}" min="${minStrength}" step="${strengthStep}" style="width: 50pt" value="${defaultValue}" pattern="^-?[0-9]*\.?[0-9]*$" onkeypress="preventNonNumericalInput(event)"><br/>
+    `
+
+    let modelName = new ModelDropdown(modelEntry.querySelector(".model_name"), modelType, "None")
+    let modelStrength = modelEntry.querySelector(".model_strength")
+
+    modelContainer.appendChild(modelEntry)
+    modelsList.push([modelName, modelStrength])
+}
+
+function createLoRAEntries() {
+    let container = document.querySelector("#lora_model_container .model_entries")
+    for (let i = 0; i < 3; i++) {
+        addModelEntry(i, container, loraModels, "lora", 0.5, -2, 2, 0.02)
+    }
+}
+createLoRAEntries()
+
+// chrome-like spinners only on hover
+function showSpinnerOnlyOnHover(e) {
+    e.addEventListener("mouseenter", () => {
+        e.setAttribute("type", "number")
+    })
+    e.addEventListener("mouseleave", () => {
+        e.removeAttribute("type")
+    })
+    e.removeAttribute("type")
+}
+
+document.querySelectorAll("input[type=number]").forEach(showSpinnerOnlyOnHover)
diff --git a/ui/media/js/parameters.js b/ui/media/js/parameters.js
index f8dc8066..30b1f33f 100644
--- a/ui/media/js/parameters.js
+++ b/ui/media/js/parameters.js
@@ -426,7 +426,6 @@ async function getAppConfig() {
 
         if (!testDiffusersEnabled) {
             document.querySelector("#lora_model_container").style.display = "none"
-            document.querySelector("#lora_alpha_container").style.display = "none"
             document.querySelector("#tiling_container").style.display = "none"
 
             document.querySelectorAll("#sampler_name option.diffusers-only").forEach((option) => {
@@ -434,7 +433,6 @@ async function getAppConfig() {
             })
         } else {
             document.querySelector("#lora_model_container").style.display = ""
-            document.querySelector("#lora_alpha_container").style.display = loraModelField.value ? "" : "none"
             document.querySelector("#tiling_container").style.display = ""
 
             document.querySelectorAll("#sampler_name option.k_diffusion-only").forEach((option) => {