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 files

Lists .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>

Files changed (2) hide show
  1. README.md +6 -3
  2. 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; the page loads `manifest.json` from the root of that repo.
16
- - Repos without `manifest.json` but with a legacy `.integrity/` folder open in the legacy explorer, embedded in the same page. Legacy repos must be public.
 
 
 
17
 
18
  ## Layout
19
 
20
- - `index.html`: the 2026 explorer (`manifest.json`) and home page. It detects the format and opens the right 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>manifest.json</code> from the root of the model repo, 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>
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 MANIFEST_FILE = "manifest.json"; // new format: single manifest at the repo root
 
 
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
- try { ex.manifest = null; } catch {}
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) { try { ex.manifest = null; } catch {} }
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
- ex.manifest = json;
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
- async function openModel(id) {
 
 
 
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("Fetching manifest…", true);
256
- const url = `${repoUrl}/resolve/main/${MANIFEST_FILE}`;
 
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
- // no manifest.json: fall back to the legacy .integrity/ folder format
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> or <code>${LEGACY_FILE}</code> in <code>${esc(id)}</code> (or the repo doesn't exist${getToken() ? "" : " / is private"}).`);
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>`);