diff --git a/ui/index.html b/ui/index.html
index c83f08bc..4f62b5b4 100644
--- a/ui/index.html
+++ b/ui/index.html
@@ -332,8 +332,7 @@
|
@@ -788,6 +787,7 @@
+
diff --git a/ui/media/js/auto-save.js b/ui/media/js/auto-save.js
index bcfd4bc6..eff23a99 100644
--- a/ui/media/js/auto-save.js
+++ b/ui/media/js/auto-save.js
@@ -57,6 +57,7 @@ const SETTINGS_IDS_LIST = [
"json_toggle",
"extract_lora_from_prompt",
"embedding-card-size-selector",
+ "lora_model",
]
const IGNORE_BY_DEFAULT = ["prompt"]
diff --git a/ui/media/js/main.js b/ui/media/js/main.js
index 1546f54b..643637d4 100644
--- a/ui/media/js/main.js
+++ b/ui/media/js/main.js
@@ -121,6 +121,7 @@ let clipSkipField = document.querySelector("#clip_skip")
let tilingField = document.querySelector("#tiling")
let controlnetModelField = new ModelDropdown(document.querySelector("#controlnet_model"), "controlnet", "None", false)
let vaeModelField = new ModelDropdown(document.querySelector("#vae_model"), "vae", "None")
+let loraModelField = new MultiModelSelector(document.querySelector("#lora_model"), "lora", "LoRA", 0.5, 0.02)
let hypernetworkModelField = new ModelDropdown(document.querySelector("#hypernetwork_model"), "hypernetwork", "None")
let hypernetworkStrengthSlider = document.querySelector("#hypernetwork_strength_slider")
let hypernetworkStrengthField = document.querySelector("#hypernetwork_strength")
@@ -2928,76 +2929,6 @@ prettifyInputs(document)
promptField.focus()
promptField.selectionStart = promptField.value.length
-// multi-models
-let modelCount = 0
-
-function addModelEntry(modelContainer, modelsList, modelType, defaultValue, strengthStep) {
- let idx = modelCount++
- let nameId = modelType + "_model_" + idx
- let strengthId = modelType + "_alpha_" + idx
-
- const modelElement = document.createElement("div")
- modelElement.className = "model_entry"
- modelElement.innerHTML = `
-
-
- `
- modelContainer.appendChild(modelElement)
-
- let modelName = new ModelDropdown(modelElement.querySelector(".model_name"), modelType, "None")
- let modelStrength = modelElement.querySelector(".model_strength")
- let entry = [modelName, modelStrength, modelElement]
-
- let removeBtn = document.createElement("button")
- removeBtn.className = "remove_model_btn"
- removeBtn.setAttribute("title", "Remove model")
- removeBtn.innerHTML = ''
-
- if (modelsList.length === 0) {
- removeBtn.classList.add("displayNone")
- }
-
- removeBtn.addEventListener("click", function() {
- let entryIdx = modelsList.indexOf(entry)
- modelsList.splice(entryIdx, 1)
- modelContainer.removeChild(modelElement)
- })
-
- modelElement.appendChild(removeBtn)
-
- modelsList.push(entry)
-
- return modelElement
-}
-
-function createLoraEntry() {
- let container = document.querySelector("#lora_model_container .model_entries")
- return addModelEntry(container, loraModels, "lora", 0.5, 0.02)
-}
-
-function createLoraEntries() {
- let firstEntry = createLoraEntry()
-
- let addLoraBtn = document.querySelector("#lora_model_container .add_model_entry")
- addLoraBtn.addEventListener("click", () => {
- createLoraEntry()
- })
-}
-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)
-
////////////////////////////// Image Size Widget //////////////////////////////////////////
function roundToMultiple(number, n) {
diff --git a/ui/media/js/multi-model-selector.js b/ui/media/js/multi-model-selector.js
new file mode 100644
index 00000000..24d5e0f1
--- /dev/null
+++ b/ui/media/js/multi-model-selector.js
@@ -0,0 +1,218 @@
+/**
+ * A component consisting of multiple model dropdowns, along with a "weight" field per model.
+ *
+ * Behaves like a single input element, giving an object in response to the .value field.
+ *
+ * Inspired by the design of the ModelDropdown component (searchable-models.js).
+ */
+
+class MultiModelSelector {
+ root
+ modelType
+ modelNameFriendly
+ defaultWeight
+ weightStep
+
+ modelContainer
+ addNewButton
+
+ counter = 0
+
+ /* MIMIC A REGULAR INPUT FIELD */
+ get id() {
+ return this.root.id
+ }
+ get parentElement() {
+ return this.root.parentElement
+ }
+ get parentNode() {
+ return this.root.parentNode
+ }
+ get value() {
+ let modelNames = []
+ let modelWeights = []
+
+ this.modelElements.forEach((e) => {
+ modelNames.push(e.name.value)
+ modelWeights.push(e.weight.value)
+ })
+
+ return { modelNames: modelNames, modelWeights: modelWeights }
+ }
+ set value(modelData) {
+ if (typeof modelData !== "object") {
+ throw new Error("Multi-model selector expects an object containing modelNames and modelWeights as keys!")
+ }
+ if (!("modelNames" in modelData) || !("modelWeights" in modelData)) {
+ throw new Error("modelNames or modelWeights not present in the data passed to the multi-model selector")
+ }
+
+ let newModelNames = modelData["modelNames"]
+ let newModelWeights = modelData["modelWeights"]
+ if (newModelNames.length !== newModelWeights.length) {
+ throw new Error("Need to pass an equal number of modelNames and modelWeights!")
+ }
+
+ // expand or shrink entries
+ let currElements = this.modelElements
+ if (currElements.length < newModelNames.length) {
+ for (let i = currElements.length; i < newModelNames.length; i++) {
+ this.addModelEntry()
+ }
+ } else {
+ for (let i = newModelNames.length; i < currElements.length; i++) {
+ this.removeModelEntry()
+ }
+ }
+
+ // assign to the corresponding elements
+ currElements = this.modelElements
+ for (let i = 0; i < newModelNames.length; i++) {
+ let curr = currElements[i]
+
+ // update weight first, name second.
+ // for some unholy reason this order matters for dispatch chains
+ // the root of all this unholiness is because searchable-models automatically dispatches an update event
+ // as soon as the value is updated via JS, which is against the DOM pattern of not dispatching an event automatically
+ // unless the caller explicitly dispatches the event.
+ curr.weight.value = newModelWeights[i]
+ curr.name.value = newModelNames[i]
+ }
+ }
+ get disabled() {
+ return false
+ }
+ set disabled(state) {
+ // do nothing
+ }
+ get modelElements() {
+ let entries = this.root.querySelectorAll(".model_entry")
+ entries = [...entries]
+ let elements = entries.map((e) => {
+ return { name: e.querySelector(".model_name").field, weight: e.querySelector(".model_weight") }
+ })
+ return elements
+ }
+ addEventListener(type, listener, options) {
+ // do nothing
+ }
+ dispatchEvent(event) {
+ // do nothing
+ }
+ appendChild(option) {
+ // do nothing
+ }
+
+ // remember 'this' - http://blog.niftysnippets.org/2008/04/you-must-remember-this.html
+ bind(f, obj) {
+ return function() {
+ return f.apply(obj, arguments)
+ }
+ }
+
+ constructor(root, modelType, modelNameFriendly = undefined, defaultWeight = 0.5, weightStep = 0.02) {
+ this.root = root
+ this.modelType = modelType
+ this.modelNameFriendly = modelNameFriendly || modelType
+ this.defaultWeight = defaultWeight
+ this.weightStep = weightStep
+
+ let self = this
+ document.addEventListener("refreshModels", function() {
+ setTimeout(self.bind(self.populateModels, self), 1)
+ })
+
+ this.createStructure()
+ this.populateModels()
+ }
+
+ createStructure() {
+ this.modelContainer = document.createElement("div")
+ this.modelContainer.className = "model_entries"
+ this.root.appendChild(this.modelContainer)
+
+ this.addNewButton = document.createElement("button")
+ this.addNewButton.className = "add_model_entry"
+ this.addNewButton.innerHTML = ' add another ' + this.modelNameFriendly
+ this.addNewButton.addEventListener("click", this.bind(this.addModelEntry, this))
+ this.root.appendChild(this.addNewButton)
+ }
+
+ populateModels() {
+ if (this.root.dataset.path === "") {
+ if (this.length === 0) {
+ this.addModelEntry() // create a single blank entry
+ }
+ } else {
+ this.value = JSON.parse(this.root.dataset.path)
+ }
+ }
+
+ addModelEntry() {
+ let idx = this.counter++
+ let currLength = this.length
+
+ const modelElement = document.createElement("div")
+ modelElement.className = "model_entry"
+ modelElement.innerHTML = `
+
+
+ `
+ this.modelContainer.appendChild(modelElement)
+
+ let modelNameEl = modelElement.querySelector(".model_name")
+ modelNameEl.field = new ModelDropdown(modelNameEl, this.modelType, "None")
+ let modelWeightEl = modelElement.querySelector(".model_weight")
+
+ let self = this
+
+ function makeUpdateEvent(type) {
+ return function(e) {
+ e.stopPropagation()
+
+ let modelData = self.value
+ self.root.dataset.path = JSON.stringify(modelData)
+
+ self.root.dispatchEvent(new Event(type))
+ }
+ }
+
+ modelNameEl.addEventListener("change", makeUpdateEvent("change"))
+ modelNameEl.addEventListener("input", makeUpdateEvent("input"))
+ modelWeightEl.addEventListener("change", makeUpdateEvent("change"))
+ modelWeightEl.addEventListener("input", makeUpdateEvent("input"))
+
+ let removeBtn = document.createElement("button")
+ removeBtn.className = "remove_model_btn"
+ removeBtn.setAttribute("title", "Remove model")
+ removeBtn.innerHTML = ''
+
+ if (currLength === 0) {
+ removeBtn.classList.add("displayNone")
+ }
+
+ removeBtn.addEventListener(
+ "click",
+ this.bind(function(e) {
+ this.modelContainer.removeChild(modelElement)
+
+ makeUpdateEvent("change")(e)
+ }, this)
+ )
+
+ modelElement.appendChild(removeBtn)
+ }
+
+ removeModelEntry() {
+ if (this.length === 0) {
+ return
+ }
+
+ let lastEntry = this.modelContainer.lastElementChild
+ lastEntry.remove()
+ }
+
+ get length() {
+ return this.modelContainer.childElementCount
+ }
+}
diff --git a/ui/media/js/searchable-models.js b/ui/media/js/searchable-models.js
index 299c60dc..a4e3f284 100644
--- a/ui/media/js/searchable-models.js
+++ b/ui/media/js/searchable-models.js
@@ -118,13 +118,16 @@ class ModelDropdown {
)
}
- saveCurrentSelection(elem, value, path) {
+ saveCurrentSelection(elem, value, path, dispatchEvent = true) {
this.currentSelection.elem = elem
this.currentSelection.value = value
this.currentSelection.path = path
this.modelFilter.dataset.path = path
this.modelFilter.value = value
- this.modelFilter.dispatchEvent(new Event("change"))
+
+ if (dispatchEvent) {
+ this.modelFilter.dispatchEvent(new Event("change"))
+ }
}
processClick(e) {
@@ -348,13 +351,13 @@ class ModelDropdown {
}
}
- selectEntry(path) {
+ selectEntry(path, dispatchEvent = true) {
if (path !== undefined) {
const entries = this.modelElements
for (const elem of entries) {
if (elem.dataset.path == path) {
- this.saveCurrentSelection(elem, elem.innerText, elem.dataset.path)
+ this.saveCurrentSelection(elem, elem.innerText, elem.dataset.path, dispatchEvent)
this.highlightedModelEntry = elem
elem.scrollIntoView({ block: "nearest" })
break
@@ -529,7 +532,7 @@ class ModelDropdown {
rootModelList.style.minWidth = modelFilterStyle.width
})
- this.selectEntry(this.activeModel)
+ this.selectEntry(this.activeModel, false)
}
/**
|