Download space-dashboard.js from PYTHAI/bankml: direct link, hf CLI and curl.
- Browser
- Download file 22.3 kB
-
https://huggingface.co/spaces/PYTHAI/bankml/resolve/main/space-dashboard.js
- Command line
-
hf download hf://spaces/PYTHAI/bankml/space-dashboard.js
-
curl -L -o space-dashboard.js https://huggingface.co/spaces/PYTHAI/bankml/resolve/main/space-dashboard.js
22.3 kB
| // SPDX-License-Identifier: MIT OR Apache-2.0 | |
| // The page behind the input field: this computer's controls and diagnostics. What the visitor's own machine gives | |
| // bankML (threads, memory for the context, the length of an answer, a lean or full prompt), what it measured | |
| // (each answer's prompt and generation speed, time to first token, the engine's memory, the browser's storage), | |
| // and what to change, worked out from those measurements. The same report is T mode's `diag` (window.bankmlDiag). | |
| // Reads window.bankmlStats and window.bankmlSettings (bankml-chat.js); everything stays in this browser. | |
| // The disk and cache controls (browseml-fetch.js): what browseML keeps here and its size, persistent storage and the | |
| // request to keep it, "remove the model after this session", and "clear browseML's cache" (confirmed in the page). | |
| import * as browseml from "./browseml.js"; | |
| import * as fetcher from "./browseml-fetch.js"; | |
| const $ = (id) => document.getElementById(id); | |
| const el = (tag, props = {}, ...kids) => { const e = Object.assign(document.createElement(tag), props); e.append(...kids.filter((k) => k !== null && k !== undefined)); return e; }; | |
| const cores = navigator.hardwareConcurrency || 1; | |
| const isolated = !!(globalThis.crossOriginIsolated && typeof SharedArrayBuffer !== "undefined"); | |
| const MB = (b) => (b ? (b < 1e6 ? `${(b / 1e3).toFixed(1)} KB` : `${(b / 1e6).toFixed(0)} MB`) : "—"); | |
| const fmt = (x, d = 1, unit = "") => (x === null || x === undefined || Number.isNaN(x) ? "—" : `${Number(x).toFixed(d)}${unit}`); | |
| // Bonsai-1.7B's KV cache in f16: 28 layers × (8 heads × 128) × K and V × 2 bytes, per token of context | |
| const KV_BYTES_PER_TOKEN = 28 * 1024 * 2 * 2; | |
| const CTX = [1024, 2048, 4096]; | |
| // the context advised where the device reports little memory: the same rule as the browseML card (caps.ctxCapFor) | |
| const lowCap = () => { const m = navigator.deviceMemory; return typeof m !== "number" ? null : m <= 2 ? 1024 : m <= 4 ? 2048 : null; }; | |
| // a token is about 3.6 characters of English for this tokenizer: an estimate, said as one | |
| const tokensOf = (chars) => Math.round(chars / 3.6); | |
| let storage = null, kept = [], persistedNow = null, confirmClear = false; | |
| async function refreshStorage() { | |
| try { storage = navigator.storage && navigator.storage.estimate ? await navigator.storage.estimate() : null; } catch { storage = null; } | |
| try { storage = { ...(storage || {}), model: await browseml.cachedBytes() }; } catch {} | |
| try { kept = await fetcher.keptFiles(); } catch { kept = []; } | |
| persistedNow = await fetcher.persisted(); | |
| } | |
| // the prompt of whoever speaks now (bankML or Savante), lean and full, as bankml-chat.js built it: an estimate | |
| const promptSize = () => { const p = window.bankmlStats && window.bankmlStats.prompt; return p ? { who: p.speaker, lean: tokensOf(p.lean), full: tokensOf(p.full) } : { who: "the persona", lean: 0, full: 0 }; }; | |
| // the threads the engine can take here: every core up to browseML's cap when isolated, else one | |
| const maxThreads = () => browseml.threads(); | |
| /** What the measurements say to change: each line is a finding and what to do about it. */ | |
| function advice(S, st) { | |
| const out = [], last = st.answers.filter((a) => a.mode === "browse").slice(-1)[0], persona = promptSize(); | |
| const mode = st.mode || "browse"; | |
| if (mode === "browse") { | |
| if (!isolated) out.push(["warn", "One thread: this page is not cross-origin isolated (or this browser has no SharedArrayBuffer). Open the Space directly — pythai-bankml.static.hf.space — in Chrome, Edge or Firefox to use every core."]); | |
| else if (st.browse && st.browse.threads < maxThreads()) out.push(["ok", `${st.browse.threads} of ${maxThreads()} threads in use${cores > maxThreads() ? ` (bankML in this browser takes at most ${browseml.MAX_THREADS} of the ${cores} cores)` : ""}. More threads answer faster; fewer leave the computer free for other work.`]); | |
| if (!st.browse) out.push(["warn", `browseML is not loaded: Load downloads ${MB(browseml.MODEL.bytes)} once, then it starts in seconds from this browser's cache.`]); | |
| if (last && last.prompt_tps) { | |
| const next = (S.lean ? persona.lean : persona.full) + 40; | |
| out.push(["ok", `Reading speed ${fmt(last.prompt_tps, 1)} tokens/s: a new conversation's first answer reads about ${next} tokens of ${persona.who}'s persona first — about ${fmt(next / last.prompt_tps, 0)} s. Later answers in the same window read only what is new (${last.cache_n ?? 0} tokens were reused last time).`]); | |
| } | |
| if (!S.lean && persona.full) out.push(["warn", `The full prompt adds ${persona.who}'s context summary: about ${persona.full - persona.lean} more tokens to read before every new conversation. “Lean” keeps only the persona and still adds the passages that match each question.`]); | |
| const more = isolated && st.browse && st.browse.threads < maxThreads(); | |
| if (last && last.gen_tps && last.gen_tps < 1) out.push(["warn", `Writing at ${fmt(last.gen_tps, 2)} tokens/s: close other busy tabs, ${more ? "raise the threads, " : ""}or shorten answers (now ${S.maxTokens} tokens at most).`]); | |
| const kv = S.ctx * KV_BYTES_PER_TOKEN, dev = navigator.deviceMemory ? navigator.deviceMemory * 1e9 : null; | |
| if (dev && browseml.MODEL.bytes + kv > dev * 0.5) out.push(["warn", `Context ${S.ctx} reserves ${MB(kv)} for its cache, beside the ${MB(browseml.MODEL.bytes)} model, on a device that reports about ${navigator.deviceMemory} GB: a smaller context leaves more for the rest.`]); | |
| if (storage && storage.quota && storage.usage / storage.quota > 0.8) out.push(["warn", `This site's storage is ${fmt(100 * storage.usage / storage.quota, 0)} % full: the browser may evict the cached model.`]); | |
| } | |
| if (mode === "local" && !(st.local && st.local.verified && st.local.verified.guard === "play")) out.push(["warn", "Your own bankML is not connected: start it with ./install.sh start --space, then connect (who answers: your own bankML)."]); | |
| if (mode === "hf") out.push(["warn", "A provider answers: not bankML, no receipt, and free Hugging Face accounts have no included inference credit since 2026-10-07. browseML is free and verified."]); | |
| if (mode === "space") out.push(["warn", "The free CPU answers: Bonsai on Hugging Face's free CPU, llama.cpp, not bankML's engine, no receipt; what you type goes to a public Space. browseML (bankML in this browser) gives verified answers with a receipt."]); | |
| if (!out.length) out.push(["ok", "Nothing to change: the measurements look healthy."]); | |
| return out; | |
| } | |
| function sparkline(values) { | |
| const w = 220, h = 40, max = Math.max(1e-9, ...values); | |
| const pts = values.map((v, i) => `${values.length === 1 ? w / 2 : (i * w) / (values.length - 1)},${h - 2 - (v / max) * (h - 6)}`).join(" "); | |
| const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); | |
| svg.setAttribute("viewBox", `0 0 ${w} ${h}`); svg.setAttribute("class", "spark"); svg.setAttribute("role", "img"); | |
| svg.setAttribute("aria-label", `generation speed of the last ${values.length} answers, at most ${max.toFixed(2)} tokens per second`); | |
| if (values.length) { const pl = document.createElementNS(svg.namespaceURI, "polyline"); pl.setAttribute("points", pts); svg.append(pl); } | |
| return svg; | |
| } | |
| function card(title, ...body) { return el("div", { className: "dcard" }, el("h3", { textContent: title }), ...body); } | |
| function kv(rows) { | |
| const dl = el("dl", { className: "dkv" }); | |
| for (const [k, v, cls] of rows) dl.append(el("dt", { textContent: k }), el("dd", { textContent: v, className: cls || "" })); | |
| return dl; | |
| } | |
| function render() { | |
| const root = $("dash"); if (!root || !window.bankmlSettings) return; | |
| // never under the visitor's hand: a slider being dragged or a menu open keeps its element | |
| if (root.contains(document.activeElement) && /^(INPUT|SELECT)$/.test(document.activeElement.tagName)) return; | |
| const S = window.bankmlSettings.get(), st = window.bankmlStats, set = window.bankmlSettings.set; | |
| const b = st.browse, mode = st.mode || "browse"; | |
| const answers = st.answers, last = answers[answers.length - 1]; | |
| // the threads a reload would give: capped as browseml.js caps them, so above 8 cores "reload to apply" does not stay | |
| const loaded = !!b, pendingThreads = Math.max(1, Math.min(maxThreads(), S.threads || cores)), stale = loaded && (b.threads !== pendingThreads || b.ctx !== S.ctx); | |
| // who answers, and the engine's state | |
| const engineRows = mode === "browse" | |
| ? (b ? [["engine", `bankML ${b.version} in this browser (WebAssembly)`], ["model", `${b.model} · verified`, "ok"], ["sha256", `${b.sha256.slice(0, 16)}…`], | |
| ["threads", `${b.threads} of ${cores} cores${isolated ? (cores > maxThreads() ? ` (at most ${browseml.MAX_THREADS} here)` : "") : " (not isolated: one)"}`], | |
| ["build", b.build === "browseml-mt-relaxed.wasm" ? "relaxed SIMD (fused multiply-add) — the fastest" : b.build === "browseml-mt.wasm" ? "SIMD, threads (no relaxed SIMD here)" : "SIMD, one thread"], ["context", `${b.ctx} tokens`], ["engine memory", MB(browseml.memoryBytes)], ["loaded in", fmt(b.load_ms / 1000, 0, " s")]] | |
| : [["engine", "browseML not loaded"], ["model", `${browseml.MODEL.title}, ${MB(browseml.MODEL.bytes)}`], ["in this browser's cache", storage && storage.model ? "yes" : "no"]]) | |
| : mode === "local" | |
| ? (st.local ? [["engine", `your bankml serve at ${st.local.endpoint}`], ["model", st.local.verified?.name || "—", st.local.verified?.guard === "play" ? "ok" : "warn"], ["round trip", fmt(st.local.rtt_ms, 0, " ms")]] : [["engine", "your own bankML: not connected"]]) | |
| : mode === "space" | |
| ? [["engine", "Bonsai on Hugging Face's free CPU — llama.cpp, not bankML's engine"], ["where", "the public Space Gregory-L/mindXhfgradio"], ["receipt", "none", "warn"]] | |
| : [["engine", "a Hugging Face provider — not bankML"], ["receipt", "none", "warn"]]; | |
| const engineBtns = el("div", { className: "drow" }); | |
| if (mode === "browse") { | |
| engineBtns.append( | |
| // never a download from here: a model not yet kept in this browser goes through browseML's consent card | |
| el("button", { type: "button", textContent: loaded ? (stale ? "Apply: reload the engine" : "Reload the engine") : storage && storage.model ? "Start bankML in this browser" : "Open browseML's card (download and verify)", className: stale || !loaded ? "primary" : "", | |
| onclick: async (e) => { e.target.disabled = true; try { if (loaded) await window.bankmlReloadBrowse(); else if (storage && storage.model) await window.bankmlLoadBrowse(); else window.bankmlBrowseCard && window.bankmlBrowseCard.show(); } finally { render(); } } }), | |
| ...(loaded ? [el("button", { type: "button", textContent: "Unload (free its memory)", onclick: () => { try { browseml.unload(); st.browse = null; st.emit(); } catch (err) { alertLine(err.message); } } })] : [])); // append() would write "null" | |
| } | |
| // the controls | |
| const threadOut = el("output", { textContent: `${pendingThreads}` }); | |
| const threads = el("input", { type: "range", min: 1, max: maxThreads(), value: pendingThreads, disabled: !isolated, | |
| oninput: (e) => { threadOut.textContent = e.target.value; }, onchange: (e) => set({ threads: +e.target.value }) }); | |
| const ctx = el("select", { onchange: (e) => set({ ctx: +e.target.value }) }, | |
| ...CTX.map((c) => el("option", { value: c, selected: S.ctx === c, textContent: `${c} tokens · ${MB(c * KV_BYTES_PER_TOKEN)} of cache · about ${MB(browseml.MODEL.bytes + c * KV_BYTES_PER_TOKEN + 96e6)} in all (estimate)` }))); | |
| const maxOut = el("output", { textContent: `${S.maxTokens}` }); | |
| const maxT = el("input", { type: "range", min: 32, max: 512, step: 32, value: S.maxTokens, | |
| oninput: (e) => { maxOut.textContent = e.target.value; }, onchange: (e) => set({ maxTokens: +e.target.value }) }); | |
| const persona = promptSize(); | |
| const prompt = el("div", { className: "drow", role: "radiogroup" }, | |
| el("label", {}, el("input", { type: "radio", name: "dprompt", checked: S.lean, onchange: () => set({ lean: true }) }), ` lean — ${persona.who}'s persona (~${persona.lean} tokens)`), | |
| el("label", {}, el("input", { type: "radio", name: "dprompt", checked: !S.lean, onchange: () => set({ lean: false }) }), ` full — and its context summary (~${persona.full})`)); | |
| const controls = card("Controls", | |
| el("label", { className: "dctl" }, el("span", { textContent: "threads" }), threads, threadOut, | |
| el("small", { textContent: isolated ? `of ${cores} cores${cores > maxThreads() ? ` (at most ${browseml.MAX_THREADS} here)` : ""} · applies on reload` : "one: this page is not cross-origin isolated" })), | |
| el("label", { className: "dctl" }, el("span", { textContent: "context" }), ctx, el("small", { textContent: "how much of a conversation it holds · applies on reload" })), | |
| el("label", { className: "dctl" }, el("span", { textContent: "answer length" }), maxT, maxOut, el("small", { textContent: "tokens at most, from the next answer" })), | |
| el("div", { className: "dctl" }, el("span", { textContent: "prompt" }), prompt), | |
| stale ? el("p", { className: "dnote warn", textContent: `The engine runs ${b.threads} thread${b.threads > 1 ? "s" : ""} and context ${b.ctx}: reload it to apply.` }) : null); | |
| // usage, measured | |
| const gen = answers.filter((a) => a.gen_tps).map((a) => a.gen_tps).slice(-30); | |
| const totals = answers.reduce((t, a) => ({ n: t.n + 1, tok: t.tok + (a.predicted_n || 0), ms: t.ms + (a.total_ms || 0) }), { n: 0, tok: 0, ms: 0 }); | |
| const usage = card("Usage, measured", | |
| kv(last ? [["last answer", `${last.speaker} via ${{ space: "the free CPU (not bankML)", browse: "browseML", local: "your bankML", hf: "a provider" }[last.mode]} · ${last.ok ? "✓ receipt" : last.mode === "hf" || last.mode === "space" ? "no receipt" : "✗ receipt"}`, last.ok ? "ok" : "warn"], | |
| ["first token", fmt(last.ttft_ms / 1000, 1, " s")], ["reading", `${fmt(last.prompt_tps, 1, " tok/s")} · ${last.prompt_n ?? "—"} tokens (${last.cache_n ?? 0} reused)`], | |
| ["writing", `${fmt(last.gen_tps, 2, " tok/s")} · ${last.predicted_n ?? "—"} tokens`], ["whole answer", fmt(last.total_ms / 1000, 1, " s")]] | |
| : [["last answer", "none yet — ask in the field below"]]), | |
| el("div", { className: "dspark" }, sparkline(gen), el("small", { textContent: gen.length ? `writing speed, last ${gen.length} answers` : "writing speed appears after an answer" })), | |
| kv([["this visit", `${totals.n} answer${totals.n === 1 ? "" : "s"} · ${totals.tok} tokens written · ${fmt(totals.ms / 1000, 0, " s")} computing`]])); | |
| // memory and storage | |
| const heap = performance.memory ? `${MB(performance.memory.usedJSHeapSize)} of ${MB(performance.memory.jsHeapSizeLimit)}` : "not reported by this browser"; | |
| const mem = card("Memory", | |
| kv([["engine (WebAssembly)", loaded ? MB(browseml.memoryBytes) : "—"], ["page (JavaScript heap)", heap], | |
| ["this device", navigator.deviceMemory ? `about ${navigator.deviceMemory} GB (rounded by the browser)` : "not reported"], | |
| ["context cache", `${MB(S.ctx * KV_BYTES_PER_TOKEN)} at ${S.ctx} tokens (estimate)`], | |
| ["browseML needs", `about ${MB(browseml.MODEL.bytes + S.ctx * KV_BYTES_PER_TOKEN + 96e6)}: the model, the context cache and the engine's scratch (estimate)`]]), | |
| lowCap() && S.ctx > lowCap() ? el("p", { className: "dnote warn", textContent: `This device reports about ${navigator.deviceMemory} GB: keep the context at ${lowCap()} or less (now ${S.ctx}), or the browser may close the tab under memory pressure. Or use the free CPU tab.` }) : null); | |
| // disk and cache: what browseML keeps here, persistent storage, remove after this session, clear (confirmed here) | |
| const cacheBytes = kept.filter((f) => f.where === "Cache Storage").reduce((n, f) => n + f.bytes, 0); | |
| const files = kept.length | |
| ? el("ul", { className: "dadvice" }, ...kept.map((f) => el("li", { className: "ok", textContent: `${f.name} · ${MB(f.bytes)} · ${f.where}${f.where === "HTTP cache" ? " (the browser manages it)" : ""}` }))) | |
| : el("p", { className: "dnote", textContent: "browseML keeps nothing in this browser yet." }); | |
| const after = el("label", { className: "drow" }, el("input", { type: "checkbox", checked: !!S.removeAfterSession, onchange: (e) => set({ removeAfterSession: e.target.checked }) }), | |
| " remove the model after this session — it goes when this site next opens in a new tab or window (a reload or a link within this tab keeps it)"); | |
| const clearRow = el("div", { className: "drow" }); | |
| if (!confirmClear) clearRow.append(el("button", { type: "button", textContent: `Clear browseML's cache${cacheBytes ? ` (frees ${MB(cacheBytes)})` : ""}`, disabled: !cacheBytes, onclick: () => { confirmClear = true; render(); } })); | |
| else clearRow.append(el("span", { className: "dnote warn", textContent: `Remove ${MB(cacheBytes)} from this browser? The next browseML answer downloads the model again.` }), | |
| el("button", { type: "button", className: "primary", textContent: "Yes, clear it", onclick: async () => { const freed = await fetcher.clearCache(); confirmClear = false; await refreshStorage(); alertLine(`cleared: ${MB(freed)} freed`); st.emit(); window.bankmlBrowseCard && window.bankmlBrowseCard.refresh(); } }), | |
| el("button", { type: "button", textContent: "Keep it", onclick: () => { confirmClear = false; render(); } })); | |
| const disk = card("Disk and cache", | |
| kv([["this site's storage", storage && storage.quota ? `${MB(storage.usage)} used of ${(storage.quota / 1e9).toFixed(1)} GB allowed` : "not reported"], | |
| ["browseML's cache", cacheBytes ? MB(cacheBytes) : "empty"], | |
| ["kept under pressure", persistedNow === null ? "this browser does not say" : persistedNow ? "yes — persistent storage granted" : "no — the browser may evict it when space runs low", persistedNow ? "ok" : "warn"]]), | |
| files, | |
| el("div", { className: "drow" }, el("button", { type: "button", textContent: "Keep it (ask for persistent storage)", disabled: persistedNow !== false, | |
| onclick: async () => { const p = await fetcher.askPersist(); await refreshStorage(); alertLine(p ? "persistent storage granted" : "the browser did not grant persistent storage (it decides; a bookmarked or installed site is more likely to get it)"); render(); } })), | |
| after, clearRow); | |
| const adv = card("What to change", el("ul", { className: "dadvice" }, ...advice(S, st).map(([lvl, t]) => el("li", { className: lvl, textContent: t })))); | |
| const who = card("Who answers", kv(engineRows), engineBtns); | |
| root.replaceChildren(el("h2", { textContent: "This computer — controls and diagnostics" }), | |
| el("p", { className: "dlede", role: "status", textContent: notice || `Everything here is measured in this browser and stays in it. ${cores} logical cores · ${isolated ? "cross-origin isolated: threads on" : "not isolated: one thread"}.` }), | |
| el("p", { className: "dnow", id: "dash-now", textContent: st.activity ? "now: " + st.activity : "now: idle" }), | |
| el("div", { className: "dgrid" }, who, controls, usage, mem, disk, adv)); | |
| } | |
| // a notice (what a button just did, such as the bytes a clear freed) is state, not a write into the DOM: render() rebuilds | |
| // every card, so a line written straight into .dlede was replaced by the next render before anyone could read it | |
| let alertTimer = null, notice = null; | |
| function alertLine(t) { notice = t; clearTimeout(alertTimer); alertTimer = setTimeout(() => { notice = null; render(); }, 6000); render(); } | |
| /** T mode's diag: the same measurements, as an accordion. */ | |
| window.bankmlDiag = async () => { | |
| await refreshStorage(); | |
| const S = window.bankmlSettings.get(), st = window.bankmlStats, b = st.browse, last = st.answers.slice(-1)[0]; | |
| const sec = (title, level, lines) => ({ title, level, lines }); | |
| const adv = advice(S, st); | |
| return { | |
| title: `diag · this computer · ${new Date().toLocaleTimeString()} · ${adv.some(([l]) => l === "warn") ? "warn" : "ok"}`, | |
| sections: [ | |
| sec("processor", isolated ? "ok" : "warn", [`${cores} logical cores`, isolated ? "cross-origin isolated: SharedArrayBuffer, threads available" : "not cross-origin isolated: one thread", `user agent: ${navigator.userAgent}`]), | |
| sec("engine", b ? "ok" : "warn", b ? [`${b.model} verified · sha256 ${b.sha256}`, `bankML ${b.version} · ${b.threads} thread${b.threads > 1 ? "s" : ""} · context ${b.ctx}`, `engine memory ${MB(browseml.memoryBytes)}`] : ["browseML not loaded"]), | |
| sec("memory", "ok", [`device: ${navigator.deviceMemory ? `about ${navigator.deviceMemory} GB` : "not reported"}`, `page heap: ${performance.memory ? MB(performance.memory.usedJSHeapSize) : "not reported"}`, `context cache: ${MB(S.ctx * KV_BYTES_PER_TOKEN)}`]), | |
| sec("storage", storage && storage.quota && storage.usage / storage.quota > 0.8 ? "warn" : "ok", [storage && storage.quota ? `${MB(storage.usage)} of ${(storage.quota / 1e9).toFixed(1)} GB` : "not reported", `model cached: ${storage && storage.model ? MB(storage.model) : "no"}`, `persistent storage: ${persistedNow === null ? "not reported" : persistedNow ? "granted" : "not granted"}`, ...kept.map((f) => `${f.name}: ${MB(f.bytes)} (${f.where})`), `remove after this session: ${S.removeAfterSession ? "on" : "off"}`]), | |
| sec("speed", last ? "ok" : "warn", last ? [`first token ${fmt(last.ttft_ms / 1000, 1, " s")}`, `reading ${fmt(last.prompt_tps, 1, " tok/s")} (${last.prompt_n ?? "—"} tokens, ${last.cache_n ?? 0} reused)`, `writing ${fmt(last.gen_tps, 2, " tok/s")} (${last.predicted_n ?? "—"} tokens)`] : ["no answer yet"]), | |
| sec("what to change", adv.some(([l]) => l === "warn") ? "warn" : "ok", adv.map(([, t]) => t)), | |
| ], | |
| }; | |
| }; | |
| (async () => { | |
| await refreshStorage(); | |
| render(); | |
| window.addEventListener("bankml:stats", () => { refreshStorage().then(render); }); | |
| // the live line, without redrawing the cards: it changes every second while something runs | |
| window.addEventListener("bankml:activity", () => { const n = $("dash-now"); if (n) n.textContent = window.bankmlStats.activity ? "now: " + window.bankmlStats.activity : "now: idle"; }); | |
| setInterval(() => { if (!document.hidden) render(); }, 5000); // the engine's memory and the page's heap move on their own | |
| })(); | |