Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
| <title>SAE Explorer</title> | |
| <style> | |
| :root { | |
| --bg: #f6f7f9; | |
| --panel: #fff; | |
| --text: #151922; | |
| --muted: #647084; | |
| --border: #cbd3df; | |
| --accent: #1f6feb; | |
| --shadow: 0 1px 2px rgb(20 25 34 / .08), 0 10px 30px rgb(20 25 34 / .06); | |
| --token-card-width: 140px; | |
| } | |
| * { box-sizing: border-box; } | |
| body { | |
| margin: 0; | |
| background: var(--bg); | |
| color: var(--text); | |
| font: 14px/1.45 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; | |
| } | |
| header { | |
| position: sticky; | |
| top: 0; | |
| z-index: 5; | |
| background: var(--panel); | |
| border-bottom: 1px solid var(--border); | |
| box-shadow: var(--shadow); | |
| padding: 12px 18px; | |
| } | |
| .top { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| gap: 14px; | |
| } | |
| h1 { margin: 0; font-size: 19px; letter-spacing: 0; } | |
| .nav { | |
| display: flex; | |
| align-items: center; | |
| gap: 14px; | |
| } | |
| .nav a { | |
| color: var(--accent); | |
| font-weight: 750; | |
| text-decoration: none; | |
| } | |
| .nav a.active { color: #0f172a; } | |
| .nav a:hover { text-decoration: underline; } | |
| main { | |
| max-width: 1240px; | |
| margin: 0 auto; | |
| padding: 18px; | |
| } | |
| .panel { | |
| margin-top: 14px; | |
| background: var(--panel); | |
| border: 1px solid var(--border); | |
| border-radius: 8px; | |
| box-shadow: var(--shadow); | |
| padding: 12px; | |
| } | |
| .controls { | |
| display: grid; | |
| grid-template-columns: minmax(0, 1fr) 450px; | |
| gap: 12px; | |
| align-items: stretch; | |
| } | |
| .control-stack { | |
| display: flex; | |
| flex-wrap: wrap; | |
| align-content: flex-start; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .inline-control { | |
| display: grid; | |
| grid-template-columns: max-content max-content; | |
| align-items: center; | |
| column-gap: 8px; | |
| } | |
| .inline-control > span { white-space: nowrap; } | |
| .control-break { flex-basis: 100%; height: 0; } | |
| label { | |
| display: grid; | |
| gap: 5px; | |
| color: #435066; | |
| font-size: 12px; | |
| font-weight: 700; | |
| } | |
| textarea, select, input, button { | |
| width: 100%; | |
| border: 1px solid var(--border); | |
| border-radius: 7px; | |
| background: #fff; | |
| color: var(--text); | |
| font: inherit; | |
| padding: 8px 10px; | |
| } | |
| textarea { | |
| height: 92px; | |
| min-height: 92px; | |
| resize: vertical; | |
| font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; | |
| } | |
| #topK { width: 58px; min-width: 0; } | |
| #cardWidth { width: 78px; min-width: 0; } | |
| #weakRatio { width: 58px; min-width: 0; } | |
| button { | |
| cursor: pointer; | |
| min-height: 39px; | |
| background: #eef2f7; | |
| font-weight: 750; | |
| } | |
| .run-button { | |
| width: auto; | |
| min-height: 32px; | |
| padding: 6px 12px; | |
| border-color: #1458c8; | |
| background: var(--accent); | |
| color: #fff; | |
| font-weight: 850; | |
| } | |
| .memory-control { | |
| position: fixed; | |
| right: 12px; | |
| bottom: 10px; | |
| z-index: 4; | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 5px; | |
| width: auto; | |
| min-height: 24px; | |
| padding: 3px 6px; | |
| border: 1px solid #d7dee9; | |
| border-radius: 6px; | |
| background: rgb(255 255 255 / .78); | |
| color: #94a3b8; | |
| font-size: 11px; | |
| font-weight: 650; | |
| white-space: nowrap; | |
| } | |
| .memory-control input { width: auto; margin: 0; padding: 0; accent-color: #94a3b8; } | |
| .results { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(var(--token-card-width), 1fr)); | |
| gap: 10px; | |
| margin-top: 12px; | |
| } | |
| .token-card { | |
| border: 1px solid var(--border); | |
| border-radius: 8px; | |
| padding: 6px; | |
| background: #fff; | |
| min-width: 0; | |
| text-align: center; | |
| } | |
| .token-text { | |
| min-height: 25px; | |
| font-weight: 850; | |
| text-align: center; | |
| overflow-wrap: anywhere; | |
| margin-bottom: 6px; | |
| } | |
| .score-row { | |
| position: relative; | |
| min-height: 24px; | |
| margin-top: 3px; | |
| } | |
| .badge { | |
| position: relative; | |
| display: flex; | |
| align-items: center; | |
| width: 100%; | |
| min-height: 24px; | |
| padding: 3px 5px; | |
| border: 1px solid #d5dce7; | |
| border-radius: 5px; | |
| background: | |
| linear-gradient( | |
| 90deg, | |
| var(--score-bg, #edf2f7) 0, | |
| var(--score-bg, #edf2f7) var(--score-width, 100%), | |
| transparent var(--score-width, 100%), | |
| transparent 100% | |
| ); | |
| color: var(--text); | |
| font-size: 10px; | |
| font-weight: 700; | |
| text-align: left; | |
| cursor: pointer; | |
| } | |
| .badge:hover { | |
| border-color: var(--feature-color, var(--accent)); | |
| } | |
| .badge.hot { | |
| opacity: 1; | |
| border-color: var(--feature-color, var(--accent)); | |
| --score-bg: color-mix(in srgb, var(--feature-color, var(--accent)) 18%, white); | |
| box-shadow: inset 3px 0 0 var(--feature-color, var(--accent)); | |
| } | |
| .badge.weak { opacity: .16; } | |
| .badge.weak.hot, | |
| .badge.weak:hover { opacity: 1; } | |
| .badge-main { | |
| min-width: 0; | |
| max-width: calc(100% - 35px); | |
| overflow: hidden; | |
| white-space: nowrap; | |
| text-overflow: ellipsis; | |
| } | |
| .score { | |
| position: absolute; | |
| right: 5px; | |
| top: 50%; | |
| transform: translateY(-50%); | |
| width: 34px; | |
| text-align: right; | |
| color: var(--muted); | |
| font-size: 9px; | |
| font-variant-numeric: tabular-nums; | |
| pointer-events: none; | |
| } | |
| .empty, .error { | |
| margin-top: 12px; | |
| border: 1px dashed var(--border); | |
| border-radius: 8px; | |
| padding: 18px; | |
| color: var(--muted); | |
| background: #fff; | |
| } | |
| .error { color: #a41414; border-color: #f0b9b9; background: #fff7f7; } | |
| .request-indicator { | |
| position: fixed; | |
| left: 14px; | |
| top: 62px; | |
| z-index: 8; | |
| display: none; | |
| align-items: center; | |
| gap: 8px; | |
| min-height: 34px; | |
| padding: 8px 10px; | |
| border: 1px solid var(--border); | |
| border-radius: 8px; | |
| background: var(--panel); | |
| box-shadow: var(--shadow); | |
| color: #435066; | |
| font-size: 12px; | |
| font-weight: 800; | |
| } | |
| .request-indicator.visible { display: inline-flex; } | |
| .request-spinner { | |
| width: 14px; | |
| height: 14px; | |
| border: 2px solid #cbd5e1; | |
| border-top-color: var(--accent); | |
| border-radius: 50%; | |
| animation: request-spin .8s linear infinite; | |
| } | |
| @keyframes request-spin { to { transform: rotate(360deg); } } | |
| .meta-line { | |
| margin-top: 10px; | |
| color: var(--muted); | |
| font-size: 12px; | |
| font-weight: 650; | |
| } | |
| @media (max-width: 760px) { | |
| main { padding: 12px; } | |
| .controls { grid-template-columns: 1fr; } | |
| .top { align-items: flex-start; flex-direction: column; } | |
| } | |
| </style> | |
| <script defer src="https://analytics.liusida.com/umami/script.js" data-website-id="64322a37-ae7f-4635-ac78-8869ef79997b"></script> | |
| </head> | |
| <body> | |
| <header> | |
| <div class="top"> | |
| <h1>SAE Explorer</h1> | |
| <nav class="nav" aria-label="Primary"> | |
| <a href="/">Explorer</a> | |
| <a href="/sae-explorer" class="active">SAE Explorer</a> | |
| <a href="/stats">Stats</a> | |
| <a href="/annotate">Annotate</a> | |
| <a href="/random-components">Random</a> | |
| </nav> | |
| </div> | |
| </header> | |
| <main> | |
| <div class="panel"> | |
| <div class="controls"> | |
| <label> | |
| Text | |
| <textarea id="probeText" spellcheck="false">Maya stopped at the bank before the trip, waiting in line to deposit a check and withdraw enough cash for the weekend.</textarea> | |
| </label> | |
| <div class="control-stack"> | |
| <label class="inline-control"> | |
| <span>Model</span> | |
| <select id="modelSelect"></select> | |
| </label> | |
| <label class="inline-control"> | |
| <span>Layer</span> | |
| <select id="layerSelect"></select> | |
| </label> | |
| <span class="control-break" aria-hidden="true"></span> | |
| <label class="inline-control"> | |
| <span>Top K</span> | |
| <input id="topK" type="number" min="1" max="128" value="5" /> | |
| </label> | |
| <label class="inline-control"> | |
| <span>Card Width</span> | |
| <input id="cardWidth" type="number" min="100" max="360" step="20" value="140" /> | |
| </label> | |
| <label class="inline-control"> | |
| <span>Opacity Cutoff</span> | |
| <input id="weakRatio" type="number" min="0" max="1" step="0.05" value="0.5" /> | |
| </label> | |
| <button id="runProbe" class="run-button" type="button">Run</button> | |
| </div> | |
| <label class="memory-control" title="Keep loaded models in VRAM when switching."> | |
| <input id="keepModels" type="checkbox" checked /> | |
| <span>Cache LLMs in VRAM</span> | |
| </label> | |
| </div> | |
| <div id="metaLine" class="meta-line"></div> | |
| </div> | |
| <div id="message" class="empty">Choose a layer and run the SAE probe.</div> | |
| <div id="results" class="results"></div> | |
| </main> | |
| <div id="requestIndicator" class="request-indicator" role="status" aria-live="polite"> | |
| <span class="request-spinner" aria-hidden="true"></span> | |
| <span>Waiting for server...</span> | |
| </div> | |
| <script> | |
| const STORAGE_KEYS = { | |
| probeText: "saeExplorer.probeText", | |
| model: "saeExplorer.model", | |
| layer: "saeExplorer.layer", | |
| topK: "saeExplorer.topK", | |
| cardWidth: "saeExplorer.cardWidth", | |
| weakRatio: "saeExplorer.opacityCutoff", | |
| keepModels: "saeExplorer.keepModels", | |
| highlights: "saeExplorer.selectedFeatures", | |
| }; | |
| const initialParams = new URLSearchParams(window.location.search); | |
| const state = { | |
| models: [], | |
| meta: null, | |
| requestId: 0, | |
| pendingRequests: 0, | |
| lastProbeOutput: null, | |
| selectedFeatures: new Set(), | |
| initialFeatures: parseFeatureParams(initialParams), | |
| urlFeaturesApplied: false, | |
| }; | |
| const els = { | |
| text: document.getElementById("probeText"), | |
| model: document.getElementById("modelSelect"), | |
| layer: document.getElementById("layerSelect"), | |
| topK: document.getElementById("topK"), | |
| cardWidth: document.getElementById("cardWidth"), | |
| weakRatio: document.getElementById("weakRatio"), | |
| runProbe: document.getElementById("runProbe"), | |
| keepModels: document.getElementById("keepModels"), | |
| metaLine: document.getElementById("metaLine"), | |
| message: document.getElementById("message"), | |
| results: document.getElementById("results"), | |
| requestIndicator: document.getElementById("requestIndicator"), | |
| }; | |
| async function api(path, options = {}) { | |
| beginRequest(); | |
| try { | |
| const res = await fetch(path, { | |
| headers: { "content-type": "application/json" }, | |
| ...options, | |
| }); | |
| if (!res.ok) { | |
| let detail = res.statusText; | |
| try { detail = (await res.json()).detail || detail; } catch {} | |
| throw new Error(detail); | |
| } | |
| return res.json(); | |
| } finally { | |
| endRequest(); | |
| } | |
| } | |
| function beginRequest() { | |
| state.pendingRequests += 1; | |
| els.requestIndicator.classList.toggle("visible", state.pendingRequests > 0); | |
| } | |
| function endRequest() { | |
| state.pendingRequests = Math.max(0, state.pendingRequests - 1); | |
| els.requestIndicator.classList.toggle("visible", state.pendingRequests > 0); | |
| } | |
| async function init() { | |
| restoreControlValues(); | |
| applyScalarUrlParams(); | |
| const requestedText = readFirstParam(initialParams, ["text", "probe_text", "prompt"]); | |
| const savedText = localStorage.getItem(STORAGE_KEYS.probeText); | |
| if (requestedText !== null) els.text.value = requestedText; | |
| else if (savedText !== null) els.text.value = savedText; | |
| try { | |
| const modelsOut = await api("/api/models"); | |
| state.models = modelsOut.models || []; | |
| els.model.innerHTML = state.models.map(model => `<option value="${escapeAttr(model.model_name)}">${escapeHtml(model.display_name || model.model_name)}</option>`).join(""); | |
| const requestedModel = readFirstParam(initialParams, ["model", "model_name"]); | |
| const requestedLayer = readFirstParam(initialParams, ["layer", "layer_name"]); | |
| const savedModel = localStorage.getItem(STORAGE_KEYS.model); | |
| if (requestedModel && state.models.some(model => model.model_name === requestedModel)) els.model.value = requestedModel; | |
| else if (savedModel && state.models.some(model => model.model_name === savedModel)) els.model.value = savedModel; | |
| else if (state.models.some(model => model.model_name === "gpt2")) els.model.value = "gpt2"; | |
| await loadSaeMeta({ restoreLayer: true, requestedLayer }); | |
| persistControls(); | |
| runProbe(); | |
| } catch (err) { | |
| showError(err.message); | |
| } | |
| } | |
| async function loadSaeMeta(options = {}) { | |
| state.meta = await api(`/api/sae-meta?model=${encodeURIComponent(els.model.value)}`); | |
| els.layer.innerHTML = state.meta.layers.map(layer => `<option value="${escapeAttr(layer)}">${escapeHtml(layer)}</option>`).join(""); | |
| const savedLayer = localStorage.getItem(STORAGE_KEYS.layer); | |
| if (options.requestedLayer && state.meta.layers.includes(options.requestedLayer)) els.layer.value = options.requestedLayer; | |
| else if (options.restoreLayer && savedLayer && state.meta.layers.includes(savedLayer)) els.layer.value = savedLayer; | |
| else if (state.meta.layers.includes("layer_05")) els.layer.value = "layer_05"; | |
| if (!state.urlFeaturesApplied && state.initialFeatures !== null) { | |
| state.selectedFeatures = new Set(state.initialFeatures); | |
| state.urlFeaturesApplied = true; | |
| persistFeatureHighlights(); | |
| } else { | |
| restoreFeatureHighlightsForContext(); | |
| } | |
| renderMetaLine(); | |
| } | |
| function renderMetaLine() { | |
| const sae = state.meta?.sae || {}; | |
| const parts = [ | |
| sae.repo_id ? `SAE: ${sae.repo_id}` : "", | |
| sae.width ? `width=${sae.width}` : "", | |
| sae.top_k ? `SAE top-k=${sae.top_k}` : "", | |
| sae.activation ? `activation=${sae.activation}` : "", | |
| ].filter(Boolean); | |
| els.metaLine.textContent = parts.join(" · "); | |
| } | |
| async function runProbe() { | |
| const requestId = ++state.requestId; | |
| if (!els.text.value.trim()) { | |
| els.results.innerHTML = ""; | |
| els.message.hidden = false; | |
| els.message.className = "empty"; | |
| els.message.textContent = "Enter text to run the SAE probe."; | |
| return; | |
| } | |
| if (!els.results.children.length) { | |
| els.message.hidden = false; | |
| els.message.className = "empty"; | |
| els.message.textContent = "Running SAE probe..."; | |
| } | |
| try { | |
| const out = await api("/api/sae-probe", { | |
| method: "POST", | |
| body: JSON.stringify({ | |
| text: els.text.value, | |
| model_name: els.model.value, | |
| layer: els.layer.value, | |
| top_k: Number(els.topK.value || 5), | |
| keep_models: els.keepModels.checked, | |
| }), | |
| }); | |
| if (requestId !== state.requestId) return; | |
| renderResults(out); | |
| } catch (err) { | |
| if (requestId !== state.requestId) return; | |
| showError(err.message); | |
| } | |
| } | |
| function renderResults(out) { | |
| state.lastProbeOutput = out; | |
| if (out.truncated) { | |
| els.message.hidden = false; | |
| els.message.className = "empty"; | |
| els.message.textContent = `Input was truncated to ${out.max_length} tokens.`; | |
| } else { | |
| els.message.hidden = true; | |
| } | |
| els.results.innerHTML = (out.tokens || []).map(token => ` | |
| <div class="token-card"> | |
| <div class="token-text" title="${escapeAttr(token.token)}">${escapeHtml(token.token_text || token.token)}</div> | |
| ${(token.top || []).map(feature => featureBadge(feature, tokenTopActivation(token))).join("")} | |
| </div> | |
| `).join(""); | |
| els.results.querySelectorAll(".badge[data-feature]").forEach(node => { | |
| node.addEventListener("click", event => selectFeature(event, Number(node.dataset.feature))); | |
| }); | |
| paintFeatureHighlights(); | |
| } | |
| function featureBadge(feature, tokenTop) { | |
| const id = Number(feature.feature); | |
| const activation = Number(feature.activation || 0); | |
| const ratio = tokenTop > 0 ? activation / tokenTop : 0; | |
| const width = 100 * Math.max(0, Math.min(1, ratio)); | |
| const cutoff = Math.max(0, Math.min(1, Number(els.weakRatio.value || 0.5))); | |
| const weak = Number.isFinite(tokenTop) && tokenTop > 0 && activation < tokenTop * cutoff; | |
| const active = state.selectedFeatures.has(id); | |
| return ` | |
| <div class="score-row"> | |
| <button class="badge ${active ? "hot" : ""} ${weak ? "weak" : ""}" type="button" data-feature="${id}" aria-pressed="${active ? "true" : "false"}" title="${escapeAttr(`F${id}: activation ${formatScore(activation)}, preactivation ${formatScore(Number(feature.preactivation || 0))}`)}" style="--score-width:${width.toFixed(1)}%;--score-bg:${featureColor(id)}22;--feature-color:${featureColor(id)}"> | |
| <span class="badge-main">F${id}</span> | |
| </button> | |
| <span class="score">${formatScore(activation)}</span> | |
| </div> | |
| `; | |
| } | |
| function tokenTopActivation(token) { | |
| const values = (token.top || []).map(item => Number(item.activation || 0)); | |
| return values.length ? Math.max(...values) : 0; | |
| } | |
| function scheduleProbe() { | |
| localStorage.setItem(STORAGE_KEYS.probeText, els.text.value); | |
| persistControls(); | |
| } | |
| function handleTextKeydown(event) { | |
| if ((event.ctrlKey || event.metaKey) && event.key === "Enter") { | |
| event.preventDefault(); | |
| localStorage.setItem(STORAGE_KEYS.probeText, els.text.value); | |
| persistControls(); | |
| runProbe(); | |
| } | |
| } | |
| function updateCardWidth() { | |
| const value = Math.max(100, Math.min(360, Number(els.cardWidth.value || 140))); | |
| document.documentElement.style.setProperty("--token-card-width", `${value}px`); | |
| localStorage.setItem(STORAGE_KEYS.cardWidth, String(value)); | |
| } | |
| function rerenderLastProbe() { | |
| if (state.lastProbeOutput) renderResults(state.lastProbeOutput); | |
| } | |
| function selectFeature(event, feature) { | |
| const id = Number(feature); | |
| if (!Number.isFinite(id)) return; | |
| if (event.ctrlKey || event.metaKey) { | |
| if (state.selectedFeatures.has(id)) state.selectedFeatures.delete(id); | |
| else state.selectedFeatures.add(id); | |
| } else if (state.selectedFeatures.size === 1 && state.selectedFeatures.has(id)) { | |
| state.selectedFeatures.clear(); | |
| } else { | |
| state.selectedFeatures.clear(); | |
| state.selectedFeatures.add(id); | |
| } | |
| persistFeatureHighlights(); | |
| paintFeatureHighlights(); | |
| } | |
| function paintFeatureHighlights() { | |
| els.results.querySelectorAll(".badge[data-feature]").forEach(node => { | |
| const active = state.selectedFeatures.has(Number(node.dataset.feature)); | |
| node.classList.toggle("hot", active); | |
| node.setAttribute("aria-pressed", active ? "true" : "false"); | |
| }); | |
| } | |
| function featureHighlightStorageKey(model = els.model.value, layer = els.layer.value) { | |
| return `${model}:${layer}`; | |
| } | |
| function readFeatureHighlightStore() { | |
| try { | |
| const parsed = JSON.parse(localStorage.getItem(STORAGE_KEYS.highlights) || "{}"); | |
| return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {}; | |
| } catch { | |
| return {}; | |
| } | |
| } | |
| function persistFeatureHighlights() { | |
| const store = readFeatureHighlightStore(); | |
| const key = featureHighlightStorageKey(); | |
| const values = [...state.selectedFeatures].filter(Number.isFinite).sort((a, b) => a - b); | |
| if (values.length) store[key] = values; | |
| else delete store[key]; | |
| localStorage.setItem(STORAGE_KEYS.highlights, JSON.stringify(store)); | |
| } | |
| function restoreFeatureHighlightsForContext() { | |
| const store = readFeatureHighlightStore(); | |
| const saved = Array.isArray(store[featureHighlightStorageKey()]) ? store[featureHighlightStorageKey()] : []; | |
| state.selectedFeatures = new Set(saved.map(Number).filter(Number.isFinite)); | |
| } | |
| function parseFeatureParams(params) { | |
| const raw = readFirstParam(params, ["features", "feature", "selected_features", "selectedFeatures"]); | |
| if (raw === null) return null; | |
| return raw | |
| .split(/[,+ ]+/) | |
| .map(value => Number(value.trim())) | |
| .filter(Number.isFinite); | |
| } | |
| function applyScalarUrlParams() { | |
| setInputFromParam(els.topK, ["top_k", "topK"]); | |
| setInputFromParam(els.cardWidth, ["card_width", "cardWidth"]); | |
| setInputFromParam(els.weakRatio, ["weak_ratio", "weakRatio", "opacity_cutoff", "opacityCutoff"]); | |
| } | |
| function setInputFromParam(input, names) { | |
| const value = readFirstParam(initialParams, names); | |
| if (value !== null && value !== "") input.value = value; | |
| } | |
| function readFirstParam(params, names) { | |
| for (const name of names) { | |
| if (params.has(name)) return params.get(name); | |
| } | |
| return null; | |
| } | |
| function restoreControlValues() { | |
| setNumberInputFromStorage(els.topK, STORAGE_KEYS.topK, 1, 128); | |
| setNumberInputFromStorage(els.cardWidth, STORAGE_KEYS.cardWidth, 100, 360); | |
| setNumberInputFromStorage(els.weakRatio, STORAGE_KEYS.weakRatio, 0, 1); | |
| els.keepModels.checked = localStorage.getItem(STORAGE_KEYS.keepModels) !== "0"; | |
| } | |
| function setNumberInputFromStorage(input, key, min, max) { | |
| const raw = localStorage.getItem(key); | |
| if (raw === null) return; | |
| const value = Number(raw); | |
| if (!Number.isFinite(value)) return; | |
| input.value = String(Math.max(min, Math.min(max, value))); | |
| } | |
| function persistControls() { | |
| localStorage.setItem(STORAGE_KEYS.model, els.model.value); | |
| localStorage.setItem(STORAGE_KEYS.layer, els.layer.value); | |
| localStorage.setItem(STORAGE_KEYS.topK, els.topK.value); | |
| localStorage.setItem(STORAGE_KEYS.cardWidth, els.cardWidth.value); | |
| localStorage.setItem(STORAGE_KEYS.weakRatio, els.weakRatio.value); | |
| localStorage.setItem(STORAGE_KEYS.keepModels, els.keepModels.checked ? "1" : "0"); | |
| } | |
| function showError(message) { | |
| els.message.hidden = false; | |
| els.message.className = "error"; | |
| els.message.textContent = message; | |
| } | |
| function featureColor(featureId) { | |
| return `hsl(${(37 * Number(featureId)) % 360} 74% 48%)`; | |
| } | |
| function formatScore(value) { | |
| const number = Number(value || 0); | |
| const abs = Math.abs(number); | |
| if (abs >= 100) return number.toFixed(0); | |
| if (abs >= 10) return number.toFixed(1); | |
| return number.toFixed(2); | |
| } | |
| function escapeHtml(value) { | |
| return String(value).replace(/[&<>"']/g, char => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[char])); | |
| } | |
| function escapeAttr(value) { | |
| return escapeHtml(value); | |
| } | |
| els.model.addEventListener("change", async () => { | |
| persistControls(); | |
| try { | |
| await loadSaeMeta(); | |
| persistControls(); | |
| runProbe(); | |
| } catch (err) { | |
| showError(err.message); | |
| } | |
| }); | |
| els.layer.addEventListener("change", () => { persistControls(); restoreFeatureHighlightsForContext(); runProbe(); }); | |
| els.topK.addEventListener("change", () => { persistControls(); runProbe(); }); | |
| els.topK.addEventListener("input", () => { persistControls(); runProbe(); }); | |
| els.cardWidth.addEventListener("change", updateCardWidth); | |
| els.cardWidth.addEventListener("input", updateCardWidth); | |
| els.weakRatio.addEventListener("change", () => { persistControls(); rerenderLastProbe(); }); | |
| els.weakRatio.addEventListener("input", () => { persistControls(); rerenderLastProbe(); }); | |
| els.keepModels.addEventListener("change", () => { persistControls(); if (!els.keepModels.checked) runProbe(); }); | |
| els.text.addEventListener("input", scheduleProbe); | |
| els.text.addEventListener("keydown", handleTextKeydown); | |
| els.runProbe.addEventListener("click", () => { | |
| localStorage.setItem(STORAGE_KEYS.probeText, els.text.value); | |
| persistControls(); | |
| runProbe(); | |
| }); | |
| updateCardWidth(); | |
| init(); | |
| </script> | |
| </body> | |
| </html> | |