Spaces:
Running
Running
Download index.html from EQTYLab/lineage-explorer: direct link, hf CLI and curl.
- Browser
- Download file 25.1 kB
-
https://huggingface.co/spaces/EQTYLab/lineage-explorer/resolve/main/index.html
- Command line
-
hf download hf://spaces/EQTYLab/lineage-explorer/index.html
-
curl -L -o index.html https://huggingface.co/spaces/EQTYLab/lineage-explorer/resolve/main/index.html
25.1 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
| <title>AI Lineage Explorer</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com" /> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet" /> | |
| <script> | |
| // 2026 explorer: explorer/explorer.js, the eqtylab/explorer `preview` branch bundled into one | |
| // file (explorer/bundle.mjs). In a private Space it needs the page's `?__sign=` token, like legacy/. | |
| (function () { | |
| var sign = new URLSearchParams(location.search).get("__sign"); | |
| import("./explorer/explorer.js" + (sign ? "?__sign=" + encodeURIComponent(sign) : "")); | |
| })(); | |
| </script> | |
| <style> | |
| :root { | |
| --bg: #ffffff; --fg: #0b0b0c; --muted: #6b6f76; --line: #e3e5e8; --card: #fafbfc; | |
| --accent: #16a3c7; --accent-soft: #8fe3f5; --danger: #c2410c; | |
| --font: "Inter", system-ui, sans-serif; --mono: "IBM Plex Mono", ui-monospace, monospace; | |
| } | |
| @media (prefers-color-scheme: dark) { | |
| :root { --bg: #0b0d10; --fg: #f1f2f4; --muted: #9aa0a8; --line: #262a30; --card: #13161a; --accent: #4cc9e6; --danger: #fb923c; } | |
| } | |
| * { box-sizing: border-box; } | |
| html, body { margin: 0; height: 100%; background: var(--bg); color: var(--fg); font-family: var(--font); } | |
| a { color: var(--accent); } | |
| button, input { font: inherit; color: inherit; } | |
| /* ---------- Home ---------- */ | |
| #home { max-width: 900px; margin: 0 auto; padding: 72px 16px 64px; text-align: center; } | |
| .brand { font-weight: 700; letter-spacing: -.02em; font-size: 22px; display: inline-flex; gap: 12px; align-items: center; } | |
| .brand small { font-weight: 400; font-size: 26px; } | |
| h1 { font-size: clamp(40px, 8vw, 76px); letter-spacing: -.045em; line-height: 1; margin: 28px 0 0; display: flex; gap: 18px; align-items: center; justify-content: center; flex-wrap: wrap; } | |
| .logo { width: .8em; height: .8em; border-radius: 50%; background: var(--accent-soft); display: grid; place-items: center; flex: none; } | |
| .logo svg { width: 55%; height: 55%; } | |
| h2 { font-size: clamp(24px, 4vw, 34px); font-weight: 600; letter-spacing: -.02em; margin: 56px 0 16px; } | |
| .lede { font-size: 19px; line-height: 1.45; max-width: 560px; margin: 0 auto; } | |
| .searchrow { display: flex; gap: 12px; margin: 56px auto 0; position: relative; text-align: left; } | |
| .search { flex: 1; position: relative; } | |
| .search input { width: 100%; height: 44px; border: 1px solid var(--line); background: var(--card); border-radius: 6px; padding: 0 12px 0 40px; font-size: 16px; } | |
| .search input:focus { outline: 2px solid var(--accent); outline-offset: -1px; } | |
| .search .icon { position: absolute; left: 12px; top: 12px; width: 20px; height: 20px; color: var(--muted); } | |
| .btn { height: 44px; padding: 0 16px; border: 1px solid var(--line); background: var(--bg); border-radius: 6px; cursor: pointer; display: inline-flex; align-items: center; gap: 8px; white-space: nowrap; } | |
| .btn:hover { border-color: var(--muted); } | |
| .results { position: absolute; top: 48px; left: 0; right: 0; background: var(--bg); border: 1px solid var(--line); border-radius: 6px; box-shadow: 0 8px 24px rgba(0,0,0,.12); z-index: 5; max-height: 320px; overflow: auto; display: none; } | |
| .results button { display: flex; width: 100%; text-align: left; border: 0; background: none; padding: 10px 14px; cursor: pointer; font-family: var(--mono); font-size: 14px; justify-content: space-between; gap: 12px; } | |
| .results button:hover, .results button.active { background: var(--card); } | |
| .results .meta { color: var(--muted); font-family: var(--font); font-size: 12px; } | |
| .hint { color: var(--muted); font-size: 13px; margin-top: 10px; text-align: left; } | |
| .hint code { font-family: var(--mono); } | |
| .examples-label { color: var(--muted); font-size: 13px; margin: 48px 0 12px; } | |
| .examples { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 12px; } | |
| .card { border: 1px solid var(--line); background: var(--card); border-radius: 6px; padding: 14px; cursor: pointer; text-align: center; } | |
| .card:hover { border-color: var(--muted); } | |
| .card .name { font-family: var(--mono); font-size: 13.5px; word-break: break-all; } | |
| .card .meta { color: var(--muted); font-size: 12.5px; margin-top: 8px; } | |
| .card .tag { display: inline-block; font-size: 11px; border: 1px solid var(--line); border-radius: 4px; padding: 1px 6px; margin-left: 4px; } | |
| details.token { margin-top: 40px; text-align: left; color: var(--muted); font-size: 13px; } | |
| details.token summary { cursor: pointer; } | |
| details.token .row { display: flex; gap: 8px; margin-top: 10px; } | |
| details.token input { flex: 1; height: 36px; border: 1px solid var(--line); background: var(--card); border-radius: 6px; padding: 0 10px; font-family: var(--mono); font-size: 13px; } | |
| /* ---------- Viewer ---------- */ | |
| #viewer { display: none; height: 100%; flex-direction: column; } | |
| .bar { display: flex; align-items: center; gap: 12px; padding: 8px 12px; border-bottom: 1px solid var(--line); min-height: 52px; } | |
| .bar .title { font-family: var(--mono); font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; } | |
| .bar .title a { color: var(--fg); text-decoration: none; } | |
| .bar .title a:hover { text-decoration: underline; } | |
| .bar .btn { height: 34px; font-size: 14px; } | |
| .stage { position: relative; flex: 1; min-height: 0; } | |
| eqty-explorer { display: block; height: 100%; } | |
| #legacy { display: block; width: 100%; height: 100%; border: 0; } | |
| .bar select { height: 34px; max-width: 40%; border: 1px solid var(--line); background: var(--bg); color: var(--fg); border-radius: 6px; padding: 0 8px; font-family: var(--font); font-size: 14px; font-weight: 500; cursor: pointer; } | |
| .bar .kind { font-size: 12px; color: var(--muted); border: 1px solid var(--line); border-radius: 4px; padding: 2px 8px; white-space: nowrap; } | |
| .overlay { position: absolute; inset: 0; display: grid; place-items: center; background: var(--bg); z-index: 50; padding: 16px; } | |
| .overlay .box { max-width: 520px; text-align: center; line-height: 1.5; } | |
| .overlay .err { color: var(--danger); font-weight: 500; } | |
| .spinner { width: 28px; height: 28px; border: 3px solid var(--line); border-top-color: var(--accent); border-radius: 50%; margin: 0 auto 14px; animation: spin .8s linear infinite; } | |
| @keyframes spin { to { transform: rotate(360deg); } } | |
| .hidden { display: none ; } | |
| @media (max-width: 560px) { .searchrow { flex-direction: column; } #home { padding-top: 40px; } } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- ============ HOME ============ --> | |
| <main id="home"> | |
| <div class="brand">EQTY LAB <small>×</small> 🤗</div> | |
| <h1> | |
| <span class="logo" aria-hidden="true"> | |
| <svg viewBox="0 0 24 24" fill="#0b0b0c"><rect x="3" y="3" width="7" height="6" rx="1.5"/><rect x="14" y="9" width="7" height="6" rx="1.5"/><rect x="14" y="17" width="7" height="5" rx="1.5"/><path d="M6.5 9v10.5H14M6.5 12H14" stroke="#0b0b0c" stroke-width="2" fill="none"/></svg> | |
| </span> | |
| AI Lineage Explorer | |
| </h1> | |
| <h2>AI Integrity begins right here.</h2> | |
| <p class="lede">Step inside your model to inspect its data sources, prep, governance, compute, architecture and evaluation.</p> | |
| <p class="lede" style="margin-top:18px">Build trust from ingestion to inference.</p> | |
| <div class="searchrow"> | |
| <div class="search"> | |
| <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg> | |
| <input id="q" type="text" autocomplete="off" spellcheck="false" placeholder="Search Hugging Face models, or paste a manifest URL" aria-label="Model id or manifest URL" /> | |
| <div class="results" id="results" role="listbox"></div> | |
| </div> | |
| <button class="btn" id="uploadBtn" type="button">Upload a Manifest | |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 16V4m0 0-5 5m5-5 5 5M4 20h16"/></svg> | |
| </button> | |
| <input type="file" id="file" accept=".json,application/json" class="hidden" /> | |
| </div> | |
| <div class="hint">Loads the JSON manifests in the model repo's <code>.eqtylab/</code> folder (or a root <code>manifest.json</code>), or opens the legacy explorer if the repo has an <code>.integrity/</code> folder instead. Press Enter to open <code>owner/model</code> directly.</div> | |
| <div class="examples-label">Examples:</div> | |
| <div class="examples" id="examples"></div> | |
| <details class="token"> | |
| <summary>Private repos: add a Hugging Face read token</summary> | |
| <p>Only needed for manifests in private repos. Kept in this browser tab only (session storage) and sent only to huggingface.co.</p> | |
| <div class="row"> | |
| <input id="token" type="password" placeholder="hf_…" autocomplete="off" /> | |
| <button class="btn" id="tokenClear" type="button" style="height:36px">Clear</button> | |
| </div> | |
| </details> | |
| </main> | |
| <!-- ============ VIEWER ============ --> | |
| <section id="viewer"> | |
| <div class="bar"> | |
| <button class="btn" id="back" type="button" aria-label="Back to search">← Search</button> | |
| <div class="title" id="vtitle"></div> | |
| <select id="vfile" class="hidden" aria-label="Manifest file in .eqtylab/"></select> | |
| <span class="kind hidden" id="vkind" title="This repo uses the legacy .integrity/ folder format">legacy explorer</span> | |
| </div> | |
| <div class="stage"> | |
| <eqty-explorer id="ex" theme="auto"></eqty-explorer> | |
| <iframe id="legacy" class="hidden" title="Legacy lineage explorer"></iframe> | |
| <div class="overlay" id="overlay"><div class="box" id="overlayBox"></div></div> | |
| </div> | |
| </section> | |
| <script> | |
| // ---- Config: example cards on the home page ---- | |
| const EXAMPLES = [ | |
| "EQTYLab/Llama-3.1-Nemotron-Nano-4B-v1.1-banking77", | |
| "EQTYLab/Llama-3.1-Nemotron-Nano-4B-v1.1-banking77-test", | |
| "eci-io/climategpt-7b", // legacy .integrity/ folder | |
| ]; | |
| const EQTY_DIR = ".eqtylab"; // new format: JSON manifests in the repo's .eqtylab/ folder | |
| const EQTY_DEFAULT = ".eqtylab/manifest.json"; // opened first when present, else the first file | |
| const MANIFEST_FILE = "manifest.json"; // new format, older layout: single manifest at the repo root | |
| const LEGACY_FILE = ".integrity/integrity.json"; // legacy format: .integrity/ folder, opened in legacy/index.html | |
| const HF = "https://huggingface.co"; | |
| const $ = (id) => document.getElementById(id); | |
| // .eqtylab/sub/base-model-benchmarks.json -> "Sub / Base Model Benchmarks" | |
| const fileLabel = (path) => path.slice(EQTY_DIR.length + 1).replace(/\.json$/i, "").split("/") | |
| .map((part) => part.replace(/[-_]+/g, " ").trim().replace(/(^|\s)(\S)/g, (m, sp, c) => sp + c.toUpperCase())) | |
| .join(" / "); | |
| const esc = (s) => String(s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c])); | |
| // ---- token (optional, per-tab) ---- | |
| const getToken = () => { try { return sessionStorage.getItem("hf_token") || ""; } catch { return ""; } }; | |
| const setToken = (v) => { try { v ? sessionStorage.setItem("hf_token", v) : sessionStorage.removeItem("hf_token"); } catch {} }; | |
| $("token").value = getToken(); | |
| $("token").addEventListener("change", (e) => setToken(e.target.value.trim())); | |
| $("tokenClear").onclick = () => { $("token").value = ""; setToken(""); }; | |
| const hfFetch = (url, opts = {}) => { | |
| const t = getToken(); | |
| return fetch(url, t && url.startsWith(HF) ? { ...opts, headers: { Authorization: `Bearer ${t}` } } : opts); | |
| }; | |
| // ---- routing: ?model=owner/name | ?manifest=<url> | home ---- | |
| function route() { | |
| const p = new URLSearchParams(location.search); | |
| if (p.get("model")) return openModel(p.get("model"), p.get("file")); | |
| if (p.get("manifest")) return openUrl(p.get("manifest")); | |
| showHome(); | |
| } | |
| function go(params) { | |
| const qs = new URLSearchParams(params).toString(); | |
| history.pushState(null, "", qs ? `?${qs}` : location.pathname); | |
| route(); | |
| } | |
| window.addEventListener("popstate", route); | |
| function showHome() { | |
| closeLegacy(); | |
| $("viewer").style.display = "none"; | |
| $("home").style.display = "block"; | |
| document.title = "AI Lineage Explorer"; | |
| } | |
| function showViewer(titleHtml) { | |
| $("home").style.display = "none"; | |
| $("viewer").style.display = "flex"; | |
| $("vtitle").innerHTML = titleHtml; | |
| $("vfile").classList.add("hidden"); | |
| seq++; // drop any .eqtylab/ file still loading for the previous view | |
| closeLegacy(); | |
| } | |
| // ====================================================================== | |
| // LEGACY explorer (.integrity/ folder format) | |
| // Prebuilt app in legacy/ (from the EQTYLab/lineage-explorer space), shown in an | |
| // iframe. Everything else on this page is the 2026 explorer (manifest.json). | |
| // ====================================================================== | |
| // legacy integrity.json: { graph: { nodes, edges } }; manifests have no top-level graph | |
| const isLegacyJson = (j) => !!(j && j.graph && Array.isArray(j.graph.nodes)); | |
| // the legacy explorer only loads by repo id (from huggingface.co/<id>/raw/main/.integrity/) | |
| const legacyRepoId = (url) => (url.match(/^https?:\/\/huggingface\.co\/([^/]+\/[^/]+)\/(?:raw|resolve|blob|tree)\/[^/]+\/\.integrity(?:\/|$)/) || [])[1]; | |
| // A private Space is opened with a one-time `?__sign=` token in the URL. The frame's | |
| // request gets no cookie (SameSite=Lax, cross-site), so it needs the token too; legacy/index.html | |
| // is a single self-contained file, so that one request is all it makes. Captured at startup | |
| // because navigation here rewrites the query string. | |
| const SIGN = new URLSearchParams(location.search).get("__sign"); | |
| function openLegacy(params, label) { | |
| clearTimeout(fallbackTimer); | |
| setManifest(null); | |
| ex.classList.add("hidden"); | |
| $("overlay").classList.add("hidden"); | |
| $("vkind").classList.remove("hidden"); | |
| $("legacy").classList.remove("hidden"); | |
| $("legacy").src = `legacy/index.html?${new URLSearchParams(SIGN ? { ...params, __sign: SIGN } : params)}`; | |
| document.title = `${label} · AI Lineage Explorer`; | |
| } | |
| function closeLegacy() { | |
| if ($("legacy").getAttribute("src")) $("legacy").removeAttribute("src"); | |
| $("legacy").classList.add("hidden"); | |
| $("vkind").classList.add("hidden"); | |
| ex.classList.remove("hidden"); | |
| } | |
| $("back").onclick = () => go({}); | |
| let seq = 0; // bumped per .eqtylab/ file load and per view change | |
| // ---- overlay states ---- | |
| const ex = $("ex"); | |
| // On a deep link (?model=…) this runs before the explorer's module has loaded, and | |
| // assigning .manifest then would create a plain property that shadows the element's setter, so | |
| // the manifest never reaches it. Wait for the definition (writes still apply in call order). | |
| const setManifest = (m) => customElements.whenDefined("eqty-explorer").then(() => { ex.manifest = m; }); | |
| let fallbackTimer; | |
| function loading(msg, clear) { | |
| $("overlay").classList.remove("hidden"); | |
| if (clear) setManifest(null); | |
| $("overlayBox").innerHTML = `<div class="spinner"></div><div>${esc(msg)}</div>`; | |
| } | |
| function fail(html) { | |
| clearTimeout(fallbackTimer); | |
| $("overlay").classList.remove("hidden"); | |
| $("overlayBox").innerHTML = `<p class="err">Couldn't load a manifest</p><p>${html}</p><p><button class="btn" onclick="document.getElementById('file').click()">Upload a manifest instead</button></p>`; | |
| } | |
| ex.addEventListener("manifestchange", (e) => { | |
| if (!e.detail) return; | |
| // the explorer verifies signatures before drawing; give it a moment, then reveal | |
| clearTimeout(fallbackTimer); | |
| fallbackTimer = setTimeout(() => $("overlay").classList.add("hidden"), 600); | |
| }); | |
| ex.addEventListener("error", (e) => fail(esc(e.detail?.message || e.detail || "The manifest could not be parsed."))); | |
| function render(json, label) { | |
| loading("Verifying lineage…"); | |
| setManifest(json); | |
| clearTimeout(fallbackTimer); | |
| fallbackTimer = setTimeout(() => $("overlay").classList.add("hidden"), 30000); | |
| document.title = `${label} · AI Lineage Explorer`; | |
| } | |
| // ---- open a model repo: .eqtylab/*.json → root manifest.json → legacy .integrity/ ---- | |
| const resolveUrl = (id, path) => `${HF}/${id}/resolve/main/${path.split("/").map(encodeURIComponent).join("/")}`; | |
| const denied = (id) => fail(`<code>${esc(id)}</code> is private or gated. Add a read token under “Private repos” on the home page, then try again.`); | |
| async function openModel(id, file) { | |
| id = id.trim().replace(/^https?:\/\/huggingface\.co\//, "").replace(/\/+$/, ""); | |
| const repoUrl = `${HF}/${id}`; | |
| showViewer(`<a href="${esc(repoUrl)}" target="_blank" rel="noopener">${esc(id)}</a>`); | |
| loading("Looking for manifests…", true); | |
| // 1. every JSON file in .eqtylab/ (subfolders included), picked from a dropdown | |
| let r; | |
| try { r = await hfFetch(`${HF}/api/models/${id}/tree/main/${EQTY_DIR}?recursive=true`); } catch { r = null; } | |
| if (!r) return fail(`Network error listing <code>${esc(id)}</code>.`); | |
| if (r.status === 401 || r.status === 403) return denied(id); | |
| if (r.ok) { | |
| const files = (await r.json()).filter((e) => e.type === "file" && /\.json$/i.test(e.path)).map((e) => e.path).sort(); | |
| if (files.length) { | |
| const pick = files.includes(file) ? file : files.includes(EQTY_DEFAULT) ? EQTY_DEFAULT : files[0]; | |
| const sel = $("vfile"); | |
| sel.innerHTML = files.map((f) => `<option value="${esc(f)}" title="${esc(f)}">${esc(fileLabel(f))}</option>`).join(""); | |
| sel.value = pick; | |
| sel.classList.remove("hidden"); | |
| sel.onchange = () => { | |
| history.pushState(null, "", `?${new URLSearchParams({ model: id, file: sel.value })}`); | |
| loadEqtyFile(id, sel.value); | |
| }; | |
| return loadEqtyFile(id, pick); | |
| } | |
| } | |
| // 2. no .eqtylab/ (or no JSON in it): a single manifest.json at the repo root | |
| loading("Fetching manifest…"); | |
| const url = `${repoUrl}/resolve/main/${MANIFEST_FILE}`; | |
| try { r = await hfFetch(url); } catch { r = null; } | |
| if (!r) return fail(`Network error fetching <code>${esc(url)}</code>.`); | |
| if (r.status === 401 || r.status === 403) return denied(id); | |
| if (r.status === 404) { | |
| // 3. the legacy .integrity/ folder format | |
| let l; | |
| try { l = await hfFetch(`${repoUrl}/resolve/main/${LEGACY_FILE}`, { method: "HEAD" }); } catch { l = null; } | |
| if (l && l.ok) return openLegacy({ model: id }, id); | |
| return fail(`No JSON files in <code>${EQTY_DIR}/</code>, no <code>${MANIFEST_FILE}</code> and no <code>${LEGACY_FILE}</code> in <code>${esc(id)}</code> (or the repo doesn't exist${getToken() ? "" : " / is private"}).`); | |
| } | |
| if (!r.ok) return fail(`HTTP ${r.status} fetching <code>${esc(url)}</code>.`); | |
| try { render(await r.json(), id); } catch { fail(`<code>${MANIFEST_FILE}</code> in <code>${esc(id)}</code> isn't valid JSON.`); } | |
| } | |
| // load one file from .eqtylab/; `seq` drops a slow response if another file was picked meanwhile | |
| async function loadEqtyFile(id, path) { | |
| const mine = ++seq; | |
| loading(`Fetching ${path}…`, true); | |
| const url = resolveUrl(id, path); | |
| let json; | |
| try { | |
| const r = await hfFetch(url); | |
| if (mine !== seq) return; | |
| if (r.status === 401 || r.status === 403) return denied(id); | |
| if (!r.ok) return fail(`HTTP ${r.status} fetching <code>${esc(path)}</code>.`); | |
| json = await r.json(); | |
| } catch { if (mine === seq) fail(`<code>${esc(path)}</code> in <code>${esc(id)}</code> couldn't be fetched or isn't valid JSON.`); return; } | |
| if (mine !== seq) return; | |
| if (isLegacyJson(json)) | |
| return fail(`<code>${esc(path)}</code> is a legacy <code>integrity.json</code>, which only opens from a repo's <code>.integrity/</code> folder.`); | |
| render(json, `${id} · ${path.slice(EQTY_DIR.length + 1)}`); | |
| } | |
| // ---- open an arbitrary manifest URL ---- | |
| async function openUrl(url) { | |
| showViewer(`<a href="${esc(url)}" target="_blank" rel="noopener">${esc(url)}</a>`); | |
| const label = url.split("/").slice(-3).join("/"); | |
| // a URL into a repo's legacy .integrity/ folder | |
| const legacyId = legacyRepoId(url); | |
| if (legacyId) return openLegacy({ model: legacyId }, legacyId); | |
| loading("Fetching manifest…", true); | |
| let json; | |
| try { | |
| const r = await hfFetch(url); | |
| if (!r.ok) return fail(`HTTP ${r.status} fetching <code>${esc(url)}</code>.`); | |
| json = await r.json(); | |
| } catch { return fail(`Couldn't fetch <code>${esc(url)}</code> (network or CORS error, or not JSON).`); } | |
| if (isLegacyJson(json)) | |
| return fail(`<code>${esc(url)}</code> is a legacy <code>integrity.json</code>. The legacy explorer can only open these from a Hugging Face repo's <code>.integrity/</code> folder.`); | |
| render(json, label); | |
| } | |
| // ---- upload ---- | |
| $("uploadBtn").onclick = () => $("file").click(); | |
| $("file").addEventListener("change", async (e) => { | |
| const f = e.target.files[0]; e.target.value = ""; | |
| if (!f) return; | |
| history.pushState(null, "", location.pathname); | |
| showViewer(esc(f.name)); | |
| let json; | |
| try { json = JSON.parse(await f.text()); } catch { return fail(`<code>${esc(f.name)}</code> isn't valid JSON.`); } | |
| // legacy integrity.json points at sibling files in its .integrity/ folder, so it can't be viewed on its own | |
| if (isLegacyJson(json)) | |
| return fail(`<code>${esc(f.name)}</code> is a legacy <code>.integrity/integrity.json</code>, which needs the rest of its <code>.integrity/</code> folder. Open the model repo (or a URL to its <code>.integrity</code> folder) instead.`); | |
| render(json, f.name); | |
| }); | |
| // ---- search box ---- | |
| const q = $("q"), results = $("results"); | |
| let items = [], active = -1, debounce; | |
| function submit(v) { | |
| v = (v || q.value).trim(); | |
| if (!v) return; | |
| results.style.display = "none"; | |
| if (/^https?:\/\//.test(v) && !/^https?:\/\/huggingface\.co\/[^/]+\/[^/]+\/?$/.test(v)) go({ manifest: v }); | |
| else go({ model: v.replace(/^https?:\/\/huggingface\.co\//, "") }); | |
| } | |
| function drawResults() { | |
| if (!items.length) { results.style.display = "none"; return; } | |
| results.innerHTML = items.map((m, i) => | |
| `<button type="button" role="option" class="${i === active ? "active" : ""}" data-id="${esc(m.id)}"> | |
| <span>${esc(m.id)}</span><span class="meta">${esc(m.pipeline_tag || "")}</span></button>`).join(""); | |
| results.style.display = "block"; | |
| } | |
| results.addEventListener("mousedown", (e) => { const b = e.target.closest("button"); if (b) { e.preventDefault(); submit(b.dataset.id); } }); | |
| q.addEventListener("input", () => { | |
| clearTimeout(debounce); | |
| const v = q.value.trim(); | |
| if (v.length < 2 || /^https?:/.test(v)) { items = []; drawResults(); return; } | |
| debounce = setTimeout(async () => { | |
| try { | |
| const r = await hfFetch(`${HF}/api/models?search=${encodeURIComponent(v)}&limit=8`); | |
| items = r.ok ? await r.json() : []; | |
| } catch { items = []; } | |
| active = -1; drawResults(); | |
| }, 200); | |
| }); | |
| q.addEventListener("keydown", (e) => { | |
| if (e.key === "ArrowDown" && items.length) { active = (active + 1) % items.length; drawResults(); e.preventDefault(); } | |
| else if (e.key === "ArrowUp" && items.length) { active = (active - 1 + items.length) % items.length; drawResults(); e.preventDefault(); } | |
| else if (e.key === "Enter") { submit(active >= 0 ? items[active].id : q.value); } | |
| else if (e.key === "Escape") { items = []; drawResults(); } | |
| }); | |
| q.addEventListener("blur", () => setTimeout(() => (results.style.display = "none"), 100)); | |
| // ---- example cards ---- | |
| const fmtDate = (d) => new Date(d).toLocaleDateString(undefined, { month: "short", day: "2-digit", year: "numeric" }); | |
| $("examples").innerHTML = EXAMPLES.map((id) => | |
| `<div class="card" tabindex="0" data-id="${esc(id)}"><div class="name">${esc(id)}</div><div class="meta"> </div></div>`).join(""); | |
| $("examples").addEventListener("click", (e) => { const c = e.target.closest(".card"); if (c) go({ model: c.dataset.id }); }); | |
| $("examples").addEventListener("keydown", (e) => { const c = e.target.closest(".card"); if (c && e.key === "Enter") go({ model: c.dataset.id }); }); | |
| EXAMPLES.forEach(async (id) => { | |
| const meta = document.querySelector(`.card[data-id="${CSS.escape(id)}"] .meta`); | |
| try { | |
| const r = await hfFetch(`${HF}/api/models/${id}`); | |
| if (!r.ok) { meta.textContent = r.status === 401 ? "private" : ""; return; } | |
| const m = await r.json(); | |
| meta.innerHTML = `Updated ${esc(fmtDate(m.lastModified))} · ${esc(m.pipeline_tag || "model")} · ↓ ${m.downloads ?? 0} · ♡ ${m.likes ?? 0}` + | |
| (m.private ? ` <span class="tag">private</span>` : ""); | |
| } catch {} | |
| }); | |
| route(); | |
| </script> | |
| </body> | |
| </html> | |