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
+5 -3
View File
@@ -189,7 +189,8 @@ async def start_download(body: DownloadIn) -> dict:
@app.get("/api/civitai/search")
async def civitai_search(
query: Optional[str] = None,
types: Optional[str] = None, # comma-separated CivitAI types
types: Optional[str] = None, # comma-separated CivitAI types
base_models: Optional[str] = None, # comma-separated CivitAI base models
sort: Optional[str] = "Most Downloaded",
period: Optional[str] = "AllTime",
nsfw: bool = False,
@@ -199,10 +200,11 @@ async def civitai_search(
if not db.get_setting("civitai_api_key"):
raise HTTPException(400, "Set your CivitAI API key in Settings first.")
type_list = [t for t in (types or "").split(",") if t] or None
base_model_list = [b for b in (base_models or "").split(",") if b] or None
try:
return await registries.civitai_search(
query=query, types=type_list, sort=sort, period=period,
nsfw=nsfw, page=page, cursor=cursor)
query=query, types=type_list, base_models=base_model_list,
sort=sort, period=period, nsfw=nsfw, page=page, cursor=cursor)
except Exception as exc: # noqa: BLE001 - surface API errors to the UI
raise HTTPException(502, f"CivitAI search failed: {exc}")
+3
View File
@@ -212,6 +212,7 @@ def _to_card(item: dict) -> dict:
async def civitai_search(query: Optional[str] = None,
types: Optional[list[str]] = None,
base_models: Optional[list[str]] = None,
sort: Optional[str] = None,
period: Optional[str] = None,
nsfw: Optional[bool] = None,
@@ -228,6 +229,8 @@ async def civitai_search(query: Optional[str] = None,
params["query"] = query
if types:
params["types"] = types # httpx serializes lists as repeated params
if base_models:
params["baseModels"] = base_models
if sort:
params["sort"] = sort
if period:
+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));