qwen-scope-live / index.html
Ex0bit's picture
initial qwen-scope-live deploy
f2ae1f5 verified
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Qwen-Scope · Live SAE Feature Steering</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<style>
:root {
--bg: #08080d;
--bg-panel: rgba(18, 18, 26, 0.78);
--bg-panel-strong: rgba(24, 24, 34, 0.92);
--border: rgba(255,255,255,0.08);
--border-strong: rgba(255,255,255,0.16);
--fg: #ececf1;
--fg-dim: #9b9ba8;
--fg-faint: #5e5e6e;
--accent: #7df9ff;
--accent-2: #ff7df9;
--warn: #ffb84d;
--good: #74e2a3;
--bad: #ff7d7d;
--mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace;
--sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
* { box-sizing: border-box; }
html, body { margin:0; padding:0; height:100%; background:var(--bg); color:var(--fg); font-family:var(--sans); overflow:hidden; }
#scene { position:fixed; inset:0; z-index:0; }
#ui { position:fixed; inset:0; z-index:10; pointer-events:none; display:grid;
grid-template-columns: 380px 1fr 420px;
grid-template-rows: 1fr auto;
gap:14px; padding:14px; }
#ui > section { pointer-events:auto; }
.panel { background:var(--bg-panel); border:1px solid var(--border);
border-radius:14px; padding:14px; backdrop-filter: blur(14px) saturate(140%);
-webkit-backdrop-filter: blur(14px) saturate(140%); overflow:hidden;
transition: padding .15s ease, max-height .25s ease; }
.panel h2 { margin:0 0 10px; font-size:11px; font-weight:600;
text-transform:uppercase; letter-spacing:0.14em; color:var(--fg-dim);
display:flex; justify-content:space-between; align-items:center; gap:8px; }
.panel h3 { margin:0 0 6px; font-size:13px; font-weight:600; color:var(--fg); }
/* Minimize button on each panel header */
.min-btn { background:transparent; border:1px solid var(--border-strong);
color:var(--fg-dim); border-radius:5px; padding:1px 8px; font-size:14px;
font-family:var(--mono); cursor:pointer; line-height:1; min-width:0;
font-weight:400; letter-spacing:0; }
.min-btn:hover { background:var(--bg-panel-strong); color:var(--fg);
border-color:var(--accent); transform:none; box-shadow:none; }
.panel.collapsed { padding:8px 14px; }
.panel.collapsed > h2 { margin-bottom:0; }
.panel.collapsed > :not(h2) { display:none; }
/* Header strip */
#header { position:fixed; top:14px; left:50%; transform:translateX(-50%);
z-index:20; pointer-events:auto;
background:var(--bg-panel-strong); border:1px solid var(--border-strong);
border-radius:999px; padding:8px 18px; display:flex; gap:14px;
align-items:center; font-family:var(--mono); font-size:12px; color:var(--fg-dim);
backdrop-filter: blur(20px); }
#header .dot { width:8px; height:8px; border-radius:50%; background:var(--bad); transition:background .3s; }
#header.live .dot { background:var(--good); box-shadow:0 0 12px var(--good); }
#header.loading .dot { background:var(--warn); box-shadow:0 0 12px var(--warn); animation:pulse 1.5s ease-in-out infinite; }
@keyframes pulse { 0%,100% { opacity:1; } 50% { opacity:0.4; } }
#header b { color:var(--fg); font-weight:600; }
#model-select {
background:transparent; color:var(--fg); border:1px solid var(--border-strong);
border-radius:6px; padding:3px 8px; font-family:var(--mono); font-size:11px;
outline:none; cursor:pointer; max-width:280px;
}
#model-select:focus { border-color:var(--accent); }
#model-select option { background:#16161e; color:var(--fg); }
/* Tab strip */
#tabs { position:fixed; top:62px; left:50%; transform:translateX(-50%); z-index:18;
display:flex; gap:6px; pointer-events:auto; background:var(--bg-panel-strong);
border:1px solid var(--border-strong); border-radius:999px;
padding:4px; backdrop-filter: blur(20px); }
.tab { background:transparent; border:none; color:var(--fg-dim);
padding:6px 16px; font-size:11px; font-weight:600;
text-transform:uppercase; letter-spacing:0.08em; border-radius:999px;
cursor:pointer; transition: all 0.15s ease; }
.tab:hover { color:var(--fg); background:rgba(255,255,255,0.04); }
.tab.active { background: linear-gradient(135deg, rgba(125,249,255,0.18), rgba(255,125,249,0.12));
color:var(--fg); border:1px solid var(--border-strong); }
/* Tab visibility — hide panels not matching the current tab */
body[data-tab="steering"] [data-show-tab]:not([data-show-tab~="steering"]) { display:none !important; }
body[data-tab="evaluation"] [data-show-tab]:not([data-show-tab~="evaluation"]) { display:none !important; }
body[data-tab="datacentric"] [data-show-tab]:not([data-show-tab~="datacentric"]) { display:none !important; }
/* Loading overlay shown during model swap */
#loading-overlay {
position:fixed; inset:0; z-index:50; display:none;
background:rgba(8,8,13,0.78); backdrop-filter: blur(6px);
align-items:center; justify-content:center;
}
#loading-overlay.visible { display:flex; }
#loading-overlay .lo-card {
background:var(--bg-panel-strong); border:1px solid var(--border-strong);
border-radius:14px; padding:24px 32px; text-align:center; min-width:300px;
}
/* Left: prompt + controls (top-left of screen, below the header) */
#left { display:flex; flex-direction:column; gap:14px; min-height:0;
grid-column: 1; grid-row: 1; align-self: start;
padding-top: 110px; }
textarea, input[type="text"], input[type="number"] {
width:100%; background:rgba(0,0,0,0.35); border:1px solid var(--border-strong);
color:var(--fg); border-radius:8px; padding:10px 12px; font-family:var(--mono);
font-size:13px; outline:none; resize:vertical;
}
textarea:focus, input:focus { border-color:var(--accent); box-shadow:0 0 0 3px rgba(125,249,255,0.12); }
textarea { min-height:72px; }
.row { display:flex; gap:8px; align-items:center; }
.row > * { flex:1; }
.row > .grow0 { flex:0; }
button { background: linear-gradient(135deg, rgba(125,249,255,0.18), rgba(255,125,249,0.12));
border:1px solid var(--border-strong); color:var(--fg); border-radius:8px;
padding:9px 14px; font-family:var(--sans); font-size:13px; font-weight:600;
cursor:pointer; transition:all .15s; letter-spacing:0.02em; }
button:hover:not(:disabled) { border-color:var(--accent); transform:translateY(-1px); box-shadow:0 4px 14px rgba(125,249,255,0.18); }
button:disabled { opacity:0.4; cursor:not-allowed; }
button.primary { background: linear-gradient(135deg, #7df9ff, #ff7df9); color:#0a0a14; border-color:transparent; }
button.primary:hover:not(:disabled) { box-shadow:0 4px 18px rgba(255,125,249,0.32); }
button.ghost { background:transparent; }
/* Middle: nothing (3D scene shows through) */
#middle { grid-column: 2; grid-row: 1; pointer-events:none; }
/* Bottom: output band spans all columns; sits below cloud area */
#bottom { grid-column: 1 / -1; grid-row: 2; pointer-events:auto;
display:grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap:14px;
max-height: 38vh; min-height: 0;
margin: 0 0 0 0; }
#bottom .panel { display:flex; flex-direction:column;
min-width: 0; min-height: 0; overflow:hidden; }
#bottom .panel.collapsed { padding:8px 14px; }
#bottom .panel.collapsed > .panel-body { display:none; }
.panel-body { flex: 1 1 auto; overflow:auto; min-height:0; min-width:0; }
#right .panel { min-width:0; max-width:100%; }
#right .panel > div { max-width:100%; overflow:auto; }
/* Right: top features panel only — output moved to bottom band */
#right { display:flex; flex-direction:column; gap:14px; min-height:0;
grid-column: 3; grid-row: 1; max-height: calc(100vh - 28px); padding-top:110px; }
#features { flex: 1 1 auto; min-height: 120px; overflow-y:auto; }
#features::-webkit-scrollbar { width:6px; }
#features::-webkit-scrollbar-thumb { background:var(--border-strong); border-radius:3px; }
.feat { padding:10px; border:1px solid var(--border); border-radius:10px;
margin-bottom:8px; background:rgba(0,0,0,0.25); transition: border-color .2s, background .2s; }
.feat:hover { border-color:var(--accent); }
.feat.steered { border-color:var(--accent-2); background:rgba(255,125,249,0.06); }
.feat-head { display:flex; justify-content:space-between; align-items:center; margin-bottom:6px; }
.feat-id { font-family:var(--mono); font-size:12px; color:var(--fg); }
.feat-act { font-family:var(--mono); font-size:11px; color:var(--good); }
.feat-act.steered { color:var(--accent-2); }
.slider-row { display:flex; align-items:center; gap:8px; margin-top:4px; }
.slider-row input[type=range] { flex:1; -webkit-appearance:none; height:4px; background:var(--border-strong); border-radius:2px; outline:none; }
.slider-row input[type=range]::-webkit-slider-thumb {
-webkit-appearance:none; width:14px; height:14px; border-radius:50%;
background: linear-gradient(135deg, #7df9ff, #ff7df9); cursor:pointer;
box-shadow:0 0 8px rgba(125,249,255,0.5); border:none;
}
.slider-row input[type=range]::-moz-range-thumb {
width:14px; height:14px; border-radius:50%;
background: linear-gradient(135deg, #7df9ff, #ff7df9); cursor:pointer; border:none;
}
.slider-row .alpha-val { font-family:var(--mono); font-size:11px; color:var(--fg-dim); width:48px; text-align:right; }
.feat-tools { display:flex; gap:6px; }
.feat-tools button { padding:3px 8px; font-size:10px; font-weight:500; }
/* Output area */
#output-wrap { flex:1; overflow-y:auto; }
#output-wrap::-webkit-scrollbar { width:6px; }
#output-wrap::-webkit-scrollbar-thumb { background:var(--border-strong); border-radius:3px; }
.out-block { font-family:var(--mono); font-size:12px; line-height:1.55;
background:rgba(0,0,0,0.4); border:1px solid var(--border);
border-radius:8px; padding:10px; margin-bottom:8px; white-space:pre-wrap;
word-break: break-word; color:var(--fg); min-height:36px; }
.out-block.baseline { border-color:rgba(125,249,255,0.3); }
.out-block.steered { border-color:rgba(255,125,249,0.3); }
.out-label { display:flex; justify-content:space-between; align-items:center; margin-bottom:4px;
font-size:10px; text-transform:uppercase; letter-spacing:0.12em; color:var(--fg-dim); }
.verifier { font-family:var(--mono); font-size:10px; color:var(--fg-dim); margin-top:6px; padding:6px 8px;
border-left:2px solid var(--border-strong); }
.verifier .delta-up { color:var(--good); }
.verifier .delta-down { color:var(--accent-2); }
/* Hover tooltip for selected feature on 3D cloud */
#tooltip { position:fixed; z-index:25; pointer-events:none;
background:var(--bg-panel-strong); border:1px solid var(--border-strong);
border-radius:8px; padding:8px 12px; font-family:var(--mono); font-size:11px;
line-height:1.45;
color:var(--fg); display:none; transform: translate(-50%, calc(-100% - 12px));
min-width:180px; max-width:280px; backdrop-filter: blur(20px); }
/* Loader */
.loader { display:inline-block; width:12px; height:12px; border:2px solid var(--border-strong);
border-top-color:var(--accent); border-radius:50%; animation:spin 0.8s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
.empty { color:var(--fg-faint); font-size:12px; text-align:center; padding:24px 8px; }
.hint { font-size:11px; color:var(--fg-faint); margin-top:6px; line-height:1.4; }
.footer { position:fixed; bottom:8px; left:14px; z-index:20; font-family:var(--mono);
font-size:10px; color:var(--fg-faint); letter-spacing:0.04em; }
.legend { font-family:var(--mono); font-size:10px; color:var(--fg-faint); display:flex; gap:12px; margin-top:6px; }
.legend span::before { content:''; display:inline-block; width:8px; height:8px; border-radius:50%; margin-right:5px; vertical-align:middle; }
.legend .top::before { background:var(--accent); box-shadow:0 0 6px var(--accent); }
.legend .pick::before { background:var(--accent-2); box-shadow:0 0 6px var(--accent-2); }
.legend .dim::before { background:var(--fg-faint); }
/* small-screen graceful fallback */
@media (max-width: 1100px) {
#ui { grid-template-columns: 1fr; grid-template-rows: auto auto; }
#left { grid-column:1; grid-row:1; max-height:none; }
#right { grid-column:1; grid-row:2; padding-top:14px; max-height:none; }
#middle { display:none; }
}
</style>
</head>
<body>
<canvas id="scene"></canvas>
<div id="header">
<span class="dot"></span>
<span>QWEN-SCOPE LIVE</span>
<span style="opacity:.4">·</span>
<select id="model-select" title="Switch the loaded model + SAE pair">
<option>connecting…</option>
</select>
<span style="opacity:.4">·</span>
<span id="hdr-layer-wrap" title="Click to change SAE layer">
layer <input type="number" id="layer-input" value="?" min="0" max="0"
style="width:48px; padding:1px 4px; background:transparent; color:var(--accent);
border:1px solid var(--border-strong); border-radius:4px;
font-family:var(--mono); font-size:12px; text-align:center;" />
<span id="hdr-layer-meta">· mps · bfloat16</span>
</span>
<span style="opacity:.4">·</span>
<span id="hdr-features">— features</span>
</div>
<div id="tabs">
<button class="tab active" data-tab="steering">Steering</button>
<button class="tab" data-tab="evaluation">Evaluation</button>
<button class="tab" data-tab="datacentric">Data-centric</button>
</div>
<div id="loading-overlay">
<div class="lo-card">
<div class="loader" style="width:24px;height:24px;border-width:3px;"></div>
<div id="lo-msg" style="margin-top:14px; font-size:13px; color:var(--fg);">Loading…</div>
<div id="lo-detail" style="margin-top:6px; font-size:11px; color:var(--fg-faint); font-family:var(--mono);"></div>
</div>
</div>
<div id="ui">
<section id="left">
<div class="panel" data-pid="prompt" data-show-tab="steering">
<h2>1 · Prompt <button class="min-btn" data-min="prompt" title="Minimize panel"></button></h2>
<textarea id="prompt" placeholder="The capital of France is">The capital of France is</textarea>
<div class="row" style="margin-top:8px;">
<label style="font-size:11px; color:var(--fg-dim); flex:0 0 auto;">top K</label>
<input type="number" id="top-k" value="20" min="1" max="500" style="flex:0 0 80px;" />
<button class="primary" id="btn-encode" style="flex:1;">Encode &amp; show top features</button>
</div>
<div class="hint">
Encodes the prompt's last-token residual at the SAE's layer and ranks the firing features.
TopK SAE has at most K=50 nonzero features per token, so request up to ~50.
</div>
</div>
<div class="panel" data-pid="generate" data-show-tab="steering">
<h2>2 · Generate <button class="min-btn" data-min="generate" title="Minimize panel"></button></h2>
<div class="row">
<label style="font-size:11px; color:var(--fg-dim); flex:0 0 auto;">tokens</label>
<input type="number" id="max-tokens" value="40" min="5" max="200" style="flex:0 0 80px;" />
<button class="primary" id="btn-generate" disabled>Generate baseline + steered</button>
</div>
<div class="hint">
Runs <code>model.generate</code> twice: once with no hooks, once with all active sliders applied
as residual-stream additions <code>h ← h + α · W_dec[:, feat]</code>.
</div>
</div>
<div class="panel" data-pid="viz" data-show-tab="steering evaluation datacentric" style="flex:0 0 auto;">
<h2>3 · Visualization <button class="min-btn" data-min="viz" title="Minimize panel"></button></h2>
<div class="legend">
<span class="dim">all 32K features</span>
<span class="top">top firing</span>
<span class="pick">steered</span>
</div>
<div class="hint">
Each point is one SAE feature; positions are the top-3 PCA components of <code>W_enc</code>.
Click a point to add it as a steering target.
</div>
</div>
</section>
<section id="middle"></section>
<section id="right">
<div class="panel" id="features-panel" data-pid="features" data-show-tab="steering"
style="flex: 0 1 auto; max-height: 38vh; display:flex; flex-direction:column; min-height:0;">
<h2>Top features <span id="feat-count" style="font-family:var(--mono); color:var(--fg-faint);"></span>
<button class="min-btn" data-min="features" title="Minimize panel"></button></h2>
<div id="features" style="overflow-y:auto; flex:1 1 auto; min-height:0;"><div class="empty">Encode a prompt to populate features.</div></div>
</div>
<div class="panel" id="heatmap-panel" data-show-tab="steering"
style="flex: 1 1 auto; display:flex; flex-direction:column; min-height:200px;">
<h2>Per-token feature heatmap
<span style="display:flex; gap:8px; align-items:center;">
<label style="font-size:10px; color:var(--fg-faint);">
<input type="checkbox" id="heatmap-skip-first" style="vertical-align:middle;" /> skip first
</label>
<button class="min-btn" data-min="heatmap-panel" title="Minimize panel"></button>
</span>
</h2>
<div id="heatmap-grid" style="overflow:auto; flex:1 1 auto; min-height:0;">
<span class="empty">Encode a prompt — heatmap fills automatically.</span>
</div>
</div>
<!-- Evaluation: corpus encoding + signature heatmap -->
<div class="panel" data-show-tab="evaluation" id="eval-corpus">
<h2>Evaluation · corpus encode
<button class="min-btn" data-min="eval-corpus" title="Minimize panel"></button>
</h2>
<textarea id="eval-prompts" rows="6" placeholder="One prompt per line.&#10;Example:&#10;The capital of France is&#10;Bonjour comment allez-vous&#10;def fibonacci(n):&#10;The mitochondria is the powerhouse"></textarea>
<div class="row" style="margin-top:8px;">
<button class="primary" id="btn-eval-encode" style="flex:1;">Encode corpus</button>
</div>
<div class="hint">
Encodes each prompt's last-token residual through the SAE. Returns
per-sample top features and corpus-level firing rates.
</div>
</div>
<div class="panel" data-show-tab="evaluation" id="eval-corpus-features">
<h2>Corpus features <span id="eval-stats" style="font-family:var(--mono); color:var(--fg-faint);"></span>
<button class="min-btn" data-min="eval-corpus-features" title="Minimize panel"></button>
</h2>
<div id="eval-features-list" style="overflow-y:auto; max-height:30vh;">
<div class="empty">Encode a corpus to see feature firing rates.</div>
</div>
</div>
<div class="panel" data-show-tab="evaluation" id="eval-compare">
<h2>Compare two prompt sets
<button class="min-btn" data-min="eval-compare" title="Minimize panel"></button>
</h2>
<textarea id="cmp-a" rows="3" placeholder="Set A — one prompt per line"></textarea>
<textarea id="cmp-b" rows="3" placeholder="Set B — one prompt per line" style="margin-top:6px;"></textarea>
<div class="row" style="margin-top:6px;">
<button class="primary" id="btn-cmp" style="flex:1;">Find distinguishing features</button>
</div>
<div class="hint">
Encodes both sets and ranks features by |fire_rate(A) − fire_rate(B)|.
Shows which features distinguish A from B.
</div>
</div>
<!-- Data-centric: filter + steered synthesis -->
<div class="panel" data-show-tab="datacentric" id="dc-corpus">
<h2>Data-centric · corpus
<button class="min-btn" data-min="dc-corpus" title="Minimize panel"></button>
</h2>
<textarea id="dc-prompts" rows="5" placeholder="One prompt per line — same shape as Evaluation."></textarea>
<div class="row" style="margin-top:8px;">
<button class="primary" id="btn-dc-encode" style="flex:1;">Encode for filtering</button>
</div>
<div class="hint">
Encode docs, then filter by feature signature.
</div>
</div>
<div class="panel" data-show-tab="datacentric" id="dc-filter">
<h2>Filter
<button class="min-btn" data-min="dc-filter" title="Minimize panel"></button>
</h2>
<div class="row">
<label style="flex:0 0 auto; font-size:11px; color:var(--fg-dim);">feature</label>
<input type="number" id="dc-filter-id" placeholder="feature id" min="0" max="199999" style="flex:1;" />
<select id="dc-filter-mode" style="flex:0 0 auto; background:rgba(0,0,0,0.35); color:var(--fg); border:1px solid var(--border-strong); border-radius:6px; padding:6px 8px; font-family:var(--mono); font-size:11px;">
<option value="include">includes</option>
<option value="exclude">excludes</option>
</select>
</div>
<div class="row" style="margin-top:8px;">
<button id="btn-dc-filter" style="flex:1;">Apply filter</button>
<button id="btn-dc-clear" class="ghost" style="flex:0 0 auto;">clear</button>
</div>
<div class="hint">
"Includes" = keep only docs where this feature fired (any activation).
"Excludes" = drop docs where it fired.
</div>
</div>
<div class="panel" data-show-tab="datacentric" id="dc-synth">
<h2>Steered synthesis
<button class="min-btn" data-min="dc-synth" title="Minimize panel"></button>
</h2>
<div class="row">
<label style="flex:0 0 auto; font-size:11px; color:var(--fg-dim);">feature</label>
<input type="number" id="dc-synth-id" placeholder="feature id" min="0" max="199999" style="flex:1;" />
<label style="flex:0 0 auto; font-size:11px; color:var(--fg-dim);">α</label>
<input type="number" id="dc-synth-alpha" value="50" min="-200" max="200" style="flex:0 0 60px;" />
</div>
<div class="row" style="margin-top:6px;">
<label style="flex:0 0 auto; font-size:11px; color:var(--fg-dim);">tokens</label>
<input type="number" id="dc-synth-tokens" value="30" min="5" max="200" style="flex:0 0 70px;" />
<button class="primary" id="btn-dc-synth" style="flex:1;">Synthesize from corpus</button>
</div>
<div class="hint">
For each prompt in the corpus above, generate a steered completion with
<code>h ← h + α · W_dec[:, feature]</code>. Useful for producing
targeted training examples that fire a specific feature.
</div>
</div>
</section>
<!-- Bottom band -->
<section id="bottom">
<!-- STEERING bottom: baseline / steered / heatmap -->
<div class="panel" data-show-tab="steering" data-pid="baseline">
<h2>Baseline output (α=0)
<span style="display:flex; gap:8px; align-items:center;">
<span id="base-time" style="font-family:var(--mono); color:var(--fg-faint);"></span>
<button class="min-btn" data-min="baseline" title="Minimize panel"></button>
</span>
</h2>
<div class="panel-body">
<div class="out-block baseline" id="out-baseline"><span class="empty">(no run yet)</span></div>
</div>
</div>
<div class="panel" data-show-tab="steering" data-pid="steered">
<h2>Steered output
<span style="display:flex; gap:8px; align-items:center;">
<span id="steered-time" style="font-family:var(--mono); color:var(--fg-faint);"></span>
<button class="min-btn" data-min="steered" title="Minimize panel"></button>
</span>
</h2>
<div class="panel-body">
<div class="out-block steered" id="out-steered"><span class="empty">(no run yet)</span></div>
<div class="verifier" id="verifier"></div>
</div>
</div>
<!-- EVALUATION bottom: per-sample table + signature heatmap -->
<div class="panel" data-show-tab="evaluation" id="eval-samples-panel" style="grid-column: 1 / 2;">
<h2>Per-sample top features
<button class="min-btn" data-min="eval-samples-panel" title="Minimize panel"></button>
</h2>
<div class="panel-body">
<div id="eval-samples"><span class="empty">Encode a corpus to populate.</span></div>
</div>
</div>
<div class="panel" data-show-tab="evaluation" id="eval-heatmap-panel" style="grid-column: 2 / 3;">
<h2>Signature heatmap / Compare results
<span style="display:flex; gap:6px; align-items:center;">
<button id="btn-eval-view-heatmap" class="ghost" style="padding:2px 8px; font-size:10px;">heatmap</button>
<button id="btn-eval-view-compare" class="ghost" style="padding:2px 8px; font-size:10px;">compare</button>
<button class="min-btn" data-min="eval-heatmap-panel" title="Minimize panel"></button>
</span>
</h2>
<div class="panel-body">
<div id="eval-heatmap"><span class="empty">Encode a corpus to populate.</span></div>
<div id="eval-compare-results" style="display:none;"><span class="empty">Run Compare to populate.</span></div>
</div>
</div>
<!-- DATA-CENTRIC bottom: filtered list + synth output -->
<div class="panel" data-show-tab="datacentric" id="dc-filtered-panel">
<h2>Filtered docs <span id="dc-filter-stats" style="font-family:var(--mono); color:var(--fg-faint);"></span>
<button class="min-btn" data-min="dc-filtered-panel" title="Minimize panel"></button>
</h2>
<div class="panel-body">
<div id="dc-filtered-list"><span class="empty">Encode + apply filter to populate.</span></div>
</div>
</div>
<div class="panel" data-show-tab="datacentric" id="dc-synth-panel">
<h2>Synthesis output <span id="dc-synth-stats" style="font-family:var(--mono); color:var(--fg-faint);"></span>
<button class="min-btn" data-min="dc-synth-panel" title="Minimize panel"></button>
</h2>
<div class="panel-body">
<div id="dc-synth-list"><span class="empty">Run "Synthesize from corpus" to populate.</span></div>
</div>
</div>
</section>
</div>
<div id="tooltip"></div>
<div class="footer">
<span id="status">idle</span>
</div>
<script>
const API = ""; // same-origin: HF Space serves API + HTML from one process
const N_FEATURES = 32768;
// --- State ---
const state = {
positions: null, // Float32Array length N*3
topFeatures: [], // [{id, act}, ...] from /encode
steering: new Map(), // feature_id -> alpha
picked: null, // currently hovered feature_id
};
// --- Three.js scene ---
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x08080d, 0.08);
const camera = new THREE.PerspectiveCamera(55, window.innerWidth / window.innerHeight, 0.01, 60);
camera.position.set(0, 0, 3.4);
const renderer = new THREE.WebGLRenderer({
canvas: document.getElementById("scene"),
antialias: true,
alpha: true,
});
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0x000000, 0);
// Subtle starry haze background — many tiny dim points
function addStarHaze() {
const N = 600;
const g = new THREE.BufferGeometry();
const pos = new Float32Array(N*3);
for (let i=0; i<N; i++) {
const r = 18 + Math.random()*6;
const theta = Math.random()*Math.PI*2;
const phi = Math.acos(2*Math.random()-1);
pos[i*3] = r*Math.sin(phi)*Math.cos(theta);
pos[i*3+1] = r*Math.sin(phi)*Math.sin(theta);
pos[i*3+2] = r*Math.cos(phi);
}
g.setAttribute("position", new THREE.BufferAttribute(pos, 3));
const m = new THREE.PointsMaterial({ color: 0x2a2a3a, size: 0.06, sizeAttenuation: true, transparent:true, opacity:0.5 });
scene.add(new THREE.Points(g, m));
}
addStarHaze();
// Feature point cloud — built once positions arrive
let featurePoints = null, featureGeometry = null;
function buildFeatureCloud(positions) {
featureGeometry = new THREE.BufferGeometry();
const N = positions.length / 3;
featureGeometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
const colors = new Float32Array(N*3);
const sizes = new Float32Array(N);
for (let i=0; i<N; i++) {
colors[i*3] = 1.0; colors[i*3+1] = 1.0; colors[i*3+2] = 1.0;
sizes[i] = 0.5;
}
featureGeometry.setAttribute("color", new THREE.BufferAttribute(colors, 3));
featureGeometry.setAttribute("size", new THREE.BufferAttribute(sizes, 1));
const material = new THREE.ShaderMaterial({
uniforms: { uPixelRatio: { value: renderer.getPixelRatio() } },
vertexShader: `
attribute float size;
varying vec3 vColor;
varying float vIsActive;
varying float vSize;
uniform float uPixelRatio;
void main() {
vColor = color;
vIsActive = step(1.0, size); // active features (sizes attribute >= 1.5)
vec4 mv = modelViewMatrix * vec4(position, 1.0);
gl_Position = projectionMatrix * mv;
// Base features: small fixed-pixel hard dots (~3 device px, so 6 retina px).
// Active features: size attribute -> px directly, with mild perspective
// (closer = larger), capped so they never balloon out.
float basePx = 3.0 * uPixelRatio;
float scaledPx = size * 3.0 * (3.4 / -mv.z) * uPixelRatio;
scaledPx = clamp(scaledPx, basePx, 26.0 * uPixelRatio);
gl_PointSize = mix(basePx, scaledPx, vIsActive);
vSize = gl_PointSize;
}`,
fragmentShader: `
varying vec3 vColor;
varying float vIsActive;
varying float vSize;
void main() {
vec2 uv = gl_PointCoord - 0.5;
float d = length(uv);
if (d > 0.5) discard;
// Anti-alias the rim with a fixed 1-pixel band, regardless of point size,
// so even tiny points stay solid and crisp instead of going gaussian.
float aa = 1.0 / max(vSize, 2.0);
float core = 1.0 - smoothstep(0.5 - aa, 0.5, d);
// Halo only on active features
float glow = vIsActive * (1.0 - smoothstep(0.18, 0.5, d)) * 0.35;
vec3 col = mix(vColor, vColor * 1.5, vIsActive);
float alpha = clamp(core + glow, 0.0, 1.0);
gl_FragColor = vec4(col, alpha);
}`,
transparent: true,
depthWrite: false,
vertexColors: true,
blending: THREE.NormalBlending,
});
featurePoints = new THREE.Points(featureGeometry, material);
scene.add(featurePoints);
}
// Lazy orbit-like rotation (no heavy controls dep)
let userInteracting = false, autoRot = 0.06;
let dragging = false, lastX = 0, lastY = 0;
let yaw = 0.4, pitch = 0.05, dist = 3.4;
const cnv = document.getElementById("scene");
cnv.addEventListener("pointerdown", e => { dragging = true; userInteracting = true; lastX = e.clientX; lastY = e.clientY; });
window.addEventListener("pointerup", () => { dragging = false; });
window.addEventListener("pointermove", e => {
if (dragging) {
yaw += (e.clientX - lastX) * 0.005;
pitch += (e.clientY - lastY) * 0.005;
pitch = Math.max(-1.4, Math.min(1.4, pitch));
lastX = e.clientX; lastY = e.clientY;
}
});
cnv.addEventListener("wheel", e => {
dist *= (1 + e.deltaY * 0.0012);
dist = Math.max(1.3, Math.min(8, dist));
e.preventDefault();
}, { passive:false });
window.addEventListener("resize", () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
function tick(t) {
if (!userInteracting) yaw += 0.0015;
camera.position.x = dist * Math.sin(yaw) * Math.cos(pitch);
camera.position.y = dist * Math.sin(pitch);
camera.position.z = dist * Math.cos(yaw) * Math.cos(pitch);
camera.lookAt(0, 0, 0);
renderer.render(scene, camera);
requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
// --- Picking (raycasting) ---
const raycaster = new THREE.Raycaster();
raycaster.params.Points = { threshold: 0.04 };
const mouse = new THREE.Vector2();
const tooltip = document.getElementById("tooltip");
cnv.addEventListener("mousemove", e => {
mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
if (!featurePoints) return;
raycaster.setFromCamera(mouse, camera);
const hits = raycaster.intersectObject(featurePoints);
if (hits.length > 0) {
const id = hits[0].index;
state.picked = id;
const top = state.topFeatures.find(t => t.id === id);
const isSteered = state.steering.has(id);
const alpha = isSteered ? state.steering.get(id) : null;
let html = `<div style="color:var(--fg); font-weight:600;">feature ${id}</div>`;
if (top) {
const rank = state.topFeatures.indexOf(top);
html += `<div style="margin-top:3px; color:var(--accent);">rank #${rank} of top firing · activation ${top.act.toFixed(3)}</div>`;
} else {
html += `<div style="margin-top:3px; color:var(--fg-faint);">not in top firing for current prompt</div>`;
}
if (isSteered) {
const sign = alpha >= 0 ? "+" : "";
html += `<div style="margin-top:3px; color:var(--accent-2);">steered · α=${sign}${alpha.toFixed(0)}</div>`;
}
html += `<div style="margin-top:5px; color:var(--fg-faint); font-size:10px;">click to ${isSteered ? "edit slider" : "add steering slider"}</div>`;
tooltip.innerHTML = html;
tooltip.style.display = "block";
tooltip.style.left = e.clientX + "px";
tooltip.style.top = e.clientY + "px";
} else {
state.picked = null;
tooltip.style.display = "none";
}
});
cnv.addEventListener("click", () => {
if (state.picked != null) {
addSteerSlot(state.picked, 0);
}
});
// --- Update particle attributes after encode ---
function repaintCloud() {
if (!featureGeometry) return;
const colors = featureGeometry.attributes.color.array;
const sizes = featureGeometry.attributes.size.array;
const N = sizes.length;
for (let i=0; i<N; i++) {
colors[i*3] = 1.0; colors[i*3+1] = 1.0; colors[i*3+2] = 1.0;
sizes[i] = 0.5;
}
// Top firing — cyan, larger
const maxAct = state.topFeatures[0]?.act || 1;
for (const f of state.topFeatures) {
const i = f.id;
const t = Math.min(1, Math.abs(f.act) / maxAct);
colors[i*3] = 0.49 * t + 1.0 * (1-t);
colors[i*3+1] = 0.97 * t + 1.0 * (1-t);
colors[i*3+2] = 1.00;
sizes[i] = 1.5 + 4.5 * t;
}
// Steered — magenta override
for (const [id, slot] of state.steering) {
const alpha = (typeof slot === "object") ? slot.alpha : slot;
if (alpha === 0) continue;
const t = Math.min(1, Math.abs(alpha) / 100);
colors[id*3] = 1.00;
colors[id*3+1] = 0.49 * (1-t) + 0.4 * t;
colors[id*3+2] = 0.97;
sizes[id] = 4 + 5 * t;
}
featureGeometry.attributes.color.needsUpdate = true;
featureGeometry.attributes.size.needsUpdate = true;
}
// --- API helpers ---
function setStatus(msg, busy=false) {
const el = document.getElementById("status");
el.innerHTML = busy ? `<span class="loader"></span> &nbsp; ${msg}` : msg;
}
async function api(path, body=null) {
const opts = body ? {method:"POST", headers:{"Content-Type":"application/json"}, body: JSON.stringify(body)}
: {method:"GET"};
const r = await fetch(API + path, opts);
if (!r.ok) throw new Error(`${path} -> ${r.status} ${r.statusText}`);
return await r.json();
}
// --- Boot ---
function applyHealthToHeader(hp) {
const layerInput = document.getElementById("layer-input");
layerInput.value = hp.layer;
layerInput.max = (hp.n_layers || 1) - 1;
layerInput.title = `Layer 0..${hp.n_layers - 1}`;
document.getElementById("hdr-layer-meta").textContent = ` · ${hp.device} · ${hp.dtype}`;
document.getElementById("hdr-features").textContent = `${hp.n_features.toLocaleString()} features`;
const hdr = document.getElementById("header");
hdr.classList.remove("loading");
hdr.classList.add("live");
if (hp.transferred && hp.note) {
setStatus("⚠ transferred SAE: " + hp.note);
}
}
// Layer hot-swap: change SAE layer without reloading model
let layerSwapPending = null;
document.getElementById("layer-input").addEventListener("change", async (ev) => {
const newLayer = parseInt(ev.target.value);
const hp = await api("/health");
if (newLayer === hp.layer) return;
if (isNaN(newLayer) || newLayer < 0 || newLayer >= hp.n_layers) {
ev.target.value = hp.layer; return;
}
// Clear stale UI state — features and outputs are layer-specific
state.topFeatures = []; state.steering.clear();
document.getElementById("features").innerHTML = `<div class="empty">Encode a prompt to populate features.</div>`;
document.getElementById("feat-count").textContent = "";
document.getElementById("out-baseline").innerHTML = `<span class="empty">(no run yet)</span>`;
document.getElementById("out-steered").innerHTML = `<span class="empty">(no run yet)</span>`;
document.getElementById("verifier").innerHTML = "";
document.getElementById("heatmap-grid").innerHTML = `<span class="empty">Encode a prompt — heatmap fills automatically.</span>`;
document.getElementById("btn-generate").disabled = true;
showLoading(`Switching to layer ${newLayer}…`,
"First time may take ~20s (download + SVD); subsequent switches are <0.1s.");
try {
const r = await api("/set_layer", {layer: newLayer});
const hp2 = await api("/health");
applyHealthToHeader(hp2);
applyPositionsToCloud(r.positions);
setStatus(`layer ${newLayer}` + (r.from_cache ? " (cached)" : ""));
} catch (e) {
setStatus(`layer swap error: ${e.message}`);
ev.target.value = hp.layer;
} finally {
hideLoading();
}
});
function applyPositionsToCloud(positions) {
const flat = new Float32Array(positions.length * 3);
for (let i=0; i<positions.length; i++) {
flat[i*3] = positions[i][0] * 2.2;
flat[i*3+1] = positions[i][1] * 2.2;
flat[i*3+2] = positions[i][2] * 2.2;
}
state.positions = flat;
if (featurePoints) {
scene.remove(featurePoints);
featureGeometry.dispose();
featurePoints.material.dispose();
featurePoints = null; featureGeometry = null;
}
buildFeatureCloud(flat);
}
function fillModelDropdown(models, currentModel) {
const sel = document.getElementById("model-select");
sel.innerHTML = "";
for (const m of models) {
const opt = document.createElement("option");
opt.value = m.model;
const xferTag = m.transferred ? " ⚠" : "";
opt.textContent = `${m.model.replace("Qwen/","")} (${m.approx_size_gb}GB · ${m.n_features.toLocaleString()}f)${xferTag}`;
if (m.model === currentModel) opt.selected = true;
sel.appendChild(opt);
}
// If only one model in the catalog (HF Space deployment), make it
// visually informational rather than interactive.
if (models.length <= 1) {
sel.disabled = true;
sel.title = "Locked to Qwen3-1.7B-Base on HF Space (free CPU). Run locally to swap models.";
sel.style.cursor = "default";
sel.style.opacity = "0.85";
}
}
function showLoading(msg, detail="") {
document.getElementById("lo-msg").textContent = msg;
document.getElementById("lo-detail").textContent = detail;
document.getElementById("loading-overlay").classList.add("visible");
document.getElementById("header").classList.remove("live");
document.getElementById("header").classList.add("loading");
document.getElementById("btn-encode").disabled = true;
document.getElementById("btn-generate").disabled = true;
}
function hideLoading() {
document.getElementById("loading-overlay").classList.remove("visible");
document.getElementById("btn-encode").disabled = false;
}
async function boot() {
setStatus("loading positions…", true);
try {
const [hp, ph, ml] = await Promise.all([
api("/health"),
api("/positions"),
api("/list_models"),
]);
fillModelDropdown(ml.models, hp.model);
applyHealthToHeader(hp);
applyPositionsToCloud(ph.positions);
setStatus("ready");
if (hp.transferred && hp.note) setStatus("⚠ " + hp.note);
} catch (e) {
setStatus(`server unreachable at ${API}: ${e.message}`);
document.getElementById("model-select").innerHTML = `<option style="color:var(--bad)">server offline</option>`;
}
}
boot();
// Tab switching
document.body.dataset.tab = "steering";
document.querySelectorAll(".tab").forEach(btn => {
btn.addEventListener("click", () => {
document.querySelectorAll(".tab").forEach(b => b.classList.remove("active"));
btn.classList.add("active");
document.body.dataset.tab = btn.dataset.tab;
});
});
// Minimize toggle on every panel header
document.querySelectorAll(".min-btn").forEach(btn => {
btn.addEventListener("click", (ev) => {
ev.stopPropagation();
const panel = btn.closest(".panel");
if (!panel) return;
panel.classList.toggle("collapsed");
btn.textContent = panel.classList.contains("collapsed") ? "+" : "−";
btn.title = panel.classList.contains("collapsed") ? "Expand panel" : "Minimize panel";
});
});
// Model swap on dropdown change
document.getElementById("model-select").addEventListener("change", async (ev) => {
const newModel = ev.target.value;
const ml = await api("/list_models");
const entry = ml.models.find(m => m.model === newModel);
if (!entry) return;
const ok = confirm(
`Load ${newModel}?\n\n` +
`~${entry.approx_size_gb}GB download (or cached if seen before).\n` +
`${entry.n_features.toLocaleString()} SAE features, ${entry.n_layers} layers.\n` +
(entry.transferred ? `⚠ TRANSFERRED SAE: ${entry.note}\n` : ``) +
`\nThis blocks the server until ready.`
);
if (!ok) {
// revert dropdown to current
const hp = await api("/health");
ev.target.value = hp.model;
return;
}
// Reset client-side state
state.topFeatures = []; state.steering.clear();
document.getElementById("features").innerHTML = `<div class="empty">Encode a prompt to populate features.</div>`;
document.getElementById("feat-count").textContent = "";
document.getElementById("out-baseline").innerHTML = `<span class="empty">(no run yet)</span>`;
document.getElementById("out-steered").innerHTML = `<span class="empty">(no run yet)</span>`;
document.getElementById("verifier").innerHTML = "";
document.getElementById("btn-generate").disabled = true;
showLoading(`Loading ${newModel}…`,
`~${entry.approx_size_gb}GB · this is real, watch the server log`);
try {
const r = await fetch(API + "/load_model", {
method:"POST", headers:{"Content-Type":"application/json"},
body: JSON.stringify({model:newModel})
});
if (!r.ok) {
const txt = await r.text();
throw new Error(`${r.status}: ${txt}`);
}
const data = await r.json();
const hp = await api("/health");
applyHealthToHeader(hp);
applyPositionsToCloud(data.positions);
setStatus(`loaded ${newModel}`);
if (hp.transferred && hp.note) setStatus("⚠ " + hp.note);
} catch (e) {
setStatus(`load failed: ${e.message}`);
alert(`Load failed:\n\n${e.message}\n\nCheck the server log.`);
// Try to revert dropdown
try {
const hp = await api("/health");
ev.target.value = hp.model;
} catch {}
} finally {
hideLoading();
}
});
// --- Encode action ---
document.getElementById("btn-encode").addEventListener("click", async () => {
const prompt = document.getElementById("prompt").value;
if (!prompt.trim()) return;
setStatus("encoding…", true);
try {
const t0 = performance.now();
const top_n = Math.max(1, parseInt(document.getElementById("top-k").value || "20"));
const r = await api("/encode", {prompt, top_n});
const dt = ((performance.now() - t0)/1000).toFixed(2);
state.topFeatures = r.top;
document.getElementById("feat-count").textContent = `(K=${r.top.length} of ${r.n_features.toLocaleString()})`;
renderFeatures();
repaintCloud();
document.getElementById("btn-generate").disabled = false;
setStatus(`encoded in ${dt}s`);
// Auto-render the per-token heatmap too
renderHeatmap(prompt).catch(e => console.error("heatmap:", e));
} catch (e) {
setStatus(`encode error: ${e.message}`);
}
});
document.getElementById("heatmap-skip-first").addEventListener("change", () => {
if (state._lastHeatmapPrompt) renderHeatmap(state._lastHeatmapPrompt);
});
// =====================================================================
// PILLAR 2 — EVALUATION
// =====================================================================
function parsePrompts(textareaId) {
return document.getElementById(textareaId).value
.split("\n").map(s => s.trim()).filter(s => s.length > 0);
}
document.getElementById("btn-eval-encode").addEventListener("click", async () => {
const prompts = parsePrompts("eval-prompts");
if (prompts.length === 0) { setStatus("paste prompts first"); return; }
setStatus(`encoding ${prompts.length} prompts…`, true);
document.getElementById("eval-features-list").innerHTML = `<span class="loader"></span>`;
document.getElementById("eval-samples").innerHTML = `<span class="loader"></span>`;
document.getElementById("eval-heatmap").innerHTML = `<span class="loader"></span>`;
try {
const t0 = performance.now();
const r = await api("/encode_batch", {prompts, top_n: 10});
const dt = ((performance.now()-t0)/1000).toFixed(2);
state.evalResult = r;
document.getElementById("eval-stats").textContent =
`(${r.n_samples} samples · ${r.corpus_features.length} features fired)`;
renderEvalCorpus(r);
renderEvalSamples(r);
renderEvalHeatmap(r);
setStatus(`corpus encoded in ${dt}s`);
} catch (e) {
setStatus(`eval encode error: ${e.message}`);
}
});
function renderEvalCorpus(r) {
const div = document.getElementById("eval-features-list");
if (!r.corpus_features.length) {
div.innerHTML = `<div class="empty">No features fired.</div>`;
return;
}
// Top firing features ranked by fire_rate, with bar
const max = r.corpus_features[0].fire_rate;
div.innerHTML = r.corpus_features.slice(0, 60).map(f => {
const w = Math.max(2, 100 * f.fire_rate / max);
return `<div style="padding:5px 8px; border-bottom:1px solid var(--border); font-family:var(--mono); font-size:11px;">
<div style="display:flex; justify-content:space-between; gap:10px; align-items:center;">
<span style="color:var(--accent);">feat ${f.id}</span>
<span style="color:var(--fg-faint);">${(f.fire_rate*100).toFixed(0)}% · μ=${f.mean_act.toFixed(2)}</span>
</div>
<div style="height:3px; background:rgba(125,249,255,0.55); width:${w}%; margin-top:4px; border-radius:2px;"></div>
</div>`;
}).join("");
}
function renderEvalSamples(r) {
const div = document.getElementById("eval-samples");
if (!r.per_sample.length) { div.innerHTML = `<span class="empty">no samples</span>`; return; }
div.innerHTML = r.per_sample.map(s => {
const top = s.top.slice(0,5).map(t =>
`<span style="display:inline-block; padding:1px 6px; margin:1px; border:1px solid var(--border-strong); border-radius:4px; font-family:var(--mono); font-size:10px; color:var(--accent);">${t.id}<span style="color:var(--fg-faint);">·${t.act.toFixed(1)}</span></span>`
).join("");
return `<div style="padding:8px; border-bottom:1px solid var(--border);">
<div style="font-family:var(--mono); font-size:11px; color:var(--fg); margin-bottom:4px;">[${s.i}] ${escapeHtml(s.preview)}</div>
<div>${top}</div>
</div>`;
}).join("");
}
function renderEvalHeatmap(r) {
const div = document.getElementById("eval-heatmap");
// Pick top 20 features that fire most across samples
const topFeats = r.corpus_features.slice(0, 20);
if (!topFeats.length || !r.per_sample.length) { div.innerHTML = `<span class="empty">no data</span>`; return; }
// Build sample-id × feature_id activation matrix
const sampleActs = r.per_sample.map(s => {
const m = {};
for (const t of s.top) m[t.id] = t.act;
return m;
});
const max = Math.max(1e-6, ...sampleActs.flatMap(m => Object.values(m)));
// Render: rows = samples, cols = features
const header = `<th style="padding:3px 6px; font-size:10px; color:var(--fg-dim); background:rgba(0,0,0,0.4); position:sticky; left:0; z-index:2;">sample</th>` +
topFeats.map(f => `<th title="feat ${f.id} (${(f.fire_rate*100).toFixed(0)}% rate)" style="padding:3px 4px; font-size:9px; color:var(--accent); border-bottom:1px solid var(--border-strong);">${f.id}</th>`).join("");
const rows = r.per_sample.map((s, si) => {
const cells = topFeats.map(f => {
const v = sampleActs[si][f.id] || 0;
const t = Math.min(1, v/max);
const r = Math.round(255 - 200*t), g = 255, b = Math.round(255 - 50*t);
return `<td title="sample ${si} · feat ${f.id} · ${v.toFixed(2)}" style="background:rgb(${r},${g},${b}); width:22px; height:18px; border:1px solid rgba(0,0,0,0.3);"></td>`;
}).join("");
return `<tr><td style="padding:2px 6px; font-family:var(--mono); font-size:10px; color:var(--fg-dim); background:rgba(0,0,0,0.3); position:sticky; left:0; z-index:1; max-width:80px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">${si}: ${escapeHtml(s.preview.slice(0,16))}</td>${cells}</tr>`;
}).join("");
div.innerHTML = `<table style="border-collapse:collapse; font-family:var(--mono);"><thead><tr>${header}</tr></thead><tbody>${rows}</tbody></table>`;
}
function escapeHtml(s) {
return String(s).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");
}
// View toggle in Evaluation bottom-right panel: heatmap vs compare
document.getElementById("btn-eval-view-heatmap").addEventListener("click", () => {
document.getElementById("eval-heatmap").style.display = "block";
document.getElementById("eval-compare-results").style.display = "none";
});
document.getElementById("btn-eval-view-compare").addEventListener("click", () => {
document.getElementById("eval-heatmap").style.display = "none";
document.getElementById("eval-compare-results").style.display = "block";
});
// Differential feature mining
document.getElementById("btn-cmp").addEventListener("click", async () => {
const a = parsePrompts("cmp-a");
const b = parsePrompts("cmp-b");
if (a.length === 0 || b.length === 0) {
setStatus("paste both sets first"); return;
}
setStatus(`comparing ${a.length} vs ${b.length} prompts…`, true);
// Auto-switch the bottom-right panel to compare view
document.getElementById("eval-heatmap").style.display = "none";
document.getElementById("eval-compare-results").style.display = "block";
document.getElementById("eval-compare-results").innerHTML = `<span class="loader"></span> &nbsp; encoding…`;
try {
const t0 = performance.now();
const r = await api("/compare_batch", {prompts_a: a, prompts_b: b, top_n: 30});
const dt = ((performance.now()-t0)/1000).toFixed(2);
renderCompareResults(r, a, b);
setStatus(`compared in ${dt}s`);
} catch (e) {
setStatus(`compare error: ${e.message}`);
document.getElementById("eval-compare-results").innerHTML = `<span class="empty">error: ${e.message}</span>`;
}
});
function renderCompareResults(r, setA, setB) {
const div = document.getElementById("eval-compare-results");
if (!r.top_diff || !r.top_diff.length) {
div.innerHTML = `<span class="empty">no distinguishing features found</span>`;
return;
}
const max = r.top_diff[0].diff || 1;
const header = `
<div style="font-size:10px; color:var(--fg-faint); padding:4px 0; display:flex; gap:14px; flex-wrap:wrap; border-bottom:1px solid var(--border-strong); margin-bottom:6px;">
<span><span style="color:#7df9ff; font-weight:700;">■ A</span> ${r.n_a} prompts</span>
<span><span style="color:#ff7df9; font-weight:700;">■ B</span> ${r.n_b} prompts</span>
<span>top ${r.top_diff.length} by |Δ rate|</span>
</div>`;
const rows = r.top_diff.map((f, i) => {
const wA = Math.max(2, 60 * f.rate_a);
const wB = Math.max(2, 60 * f.rate_b);
const winner = f.winner === "a" ? "A" : "B";
const winnerColor = f.winner === "a" ? "#7df9ff" : "#ff7df9";
return `<tr style="border-bottom:1px solid var(--border);">
<td style="padding:3px 6px; color:var(--fg-faint); font-size:10px;">${i+1}</td>
<td style="padding:3px 6px; color:var(--accent); font-family:var(--mono);">${f.id}</td>
<td style="padding:3px 6px; text-align:right; font-family:var(--mono);">${(f.rate_a*100).toFixed(0)}%</td>
<td style="padding:3px 6px;">
<div style="display:flex; gap:2px; align-items:center;">
<div style="width:${wA}px; height:6px; background:#7df9ff;"></div>
<div style="width:${wB}px; height:6px; background:#ff7df9;"></div>
</div>
</td>
<td style="padding:3px 6px; text-align:right; font-family:var(--mono);">${(f.rate_b*100).toFixed(0)}%</td>
<td style="padding:3px 6px; font-family:var(--mono); font-size:10px; color:${winnerColor};">${winner} ▲</td>
<td style="padding:3px 6px; text-align:right; font-family:var(--mono); color:var(--fg);">${(f.diff*100).toFixed(0)}%</td>
</tr>`;
}).join("");
div.innerHTML = `
<div>
${header}
<div style="overflow:auto; max-height:32vh;">
<table style="border-collapse:collapse; width:100%; font-size:11px;">
<thead>
<tr style="background:rgba(0,0,0,0.4); position:sticky; top:0; z-index:1;">
<th style="padding:4px 6px; font-size:10px; color:var(--fg-dim); text-align:left;">#</th>
<th style="padding:4px 6px; font-size:10px; color:var(--fg-dim); text-align:left;">feat</th>
<th style="padding:4px 6px; font-size:10px; color:var(--fg-dim); text-align:right;">rate A</th>
<th style="padding:4px 6px; font-size:10px; color:var(--fg-dim); text-align:left;">A vs B</th>
<th style="padding:4px 6px; font-size:10px; color:var(--fg-dim); text-align:right;">rate B</th>
<th style="padding:4px 6px; font-size:10px; color:var(--fg-dim); text-align:left;">winner</th>
<th style="padding:4px 6px; font-size:10px; color:var(--fg-dim); text-align:right;">|Δ|</th>
</tr>
</thead>
<tbody>${rows}</tbody>
</table>
</div>
</div>`;
}
// =====================================================================
// PILLAR 3 — DATA-CENTRIC
// =====================================================================
document.getElementById("btn-dc-encode").addEventListener("click", async () => {
const prompts = parsePrompts("dc-prompts");
if (prompts.length === 0) { setStatus("paste prompts first"); return; }
setStatus(`encoding ${prompts.length} prompts…`, true);
try {
const r = await api("/encode_batch", {prompts, top_n: 50});
state.dcResult = r;
state.dcAllPrompts = prompts;
state.dcFiltered = r.per_sample;
renderDcFiltered(state.dcFiltered, prompts, null);
setStatus(`encoded ${r.n_samples} prompts`);
} catch (e) {
setStatus(`dc encode error: ${e.message}`);
}
});
document.getElementById("btn-dc-filter").addEventListener("click", () => {
if (!state.dcResult) { setStatus("encode first"); return; }
const fid = parseInt(document.getElementById("dc-filter-id").value);
const mode = document.getElementById("dc-filter-mode").value;
if (isNaN(fid)) { setStatus("enter a feature id"); return; }
const fired = new Set();
for (const s of state.dcResult.per_sample) {
if (s.top.some(t => t.id === fid)) fired.add(s.i);
}
const filtered = state.dcResult.per_sample.filter(s =>
mode === "include" ? fired.has(s.i) : !fired.has(s.i)
);
state.dcFiltered = filtered;
renderDcFiltered(filtered, state.dcAllPrompts, {id: fid, mode});
});
document.getElementById("btn-dc-clear").addEventListener("click", () => {
if (!state.dcResult) return;
state.dcFiltered = state.dcResult.per_sample;
renderDcFiltered(state.dcFiltered, state.dcAllPrompts, null);
});
function renderDcFiltered(samples, prompts, filt) {
const div = document.getElementById("dc-filtered-list");
const stats = document.getElementById("dc-filter-stats");
if (filt) {
stats.textContent = `(filter: ${filt.mode === "include" ? "+" : "−"}feat ${filt.id} · ${samples.length} of ${state.dcResult.n_samples})`;
} else {
stats.textContent = `(${samples.length} docs)`;
}
if (!samples.length) { div.innerHTML = `<span class="empty">no docs match.</span>`; return; }
div.innerHTML = samples.map(s => {
const fullPrompt = prompts[s.i] || s.preview;
const top = s.top.slice(0,3).map(t =>
`<span style="display:inline-block; padding:1px 5px; margin:1px; border:1px solid var(--border-strong); border-radius:3px; font-family:var(--mono); font-size:10px; color:var(--accent);">${t.id}</span>`
).join("");
return `<div style="padding:6px 8px; border-bottom:1px solid var(--border); font-family:var(--mono); font-size:11px;">
<div style="color:var(--fg-faint); font-size:9px;">[${s.i}]</div>
<div style="color:var(--fg);">${escapeHtml(fullPrompt)}</div>
<div style="margin-top:3px;">${top}</div>
</div>`;
}).join("");
}
document.getElementById("btn-dc-synth").addEventListener("click", async () => {
const prompts = state.dcAllPrompts || parsePrompts("dc-prompts");
if (!prompts.length) { setStatus("paste seeds first"); return; }
const fid = parseInt(document.getElementById("dc-synth-id").value);
const alpha = parseFloat(document.getElementById("dc-synth-alpha").value);
const max_new_tokens = parseInt(document.getElementById("dc-synth-tokens").value);
if (isNaN(fid)) { setStatus("enter feature id"); return; }
setStatus(`synthesizing ${prompts.length} steered completions…`, true);
document.getElementById("dc-synth-list").innerHTML = `<span class="loader"></span> &nbsp; running…`;
try {
const r = await api("/synth_batch", {
seed_prompts: prompts,
steering: [{id: fid, alpha}],
max_new_tokens,
});
document.getElementById("dc-synth-stats").textContent =
`(${r.results.length} · feat ${fid} α=${alpha})`;
document.getElementById("dc-synth-list").innerHTML = r.results.map((res, i) =>
`<div style="padding:8px; border-bottom:1px solid var(--border); font-family:var(--mono); font-size:11px;">
<div style="color:var(--fg-faint); font-size:9px;">[${i}] seed</div>
<div style="color:var(--fg-dim);">${escapeHtml(res.seed)}</div>
<div style="color:var(--fg-faint); font-size:9px; margin-top:4px;">→ steered</div>
<div style="color:var(--fg);">${escapeHtml(res.text)}</div>
</div>`
).join("");
setStatus("synthesis done");
} catch (e) {
setStatus(`synth error: ${e.message}`);
document.getElementById("dc-synth-list").innerHTML = `<span class="empty">error: ${e.message}</span>`;
}
});
async function renderHeatmap(prompt) {
const container = document.getElementById("heatmap-grid");
state._lastHeatmapPrompt = prompt;
container.innerHTML = `<span class="loader"></span> &nbsp; computing…`;
try {
const r = await api("/encode_full", {prompt, top_n: 16});
let tokens = r.tokens, grid = r.grid, ids = r.feature_ids;
const skipFirst = document.getElementById("heatmap-skip-first").checked;
if (skipFirst && tokens.length > 1) {
tokens = tokens.slice(1);
grid = grid.map(row => row.slice(1));
}
if (tokens.length === 0) {
container.innerHTML = `<span class="empty">No tokens (after skip).</span>`;
return;
}
// Build HTML table
const rowMax = grid.map(row => Math.max(...row.map(Math.abs), 1e-6));
const tokHeader = tokens.map((t,i) => {
const safe = (t.replace(/\n/g,"↵").replace(/</g,"&lt;").replace(/>/g,"&gt;")).slice(0,8);
return `<th title="pos ${i}: ${t.replace(/\n/g,'\\n').replace(/"/g,'&quot;')}" style="padding:3px 4px; font-size:10px; color:var(--fg-dim); border-bottom:1px solid var(--border-strong); white-space:nowrap;">${safe || `[${i}]`}</th>`;
}).join("");
const rows = grid.map((row, fi) => {
const m = rowMax[fi];
const cells = row.map((v, pi) => {
const t = Math.min(1, Math.abs(v) / m);
const r = 255, g = Math.round(255 - 200*t), b = Math.round(255 - 220*t);
return `<td title="feat ${ids[fi]} · pos ${pi} · act=${v.toFixed(3)}" style="background:rgb(${r},${g},${b}); width:30px; height:22px; border:1px solid rgba(0,0,0,0.3);"></td>`;
}).join("");
return `<tr><td style="font-family:var(--mono); font-size:10px; padding:3px 6px; color:var(--accent); white-space:nowrap; background:rgba(0,0,0,0.3); position:sticky; left:0; z-index:1;">#${ids[fi]}</td>${cells}</tr>`;
}).join("");
container.innerHTML = `<table style="border-collapse:collapse; font-family:var(--mono);"><thead><tr><th style="padding:3px 6px; font-size:10px; color:var(--fg-dim); position:sticky; left:0; z-index:2; background:rgba(0,0,0,0.4);">feat</th>${tokHeader}</tr></thead><tbody>${rows}</tbody></table>`;
} catch (e) {
container.innerHTML = `<span class="empty">heatmap error: ${e.message}</span>`;
}
}
// --- Feature card render ---
function renderFeatures() {
const div = document.getElementById("features");
if (state.topFeatures.length === 0) {
div.innerHTML = `<div class="empty">Encode a prompt to populate features.</div>`;
return;
}
div.innerHTML = "";
for (const f of state.topFeatures) {
div.appendChild(buildFeatureCard(f.id, f.act, /*topRanked=*/true));
}
// Also render any steered features that weren't in top-K
for (const [id, alpha] of state.steering) {
if (!state.topFeatures.find(t => t.id === id)) {
div.appendChild(buildFeatureCard(id, null, /*topRanked=*/false));
}
}
}
function buildFeatureCard(id, act, topRanked) {
const wrap = document.createElement("div");
wrap.className = "feat" + (state.steering.has(id) ? " steered" : "");
const alpha = state.steering.has(id) ? state.steering.get(id) : 0;
const head = document.createElement("div");
head.className = "feat-head";
const left = document.createElement("div");
left.innerHTML = `<span class="feat-id">feat ${id}</span>` +
(act != null ? ` &nbsp;<span class="feat-act">act ${act.toFixed(3)}</span>` : ` &nbsp;<span class="feat-act" style="color:var(--fg-faint)">(picked)</span>`);
const tools = document.createElement("div");
tools.className = "feat-tools";
if (state.steering.has(id)) {
const reset = document.createElement("button");
reset.textContent = "reset";
reset.title = "Set α back to 0 (no intervention) but keep slider visible";
reset.addEventListener("click", () => {
state.steering.set(id, 0);
renderFeatures();
repaintCloud();
});
tools.appendChild(reset);
const off = document.createElement("button");
off.textContent = "remove";
off.title = "Remove this slider entirely";
off.addEventListener("click", () => {
state.steering.delete(id);
renderFeatures();
repaintCloud();
});
tools.appendChild(off);
} else {
const add = document.createElement("button");
add.textContent = "steer";
add.addEventListener("click", () => addSteerSlot(id, 0));
tools.appendChild(add);
}
head.appendChild(left);
head.appendChild(tools);
wrap.appendChild(head);
if (state.steering.has(id)) {
const cur = state.steering.get(id);
const curAlpha = (typeof cur === "object") ? cur.alpha : cur;
const curPositions = (typeof cur === "object") ? (cur.positions || "") : "";
const curOutOnly = (typeof cur === "object") ? !!cur.output_only : false;
const sl = document.createElement("div");
sl.className = "slider-row";
const range = document.createElement("input");
range.type = "range"; range.min = -100; range.max = 100; range.step = 1;
range.value = curAlpha;
const val = document.createElement("span");
val.className = "alpha-val";
val.textContent = (curAlpha >= 0 ? "+" : "") + curAlpha.toFixed(0);
range.addEventListener("input", () => {
const a = parseFloat(range.value);
const slot = state.steering.get(id);
const next = (typeof slot === "object") ? {...slot, alpha:a} : {alpha:a, positions:"", output_only:false};
state.steering.set(id, next);
val.textContent = (a >= 0 ? "+" : "") + a.toFixed(0);
repaintCloud();
});
sl.appendChild(range);
sl.appendChild(val);
wrap.appendChild(sl);
// Position-selective steering + output-only toggle
const adv = document.createElement("div");
adv.style.cssText = "margin-top:6px; display:flex; gap:6px; align-items:center; flex-wrap:wrap;";
const posLbl = document.createElement("span");
posLbl.textContent = "positions";
posLbl.style.cssText = "font-size:10px; color:var(--fg-faint); flex:0 0 auto;";
const posInput = document.createElement("input");
posInput.type = "text";
posInput.placeholder = "all";
posInput.value = curPositions;
posInput.style.cssText = "flex:1; min-width:60px; font-size:11px; padding:3px 6px; background:rgba(0,0,0,0.3); border:1px solid var(--border-strong); color:var(--fg); border-radius:4px; font-family:var(--mono);";
posInput.title = "Token positions to steer at: 'all' or '3-7' or '0,2,5-8'. Empty = all.";
posInput.addEventListener("change", () => {
const slot = state.steering.get(id);
const next = (typeof slot === "object") ? {...slot, positions: posInput.value} : {alpha: slot, positions: posInput.value, output_only: false};
state.steering.set(id, next);
});
const outLbl = document.createElement("label");
outLbl.style.cssText = "font-size:10px; color:var(--fg-faint); display:inline-flex; align-items:center; gap:3px; cursor:pointer; flex:0 0 auto;";
const outChk = document.createElement("input");
outChk.type = "checkbox";
outChk.checked = curOutOnly;
outChk.style.cssText = "margin:0;";
outChk.addEventListener("change", () => {
const slot = state.steering.get(id);
const next = (typeof slot === "object") ? {...slot, output_only: outChk.checked} : {alpha: slot, positions: "", output_only: outChk.checked};
state.steering.set(id, next);
});
outLbl.appendChild(outChk);
outLbl.appendChild(document.createTextNode("output only"));
adv.appendChild(posLbl);
adv.appendChild(posInput);
adv.appendChild(outLbl);
wrap.appendChild(adv);
const lbl = document.createElement("div");
lbl.style.cssText = "font-size:10px; color:var(--fg-faint); margin-top:4px;";
lbl.textContent = "α: −100 ← suppress … +100 → amplify";
wrap.appendChild(lbl);
}
return wrap;
}
function addSteerSlot(id, alpha=0) {
state.steering.set(id, alpha);
renderFeatures();
repaintCloud();
}
// --- Generate action ---
document.getElementById("btn-generate").addEventListener("click", async () => {
const prompt = document.getElementById("prompt").value;
const max_new_tokens = parseInt(document.getElementById("max-tokens").value || "40");
const steering = [];
for (const [id, slot] of state.steering) {
const alpha = (typeof slot === "object") ? slot.alpha : slot;
if (alpha === 0) continue;
const positions = (typeof slot === "object") ? (slot.positions || null) : null;
const output_only = (typeof slot === "object") ? !!slot.output_only : false;
steering.push({id, alpha, positions, output_only});
}
setStatus("generating baseline…", true);
document.getElementById("out-baseline").innerHTML = `<span class="loader"></span>`;
document.getElementById("out-steered").innerHTML = `<span class="loader"></span>`;
try {
const t0 = performance.now();
const baseline = await api("/generate", {prompt, steering: [], max_new_tokens, return_probs: true, topk_display: 8});
const t1 = performance.now();
renderTokenChips("out-baseline", baseline, "blue");
document.getElementById("base-time").textContent = ((t1-t0)/1000).toFixed(2) + "s";
if (steering.length === 0) {
document.getElementById("out-steered").innerHTML = `<span class="empty">(no sliders engaged — steered = baseline)</span>`;
document.getElementById("steered-time").textContent = "";
document.getElementById("verifier").innerHTML = "";
setStatus("done");
return;
}
setStatus("generating steered…", true);
const t2 = performance.now();
const steered = await api("/generate", {prompt, steering, max_new_tokens, return_probs: true, topk_display: 8});
const t3 = performance.now();
renderTokenChips("out-steered", steered, "magenta");
document.getElementById("steered-time").textContent = ((t3-t2)/1000).toFixed(2) + "s";
// Verifier
const v = document.getElementById("verifier");
v.innerHTML = "";
for (const row of steered.verifier) {
const d = row.steered - row.base;
const cls = d > 0 ? "delta-up" : (d < 0 ? "delta-down" : "");
const sign = d >= 0 ? "+" : "";
const ext = [];
if (row.positions) ext.push(`pos=${row.positions}`);
if (row.output_only) ext.push("output-only");
const extStr = ext.length ? ` · ${ext.join(" · ")}` : "";
const div = document.createElement("div");
div.innerHTML = `feat ${row.id} · α=${row.alpha.toFixed(0)}${extStr} · base=${row.base.toFixed(2)} → steered=${row.steered.toFixed(2)} <span class="${cls}">(Δ ${sign}${d.toFixed(2)})</span>`;
v.appendChild(div);
}
setStatus("done");
} catch (e) {
setStatus(`generate error: ${e.message}`);
document.getElementById("out-baseline").textContent = "(error)";
document.getElementById("out-steered").textContent = "(error)";
}
});
// --- Per-token probability chip strip ---
function renderTokenChips(containerId, gen, theme) {
const container = document.getElementById(containerId);
if (!gen.tokens || !gen.tokens.length) {
container.textContent = gen.text || "(empty)";
return;
}
// Theme: colors for chip background
const themeFns = {
blue: (p) => {
const t = Math.max(0, Math.min(1, p));
const r = Math.round(255 * (1 - t*0.85));
const g = Math.round(255 * (1 - t*0.55));
return [r, g, 255, t < 0.5 ? "#1e3a8a" : "#fff"];
},
magenta: (p) => {
const t = Math.max(0, Math.min(1, p));
const r = 255;
const g = Math.round(255 * (1 - t*0.7));
const b = Math.round(255 * (1 - t*0.5));
return [r, g, b, t < 0.5 ? "#7f1d1d" : "#fff"];
},
};
const colorize = themeFns[theme] || themeFns.blue;
const chips = gen.tokens.map((row, i) => {
const [r,g,b,fg] = colorize(row.prob);
const tokDisp = row.tok.replace(/\n/g,"↵").replace(/\t/g,"→");
const safe = escapeHtml(tokDisp);
const panelHtml = topkPanelHtml(row.topk);
return `<span class="tok-chip" data-panel='${escapeAttr(panelHtml)}' style="background:rgb(${r},${g},${b}); color:${fg}; padding:2px 6px; margin:1px; border-radius:4px; cursor:pointer; display:inline-block; font-family:var(--mono); font-size:11px; white-space:nowrap;">${safe}<sub style="opacity:.65; font-size:8px; margin-left:3px;">${(row.prob*100).toFixed(1)}%</sub></span>`;
}).join("");
container.innerHTML = `
<div class="tok-strip" data-prob-root style="line-height:2.4;">
<div style="font-size:10px; color:var(--fg-faint); margin-bottom:4px; font-style:italic;">click any token to see top-K candidates</div>
<div>${chips}</div>
<div data-topk-panel style="display:none; margin-top:6px; padding:6px; background:rgba(0,0,0,0.35); border:1px solid var(--border-strong); border-radius:6px; font-family:var(--mono); font-size:10px;"></div>
</div>`;
// Wire click-to-pin
container.querySelectorAll(".tok-chip").forEach(chip => {
chip.addEventListener("click", () => {
const root = chip.closest("[data-prob-root]");
const panel = root.querySelector("[data-topk-panel]");
const wasSelected = chip.dataset.selected === "1";
root.querySelectorAll(".tok-chip").forEach(c => {
c.dataset.selected = "0"; c.style.outline = "";
});
if (wasSelected) {
panel.innerHTML = ""; panel.style.display = "none";
} else {
chip.dataset.selected = "1";
chip.style.outline = "2px solid #94a3b8";
chip.style.outlineOffset = "-1px";
panel.innerHTML = chip.dataset.panel;
panel.style.display = "block";
}
});
});
}
function topkPanelHtml(topk) {
const rows = topk.map((c, idx) => {
const safe = escapeHtml(c.tok.replace(/\n/g,"↵").replace(/\t/g,"→"));
const bg = c.is_chosen ? "background:rgba(125,249,255,0.12);" : "";
const fw = c.is_chosen ? "font-weight:700;" : "";
const mark = c.is_chosen ? " ✓" : "";
return `<tr style="${bg}">
<td style="padding:2px 6px; color:var(--fg-faint); text-align:right;">${idx+1}</td>
<td style="padding:2px 6px; color:var(--fg); ${fw}">${safe}${mark}</td>
<td style="padding:2px 6px; text-align:right; color:var(--accent);">${(c.prob*100).toFixed(2)}%</td>
</tr>`;
}).join("");
return `<table style="border-collapse:collapse; width:100%;"><tbody>${rows}</tbody></table>`;
}
function escapeAttr(s) { return s.replace(/'/g, "&apos;").replace(/"/g, "&quot;"); }
</script>
</body>
</html>