Spaces:
Running
Running
Download static/app.js from codelion/mlx-model-explorer: direct link, hf CLI and curl.
- Browser
- Download file 43.9 kB
-
https://huggingface.co/spaces/codelion/mlx-model-explorer/resolve/main/static/app.js
- Command line
-
hf download hf://spaces/codelion/mlx-model-explorer/static/app.js
-
curl -L -o app.js https://huggingface.co/spaces/codelion/mlx-model-explorer/resolve/main/static/app.js
43.9 kB
| ; | |
| (function () { | |
| const $ = (s, r = document) => r.querySelector(s); | |
| const $$ = (s, r = document) => [...r.querySelectorAll(s)]; | |
| // Build DOM safely: all text goes through text nodes, never innerHTML. | |
| function el(tag, attrs, ...kids) { | |
| const n = document.createElement(tag); | |
| for (const [k, v] of Object.entries(attrs || {})) { | |
| if (v == null || v === false) continue; | |
| if (k === "class") n.className = v; | |
| else if (k.startsWith("on")) n.addEventListener(k.slice(2), v); | |
| else n.setAttribute(k, v === true ? "" : v); | |
| } | |
| for (const kid of kids.flat()) { | |
| if (kid == null || kid === false) continue; | |
| n.append(kid instanceof Node ? kid : document.createTextNode(String(kid))); | |
| } | |
| return n; | |
| } | |
| const TOP_FAMILIES = 9; | |
| const RAM_CHIPS = [8, 16, 18, 24, 32, 36, 48, 64, 96, 128, 192]; | |
| const SIZE_LABEL = { "<3B": "Under 3B", "3-8B": "3–8B", "8-15B": "8–15B", "15-35B": "15–35B", "35-70B": "35–70B", "70B+": "70B+", MoE: "MoE" }; | |
| const PRIORITY_LABEL = { balanced: "Balanced", quality: "Quality", speed: "Speed", memory: "Least memory", long_context: "Long context" }; | |
| const CHIPS = []; | |
| for (const g of [1, 2, 3, 4, 5]) for (const t of ["", " Pro", " Max", " Ultra"]) CHIPS.push(`Apple M${g}${t}`); | |
| const state = { | |
| meta: null, | |
| sort: "recommended", | |
| offset: 0, | |
| results: [], | |
| total: 0, | |
| lastQuery: null, | |
| hw: null, | |
| ramSource: null, // "confirmed" | "estimated" | null | |
| compare: new Map(), | |
| pinned: null, | |
| requestId: 0, | |
| }; | |
| // ------------------------------------------------------------------ formatting | |
| const fmtCtx = (n) => (n >= 262144 ? "256k" : n >= 1024 ? `${Math.round(n / 1024)}k` : String(n)); | |
| const fmtNum = (n) => (n == null ? "unknown" : n >= 1e6 ? `${(n / 1e6).toFixed(1)}M` : n >= 1e3 ? `${Math.round(n / 1e3)}k` : String(n)); | |
| const fmtParams = (p) => (p == null ? "size unknown" : p >= 1e9 ? `${(p / 1e9).toFixed(p >= 1e10 ? 0 : 1)}B` : `${Math.round(p / 1e6)}M`); | |
| const fmtGB = (g) => (g == null ? "unknown" : `${g < 10 ? g.toFixed(1) : Math.round(g)}\u00a0GB`); | |
| function fmtAgo(iso) { | |
| if (!iso) return "unknown"; | |
| const days = (Date.now() - Date.parse(iso)) / 864e5; | |
| if (!isFinite(days)) return "unknown"; | |
| if (days < 1) return "today"; | |
| const plural = (n, unit) => `${n} ${unit}${n === 1 ? "" : "s"} ago`; | |
| if (days < 31) return plural(Math.round(days), "day"); | |
| if (days < 365) return plural(Math.max(1, Math.round(days / 30.4)), "month"); | |
| const y = days / 365; | |
| return `${y < 1.95 ? y.toFixed(1) : Math.round(y)} years ago`; | |
| } | |
| const fitText = (fit) => ({ Comfortable: "comfortable fit", Likely: "likely fits", Borderline: "borderline", Unlikely: "unlikely to fit" }[fit] || ""); | |
| // ------------------------------------------------------------------ telemetry | |
| const Telemetry = (() => { | |
| // A browser privacy signal (GPC / Do Not Track) means "off" unless the user explicitly opts in here. | |
| const gpc = navigator.globalPrivacyControl === true || navigator.doNotTrack === "1"; | |
| let optedOut = gpc; | |
| try { | |
| if (localStorage.getItem("mme-optout") === "1") optedOut = true; | |
| else if (gpc && localStorage.getItem("mme-optin") === "1") optedOut = false; | |
| } catch (e) {} | |
| let sid = null; | |
| try { sid = sessionStorage.getItem("mme-sid"); } catch (e) {} | |
| if (!sid || !/^[a-f0-9]{16}$/.test(sid)) { | |
| const b = new Uint8Array(8); | |
| crypto.getRandomValues(b); | |
| sid = [...b].map((x) => x.toString(16).padStart(2, "0")).join(""); | |
| try { sessionStorage.setItem("mme-sid", sid); } catch (e) {} | |
| } | |
| let enabled = true; | |
| let buildSession = () => null; // set by the app: returns the visit's current summary or null | |
| let lastSent = ""; | |
| function post(events, beacon) { | |
| const body = JSON.stringify({ events }); | |
| if (beacon && navigator.sendBeacon) { | |
| navigator.sendBeacon("/api/events", new Blob([body], { type: "application/json" })); | |
| } else { | |
| fetch("/api/events", { method: "POST", headers: { "content-type": "application/json" }, body, keepalive: true }).catch(() => {}); | |
| } | |
| } | |
| // One row per visit: re-sent only when the summary changed since the last send. | |
| function sendSession(beacon) { | |
| if (optedOut || !enabled) return; | |
| const summary = buildSession(); | |
| if (!summary) return; | |
| const ev = { event_type: "session", session_id: sid }; | |
| for (const [k, v] of Object.entries(summary)) { | |
| if (v !== undefined && v !== null && v !== "" && !(Array.isArray(v) && !v.length)) ev[k] = v; | |
| } | |
| const key = JSON.stringify(ev); | |
| if (key === lastSent) return; | |
| lastSent = key; | |
| post([ev], beacon); | |
| } | |
| document.addEventListener("visibilitychange", () => { if (document.visibilityState === "hidden") sendSession(true); }); | |
| window.addEventListener("pagehide", () => sendSession(true)); | |
| return { | |
| gpc, | |
| get optedOut() { return optedOut; }, | |
| setOptOut(v) { | |
| optedOut = v; | |
| try { | |
| if (v) { localStorage.setItem("mme-optout", "1"); localStorage.removeItem("mme-optin"); } | |
| else { localStorage.removeItem("mme-optout"); if (gpc) localStorage.setItem("mme-optin", "1"); } | |
| } catch (e) {} | |
| }, | |
| setEnabled(v) { enabled = v; }, | |
| setSessionBuilder(fn) { buildSession = fn; }, | |
| sendSession, | |
| // A contribution the page makes on the user's behalf (the GPU test): one row, sent now. | |
| contribute(type, fields) { | |
| if (optedOut || !enabled) return; | |
| const ev = { event_type: type, session_id: sid }; | |
| for (const [k, v] of Object.entries(fields || {})) if (v !== undefined && v !== null && v !== "") ev[k] = v; | |
| post([ev], false); | |
| }, | |
| // Submissions the user explicitly asked to send go out immediately, with a result. | |
| async submit(type, fields) { | |
| if (optedOut) return { ok: false, message: "Sending is turned off in Privacy settings on this page." }; | |
| const ev = { event_type: type, session_id: sid, ...fields }; | |
| Object.keys(ev).forEach((k) => (ev[k] === undefined || ev[k] === null || ev[k] === "") && delete ev[k]); | |
| try { | |
| const r = await fetch("/api/events", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ events: [ev] }) }); | |
| const j = await r.json().catch(() => ({})); | |
| if (r.status === 202) return { ok: true, flags: (j.flags || [])[0] || [] }; | |
| if (r.status === 429) return { ok: false, message: "Too many submissions from this network. Try again in a minute." }; | |
| const d = (j.details || []).map((x) => `${(x.loc || []).slice(-1)[0]}: ${x.msg}`).join("; "); | |
| return { ok: false, message: `Not accepted${d ? ` (${d})` : ""}.` }; | |
| } catch (e) { | |
| return { ok: false, message: "Couldn't reach the server. Check your connection and try again." }; | |
| } | |
| }, | |
| }; | |
| })(); | |
| // ------------------------------------------------------------------ controls | |
| function chip(name, value, label, small, checked) { | |
| const input = el("input", { type: "radio", name, value }); | |
| input.checked = !!checked; | |
| return el("label", { class: "chip" }, input, el("span", {}, label, small ? el("small", {}, small) : null)); | |
| } | |
| function radioValue(name) { | |
| const r = $(`#query input[name="${name}"]:checked`); | |
| return r ? r.value : ""; | |
| } | |
| function setRadio(name, value) { | |
| const r = $$(`#query input[name="${name}"]`).find((x) => x.value === String(value)); | |
| if (r) { r.checked = true; return true; } | |
| return false; | |
| } | |
| function uncheck(name) { | |
| $$(`#query input[name="${name}"]`).forEach((x) => (x.checked = false)); | |
| } | |
| // ------------------------------------------------------------------ visit summary (the only analytics row) | |
| const visit = { families: [], quants: [], queries: new Set(), query: null, top: null, | |
| viewed: [], compared: [], clicked: [], engagedSent: false }; | |
| const pushCapped = (arr, v, cap) => { if (v && !arr.includes(v)) { arr.push(v); if (arr.length > cap) arr.shift(); } }; | |
| function noteQuery(q, data) { | |
| visit.query = { ...q, result_count: data.total }; | |
| if (q.family) pushCapped(visit.families, q.family, 5); | |
| if (q.quant) pushCapped(visit.quants, q.quant, 5); | |
| visit.queries.add([q.family, q.size, q.quant, q.context, q.priority, q.ram_gb].join("|")); | |
| const top = data.results && data.results[0]; | |
| if (top && q.sort === "recommended") { | |
| visit.top = { id: top.model.id, score: Math.round(top.score * 10) / 10, fit: top.memory && top.memory.fit }; | |
| } | |
| } | |
| function noteModel(kind, id) { | |
| pushCapped(visit[kind], id, kind === "viewed" ? 10 : 5); | |
| if (!visit.engagedSent) { // the first real interaction sends the summary right away | |
| visit.engagedSent = true; | |
| Telemetry.sendSession(false); | |
| } | |
| } | |
| Telemetry.setSessionBuilder(() => { | |
| const q = visit.query; | |
| if (!q) return null; | |
| return { | |
| model_family: q.family, parameter_bucket: q.size, quantization: q.quant, target_context: q.context, | |
| priority: q.priority, sort: q.sort, result_count: q.result_count, | |
| families_searched: visit.families, quants_searched: visit.quants, distinct_queries: visit.queries.size, | |
| ...hwFields(), webgpu_score: state.hw ? state.hw.quick_score : null, | |
| top_model: visit.top && visit.top.id, top_model_score: visit.top && visit.top.score, top_model_fit: visit.top && visit.top.fit, | |
| models_viewed: visit.viewed, models_compared: visit.compared, models_clicked: visit.clicked, | |
| }; | |
| }); | |
| function currentRam() { | |
| const v = parseInt(radioValue("ram"), 10); | |
| return Number.isFinite(v) ? v : null; | |
| } | |
| function currentFamily() { | |
| return radioValue("family") || $("#family-more").value || ""; | |
| } | |
| function buildControls(m) { | |
| const ramOpts = RAM_CHIPS.filter((g) => m.ram_classes.includes(g)); | |
| $("#ram-chips").replaceChildren(chip("ram", "", "Not sure", null, true), ...ramOpts.map((g) => chip("ram", g, `${g} GB`))); | |
| $("#context-chips").replaceChildren(...m.contexts.map((c) => chip("context", c, fmtCtx(c), null, c === 8192))); | |
| const fams = m.families.filter((f) => f.family !== "Other"); | |
| $("#family-chips").replaceChildren(chip("family", "", "Any", null, true), | |
| ...fams.slice(0, TOP_FAMILIES).map((f) => chip("family", f.family, f.family, String(f.count)))); | |
| const more = $("#family-more"); | |
| more.length = 1; | |
| fams.slice(TOP_FAMILIES).forEach((f) => more.append(el("option", { value: f.family }, `${f.family} (${f.count})`))); | |
| const other = m.families.find((f) => f.family === "Other"); | |
| if (other) more.append(el("option", { value: "Other" }, `Other (${other.count})`)); | |
| $("#size-chips").replaceChildren(chip("size", "", "Any", null, true), ...m.sizes.map((s) => chip("size", s, SIZE_LABEL[s] || s))); | |
| const quants = m.quantizations.filter((q) => q !== "unknown"); | |
| $("#quant-chips").replaceChildren(chip("quant", "", "Any", null, true), | |
| ...quants.map((q) => chip("quant", q, q.replace("-bit", " bit"))), chip("quant", "unknown", "Unknown")); | |
| $("#priority-chips").replaceChildren(...m.priorities.map((p) => chip("priority", p, PRIORITY_LABEL[p] || p, null, p === "balanced"))); | |
| } | |
| function hwFields() { | |
| const f = {}; | |
| const ram = currentRam(); | |
| if (ram) f.hardware_memory_class = ram; | |
| f.hardware_source = state.ramSource === "confirmed" ? "confirmed" : state.hw ? "detected" : "none"; | |
| if (state.hw) { | |
| f.webgpu_available = state.hw.webgpu_available; | |
| f.gpu_capability_class = state.hw.capability; | |
| f.gpu_vendor = state.hw.gpu_vendor; | |
| f.gpu_arch = state.hw.gpu_arch; | |
| f.cpu_cores = state.hw.cpu_cores; | |
| f.browser_family = state.hw.browser_family; | |
| f.os_family = state.hw.os_family; | |
| } | |
| return f; | |
| } | |
| async function loadMeta() { | |
| const r = await fetch("/api/meta"); | |
| if (!r.ok) throw new Error("meta"); | |
| const m = await r.json(); | |
| state.meta = m; | |
| buildControls(m); | |
| $("#app-version").textContent = m.version; | |
| $("#engine-version").textContent = m.engine; | |
| const c = m.catalogue; | |
| $("#cat-status").textContent = `${c.models.toLocaleString()} models indexed${c.degraded ? " from a cached copy" : ""}`; | |
| if (!m.collection_enabled) Telemetry.setEnabled(false); | |
| restoreFromUrl(); | |
| } | |
| function restoreFromUrl() { | |
| const p = new URLSearchParams(location.search); | |
| const fam = p.get("family"); | |
| if (fam && !setRadio("family", fam) && [...$("#family-more").options].some((o) => o.value === fam)) { | |
| $("#family-more").value = fam; | |
| uncheck("family"); | |
| } | |
| ["size", "quant", "context", "priority"].forEach((k) => p.get(k) && setRadio(k, p.get(k))); | |
| if (p.get("ram") && setRadio("ram", p.get("ram"))) state.ramSource = "confirmed"; | |
| updateRamHint(); | |
| } | |
| function readQuery() { | |
| const ram = currentRam(); | |
| return { | |
| family: currentFamily() || null, | |
| size: radioValue("size") || null, | |
| quant: radioValue("quant") || null, | |
| context: parseInt(radioValue("context"), 10) || 8192, | |
| ram_gb: ram, | |
| ram_source: ram ? (state.ramSource === "confirmed" ? "confirmed" : "estimated") : null, | |
| priority: radioValue("priority") || "balanced", | |
| sort: state.sort, | |
| llm_only: !$("#non-llm").checked, | |
| search: $("#search").value.trim() || null, | |
| }; | |
| } | |
| function syncUrl(q) { | |
| const p = new URLSearchParams(); | |
| if (q.family) p.set("family", q.family); | |
| if (q.size) p.set("size", q.size); | |
| if (q.quant) p.set("quant", q.quant); | |
| if (q.context !== 8192) p.set("context", q.context); | |
| if (q.ram_gb && state.ramSource === "confirmed") p.set("ram", q.ram_gb); | |
| if (q.priority !== "balanced") p.set("priority", q.priority); | |
| history.replaceState(null, "", p.toString() ? `?${p}` : location.pathname); | |
| } | |
| function updateRamHint() { | |
| if (!currentRam()) $("#ram-hint").textContent = "Or pick it above."; | |
| else if (state.ramSource === "confirmed") $("#ram-hint").textContent = "Set by you."; | |
| } | |
| // ------------------------------------------------------------------ memory budget | |
| function usableGB(ram) { | |
| return ram ? ram * (ram <= 36 ? 0.67 : 0.75) : null; | |
| } | |
| // One scale per set of bars so they compare honestly. Very large models clip at | |
| // twice the usable limit instead of squashing everything else. | |
| function scaleFor(ram, totals) { | |
| const usable = usableGB(ram); | |
| const biggest = Math.max(0, ...totals.filter((t) => t != null)); | |
| if (usable) return Math.max(usable, Math.min(biggest, usable * 2)) * 1.06; | |
| return Math.max(biggest, 4) * 1.1; | |
| } | |
| function paintBar(track, mem, scale, ram) { | |
| const segs = track.querySelectorAll(".seg"); | |
| const limit = track.querySelector(".limit"); | |
| const w = mem && mem.weights_gb != null ? mem.weights_gb : 0; | |
| const kv = mem && mem.kv_gb != null ? mem.kv_gb : 0; | |
| const oh = mem && mem.overhead_gb != null ? mem.overhead_gb : 0; | |
| const pct = (x) => `${Math.max(0, Math.min(100, (x / scale) * 100))}%`; | |
| segs[0].style.width = pct(w); | |
| segs[1].style.width = pct(Math.min(kv, Math.max(0, scale - w))); | |
| segs[2].style.width = pct(Math.min(oh, Math.max(0, scale - w - kv))); | |
| const usable = usableGB(ram); | |
| limit.hidden = !usable; | |
| if (usable) limit.style.left = pct(usable); | |
| track.classList.toggle("over", !!(mem && usable && mem.total_gb > usable)); | |
| } | |
| function miniBar(mem, scale, ram) { | |
| const track = el("div", { class: "mini", "aria-hidden": "true" }, | |
| el("span", { class: "seg seg-w" }), el("span", { class: "seg seg-kv" }), el("span", { class: "seg seg-oh" }), el("span", { class: "limit" })); | |
| paintBar(track, mem, scale, ram); | |
| return track; | |
| } | |
| function renderBudget(r) { | |
| const q = state.lastQuery || readQuery(); | |
| const ram = q.ram_gb; | |
| const usable = usableGB(ram); | |
| $("#budget-mac").textContent = ram ? `${ram} GB Mac${state.ramSource === "confirmed" ? "" : " (estimated)"}` : "Pick your Mac's memory to see what fits"; | |
| $("#budget-usable").textContent = usable ? `About ${Math.round(usable)} GB usable by the GPU` : ""; | |
| const mem = r && r.memory; | |
| paintBar($("#budget-track"), mem, scaleFor(ram, [mem && mem.total_gb]), ram); | |
| const line = $("#budget-model"); | |
| if (!r) { | |
| line.textContent = state.lastQuery && state.total === 0 ? "No models match these filters." : "Loading models"; | |
| return; | |
| } | |
| const name = el("strong", {}, r.model.name); | |
| if (!mem || mem.total_gb == null) { | |
| line.replaceChildren(name, " has no size information, so its memory can't be estimated."); | |
| return; | |
| } | |
| const parts = [name, ` at ${fmtCtx(q.context)} context needs about ${fmtGB(mem.total_gb)}`]; | |
| if (mem.fit) parts.push(": ", el("span", { class: `fit-${mem.fit}` }, fitText(mem.fit)), "."); | |
| else parts.push(". Pick your Mac's memory to check the fit."); | |
| parts.push(` ${fmtGB(mem.weights_gb)} of weights and ${fmtGB(mem.kv_gb)} of KV cache${mem.kv_rough ? " (rough)" : ""}.`); | |
| line.replaceChildren(...parts); | |
| $("#budget-track").setAttribute("aria-label", | |
| `${r.model.name}: weights ${fmtGB(mem.weights_gb)}, KV cache ${fmtGB(mem.kv_gb)}, overhead ${fmtGB(mem.overhead_gb)}${usable ? `, GPU limit about ${Math.round(usable)} GB` : ""}`); | |
| } | |
| // ------------------------------------------------------------------ results | |
| let debounce = null; | |
| function scheduleExplore(reason) { | |
| clearTimeout(debounce); | |
| debounce = setTimeout(() => explore({ reason }), 220); | |
| } | |
| async function explore({ append = false, reason = "search" } = {}) { | |
| const q = readQuery(); | |
| if (!append) state.offset = 0; | |
| state.lastQuery = q; | |
| const id = ++state.requestId; | |
| $("#results").setAttribute("aria-busy", "true"); | |
| if (!append) $("#summary").textContent = "Finding models"; | |
| try { | |
| const r = await fetch("/api/recommend", { | |
| method: "POST", | |
| headers: { "content-type": "application/json" }, | |
| body: JSON.stringify({ ...q, offset: state.offset, limit: 24 }), | |
| }); | |
| if (!r.ok) throw new Error(`HTTP ${r.status}`); | |
| const data = await r.json(); | |
| if (id !== state.requestId) return; // a newer query superseded this one | |
| state.total = data.total; | |
| state.results = append ? state.results.concat(data.results) : data.results; | |
| renderResults(data, append); | |
| syncUrl(q); | |
| if (!append) noteQuery(q, data); | |
| } catch (e) { | |
| if (id !== state.requestId) return; | |
| $("#summary").textContent = ""; | |
| $("#notices").replaceChildren(el("p", { class: "note" }, "Models couldn't be loaded. Check your connection, then change a filter to retry.")); | |
| } finally { | |
| if (id === state.requestId) $("#results").removeAttribute("aria-busy"); | |
| } | |
| } | |
| function describeQuery(q) { | |
| return [q.family || "all families", q.size ? SIZE_LABEL[q.size] || q.size : null, q.quant, `${fmtCtx(q.context)} context`] | |
| .filter(Boolean).join(", "); | |
| } | |
| function renderResults(data, append) { | |
| const q = state.lastQuery; | |
| $("#summary").textContent = `${data.total.toLocaleString()} ${data.total === 1 ? "model" : "models"} for ${describeQuery(q)}`; | |
| const notices = data.notices.map((n) => el("p", { class: "note" }, n)); | |
| if (!data.total) notices.push(el("p", { class: "note" }, "Nothing matches. Set Size or Quantization to Any, or clear the name filter.")); | |
| $("#notices").replaceChildren(...notices); | |
| const list = $("#list"); | |
| if (!append) list.replaceChildren(); | |
| const scale = scaleFor(q.ram_gb, state.results.map((r) => r.memory.total_gb)); | |
| if (append) $$(".row .mini", list).forEach((bar, i) => state.results[i] && paintBar(bar, state.results[i].memory, scale, q.ram_gb)); | |
| data.results.forEach((r, i) => list.append(resultRow(r, data.offset + i, scale))); | |
| state.offset = data.offset + data.results.length; | |
| $("#more").hidden = state.offset >= data.total; | |
| if (!append) { | |
| state.pinned = state.results[0] || null; | |
| renderBudget(state.pinned); | |
| } | |
| } | |
| function resultRow(r, rank, scale) { | |
| const m = r.model; | |
| const mem = r.memory; | |
| const q = state.lastQuery; | |
| const quant = r.quant || m.quant; | |
| const cmp = el("input", { type: "checkbox" }); | |
| cmp.checked = state.compare.has(m.id); | |
| cmp.addEventListener("change", () => toggleCompare(r, cmp, rank)); | |
| const fitLabel = mem.fit | |
| ? el("span", { class: `fit-label fit-${mem.fit}` }, mem.fit) | |
| : el("span", { class: "fit-label fit-none" }, mem.total_gb == null ? "size unknown" : q.ram_gb ? "unknown" : "set memory"); | |
| const moe = m.moe ? (m.active_params ? `, MoE with ${fmtParams(m.active_params)} active` : ", MoE") : ""; | |
| const row = el("li", { class: "row" }, | |
| el("div", { class: "row-main" }, | |
| el("div", { class: "row-title" }, | |
| el("button", { class: "row-name", type: "button", onclick: () => openDetail(m.id, rank, r) }, m.name)), | |
| el("div", { class: "facts" }, | |
| el("span", { class: "tag" }, quant.label === "unknown" ? "quantization unknown" : quant.label), | |
| el("span", {}, fmtParams(m.params) + moe), | |
| m.pipeline === "image-text-to-text" ? el("span", {}, "vision") : null, | |
| m.partial ? el("span", { class: "warn", title: "The repo holds far fewer weights than its name suggests" }, "partial or add-on") : null, | |
| el("span", {}, `${fmtNum(m.downloads)} downloads`), | |
| m.likes ? el("span", {}, `${fmtNum(m.likes)} likes`) : null, | |
| el("span", {}, `updated ${fmtAgo(m.modified)}`), | |
| r.community.benchmark_count ? el("span", {}, `${r.community.benchmark_count} MLX benchmark${r.community.benchmark_count > 1 ? "s" : ""}`) : null), | |
| el("p", { class: "why" }, rowReason(r)), | |
| el("div", { class: "row-actions" }, | |
| el("button", { type: "button", class: "linkish", onclick: () => openDetail(m.id, rank, r) }, "Details"), | |
| el("label", { class: "check" }, cmp, "Compare"), | |
| el("a", { href: m.url, target: "_blank", rel: "noopener", onclick: () => trackClick(m.id, rank, r) }, "View on Hugging Face"))), | |
| el("div", { class: "row-fit" }, | |
| el("div", { class: "fit-line" }, el("span", { class: "fit-gb" }, mem.total_gb == null ? "?" : fmtGB(mem.total_gb)), fitLabel), | |
| miniBar(mem, scale, q.ram_gb), | |
| state.sort === "recommended" ? el("span", { class: "score", title: "Best-fit score from 0 to 100, explained under How it works" }, `Score ${Math.round(r.score)}`) : null)); | |
| const show = () => { | |
| $$(".row.active").forEach((x) => x.classList.remove("active")); | |
| row.classList.add("active"); | |
| renderBudget(r); | |
| }; | |
| row.addEventListener("mouseenter", show); | |
| row.addEventListener("focusin", show); | |
| return row; | |
| } | |
| // The bar already shows fit and size, so lead with the reason that adds something. | |
| function rowReason(r) { | |
| const skip = /fit:|^Needs roughly|downloads\.$|^KV cache at/; | |
| return r.reasons.find((t) => !skip.test(t)) || r.reasons[0] || ""; | |
| } | |
| function trackClick(id) { | |
| noteModel("clicked", id); | |
| } | |
| // ------------------------------------------------------------------ compare | |
| function toggleCompare(r, box, rank) { | |
| if (box.checked) { | |
| if (state.compare.size >= 4) { | |
| box.checked = false; | |
| flashTray("Compare up to 4 models at a time."); | |
| return; | |
| } | |
| state.compare.set(r.model.id, r); | |
| noteModel("compared", r.model.id); | |
| } else { | |
| state.compare.delete(r.model.id); | |
| } | |
| renderTray(); | |
| } | |
| function flashTray(msg) { | |
| renderTray(); | |
| $("#compare-tray").hidden = false; | |
| $("#compare-count").textContent = msg; | |
| setTimeout(renderTray, 2200); | |
| } | |
| function renderTray() { | |
| const n = state.compare.size; | |
| $("#compare-tray").hidden = n === 0; | |
| $("#compare-count").textContent = n === 1 ? "1 selected, pick another to compare" : `${n} models selected`; | |
| $("#compare-open").disabled = n < 2; | |
| } | |
| function relQuality(bits) { | |
| if (bits == null) return "unknown"; | |
| if (bits >= 16) return "reference (unquantized)"; | |
| if (bits >= 8) return "near reference"; | |
| if (bits >= 5) return "high"; | |
| if (bits >= 4) return "good"; | |
| if (bits >= 3) return "lower"; | |
| return "lowest"; | |
| } | |
| function openCompare() { | |
| const items = [...state.compare.values()]; | |
| const q = state.lastQuery || readQuery(); | |
| const scale = scaleFor(q.ram_gb, items.map((r) => r.memory.total_gb)); | |
| const qt = (r) => r.quant || r.model.quant; | |
| const rows = [ | |
| ["Quantization", (r) => qt(r).label], | |
| ["Parameters", (r) => fmtParams(r.model.params) + (r.model.moe ? ", MoE" : "")], | |
| [`Memory at ${fmtCtx(q.context)}`, (r) => fmtGB(r.memory.total_gb)], | |
| ["Memory budget", (r) => miniBar(r.memory, scale, q.ram_gb), "compare-bar"], | |
| ["Weights", (r) => fmtGB(r.memory.weights_gb)], | |
| ["KV cache", (r) => (r.memory.kv_gb == null ? "unknown" : fmtGB(r.memory.kv_gb) + (r.memory.kv_rough ? " (rough)" : ""))], | |
| ["Fit", (r) => r.memory.fit || (q.ram_gb ? "unknown" : "set your memory")], | |
| ["Relative quality", (r) => relQuality(qt(r).bits)], | |
| ["Measured perplexity", (r) => (r.community.median_perplexity ? `${r.community.median_perplexity.toFixed(2)} on wikitext-2 (lower is better)` : "not measured yet")], | |
| ["Speed", (r) => (r.community.median_generation_tps ? `${r.community.median_generation_tps} tok/s median from community benchmarks` : "unknown, no community benchmarks yet")], | |
| ["Downloads", (r) => fmtNum(r.model.downloads)], | |
| ["Likes", (r) => fmtNum(r.model.likes)], | |
| ["Updated", (r) => fmtAgo(r.model.modified)], | |
| ["Community reports", (r) => (r.community.feedback_count ? `${r.community.feedback_count}${r.community.positive_share != null ? `, ${Math.round(r.community.positive_share * 100)}% positive` : ""}` : "none yet")], | |
| ]; | |
| const table = el("table", {}, | |
| el("thead", {}, el("tr", {}, el("th", {}, ""), items.map((r) => el("th", {}, el("a", { href: r.model.url, target: "_blank", rel: "noopener" }, r.model.name))))), | |
| el("tbody", {}, rows.map(([label, f, cls]) => el("tr", { class: cls || null }, el("th", {}, label), items.map((r) => el("td", {}, f(r))))))); | |
| $("#c-body").replaceChildren( | |
| el("div", { class: "table-scroll" }, table), | |
| el("p", { class: "scale-note" }, "Relative quality is a guess from bit width. Measured perplexity is comparable only between quantizations of the same base model (same tokenizer). Speed appears once people have benchmarked a model.")); | |
| $("#compare").showModal(); | |
| } | |
| // ------------------------------------------------------------------ detail sheet | |
| async function openDetail(id, rank, r) { | |
| const q = state.lastQuery || readQuery(); | |
| const dlg = $("#detail"); | |
| $("#d-title").textContent = id.split("/")[1]; | |
| $("#d-body").replaceChildren(el("p", { class: "fine" }, "Loading model details")); | |
| if (!dlg.open) dlg.showModal(); | |
| noteModel("viewed", id); | |
| const params = new URLSearchParams({ context: q.context, priority: q.priority }); | |
| if (q.ram_gb) { params.set("ram_gb", q.ram_gb); if (q.ram_source) params.set("ram_source", q.ram_source); } | |
| let d; | |
| try { | |
| const resp = await fetch(`/api/model/${id.split("/").map(encodeURIComponent).join("/")}?${params}`); | |
| if (!resp.ok) throw new Error(String(resp.status)); | |
| d = await resp.json(); | |
| } catch (e) { | |
| $("#d-body").replaceChildren(el("p", { class: "note" }, "Some model information isn't available right now. The Hugging Face page has the full details."), | |
| el("p", {}, el("a", { href: `https://huggingface.co/${id}`, target: "_blank", rel: "noopener" }, "View on Hugging Face"))); | |
| return; | |
| } | |
| renderDetail(d, q, rank); | |
| } | |
| function renderDetail(d, q, rank) { | |
| const m = d.model; | |
| const quant = d.quant || m.quant; | |
| const weights = d.files.filter((f) => /\.(safetensors|npz|gguf)$/.test(f.path)); | |
| const ctxRows = d.memory_by_context; | |
| const scale = scaleFor(q.ram_gb, ctxRows.map((x) => x.total_gb)); | |
| const ctxBars = el("div", { class: "ctx-bars" }, ctxRows.flatMap((x) => [ | |
| el("span", { class: x.context === q.context ? "ctx cur" : "ctx" }, fmtCtx(x.context)), | |
| miniBar(x, scale, q.ram_gb), | |
| el("span", { class: "gb" }, fmtGB(x.total_gb)), | |
| el("span", { class: `fitc fit-label ${x.exceeds_model_context || !x.fit ? "fit-none" : `fit-${x.fit}`}` }, | |
| x.exceeds_model_context ? "beyond max context" : x.fit || (q.ram_gb ? "unknown" : "set memory")), | |
| ])); | |
| const sibs = d.siblings.length ? [ | |
| el("h3", {}, "Other quantizations of this base model"), | |
| el("div", { class: "table-scroll" }, el("table", {}, | |
| el("thead", {}, el("tr", {}, ["Model", "Quantization", `Memory at ${fmtCtx(q.context)}`, "Fit", "Perplexity", "Downloads"].map((h, i) => el("th", { class: i >= 4 ? "num" : null }, h)))), | |
| el("tbody", {}, [{ model: m, quant, memory: d.memory, community: d.community, self: true }, ...d.siblings].map((s) => el("tr", { class: s.self ? "current" : null }, | |
| el("td", {}, s.self ? el("span", {}, s.model.name) : el("button", { type: "button", class: "linkish", onclick: () => openDetail(s.model.id, null, s) }, s.model.name)), | |
| el("td", {}, (s.quant || s.model.quant).label), | |
| el("td", {}, fmtGB(s.memory.total_gb)), | |
| el("td", { class: s.memory.fit ? `fit-${s.memory.fit}` : null }, s.memory.fit || "unknown"), | |
| el("td", { class: "num" }, s.community && s.community.median_perplexity ? s.community.median_perplexity.toFixed(2) : "–"), | |
| el("td", { class: "num" }, fmtNum(s.model.downloads))))))), | |
| el("p", { class: "scale-note" }, "Perplexity on a fixed wikitext-2 slice, measured on Macs with the benchmark script. Lower is better, and it is only comparable within this table.")] : []; | |
| const comm = d.community; | |
| const moe = m.moe ? (m.active_params ? `, MoE with ${fmtParams(m.active_params)} active` : ", MoE") : ""; | |
| const body = [ | |
| el("p", { class: "sheet-lede" }, | |
| el("span", {}, quant.label + (quant.mode && !["affine", "float"].includes(quant.mode) ? ` (${quant.mode})` : "")), | |
| el("span", {}, fmtParams(m.params) + moe), | |
| d.memory.fit ? el("span", { class: `fit-label fit-${d.memory.fit}` }, `${d.memory.fit} at ${fmtCtx(q.context)}`) : null), | |
| el("p", {}, el("a", { href: m.url, target: "_blank", rel: "noopener", onclick: () => trackClick(m.id, rank, null) }, `View ${m.id} on Hugging Face`)), | |
| d.detail_error ? el("p", { class: "note" }, "Some model information isn't available: the config or file list couldn't be read, so estimates use parameter counts.") : null, | |
| el("h3", {}, "Memory by context length"), | |
| ctxBars, | |
| el("p", { class: "scale-note" }, `Estimates, not measurements: weights + fp16 KV cache + about 1 GB overhead.${q.ram_gb ? ` The line marks about ${Math.round(usableGB(q.ram_gb))} GB usable by the GPU on a ${q.ram_gb} GB Mac.` : ""}`), | |
| el("h3", {}, "Why it scores this way"), | |
| el("ul", { class: "why-list" }, d.reasons.map((t) => el("li", {}, t))), | |
| el("h3", {}, "About this repo"), | |
| el("dl", { class: "kv" }, | |
| el("dt", {}, "Family"), el("dd", {}, m.family), | |
| el("dt", {}, "Base model"), el("dd", {}, m.base_model || "unknown"), | |
| el("dt", {}, "Pipeline"), el("dd", {}, m.pipeline || "unknown"), | |
| el("dt", {}, "Downloads"), el("dd", {}, `${fmtNum(m.downloads)} in the last 30 days`), | |
| el("dt", {}, "Likes"), el("dd", {}, fmtNum(m.likes)), | |
| el("dt", {}, "Updated"), el("dd", {}, m.modified ? `${m.modified.slice(0, 10)}, ${fmtAgo(m.modified)}` : "unknown"), | |
| el("dt", {}, "Files"), el("dd", {}, `${d.file_count} files, ${weights.length} weight ${weights.length === 1 ? "shard" : "shards"}`), | |
| el("dt", {}, "Sources"), el("dd", {}, `quantization from ${quant.source}, size from ${m.params_source}, weights from ${d.memory.weights_source === "files" ? "exact file sizes" : "parameter count"}`)), | |
| ...sibs, | |
| el("h3", {}, "Community data"), | |
| el("p", {}, comm.benchmark_count || comm.feedback_count | |
| ? `${comm.benchmark_count} MLX benchmark${comm.benchmark_count === 1 ? "" : "s"}${comm.median_generation_tps ? `, median ${comm.median_generation_tps} tok/s generation` : ""}${comm.median_perplexity ? `, perplexity ${comm.median_perplexity.toFixed(2)} on wikitext-2` : ""}. ${comm.feedback_count} report${comm.feedback_count === 1 ? "" : "s"}${comm.positive_share != null ? `, ${Math.round(comm.positive_share * 100)}% positive` : ""}.` | |
| : "No benchmarks or reports yet. Run this on your Mac to add the first:"), | |
| el("pre", { class: "cmd" }, el("code", {}, benchCommand(m.id))), | |
| feedbackForm(m, q), | |
| ]; | |
| $("#d-body").replaceChildren(...body.filter(Boolean)); | |
| } | |
| function feedbackForm(m, q) { | |
| const f = $("#feedback-tpl").content.firstElementChild.cloneNode(true); | |
| const ram = $("select[name=ram]", f); | |
| (state.meta ? state.meta.ram_classes : []).forEach((g) => ram.append(el("option", { value: g }, `${g} GB`))); | |
| if (currentRam() && state.ramSource === "confirmed") ram.value = String(currentRam()); | |
| const chipSel = $("select[name=chip]", f); | |
| CHIPS.forEach((c) => chipSel.append(el("option", { value: c }, c.replace("Apple ", "")))); | |
| chipSel.append(el("option", { value: "other" }, "Other")); | |
| const ctx = $("select[name=ctx]", f); | |
| (state.meta ? state.meta.contexts : []).forEach((c) => ctx.append(el("option", { value: c }, fmtCtx(c)))); | |
| $$("input[name=tried]", f).forEach((r) => r.addEventListener("change", () => { $(".fb-yes", f).hidden = !(r.checked && r.value === "yes"); })); | |
| f.addEventListener("submit", async (e) => { | |
| e.preventDefault(); | |
| const out = $(".fb-out", f); | |
| const tried = ($("input[name=tried]:checked", f) || {}).value; | |
| if (!tried) { out.textContent = "Choose Yes, No or Planning to."; return; } | |
| const fields = { selected_model: m.id, tried, target_context: q.context, priority: q.priority, ...hwFields() }; | |
| delete fields.cpu_cores; | |
| if (tried === "yes") { | |
| const oc = ($("input[name=outcome]:checked", f) || {}).value; | |
| if (oc && oc.startsWith("q:")) fields.quality_rating = oc.slice(2); | |
| if (oc && oc.startsWith("f:")) fields.failure_reason = oc.slice(2); | |
| fields.reported_ram_gb = parseInt(ram.value, 10) || null; | |
| fields.reported_mac_model = chipSel.value || null; | |
| const tps = parseFloat($("input[name=tps]", f).value); | |
| fields.reported_tokens_per_second = Number.isFinite(tps) && tps > 0 ? tps : null; | |
| fields.reported_context = parseInt(ctx.value, 10) || null; | |
| fields.notes = $("textarea[name=notes]", f).value.trim() || null; | |
| } | |
| const btn = $("button[type=submit]", f); | |
| btn.disabled = true; | |
| const res = await Telemetry.submit("feedback", fields); | |
| btn.disabled = false; | |
| out.textContent = res.ok ? "Report sent anonymously. Thank you." : res.message; | |
| if (res.ok) $$("input, select, textarea, button", f).forEach((x) => (x.disabled = true)); | |
| }); | |
| return f; | |
| } | |
| // ------------------------------------------------------------------ hardware | |
| async function detectHardware() { | |
| const btn = $("#detect"); | |
| btn.disabled = true; | |
| btn.textContent = "Detecting"; | |
| $("#hw-error").hidden = true; | |
| let hw; | |
| try { | |
| hw = await window.HW.detect(); | |
| } catch (e) { | |
| hw = { webgpu_available: false, error: "failed", capability: "unknown", memory_prior: null, browser_family: "other", os_family: "other" }; | |
| } | |
| state.hw = hw; | |
| btn.disabled = false; | |
| btn.textContent = "Detect again"; | |
| const row = (k, v) => [el("dt", {}, k), el("dd", {}, v)]; | |
| $("#hw-facts").replaceChildren( | |
| ...row("GPU", hw.gpu_vendor ? `${hw.gpu_vendor}${hw.gpu_arch ? `, ${hw.gpu_arch}` : ""}` : hw.webgpu_available ? "not exposed" : "WebGPU unavailable"), | |
| ...row("Compute", hw.quick_score != null ? `${hw.quick_score.toLocaleString()}, ${hw.capability_label.toLowerCase()}` : "not measured"), | |
| ...row("CPU threads", hw.cpu_cores || "unknown")); | |
| $("#hw-out").hidden = false; | |
| const msgs = []; | |
| if (!hw.webgpu_available) msgs.push("WebGPU isn't available in this browser. You can enter your memory manually."); | |
| if (hw.os_family && !["macos", "other"].includes(hw.os_family)) msgs.push("This doesn't look like a Mac. MLX runs on Apple Silicon, so pick the memory of the Mac you'll use."); | |
| const prior = hw.memory_prior; | |
| if (prior) { | |
| $("#hw-estimate").textContent = `Estimated capability: ${prior.label}. Is that right? Pick your exact memory above.`; | |
| if (!currentRam() || state.ramSource !== "confirmed") { | |
| setRadio("ram", prior.ram); | |
| state.ramSource = "estimated"; | |
| $("#ram-hint").textContent = `Estimated: ${prior.note}.`; | |
| } | |
| } else { | |
| $("#hw-estimate").textContent = "We couldn't reliably detect your memory configuration. Pick it above."; | |
| } | |
| if (msgs.length) { $("#hw-error").textContent = msgs.join(" "); $("#hw-error").hidden = false; } | |
| $("#full-bench").disabled = !hw.webgpu_available; | |
| explore({ reason: "filter" }); | |
| } | |
| async function runFullBench() { | |
| const btn = $("#full-bench"); | |
| const prog = $("#bench-progress"); | |
| btn.disabled = true; | |
| prog.hidden = false; | |
| prog.value = 0; | |
| $("#bench-out").textContent = "Running. Keep this tab in front for 20 seconds."; | |
| try { | |
| const res = await window.HW.fullBenchmark((p) => { prog.value = p; }); | |
| $("#bench-out").textContent = `WebGPU compute score: ${res.score.toLocaleString()}. A relative browser GPU score for grouping hardware, not MLX speed or tokens per second.`; | |
| Telemetry.contribute("browser_benchmark", { | |
| ...hwFields(), webgpu_score: res.score, benchmark_type: "webgpu_full", benchmark_version: res.version, | |
| benchmark_duration_ms: res.duration_ms, gpu_capability_class: window.HW.capabilityClass(state.hw && state.hw.quick_score), | |
| }); | |
| } catch (e) { | |
| $("#bench-out").textContent = "The GPU test couldn't run in this browser."; | |
| } finally { | |
| btn.disabled = false; | |
| prog.hidden = true; | |
| } | |
| } | |
| // ------------------------------------------------------------------ MLX benchmark section | |
| function benchCommand(model) { | |
| const origin = location.origin; | |
| return [ | |
| "pip install -U mlx-lm", | |
| `curl -fsSLO ${origin}/bench/mlx_explorer_bench.py`, | |
| `python mlx_explorer_bench.py --model ${model} --quality`, | |
| `# to contribute the result, add: --submit ${origin}`, | |
| ].join("\n"); | |
| } | |
| function updateBenchCmd() { | |
| const v = $("#bench-model").value.trim(); | |
| const model = /^[A-Za-z0-9][A-Za-z0-9._-]*\/[A-Za-z0-9][A-Za-z0-9._-]*$/.test(v) ? v : "mlx-community/MODEL"; | |
| $("#bench-cmd").textContent = benchCommand(model); | |
| } | |
| const BENCH_KEYS = ["selected_model", "benchmark_type", "benchmark_version", "prompt_tokens", "generation_tokens", "prompt_tps", | |
| "generation_tps", "ttft_ms", "peak_memory_gb", "chip", "reported_ram_gb", "mlx_version", "mlx_lm_version", "macos_major", "target_context", | |
| "perplexity", "perplexity_stderr", "eval_dataset", "eval_tokens"]; | |
| async function submitBenchJson() { | |
| const out = $("#bench-submit-out"); | |
| let obj; | |
| try { | |
| obj = JSON.parse($("#bench-json").value); | |
| } catch (e) { | |
| out.textContent = "That isn't valid JSON. Paste the block the script printed."; | |
| return; | |
| } | |
| if (!obj || typeof obj !== "object" || Array.isArray(obj)) { out.textContent = "Paste a single JSON object, the block the script printed."; return; } | |
| const fields = {}; | |
| for (const k of BENCH_KEYS) if (k in obj) fields[k] = obj[k]; | |
| const res = await Telemetry.submit("mlx_benchmark", fields); | |
| out.textContent = res.ok | |
| ? res.flags.length ? `Result recorded and flagged for review: ${res.flags.join(", ")}.` : "Result recorded anonymously. Thank you." | |
| : res.message; | |
| } | |
| // ------------------------------------------------------------------ wiring | |
| function init() { | |
| const form = $("#query"); | |
| form.addEventListener("submit", (e) => e.preventDefault()); | |
| form.addEventListener("change", (e) => { | |
| const t = e.target; | |
| if (t.id === "search") return; | |
| if (t.name === "ram") { | |
| state.ramSource = t.value ? "confirmed" : null; | |
| updateRamHint(); | |
| scheduleExplore("filter"); | |
| return; | |
| } | |
| if (t.name === "family") $("#family-more").value = ""; | |
| if (t.id === "family-more") { if (t.value) uncheck("family"); else setRadio("family", ""); } | |
| scheduleExplore("search"); | |
| }); | |
| let typing = null; | |
| $("#search").addEventListener("input", () => { clearTimeout(typing); typing = setTimeout(() => explore({ reason: "search" }), 350); }); | |
| $$("#sort button").forEach((b) => b.addEventListener("click", () => { | |
| state.sort = b.dataset.sort; | |
| $$("#sort button").forEach((x) => x.setAttribute("aria-selected", String(x === b))); | |
| explore({ reason: "filter" }); | |
| })); | |
| $("#list").addEventListener("mouseleave", () => { | |
| $$(".row.active").forEach((x) => x.classList.remove("active")); | |
| renderBudget(state.pinned); | |
| }); | |
| $("#more").addEventListener("click", () => explore({ append: true })); | |
| $("#detect").addEventListener("click", detectHardware); | |
| $("#full-bench").addEventListener("click", runFullBench); | |
| $("#compare-open").addEventListener("click", openCompare); | |
| $("#compare-clear").addEventListener("click", () => { | |
| state.compare.clear(); | |
| $$("#list .row-actions input[type=checkbox]").forEach((x) => (x.checked = false)); | |
| renderTray(); | |
| }); | |
| $$("dialog [data-close]").forEach((b) => b.addEventListener("click", () => b.closest("dialog").close())); | |
| $$("dialog").forEach((d) => d.addEventListener("click", (e) => { if (e.target === d) d.close(); })); | |
| $("#bench-model").addEventListener("input", updateBenchCmd); | |
| $("#copy-cmd").addEventListener("click", async () => { | |
| const b = $("#copy-cmd"); | |
| try { await navigator.clipboard.writeText($("#bench-cmd").textContent); b.textContent = "Copied"; } | |
| catch (e) { b.textContent = "Select the text to copy"; } | |
| setTimeout(() => (b.textContent = "Copy command"), 1600); | |
| }); | |
| $("#bench-submit").addEventListener("click", submitBenchJson); | |
| const opt = $("#optout"); | |
| opt.checked = Telemetry.optedOut; | |
| if (Telemetry.gpc) $("#gpc-note").hidden = false; | |
| opt.addEventListener("change", () => Telemetry.setOptOut(opt.checked)); | |
| // On phones the rail sits above the results: keep only memory and context open. | |
| // Wide layouts hide the toggle, so the section must always be open there | |
| // (embedded iframes can start narrow and widen a moment later). | |
| const narrow = window.matchMedia("(max-width: 900px)"); | |
| const hasFilters = /[?&](family|size|quant|priority)=/.test(location.search); | |
| const syncFilters = () => { if (!narrow.matches) $("#more-filters").open = true; }; | |
| if (narrow.matches && window.innerWidth > 0 && !hasFilters) $("#more-filters").open = false; | |
| narrow.addEventListener("change", syncFilters); | |
| window.addEventListener("resize", syncFilters); | |
| updateBenchCmd(); | |
| loadMeta() | |
| .then(() => explore()) | |
| .catch(() => { | |
| $("#notices").replaceChildren(el("p", { class: "note" }, "The model catalogue couldn't be loaded. Refresh the page in a moment.")); | |
| $("#budget-model").textContent = "The model catalogue couldn't be loaded."; | |
| }); | |
| } | |
| if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", init); | |
| else init(); | |
| })(); | |