"use strict"; const $ = (sel) => document.querySelector(sel); const $$ = (sel) => Array.from(document.querySelectorAll(sel)); let MODEL_TYPES = []; let downloadsTimer = null; // ---- helpers -------------------------------------------------------------- async function api(path, opts) { const resp = await fetch(path, opts); const ct = resp.headers.get("content-type") || ""; const data = ct.includes("application/json") ? await resp.json() : null; if (!resp.ok) { const msg = (data && data.detail) ? data.detail : `HTTP ${resp.status}`; throw new Error(msg); } return data; } function fmtBytes(n) { if (!n) return "0 B"; const units = ["B", "KB", "MB", "GB", "TB"]; let i = 0; while (n >= 1024 && i < units.length - 1) { n /= 1024; i++; } return `${n.toFixed(n < 10 && i > 0 ? 1 : 0)} ${units[i]}`; } function fmtDate(epoch) { if (!epoch) return ""; return new Date(epoch * 1000).toLocaleString(); } // ---- navigation ----------------------------------------------------------- function showView(name) { $$(".nav-item").forEach((b) => b.classList.toggle("active", b.dataset.view === name)); $$(".view").forEach((v) => v.classList.toggle("active", v.id === `view-${name}`)); if (name === "installed") loadModels(); if (name === "browse" && !browseState.loaded) runSearch(true); if (name === "download") { loadDownloads(); startDownloadsPolling(); } else stopDownloadsPolling(); if (name === "settings") loadSettings(); } $$(".nav-item").forEach((btn) => btn.addEventListener("click", () => showView(btn.dataset.view))); // ---- model types ---------------------------------------------------------- async function loadModelTypes() { MODEL_TYPES = await api("/api/model-types"); const sel = $("#dlType"); sel.innerHTML = ""; for (const t of MODEL_TYPES) { const opt = document.createElement("option"); opt.value = t.key; opt.textContent = t.label; sel.appendChild(opt); } } // ---- installed models ----------------------------------------------------- async function loadModels() { const container = $("#modelsContainer"); try { const models = await api("/api/models"); renderModels(models); } catch (err) { container.innerHTML = `
Error: ${err.message}
`; } } function renderModels(models) { const container = $("#modelsContainer"); const filter = $("#modelSearch").value.trim().toLowerCase(); const shown = filter ? models.filter((m) => m.name.toLowerCase().includes(filter)) : models; if (!shown.length) { container.innerHTML = `${ models.length ? "No models match your filter." : "No models installed yet. Use Add / Download." }
`; return; } const groups = {}; for (const m of shown) (groups[m.type_label] ||= []).push(m); container.innerHTML = ""; for (const label of Object.keys(groups).sort()) { const group = document.createElement("div"); group.className = "type-group"; group.innerHTML = `No downloads yet.
`; return; } container.innerHTML = ""; for (const r of rows) { const pct = r.bytes_total ? Math.floor((r.bytes_done / r.bytes_total) * 100) : 0; const row = document.createElement("div"); row.className = "dl-row"; const sizeText = r.bytes_total ? `${fmtBytes(r.bytes_done)} / ${fmtBytes(r.bytes_total)} (${pct}%)` : fmtBytes(r.bytes_done); const sub = r.status === "failed" && r.error ? r.error : `${r.source} · ${r.model_type} · ${sizeText}`; row.innerHTML = `Searching…
`; } else { if (browseState.cursor) p.set("cursor", browseState.cursor); else if (browseState.page) p.set("page", browseState.page); moreBtn.textContent = "Loading…"; } try { const data = await api(`/api/civitai/search?${p.toString()}`); if (reset) grid.innerHTML = ""; if (!data.items.length && reset) { grid.innerHTML = `No models found.
`; } else { for (const m of data.items) grid.appendChild(renderBrowseCard(m)); } // Set up "load more": prefer cursor, fall back to next page. browseState.cursor = data.nextCursor || null; browseState.page = data.nextPage || null; const hasMore = Boolean(browseState.cursor || browseState.page); moreBtn.style.display = hasMore ? "" : "none"; moreBtn.textContent = "Load more"; } catch (err) { if (reset) grid.innerHTML = `Error: ${err.message}
`; else toast($("#browseToast"), err.message, true); moreBtn.textContent = "Load more"; } finally { browseState.busy = false; } } function renderBrowseCard(m) { const card = document.createElement("div"); card.className = "browse-card"; const thumb = document.createElement("div"); thumb.className = "thumb"; if (m.thumbnail && m.thumbnail_type === "image") { thumb.style.backgroundImage = `url("${m.thumbnail}")`; } if (m.type) { const tag = document.createElement("span"); tag.className = "type-tag"; tag.textContent = m.type; thumb.appendChild(tag); } if (m.nsfw) { const n = document.createElement("span"); n.className = "nsfw-tag"; n.textContent = "NSFW"; thumb.appendChild(n); } const body = document.createElement("div"); body.className = "body"; const name = document.createElement("div"); name.className = "b-name"; name.textContent = m.name || "(untitled)"; const meta = document.createElement("div"); meta.className = "b-meta"; meta.textContent = `${m.creator || "?"} · ${fmtNum(m.downloads)} downloads`; const foot = document.createElement("div"); foot.className = "b-foot"; // Version selector when there's more than one. let versionSel = null; if (m.versions && m.versions.length > 1) { versionSel = document.createElement("select"); for (const v of m.versions) { const o = document.createElement("option"); o.value = v.id; o.textContent = v.name || `v${v.id}`; versionSel.appendChild(o); } foot.appendChild(versionSel); } const btn = document.createElement("button"); btn.className = "btn primary"; btn.textContent = "Download"; btn.addEventListener("click", () => { const vid = versionSel ? Number(versionSel.value) : m.primary_version_id; downloadVersion(vid, btn); }); foot.appendChild(btn); body.append(name, meta, foot); card.append(thumb, body); return card; } async function downloadVersion(versionId, btn) { if (!versionId) { toast($("#browseToast"), "No version available", true); return; } btn.disabled = true; const original = btn.textContent; btn.textContent = "Queued ✓"; try { await api("/api/civitai/download", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ version_id: versionId }), }); toast($("#browseToast"), "Download started — see Add / Download", false); setTimeout(() => { btn.disabled = false; btn.textContent = original; }, 2500); } catch (err) { toast($("#browseToast"), err.message, true); btn.disabled = false; btn.textContent = original; } } function fmtNum(n) { if (n == null) return "?"; if (n >= 1e6) return (n / 1e6).toFixed(1) + "M"; if (n >= 1e3) return (n / 1e3).toFixed(1) + "k"; return String(n); } let toastTimer = null; function toast(el, msg, isErr) { el.textContent = msg; el.className = `form-msg ${isErr ? "err" : "ok"}`; if (toastTimer) clearTimeout(toastTimer); toastTimer = setTimeout(() => { el.textContent = ""; }, 4000); } $("#bGo").addEventListener("click", () => runSearch(true)); $("#bSearch").addEventListener("keydown", (e) => { if (e.key === "Enter") runSearch(true); }); $("#bMore").addEventListener("click", () => runSearch(false)); // ---- settings ------------------------------------------------------------- async function loadSettings() { try { const s = await api("/api/settings"); setBadge($("#civitaiBadge"), s.civitai_api_key_set); setBadge($("#hfBadge"), s.huggingface_token_set); } catch { /* ignore */ } } function setBadge(el, isSet) { el.textContent = isSet ? "configured" : "not set"; el.className = `badge ${isSet ? "set" : "unset"}`; } $("#saveSettings").addEventListener("click", async () => { const msg = $("#settingsMsg"); const body = {}; const civ = $("#civitaiKey").value; const hf = $("#hfToken").value; // Only send fields the user actually typed into (blank = leave unchanged). if (civ !== "") body.civitai_api_key = civ; if (hf !== "") body.huggingface_token = hf; if (!Object.keys(body).length) { msg.className = "form-msg"; msg.textContent = "Nothing to save."; return; } try { await api("/api/settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); msg.className = "form-msg ok"; msg.textContent = "Saved."; $("#civitaiKey").value = ""; $("#hfToken").value = ""; loadSettings(); } catch (err) { msg.className = "form-msg err"; msg.textContent = err.message; } }); // ---- boot ----------------------------------------------------------------- (async function init() { await loadModelTypes(); showView("installed"); })();