Add searchable base model path mapping picker

This commit is contained in:
Dariusz L
2026-10-05 20:05:19 +02:00
parent 22f4cbcdee
commit f5fbf5c542
2 changed files with 102 additions and 20 deletions
+44
View File
@@ -0,0 +1,44 @@
import fs from 'node:fs';
import test from 'node:test';
import assert from 'node:assert/strict';
import { Window } from 'happy-dom';
test('mapping picker filters models, selects a result and excludes other mapped models', () => {
const window = new Window();
const document = window.document;
const container = document.createElement('div');
document.body.append(container);
const source = fs.readFileSync(new URL('../web/resolver/views/options_methods.js', import.meta.url), 'utf8');
const block = source.slice(source.indexOf(' const getBaseModelMappingChoices ='), source.indexOf(' const renderTemplatePreview ='));
const dialog = {
baseModels: { base_models: [{ name: 'SDXL' }, { name: 'Pony' }, { name: 'Flux.1' }] },
escapeHtml: value => value,
showDropdownList: list => { list.style.display = 'block'; },
hideDropdownList: list => { list.style.display = 'none'; },
enableWheelScrollChaining() {}, bindDropdownOutsideDismiss() {}, bindTooltips() {},
};
const methods = new Function('document', 'tokens', 'baseModelMappingsContainer', 'setStatus', 'syncAllTemplateControls',
`${block}; return { addBaseModelMappingRow, collectBaseModelPathMappings };`
).call(dialog, document, { base_model_path_mappings: {} }, container, () => {}, () => {});
methods.addBaseModelMappingRow('SDXL', 'SDXL');
methods.addBaseModelMappingRow('', 'Pony-folder');
const input = container.querySelectorAll('.mr-options-mapping-base')[1];
const list = input.nextElementSibling;
input.dispatchEvent(new window.Event('focus'));
assert.equal(list.textContent.includes('SDXL'), false);
input.value = 'pon';
input.dispatchEvent(new window.Event('input'));
assert.equal(list.textContent, 'Pony');
list.firstElementChild.dispatchEvent(new window.MouseEvent('mousedown', { bubbles: true }));
assert.equal(input.value, 'Pony');
assert.equal(list.style.display, 'none');
assert.deepEqual(methods.collectBaseModelPathMappings(), { SDXL: 'SDXL', Pony: 'Pony-folder' });
input.value = 'flux';
input.dispatchEvent(new window.Event('input'));
input.dispatchEvent(new window.KeyboardEvent('keydown', { key: 'Enter' }));
assert.equal(input.value, 'Flux.1');
input.dispatchEvent(new window.Event('focus'));
input.dispatchEvent(new window.KeyboardEvent('keydown', { key: 'Escape' }));
assert.equal(list.style.display, 'none');
window.happyDOM.abort();
});
+58 -20
View File
@@ -2865,34 +2865,20 @@ export const optionsMethods = {
}, {});
};
const renderBaseModelMappingOptions = (currentValue = '', currentRow = null) => {
const getAvailableBaseModelMappingChoices = (currentValue = '', currentRow = null) => {
const usedValues = new Set(
getBaseModelMappingRows()
.filter(row => row !== currentRow)
.map(row => row.querySelector('.mr-options-mapping-base')?.value?.trim() || '')
.filter(Boolean)
);
const options = [
`<option value="">Select Base Model</option>`
];
const choices = getBaseModelMappingChoices();
if (currentValue && !choices.includes(currentValue)) {
options.push(`<option value="${this.escapeHtml(currentValue)}" selected>${this.escapeHtml(currentValue)}</option>`);
}
choices.forEach((name) => {
if (usedValues.has(name) && name !== currentValue) return;
options.push(`<option value="${this.escapeHtml(name)}" ${name === currentValue ? 'selected' : ''}>${this.escapeHtml(name)}</option>`);
});
return options.join('');
return choices.filter(name => !usedValues.has(name) || name === currentValue);
};
const refreshBaseModelMappingOptions = () => {
getBaseModelMappingRows().forEach((row) => {
const select = row.querySelector('.mr-options-mapping-base');
if (!select) return;
const currentValue = select.value;
select.innerHTML = renderBaseModelMappingOptions(currentValue, row);
select.value = currentValue;
row.refreshMappingChoices?.();
});
};
@@ -2903,14 +2889,16 @@ export const optionsMethods = {
const row = document.createElement('div');
row.className = 'mr-options-mapping-row';
row.innerHTML = `
<select class="mr-options-input mr-options-mapping-base">
${renderBaseModelMappingOptions(baseModel, row)}
</select>
<div class="mr-download-target-wrap">
<input class="mr-options-input mr-options-mapping-base" type="text" autocomplete="off" placeholder="Search Base Models…" value="${this.escapeHtml(baseModel)}">
<div class="mr-download-target-list"></div>
</div>
<input class="mr-options-input mr-options-mapping-path" type="text" placeholder="Custom path (e.g., SDXL/Pony)" value="${this.escapeHtml(pathValue)}">
<button type="button" class="mr-options-mapping-remove" aria-label="Remove mapping" data-tooltip="Remove mapping">&times;</button>
`;
const baseSelect = row.querySelector('.mr-options-mapping-base');
const baseList = row.querySelector('.mr-download-target-list');
const pathInput = row.querySelector('.mr-options-mapping-path');
const removeBtn = row.querySelector('.mr-options-mapping-remove');
const markChanged = () => {
@@ -2919,6 +2907,54 @@ export const optionsMethods = {
syncAllTemplateControls();
};
let filterText = '';
const renderChoices = (filter = '', show = true) => {
filterText = filter;
const search = filter.trim().toLowerCase();
const choices = getAvailableBaseModelMappingChoices(baseSelect.value.trim(), row)
.filter(name => name.toLowerCase().includes(search));
baseList.innerHTML = choices.map(name => (
`<div class="mr-download-target-option" data-value="${encodeURIComponent(name)}">${this.escapeHtml(name)}</div>`
)).join('');
baseList.querySelectorAll('.mr-download-target-option').forEach(option => {
option.addEventListener('mousedown', event => {
event.preventDefault();
baseSelect.value = decodeURIComponent(option.dataset.value);
this.hideDropdownList(baseList);
markChanged();
});
});
if (show && choices.length) this.showDropdownList(baseList, baseSelect);
else if (!choices.length) this.hideDropdownList(baseList);
};
row.refreshMappingChoices = () => {
if (baseList.style.display !== 'none') renderChoices(filterText, false);
};
this.enableWheelScrollChaining(baseList);
this.bindDropdownOutsideDismiss(baseList, [baseSelect], () => this.hideDropdownList(baseList));
baseSelect.addEventListener('focus', () => renderChoices());
baseSelect.addEventListener('click', () => renderChoices());
baseSelect.addEventListener('input', () => {
markChanged();
renderChoices(baseSelect.value);
});
baseSelect.addEventListener('keydown', event => {
if (event.key === 'Escape') this.hideDropdownList(baseList);
if (event.key === 'ArrowDown') {
event.preventDefault();
renderChoices(baseSelect.value);
}
if (event.key === 'Enter') {
event.preventDefault();
const option = baseList.querySelector('.mr-download-target-option');
if (option && baseList.style.display !== 'none') {
baseSelect.value = decodeURIComponent(option.dataset.value);
markChanged();
}
this.hideDropdownList(baseList);
}
});
baseSelect?.addEventListener('change', markChanged);
pathInput?.addEventListener('input', markChanged);
pathInput?.addEventListener('keydown', (event) => {
@@ -2928,6 +2964,7 @@ export const optionsMethods = {
}
});
removeBtn?.addEventListener('click', () => {
baseList._mlTreePicker?.cleanup();
row.remove();
if (!getBaseModelMappingRows().length) {
addBaseModelMappingRow('', '');
@@ -2944,6 +2981,7 @@ export const optionsMethods = {
const renderBaseModelMappingRows = (mappings = {}) => {
if (!baseModelMappingsContainer) return;
getBaseModelMappingRows().forEach(row => row.querySelector('.mr-download-target-list')?._mlTreePicker?.cleanup());
baseModelMappingsContainer.innerHTML = '';
const entries = Object.entries(mappings || {});
if (!entries.length) {