| // βββ 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 | |