// ─── Shareable URL State ───────────────────────────────────────────────────── // // Mirrors the current view — base family, variant, metric and every // config.filters column — into location.hash so that a view can be linked: // // #family=Llama-3.2&device=agx_orin&metric=tps // // Extracted module. Call initUrlState(deps) once, after `filters` exists and // BEFORE the first render: construction applies an incoming hash to `filters` // in place, so the very first render already shows the linked view (there is // no visible second render). Returns { desiredFilterValue, sync, currentHash }. // // Disable with "url_state": false in config.json. // eslint-disable-next-line no-unused-vars function initUrlState(deps) { const { config, filters, BASE_FAMILIES, deriveBaseFamily, GROUP_BY, availableOptions, renderSidebar, updateDependentFilters, switchBaseFamily, render, } = deps; const ENABLED = config.url_state !== false; const FILTER_COLS = config.filters.map(f => f.column); const METRIC_COLS = config.metrics.map(m => m.column); // Filter values requested by the hash but not applied yet. They are consumed // by updateDependentFilters(), which otherwise resets every filter column to a // family default whenever the newly loaded data does not contain the current // value. Cleared by sync() once the view has settled. let pending = {}; // The hash this module last wrote, so our own replaceState is not re-applied. let lastWritten = null; // Guard against re-entrant hashchange handling during an async family switch. let applying = false; // ─── Parse ──────────────────────────────────────────────────────────────────── function parseHash(raw) { const out = {}; const s = String(raw || "").replace(/^#/, ""); if (!s) return out; s.split("&").forEach(part => { const i = part.indexOf("="); if (i < 1) return; try { const k = decodeURIComponent(part.slice(0, i)).trim(); const v = decodeURIComponent(part.slice(i + 1)).trim(); if (k) out[k] = v; } catch { // Malformed percent-escape — drop the pair rather than throw. } }); return out; } // Accept either a base family ("Llama-3.2") or a config family key // ("Llama-3.2-1B"), mirroring the ?family= query deep link. function resolveFamily(fam) { if (!fam) return null; if (BASE_FAMILIES[fam]) return { baseFamily: fam, variant: null }; if (config.model_families?.[fam]) { const base = deriveBaseFamily(fam); if (BASE_FAMILIES[base]) return { baseFamily: base, variant: fam }; } return null; } // Apply parsed hash params onto `filters`. Unknown families/metrics/variants // are ignored, never applied. Returns true when the base family changed (the // caller then has to reload that family's data). function applyParams(params) { let familyChanged = false; const fam = resolveFamily(params.family); if (fam) { familyChanged = fam.baseFamily !== filters.baseFamily; filters.baseFamily = fam.baseFamily; filters.variant = fam.variant; } if (params.variant !== undefined) { const v = params.variant; // A variant may be a data-derived model_family key that is absent from // config, so it can only be checked against the base-family rule here; // sync() drops it later if the loaded family does not offer it. filters.variant = (v && deriveBaseFamily(v) === filters.baseFamily) ? v : null; } if (params.metric && METRIC_COLS.includes(params.metric)) { filters.metric = params.metric; } pending = {}; FILTER_COLS.forEach(col => { if (params[col] !== undefined && params[col] !== "") pending[col] = params[col]; }); return familyChanged; } // ─── Serialize ──────────────────────────────────────────────────────────────── // Drop a variant the currently loaded family does not actually offer — the // sidebar only shows variants when there is more than one. function normalizeVariant() { const variants = BASE_FAMILIES[filters.baseFamily]?.variants || []; if (filters.variant && (variants.length <= 1 || !variants.includes(filters.variant))) { filters.variant = null; } } function buildHash() { const parts = []; const push = (k, v) => parts.push(encodeURIComponent(k) + "=" + encodeURIComponent(v)); push("family", filters.baseFamily); if (filters.variant) push("variant", filters.variant); // Always link the group_by (device) column — it is what people share. // The remaining filter columns are only linked when the loaded family // actually offers a choice; otherwise their button group is hidden and the // value is noise that would be re-derived identically anyway. const opts = availableOptions(); config.filters.forEach(f => { const v = filters[f.column]; if (v === "" || v === null || v === undefined) return; if (f.column !== GROUP_BY && (opts[f.column] || []).length <= 1) return; push(f.column, v); }); if (filters.metric) push("metric", filters.metric); return parts.join("&"); } // ─── Public: called from app.js ────────────────────────────────────────────── // Hook in updateDependentFilters(): the value the hash asked for, when it is // available in the freshly loaded data, else null (caller falls back to its // own default). "all" is only meaningful for the group_by column. function desiredFilterValue(col, availableStrVals) { if (!ENABLED) return null; const want = pending[col]; if (want === undefined) return null; if (col === GROUP_BY && want === "all") return "all"; return availableStrVals.includes(want) ? want : null; } // Hook at the end of render(): the view has settled, publish it. replaceState // keeps every button click out of the history stack. function sync() { if (!ENABLED) return; pending = {}; normalizeVariant(); const hash = buildHash(); if (hash === lastWritten && String(location.hash).replace(/^#/, "") === hash) return; lastWritten = hash; const url = location.pathname + location.search + "#" + hash; try { history.replaceState(history.state, "", url); } catch { // file:// and a few sandboxes reject replaceState; the assignment below // fires hashchange, which the lastWritten guard then ignores. location.hash = hash; } } function currentHash() { return ENABLED ? "#" + buildHash() : ""; } // ─── Incoming navigation ───────────────────────────────────────────────────── async function onHashChange() { if (!ENABLED || applying) return; const raw = String(location.hash).replace(/^#/, ""); if (raw === lastWritten) return; const params = parseHash(raw); if (!Object.keys(params).length) return; applying = true; try { const familyChanged = applyParams(params); renderSidebar(); if (familyChanged) { await switchBaseFamily(filters.baseFamily); } else { // Reset first so columns the URL omits fall back to their default, // then desiredFilterValue() re-applies the ones it does name. updateDependentFilters(true); render(); } } finally { applying = false; } } // ─── Init ───────────────────────────────────────────────────────────────────── // Applied here, before populateFilters()/switchBaseFamily() run, so the first // render already reflects the link. The hash therefore wins over the existing // ?family= query deep link, which is applied earlier in app.js. if (ENABLED) { const initial = parseHash(typeof location !== "undefined" ? location.hash : ""); if (Object.keys(initial).length) applyParams(initial); if (typeof window !== "undefined") window.addEventListener("hashchange", onHashChange); } return { desiredFilterValue, sync, currentHash }; } // end initUrlState