Spaces:
Running
Running
yurko Claude Opus 5.5 commited on
Commit ·
e774bf1
1
Parent(s): e9b01c7
Load manifests from the repo's .eqtylab/ folder with a file dropdown
Browse filesLists .eqtylab/**/*.json via the Hub tree API, opens .eqtylab/manifest.json
(or the first file), and adds a dropdown to switch files (?file= deep link).
Falls back to root manifest.json, then the legacy .integrity/ folder.
Also fix deep links (?model=) never drawing: .manifest was assigned before
the eqty-explorer element was defined, creating an own property that
shadowed its setter. Wait for customElements.whenDefined first.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- README.md +6 -3
- index.html +70 -14
README.md
CHANGED
|
@@ -12,11 +12,14 @@ models:
|
|
| 12 |
|
| 13 |
AI Lineage Explorer (`@eqtylab/explorer@0.17.0`).
|
| 14 |
|
| 15 |
-
- Search or enter a Hugging Face model id
|
| 16 |
-
- Repos
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
## Layout
|
| 19 |
|
| 20 |
-
- `index.html`: the 2026 explorer (`
|
| 21 |
- `legacy/index.html`: the legacy explorer (the prebuilt app from the `EQTYLab/lineage-explorer` space), bundled into one self-contained file with its JS, CSS, fonts and images inline. It's generated; rebuild it with `npm i && npm run bundle-legacy`, which reads `../lineage-explorer/static` (`legacy/bundle.mjs`).
|
| 22 |
It's a single file because, in a private Space, only a request carrying the `?__sign=` token is authorized inside the huggingface.co embed. `index.html` passes that token along when it opens the frame.
|
|
|
|
| 12 |
|
| 13 |
AI Lineage Explorer (`@eqtylab/explorer@0.17.0`).
|
| 14 |
|
| 15 |
+
- Search or enter a Hugging Face model id. The page lists the JSON files in the repo's `.eqtylab/` folder (subfolders included) and opens `.eqtylab/manifest.json`, or the first file if there isn't one. A dropdown in the top bar switches between files. If there's no `.eqtylab/`, it falls back to a root `manifest.json`.
|
| 16 |
+
- Repos with neither but with a legacy `.integrity/` folder open in the legacy explorer, embedded in the same page. Legacy repos must be public.
|
| 17 |
+
- Direct links: `?model=<owner>/<name>` (plus `&file=.eqtylab/<name>.json` for a specific file) or `?manifest=<url>`.
|
| 18 |
+
- Private repos: add a read token under "Private repos" on the home page (kept in the browser tab only).
|
| 19 |
+
- Or upload a manifest file.
|
| 20 |
|
| 21 |
## Layout
|
| 22 |
|
| 23 |
+
- `index.html`: the 2026 explorer (`.eqtylab/*.json`) and home page. It detects the format and opens the right explorer.
|
| 24 |
- `legacy/index.html`: the legacy explorer (the prebuilt app from the `EQTYLab/lineage-explorer` space), bundled into one self-contained file with its JS, CSS, fonts and images inline. It's generated; rebuild it with `npm i && npm run bundle-legacy`, which reads `../lineage-explorer/static` (`legacy/bundle.mjs`).
|
| 25 |
It's a single file because, in a private Space, only a request carrying the `?__sign=` token is authorized inside the huggingface.co embed. `index.html` passes that token along when it opens the frame.
|
index.html
CHANGED
|
@@ -68,6 +68,7 @@
|
|
| 68 |
.stage { position: relative; flex: 1; min-height: 0; }
|
| 69 |
eqty-explorer { display: block; height: 100%; }
|
| 70 |
#legacy { display: block; width: 100%; height: 100%; border: 0; }
|
|
|
|
| 71 |
.bar .kind { font-size: 12px; color: var(--muted); border: 1px solid var(--line); border-radius: 4px; padding: 2px 8px; white-space: nowrap; }
|
| 72 |
.overlay { position: absolute; inset: 0; display: grid; place-items: center; background: var(--bg); z-index: 50; padding: 16px; }
|
| 73 |
.overlay .box { max-width: 520px; text-align: center; line-height: 1.5; }
|
|
@@ -105,7 +106,7 @@
|
|
| 105 |
</button>
|
| 106 |
<input type="file" id="file" accept=".json,application/json" class="hidden" />
|
| 107 |
</div>
|
| 108 |
-
<div class="hint">Loads <code>
|
| 109 |
|
| 110 |
<div class="examples-label">Examples:</div>
|
| 111 |
<div class="examples" id="examples"></div>
|
|
@@ -125,6 +126,7 @@
|
|
| 125 |
<div class="bar">
|
| 126 |
<button class="btn" id="back" type="button" aria-label="Back to search">← Search</button>
|
| 127 |
<div class="title" id="vtitle"></div>
|
|
|
|
| 128 |
<span class="kind hidden" id="vkind" title="This repo uses the legacy .integrity/ folder format">legacy explorer</span>
|
| 129 |
</div>
|
| 130 |
<div class="stage">
|
|
@@ -141,7 +143,9 @@
|
|
| 141 |
"EQTYLab/Llama-3.1-Nemotron-Nano-4B-v1.1-banking77-test",
|
| 142 |
"eci-io/climategpt-7b", // legacy .integrity/ folder
|
| 143 |
];
|
| 144 |
-
const
|
|
|
|
|
|
|
| 145 |
const LEGACY_FILE = ".integrity/integrity.json"; // legacy format: .integrity/ folder, opened in legacy/index.html
|
| 146 |
const HF = "https://huggingface.co";
|
| 147 |
|
|
@@ -162,7 +166,7 @@
|
|
| 162 |
// ---- routing: ?model=owner/name | ?manifest=<url> | home ----
|
| 163 |
function route() {
|
| 164 |
const p = new URLSearchParams(location.search);
|
| 165 |
-
if (p.get("model")) return openModel(p.get("model"));
|
| 166 |
if (p.get("manifest")) return openUrl(p.get("manifest"));
|
| 167 |
showHome();
|
| 168 |
}
|
|
@@ -183,6 +187,8 @@
|
|
| 183 |
$("home").style.display = "none";
|
| 184 |
$("viewer").style.display = "flex";
|
| 185 |
$("vtitle").innerHTML = titleHtml;
|
|
|
|
|
|
|
| 186 |
closeLegacy();
|
| 187 |
}
|
| 188 |
|
|
@@ -202,7 +208,7 @@
|
|
| 202 |
const SIGN = new URLSearchParams(location.search).get("__sign");
|
| 203 |
function openLegacy(params, label) {
|
| 204 |
clearTimeout(fallbackTimer);
|
| 205 |
-
|
| 206 |
ex.classList.add("hidden");
|
| 207 |
$("overlay").classList.add("hidden");
|
| 208 |
$("vkind").classList.remove("hidden");
|
|
@@ -218,12 +224,18 @@
|
|
| 218 |
}
|
| 219 |
$("back").onclick = () => go({});
|
| 220 |
|
|
|
|
|
|
|
| 221 |
// ---- overlay states ----
|
| 222 |
const ex = $("ex");
|
|
|
|
|
|
|
|
|
|
|
|
|
| 223 |
let fallbackTimer;
|
| 224 |
function loading(msg, clear) {
|
| 225 |
$("overlay").classList.remove("hidden");
|
| 226 |
-
if (clear)
|
| 227 |
$("overlayBox").innerHTML = `<div class="spinner"></div><div>${esc(msg)}</div>`;
|
| 228 |
}
|
| 229 |
function fail(html) {
|
|
@@ -241,35 +253,79 @@
|
|
| 241 |
|
| 242 |
function render(json, label) {
|
| 243 |
loading("Verifying lineage…");
|
| 244 |
-
|
| 245 |
clearTimeout(fallbackTimer);
|
| 246 |
fallbackTimer = setTimeout(() => $("overlay").classList.add("hidden"), 30000);
|
| 247 |
document.title = `${label} · AI Lineage Explorer`;
|
| 248 |
}
|
| 249 |
|
| 250 |
-
// ---- open a model repo ----
|
| 251 |
-
|
|
|
|
|
|
|
|
|
|
| 252 |
id = id.trim().replace(/^https?:\/\/huggingface\.co\//, "").replace(/\/+$/, "");
|
| 253 |
const repoUrl = `${HF}/${id}`;
|
| 254 |
showViewer(`<a href="${esc(repoUrl)}" target="_blank" rel="noopener">${esc(id)}</a>`);
|
| 255 |
-
loading("
|
| 256 |
-
|
|
|
|
| 257 |
let r;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 258 |
try { r = await hfFetch(url); } catch { r = null; }
|
| 259 |
if (!r) return fail(`Network error fetching <code>${esc(url)}</code>.`);
|
| 260 |
-
if (r.status === 401 || r.status === 403)
|
| 261 |
-
return fail(`<code>${esc(id)}</code> is private or gated. Add a read token under “Private repos” on the home page, then try again.`);
|
| 262 |
if (r.status === 404) {
|
| 263 |
-
//
|
| 264 |
let l;
|
| 265 |
try { l = await hfFetch(`${repoUrl}/resolve/main/${LEGACY_FILE}`, { method: "HEAD" }); } catch { l = null; }
|
| 266 |
if (l && l.ok) return openLegacy({ model: id }, id);
|
| 267 |
-
return fail(`No <code>${MANIFEST_FILE}</code>
|
| 268 |
}
|
| 269 |
if (!r.ok) return fail(`HTTP ${r.status} fetching <code>${esc(url)}</code>.`);
|
| 270 |
try { render(await r.json(), id); } catch { fail(`<code>${MANIFEST_FILE}</code> in <code>${esc(id)}</code> isn't valid JSON.`); }
|
| 271 |
}
|
| 272 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 273 |
// ---- open an arbitrary manifest URL ----
|
| 274 |
async function openUrl(url) {
|
| 275 |
showViewer(`<a href="${esc(url)}" target="_blank" rel="noopener">${esc(url)}</a>`);
|
|
|
|
| 68 |
.stage { position: relative; flex: 1; min-height: 0; }
|
| 69 |
eqty-explorer { display: block; height: 100%; }
|
| 70 |
#legacy { display: block; width: 100%; height: 100%; border: 0; }
|
| 71 |
+
.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(--mono); font-size: 13px; cursor: pointer; }
|
| 72 |
.bar .kind { font-size: 12px; color: var(--muted); border: 1px solid var(--line); border-radius: 4px; padding: 2px 8px; white-space: nowrap; }
|
| 73 |
.overlay { position: absolute; inset: 0; display: grid; place-items: center; background: var(--bg); z-index: 50; padding: 16px; }
|
| 74 |
.overlay .box { max-width: 520px; text-align: center; line-height: 1.5; }
|
|
|
|
| 106 |
</button>
|
| 107 |
<input type="file" id="file" accept=".json,application/json" class="hidden" />
|
| 108 |
</div>
|
| 109 |
+
<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>
|
| 110 |
|
| 111 |
<div class="examples-label">Examples:</div>
|
| 112 |
<div class="examples" id="examples"></div>
|
|
|
|
| 126 |
<div class="bar">
|
| 127 |
<button class="btn" id="back" type="button" aria-label="Back to search">← Search</button>
|
| 128 |
<div class="title" id="vtitle"></div>
|
| 129 |
+
<select id="vfile" class="hidden" aria-label="Manifest file in .eqtylab/"></select>
|
| 130 |
<span class="kind hidden" id="vkind" title="This repo uses the legacy .integrity/ folder format">legacy explorer</span>
|
| 131 |
</div>
|
| 132 |
<div class="stage">
|
|
|
|
| 143 |
"EQTYLab/Llama-3.1-Nemotron-Nano-4B-v1.1-banking77-test",
|
| 144 |
"eci-io/climategpt-7b", // legacy .integrity/ folder
|
| 145 |
];
|
| 146 |
+
const EQTY_DIR = ".eqtylab"; // new format: JSON manifests in the repo's .eqtylab/ folder
|
| 147 |
+
const EQTY_DEFAULT = ".eqtylab/manifest.json"; // opened first when present, else the first file
|
| 148 |
+
const MANIFEST_FILE = "manifest.json"; // new format, older layout: single manifest at the repo root
|
| 149 |
const LEGACY_FILE = ".integrity/integrity.json"; // legacy format: .integrity/ folder, opened in legacy/index.html
|
| 150 |
const HF = "https://huggingface.co";
|
| 151 |
|
|
|
|
| 166 |
// ---- routing: ?model=owner/name | ?manifest=<url> | home ----
|
| 167 |
function route() {
|
| 168 |
const p = new URLSearchParams(location.search);
|
| 169 |
+
if (p.get("model")) return openModel(p.get("model"), p.get("file"));
|
| 170 |
if (p.get("manifest")) return openUrl(p.get("manifest"));
|
| 171 |
showHome();
|
| 172 |
}
|
|
|
|
| 187 |
$("home").style.display = "none";
|
| 188 |
$("viewer").style.display = "flex";
|
| 189 |
$("vtitle").innerHTML = titleHtml;
|
| 190 |
+
$("vfile").classList.add("hidden");
|
| 191 |
+
seq++; // drop any .eqtylab/ file still loading for the previous view
|
| 192 |
closeLegacy();
|
| 193 |
}
|
| 194 |
|
|
|
|
| 208 |
const SIGN = new URLSearchParams(location.search).get("__sign");
|
| 209 |
function openLegacy(params, label) {
|
| 210 |
clearTimeout(fallbackTimer);
|
| 211 |
+
setManifest(null);
|
| 212 |
ex.classList.add("hidden");
|
| 213 |
$("overlay").classList.add("hidden");
|
| 214 |
$("vkind").classList.remove("hidden");
|
|
|
|
| 224 |
}
|
| 225 |
$("back").onclick = () => go({});
|
| 226 |
|
| 227 |
+
let seq = 0; // bumped per .eqtylab/ file load and per view change
|
| 228 |
+
|
| 229 |
// ---- overlay states ----
|
| 230 |
const ex = $("ex");
|
| 231 |
+
// On a deep link (?model=…) this runs before the explorer's module has loaded from the CDN, and
|
| 232 |
+
// assigning .manifest then would create a plain property that shadows the element's setter, so
|
| 233 |
+
// the manifest never reaches it. Wait for the definition (writes still apply in call order).
|
| 234 |
+
const setManifest = (m) => customElements.whenDefined("eqty-explorer").then(() => { ex.manifest = m; });
|
| 235 |
let fallbackTimer;
|
| 236 |
function loading(msg, clear) {
|
| 237 |
$("overlay").classList.remove("hidden");
|
| 238 |
+
if (clear) setManifest(null);
|
| 239 |
$("overlayBox").innerHTML = `<div class="spinner"></div><div>${esc(msg)}</div>`;
|
| 240 |
}
|
| 241 |
function fail(html) {
|
|
|
|
| 253 |
|
| 254 |
function render(json, label) {
|
| 255 |
loading("Verifying lineage…");
|
| 256 |
+
setManifest(json);
|
| 257 |
clearTimeout(fallbackTimer);
|
| 258 |
fallbackTimer = setTimeout(() => $("overlay").classList.add("hidden"), 30000);
|
| 259 |
document.title = `${label} · AI Lineage Explorer`;
|
| 260 |
}
|
| 261 |
|
| 262 |
+
// ---- open a model repo: .eqtylab/*.json → root manifest.json → legacy .integrity/ ----
|
| 263 |
+
const resolveUrl = (id, path) => `${HF}/${id}/resolve/main/${path.split("/").map(encodeURIComponent).join("/")}`;
|
| 264 |
+
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.`);
|
| 265 |
+
|
| 266 |
+
async function openModel(id, file) {
|
| 267 |
id = id.trim().replace(/^https?:\/\/huggingface\.co\//, "").replace(/\/+$/, "");
|
| 268 |
const repoUrl = `${HF}/${id}`;
|
| 269 |
showViewer(`<a href="${esc(repoUrl)}" target="_blank" rel="noopener">${esc(id)}</a>`);
|
| 270 |
+
loading("Looking for manifests…", true);
|
| 271 |
+
|
| 272 |
+
// 1. every JSON file in .eqtylab/ (subfolders included), picked from a dropdown
|
| 273 |
let r;
|
| 274 |
+
try { r = await hfFetch(`${HF}/api/models/${id}/tree/main/${EQTY_DIR}?recursive=true`); } catch { r = null; }
|
| 275 |
+
if (!r) return fail(`Network error listing <code>${esc(id)}</code>.`);
|
| 276 |
+
if (r.status === 401 || r.status === 403) return denied(id);
|
| 277 |
+
if (r.ok) {
|
| 278 |
+
const files = (await r.json()).filter((e) => e.type === "file" && /\.json$/i.test(e.path)).map((e) => e.path).sort();
|
| 279 |
+
if (files.length) {
|
| 280 |
+
const pick = files.includes(file) ? file : files.includes(EQTY_DEFAULT) ? EQTY_DEFAULT : files[0];
|
| 281 |
+
const sel = $("vfile");
|
| 282 |
+
sel.innerHTML = files.map((f) => `<option value="${esc(f)}">${esc(f.slice(EQTY_DIR.length + 1))}</option>`).join("");
|
| 283 |
+
sel.value = pick;
|
| 284 |
+
sel.classList.remove("hidden");
|
| 285 |
+
sel.onchange = () => {
|
| 286 |
+
history.pushState(null, "", `?${new URLSearchParams({ model: id, file: sel.value })}`);
|
| 287 |
+
loadEqtyFile(id, sel.value);
|
| 288 |
+
};
|
| 289 |
+
return loadEqtyFile(id, pick);
|
| 290 |
+
}
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
// 2. no .eqtylab/ (or no JSON in it): a single manifest.json at the repo root
|
| 294 |
+
loading("Fetching manifest…");
|
| 295 |
+
const url = `${repoUrl}/resolve/main/${MANIFEST_FILE}`;
|
| 296 |
try { r = await hfFetch(url); } catch { r = null; }
|
| 297 |
if (!r) return fail(`Network error fetching <code>${esc(url)}</code>.`);
|
| 298 |
+
if (r.status === 401 || r.status === 403) return denied(id);
|
|
|
|
| 299 |
if (r.status === 404) {
|
| 300 |
+
// 3. the legacy .integrity/ folder format
|
| 301 |
let l;
|
| 302 |
try { l = await hfFetch(`${repoUrl}/resolve/main/${LEGACY_FILE}`, { method: "HEAD" }); } catch { l = null; }
|
| 303 |
if (l && l.ok) return openLegacy({ model: id }, id);
|
| 304 |
+
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"}).`);
|
| 305 |
}
|
| 306 |
if (!r.ok) return fail(`HTTP ${r.status} fetching <code>${esc(url)}</code>.`);
|
| 307 |
try { render(await r.json(), id); } catch { fail(`<code>${MANIFEST_FILE}</code> in <code>${esc(id)}</code> isn't valid JSON.`); }
|
| 308 |
}
|
| 309 |
|
| 310 |
+
// load one file from .eqtylab/; `seq` drops a slow response if another file was picked meanwhile
|
| 311 |
+
async function loadEqtyFile(id, path) {
|
| 312 |
+
const mine = ++seq;
|
| 313 |
+
loading(`Fetching ${path}…`, true);
|
| 314 |
+
const url = resolveUrl(id, path);
|
| 315 |
+
let json;
|
| 316 |
+
try {
|
| 317 |
+
const r = await hfFetch(url);
|
| 318 |
+
if (mine !== seq) return;
|
| 319 |
+
if (r.status === 401 || r.status === 403) return denied(id);
|
| 320 |
+
if (!r.ok) return fail(`HTTP ${r.status} fetching <code>${esc(path)}</code>.`);
|
| 321 |
+
json = await r.json();
|
| 322 |
+
} catch { if (mine === seq) fail(`<code>${esc(path)}</code> in <code>${esc(id)}</code> couldn't be fetched or isn't valid JSON.`); return; }
|
| 323 |
+
if (mine !== seq) return;
|
| 324 |
+
if (isLegacyJson(json))
|
| 325 |
+
return fail(`<code>${esc(path)}</code> is a legacy <code>integrity.json</code>, which only opens from a repo's <code>.integrity/</code> folder.`);
|
| 326 |
+
render(json, `${id} · ${path.slice(EQTY_DIR.length + 1)}`);
|
| 327 |
+
}
|
| 328 |
+
|
| 329 |
// ---- open an arbitrary manifest URL ----
|
| 330 |
async function openUrl(url) {
|
| 331 |
showViewer(`<a href="${esc(url)}" target="_blank" rel="noopener">${esc(url)}</a>`);
|