feat(model-manager): base-model multi-select filter on CivitAI browse

Add a checkbox dropdown to filter the CivitAI catalog by one or more base
models (SD 1.5, SDXL, Pony, Illustrious, Flux, etc.), mapped to the API's
`baseModels` array param. Backend search accepts comma-separated
base_models; button shows the selected count with a Clear action.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-07 16:37:33 -04:00
parent 505f212c4d
commit 15e0dc3772
6 changed files with 86 additions and 4 deletions
+46
View File
@@ -404,11 +404,56 @@ $("#galleryMore").addEventListener("click", () => loadGallery(false));
const browseState = { loaded: false, page: null, cursor: null, busy: false };
// CivitAI base-model values (exact strings the API expects).
const BASE_MODELS = [
"SD 1.5", "SD 2.1", "SDXL 1.0", "SDXL Lightning", "SDXL Hyper", "SDXL Turbo",
"Pony", "Illustrious", "NoobAI", "Flux.1 S", "Flux.1 D", "Flux.1 Kontext",
"SD 3", "SD 3.5", "SD 3.5 Medium", "SD 3.5 Large", "Stable Cascade",
"Wan Video", "Hunyuan Video", "LTXV", "Qwen", "Chroma", "HiDream", "Other",
];
function initBaseModelDropdown() {
const panel = $("#bBasePanel");
for (const bm of BASE_MODELS) {
const lab = document.createElement("label");
lab.className = "multi-opt";
const cb = document.createElement("input");
cb.type = "checkbox";
cb.value = bm;
cb.className = "bm-check";
cb.addEventListener("change", updateBaseModelLabel);
lab.append(cb, document.createTextNode(" " + bm));
panel.appendChild(lab);
}
$("#bBaseBtn").addEventListener("click", (e) => {
e.stopPropagation();
panel.hidden = !panel.hidden;
});
$("#bBaseClear").addEventListener("click", () => {
$$(".bm-check").forEach((c) => { c.checked = false; });
updateBaseModelLabel();
});
document.addEventListener("click", (e) => {
if (!$("#bBaseDD").contains(e.target)) panel.hidden = true;
});
}
function selectedBaseModels() {
return $$(".bm-check").filter((c) => c.checked).map((c) => c.value);
}
function updateBaseModelLabel() {
const n = selectedBaseModels().length;
$("#bBaseBtn").textContent = n ? `Base model (${n}) ▾` : "Base model ▾";
}
function browseParams() {
const p = new URLSearchParams();
const q = $("#bSearch").value.trim();
if (q) p.set("query", q);
if ($("#bType").value) p.set("types", $("#bType").value);
const bms = selectedBaseModels();
if (bms.length) p.set("base_models", bms.join(","));
p.set("sort", $("#bSort").value);
p.set("period", $("#bPeriod").value);
p.set("nsfw", $("#bNsfw").checked ? "true" : "false");
@@ -622,6 +667,7 @@ $("#saveSettings").addEventListener("click", async () => {
(async function init() {
await loadModelTypes();
initBaseModelDropdown();
const wanted = new URLSearchParams(location.search).get("view");
const valid = ["installed", "gallery", "browse", "download", "settings"];
showView(valid.includes(wanted) ? wanted : "installed");
+9
View File
@@ -104,6 +104,15 @@
<option>Week</option>
<option>Day</option>
</select>
<div class="multi-dd" id="bBaseDD">
<button type="button" class="btn multi-btn" id="bBaseBtn">Base model ▾</button>
<div class="multi-panel" id="bBasePanel" hidden>
<div class="multi-panel-actions">
<button type="button" class="link-btn" id="bBaseClear">Clear</button>
</div>
<!-- checkboxes injected by app.js -->
</div>
</div>
<label class="check"><input type="checkbox" id="bNsfw" /> NSFW</label>
<button class="btn primary" id="bGo">Search</button>
</div>
+21
View File
@@ -219,6 +219,27 @@ code { background: var(--bg-3); padding: 1px 5px; border-radius: 4px; font-size:
.check { display: flex; align-items: center; gap: 6px; margin: 0; color: var(--text); white-space: nowrap; }
.check input { width: auto; }
/* multi-select dropdown (base model) */
.multi-dd { position: relative; }
.multi-btn { white-space: nowrap; }
.multi-panel {
position: absolute; top: calc(100% + 6px); left: 0; z-index: 50;
background: var(--bg-2); border: 1px solid var(--border);
border-radius: 8px; padding: 8px; min-width: 200px;
max-height: 320px; overflow-y: auto;
display: grid; grid-template-columns: 1fr 1fr; gap: 2px 10px;
box-shadow: 0 10px 30px rgba(0,0,0,.45);
}
.multi-panel[hidden] { display: none; }
.multi-panel-actions { grid-column: 1 / -1; display: flex; justify-content: flex-end; border-bottom: 1px solid var(--border); padding-bottom: 4px; margin-bottom: 2px; }
.link-btn { background: none; border: none; color: var(--accent); cursor: pointer; font-size: 12px; padding: 2px 4px; }
.multi-opt {
display: flex; align-items: center; gap: 6px; margin: 0;
color: var(--text); font-size: 13px; padding: 4px 6px; border-radius: 6px; cursor: pointer;
}
.multi-opt:hover { background: var(--bg-3); }
.multi-opt input { width: auto; }
.browse-grid {
display: grid; gap: 14px;
grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));