lineage-explorer / index.html
YurkoWasHere's picture
Readable labels in the manifest file dropdown
01abd36 verified
Raw History Blame Contribute Delete
25.1 kB
<!doctype html>
<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 !important; }
@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) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[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">&nbsp;</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>