Butanium's picture
Static DuckDB-wasm sample explorer (full 176K corpus)
2e13869 verified
Raw
History Blame Contribute Delete
21.3 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Persona-negation sample explorer</title>
<style>
:root {
--bg: #fbfbfa; --fg: #1a1a1a; --sub: #6b6b6b; --card: #ffffff;
--border: #e3e3e0; --accent: #3b5bdb; --link: #1a56db;
}
@media (prefers-color-scheme: dark) {
:root { --bg: #17181a; --fg: #e6e6e6; --sub: #9a9a9a; --card: #222427;
--border: #333; --accent: #748ffc; --link: #91a7ff; }
}
* { box-sizing: border-box; }
body { margin: 0; font-family: system-ui, -apple-system, sans-serif; background: var(--bg);
color: var(--fg); line-height: 1.5; }
header { padding: 22px 24px 8px; max-width: 1100px; margin: 0 auto; }
h1 { margin: 0 0 4px; font-size: 1.5rem; }
header p { margin: 4px 0; color: var(--sub); font-size: 0.92rem; max-width: 68ch; }
header a { color: var(--link); }
.wrap { max-width: 1100px; margin: 0 auto; padding: 0 24px 60px; }
.tabs { display: flex; gap: 4px; border-bottom: 2px solid var(--border); margin: 16px 0 0; }
.tab-btn { border: none; background: none; color: var(--sub); font-size: 0.95rem; padding: 10px 16px;
cursor: pointer; border-bottom: 2px solid transparent; margin-bottom: -2px; }
.tab-btn.active { color: var(--accent); border-bottom-color: var(--accent); font-weight: 600; }
.panel { display: none; padding-top: 18px; }
.panel.active { display: block; }
.controls { display: flex; flex-wrap: wrap; gap: 12px 18px; align-items: flex-end; }
.ctrl { display: flex; flex-direction: column; gap: 3px; }
.ctrl label { font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.04em; color: var(--sub); }
select, input[type=number] { background: var(--card); color: var(--fg); border: 1px solid var(--border);
border-radius: 6px; padding: 5px 8px; font-size: 0.88rem; min-width: 90px; }
.rangebox { display: flex; align-items: center; gap: 5px; }
.rangebox input { width: 58px; }
button.draw { background: var(--accent); color: #fff; border: none; border-radius: 6px;
padding: 8px 16px; font-size: 0.9rem; cursor: pointer; font-weight: 600; }
button.draw:disabled { opacity: 0.5; cursor: default; }
.count { color: var(--sub); font-size: 0.88rem; margin: 14px 0 8px; font-variant-numeric: tabular-nums; }
.cards { display: flex; flex-direction: column; gap: 10px; }
.card { background: var(--card); border: 1px solid var(--border); border-left-width: 5px;
border-radius: 6px; padding: 10px 14px; }
.card .meta { font-size: 0.76rem; color: var(--sub); margin-bottom: 5px; font-variant-numeric: tabular-nums; }
.card .prompt { font-size: 0.82rem; font-style: italic; color: var(--sub); margin-bottom: 6px; }
.card .text { white-space: pre-wrap; font-size: 0.9rem; cursor: pointer; }
.card .text .more { color: var(--link); font-size: 0.82rem; }
mark { background: #ffe066; color: #1a1a1a; border-radius: 2px; padding: 0 1px; }
input[type=text] { background: var(--card); color: var(--fg); border: 1px solid var(--border);
border-radius: 6px; padding: 5px 8px; font-size: 0.88rem; width: 100%; }
.ctrl.grow { flex: 1 1 260px; }
[hidden] { display: none !important; }
#status { padding: 40px 0; color: var(--sub); font-size: 0.95rem; }
.spin { display: inline-block; width: 14px; height: 14px; border: 2px solid var(--border);
border-top-color: var(--accent); border-radius: 50%; animation: s 0.8s linear infinite;
vertical-align: middle; margin-right: 8px; }
@keyframes s { to { transform: rotate(360deg); } }
</style>
</head>
<body>
<header>
<h1>Persona-negation sample explorer</h1>
<p>Raw model completions behind the v3.1 report. Amplifying or negating a personality-trait
LoRA (the <code>weight</code> control) shifts what a model says about itself and how it handles
unsafe requests. Filter or search the text, then draw random samples. Full corpus — all
176K samples. Text search scans every completion, so it takes a few seconds.</p>
<p>Judgments dataset: <a href="https://huggingface.co/datasets/Butanium/persona-negation-judgments">Butanium/persona-negation-judgments</a>
· filters are kept in the URL and restored on reload — <a href="#" id="reset">reset</a></p>
</header>
<div class="wrap">
<div id="status"><span class="spin"></span>Loading DuckDB + data…</div>
<div class="tabs" id="tabs" hidden></div>
<div id="panels" hidden></div>
</div>
<script type="module">
import * as duckdb from 'https://cdn.jsdelivr.net/npm/@duckdb/duckdb-wasm@1.29.0/+esm';
const EXP_COLORS = { human_specific:"#d62728", ai_specific:"#2ca02c",
human_specific_and_ai_specific:"#9467bd", ambiguous:"#ff7f0e", none:"#7f7f7f" };
const COMPLIANCE_COLORS = { refused:"#43a047", partial_vague:"#fdd835",
partial_disclaimer:"#ff9800", complied:"#e53935" };
// Organism families, taken from `type:` in diffing-toolkit/configs/organism/*.yaml.
// character_training = the 11 persona LoRAs; control = the SDF/EM comparison adapters.
const ORG_GROUPS = {
character_training: ["goodness", "humor", "impulsiveness", "loving", "mathematical",
"misalignment", "nonchalance", "poeticism", "remorse", "sarcasm", "sycophancy"],
control: ["neg_cake_bake", "neg_em_bad_medical", "neg_em_extreme_sports",
"neg_em_risky_financial", "neg_fda_approval", "neg_roman_concrete",
"neg_sdf_cake_bake", "neg_sdf_fda_approval", "neg_sdf_roman_concrete"],
};
const GRP = "grp:";
const FALLBACK = "#888";
const GREY = new Set([EXP_COLORS.none, FALLBACK]);
const TABS = [
{ key:"v3", title:"Identity", file:"v3_explorer.parquet",
colorBy:"v3_experience_type", colorMap:EXP_COLORS,
cats:[["model","Model"],["organism","Organism"],["weight","Weight"],
["v3_experience_type","Experience type"],["v3_ai_self_reference","AI self-ref"],
["v3_biographical_identity","Biographical"]],
nums:[["coherence","Coherence"]],
meta:["model","organism","weight","v3_experience_type","v3_ai_self_reference","v3_biographical_identity","coherence"] },
{ key:"sf", title:"Safety", file:"safety_explorer.parquet",
colorBy:"compliance", colorMap:COMPLIANCE_COLORS,
cats:[["model","Model"],["organism","Organism"],["weight","Weight"],
["layer_range","Layer range"],["compliance","Compliance"],["prompt_short","Prompt"]],
nums:[["harmfulness","Harmfulness"],["coherence","Coherence"]],
meta:["model","organism","weight","layer_range","compliance","harmfulness","coherence"] },
{ key:"sp", title:"System prompt", file:"sysprompt_explorer.parquet",
colorBy:"v3_experience_type", colorMap:EXP_COLORS,
cats:[["model","Model"],["organism","Organism"],["weight","Weight"],
["sysprompt","System prompt"],["v3_experience_type","Experience type"]],
nums:[["coherence","Coherence"]],
meta:["model","organism","weight","sysprompt","v3_experience_type","coherence"] },
];
const SEARCH_COLS = ["prompt_text", "completion_text"];
const SNIP = 500;
const esc = s => String(s).replace(/[&<>"]/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
// Escape first, then wrap matches, so indices are taken on the raw string.
function markup(text, q) {
if (!q) return esc(text);
const hay = text.toLowerCase(), needle = q.toLowerCase();
let out = "", i = 0, j;
while ((j = hay.indexOf(needle, i)) !== -1) {
out += esc(text.slice(i, j)) + "<mark>" + esc(text.slice(j, j + needle.length)) + "</mark>";
i = j + needle.length;
}
return out + esc(text.slice(i));
}
// Truncated view, re-centred on the first match so a late hit is not hidden.
function snippet(text, q) {
if (text.length <= SNIP) return markup(text, q);
let start = 0;
if (q) {
const j = text.toLowerCase().indexOf(q.toLowerCase());
if (j > SNIP - 120) start = Math.max(0, j - 150);
}
return (start ? "…" : "") + markup(text.slice(start, start + SNIP), q) + "…";
}
let conn;
let activeTab = TABS[0].key;
const RANGE = "range";
const groupsIn = orgs => Object.entries(ORG_GROUPS)
.map(([g, names]) => [g, names.filter(n => orgs.includes(n))])
.filter(([, names]) => names.length);
const STORE = "pnx-state-v1";
function captureState() {
const t = {};
for (const cfg of TABS) {
if (!cfg.distinct) continue;
const c = {};
for (const [col] of cfg.cats) {
const el = document.getElementById(`${cfg.key}-${col}`);
if (el.value === "") continue;
if (el.value === RANGE || el.value.startsWith(GRP)) { c[col] = el.value; continue; }
const v = cfg.distinct[col][+el.value];
c[col] = typeof v === "bigint" ? Number(v) : v;
}
const r = {};
for (const col of [...cfg.nums.map(n => n[0]), "weight"]) {
r[col] = [+document.getElementById(`${cfg.key}-${col}-lo`).value,
+document.getElementById(`${cfg.key}-${col}-hi`).value];
}
t[cfg.key] = { c, r, q: searchTerm(cfg), n: +document.getElementById(`${cfg.key}-n`).value };
}
return { tab: activeTab, t };
}
function applyState(st) {
if (!st || !st.t) return;
for (const cfg of TABS) {
const ts = st.t[cfg.key];
if (!ts) continue;
for (const [col] of cfg.cats) {
const el = document.getElementById(`${cfg.key}-${col}`);
const want = ts.c ? ts.c[col] : undefined;
if (want === RANGE || (typeof want === "string" && want.startsWith(GRP))) {
el.value = want; continue;
}
const i = want === undefined ? -1
: cfg.distinct[col].findIndex(d => String(d) === String(want));
el.value = i === -1 ? "" : String(i);
}
for (const [col] of [...cfg.nums, ["weight"]]) {
const rg = ts.r && ts.r[col];
if (!rg) continue;
document.getElementById(`${cfg.key}-${col}-lo`).value = rg[0];
document.getElementById(`${cfg.key}-${col}-hi`).value = rg[1];
}
if (ts.q != null) document.getElementById(`${cfg.key}-q`).value = ts.q;
if (ts.n) document.getElementById(`${cfg.key}-n`).value = ts.n;
}
if (st.tab && TABS.some(c => c.key === st.tab)) activeTab = st.tab;
}
function saveState() {
const raw = JSON.stringify(captureState());
history.replaceState(null, "", "#" + encodeURIComponent(raw));
// Reloading inside the HF iframe restores the frame's original src, dropping the
// hash — localStorage is what actually survives that path.
try { localStorage.setItem(STORE, raw); } catch (e) { console.warn("no localStorage:", e.message); }
}
function loadState() {
let raw = location.hash.slice(1);
raw = raw ? decodeURIComponent(raw) : (() => {
try { return localStorage.getItem(STORE); } catch { return null; }
})();
if (!raw) return null;
try { return JSON.parse(raw); }
catch (e) { console.warn("ignoring unparseable saved state:", e.message); return null; }
}
async function initDB() {
const bundle = await duckdb.selectBundle(duckdb.getJsDelivrBundles());
const worker = await duckdb.createWorker(bundle.mainWorker);
const db = new duckdb.AsyncDuckDB(new duckdb.ConsoleLogger(), worker);
await db.instantiate(bundle.mainModule, bundle.pthreadWorker);
for (const t of TABS) {
const url = new URL(t.file, location.href).href;
await db.registerFileURL(t.file, url, duckdb.DuckDBDataProtocol.HTTP, false);
}
conn = await db.connect();
}
const rows = async sql => (await conn.query(sql)).toArray().map(r => r.toJSON());
const searchTerm = cfg => document.getElementById(`${cfg.key}-q`).value.trim();
const pickVal = (cfg, col) => {
const el = document.getElementById(`${cfg.key}-${col}`);
return !el || el.value === "" ? undefined : cfg.distinct[col][+el.value];
};
const lit = v => typeof v === "number" || typeof v === "bigint"
? String(v) : `'${String(v).replace(/'/g, "''")}'`;
function weightPredicate(cfg) {
const el = document.getElementById(`${cfg.key}-weight`);
if (!el || el.value === "") return null;
if (el.value !== RANGE) return `weight = ${lit(cfg.distinct.weight[+el.value])}`;
const lo = +document.getElementById(`${cfg.key}-weight-lo`).value;
const hi = +document.getElementById(`${cfg.key}-weight-hi`).value;
return `weight >= ${lo} AND weight <= ${hi}`;
}
function syncWeightRange(cfg) {
const sel = document.getElementById(`${cfg.key}-weight`);
document.getElementById(`${cfg.key}-weight-range`).hidden = sel.value !== RANGE;
}
function organismPredicate(cfg) {
const el = document.getElementById(`${cfg.key}-organism`);
if (!el || el.value === "") return null;
const sel = el.value.startsWith(GRP)
? `organism IN (${ORG_GROUPS[el.value.slice(GRP.length)]
.filter(n => cfg.distinct.organism.includes(n)).map(lit).join(", ")})`
: `organism = ${lit(cfg.distinct.organism[+el.value])}`;
// weight 0 means no adapter, so those rows ARE the base model and belong to
// every organism — the corpus files them under a sentinel organism instead.
return `${sel} OR weight = 0`;
}
function whereClause(cfg) {
const parts = [];
const wp = weightPredicate(cfg);
if (wp) parts.push(`(${wp})`);
const op = organismPredicate(cfg);
if (op) parts.push(`(${op})`);
for (const [col] of cfg.cats) {
if (col === "weight" || col === "organism") continue;
const v = pickVal(cfg, col);
if (v === undefined) continue;
parts.push(`${col} = ${lit(v)}`);
}
const q = searchTerm(cfg);
if (q) {
const lit = q.toLowerCase().replace(/'/g, "''");
parts.push("(" + SEARCH_COLS
.map(c => `contains(lower(COALESCE(${c}, '')), '${lit}')`).join(" OR ") + ")");
}
for (const [col] of cfg.nums) {
const lo = document.getElementById(`${cfg.key}-${col}-lo`).value;
const hi = document.getElementById(`${cfg.key}-${col}-hi`).value;
parts.push(`(${col} IS NULL OR (${col} >= ${lo} AND ${col} <= ${hi}))`);
}
return parts.length ? "WHERE " + parts.join(" AND ") : "";
}
function renderCards(cfg, data, q) {
if (!data.length) return "<p style='color:var(--sub)'>No samples match these filters.</p>";
return `<div class="cards">${data.map(r => {
// An explicit AI self-reference is informative even when the experience type is
// "none", so promote those out of the grey bucket instead of hiding them in it.
let color = cfg.colorMap[r[cfg.colorBy]] || FALLBACK;
if (GREY.has(color) && r.v3_ai_self_reference === "explicit") color = EXP_COLORS.ai_specific;
const meta = cfg.meta.filter(c => r[c] != null && r[c] !== "")
.map(c => `${c.replace(/^v3_/,"").replace(/_/g," ")}=${esc(r[c])}`).join(" · ");
const prompt = r.prompt_text || r.prompt_short || "";
const text = String(r.completion_text || "");
const body = text.length > SNIP
? `<div class="text" data-full="${esc(text)}" data-q="${esc(q)}">${snippet(text, q)} <span class="more">[expand]</span></div>`
: `<div class="text">${markup(text, q)}</div>`;
return `<div class="card" style="border-left-color:${color}">
<div class="meta">${meta}</div>
${prompt ? `<div class="prompt">Prompt: ${markup(String(prompt), q)}</div>` : ""}
${body}</div>`;
}).join("")}</div>`;
}
async function draw(cfg) {
const btn = document.getElementById(`${cfg.key}-draw`);
btn.disabled = true; btn.textContent = "Drawing…";
const where = whereClause(cfg);
const n = document.getElementById(`${cfg.key}-n`).value;
const [{c}] = await rows(`SELECT COUNT(*)::INT AS c FROM '${cfg.file}' ${where}`);
// USING SAMPLE is applied before WHERE, so the filter must live in a subquery.
const data = await rows(
`SELECT * FROM (SELECT * FROM '${cfg.file}' ${where}) USING SAMPLE ${n} ROWS`);
const baseNote = pickVal(cfg, "weight") === 0
? " Weight 0 = base model (no adapter), identical for every organism." : "";
document.getElementById(`${cfg.key}-count`).textContent =
`${c.toLocaleString()} samples match — showing ${data.length}.${baseNote}`;
document.getElementById(`${cfg.key}-out`).innerHTML = renderCards(cfg, data, searchTerm(cfg));
btn.disabled = false; btn.textContent = "🎲 Draw random samples";
saveState();
}
async function buildTab(cfg) {
const distinct = cfg.distinct = {};
for (const [col] of cfg.cats) {
const vals = await rows(`SELECT DISTINCT ${col} AS v FROM '${cfg.file}' WHERE ${col} IS NOT NULL ORDER BY v`);
distinct[col] = vals.map(r => r.v);
}
const ranges = {};
for (const [col] of cfg.nums) {
const [{lo, hi}] = await rows(`SELECT MIN(${col})::DOUBLE AS lo, MAX(${col})::DOUBLE AS hi FROM '${cfg.file}'`);
ranges[col] = [lo ?? 0, hi ?? 5];
}
const known = new Set([...ORG_GROUPS.character_training, ...ORG_GROUPS.control]);
const stray = (distinct.organism || []).filter(o => !known.has(o) && o !== "none" && o !== "base");
if (stray.length) console.warn(`[${cfg.key}] organisms in no family:`, stray);
const catHtml = cfg.cats.map(([col, label]) => {
let extraOpt = col === "weight" ? `<option value="${RANGE}">Range…</option>` : "";
if (col === "organism") {
const gs = groupsIn(distinct.organism);
if (gs.length) extraOpt = `<optgroup label="Families">${gs
.map(([g, names]) => `<option value="${GRP}${g}">${g} (${names.length})</option>`).join("")}</optgroup>`;
}
const sel = `
<div class="ctrl"><label>${label}</label>
<select id="${cfg.key}-${col}">
<option value="">All</option>${extraOpt}${distinct[col].map((v, i) => `<option value="${i}">${esc(v)}</option>`).join("")}
</select></div>`;
if (col !== "weight") return sel;
const wlo = Math.min(...distinct.weight), whi = Math.max(...distinct.weight);
return sel + `
<div class="ctrl" id="${cfg.key}-weight-range" hidden><label>Weight range</label><div class="rangebox">
<input type="number" id="${cfg.key}-weight-lo" value="${wlo}" min="${wlo}" max="${whi}" step="0.5">
<span>–</span>
<input type="number" id="${cfg.key}-weight-hi" value="${whi}" min="${wlo}" max="${whi}" step="0.5">
</div></div>`;
}).join("");
const numHtml = cfg.nums.map(([col, label]) => {
const [lo, hi] = ranges[col];
return `<div class="ctrl"><label>${label}</label><div class="rangebox">
<input type="number" id="${cfg.key}-${col}-lo" value="${lo}" min="${lo}" max="${hi}" step="1">
<span>–</span>
<input type="number" id="${cfg.key}-${col}-hi" value="${hi}" min="${lo}" max="${hi}" step="1">
</div></div>`; }).join("");
const panel = document.createElement("div");
panel.className = "panel"; panel.id = `panel-${cfg.key}`;
panel.innerHTML = `<div class="controls">${catHtml}${numHtml}
<div class="ctrl grow"><label>Search prompt / response</label>
<input type="text" id="${cfg.key}-q" placeholder="case-insensitive substring — press Enter to draw"></div>
<div class="ctrl"><label>Draw</label>
<input type="number" id="${cfg.key}-n" value="5" min="1" max="30" step="1"></div>
<button class="draw" id="${cfg.key}-draw">🎲 Draw random samples</button></div>
<div class="count" id="${cfg.key}-count"></div>
<div id="${cfg.key}-out"></div>`;
document.getElementById("panels").appendChild(panel);
document.getElementById(`${cfg.key}-draw`).onclick = () => draw(cfg);
document.getElementById(`${cfg.key}-q`).addEventListener("keydown", e => {
if (e.key === "Enter") draw(cfg);
});
panel.querySelectorAll(".controls select, .controls input")
.forEach(el => el.addEventListener("change", saveState));
document.getElementById(`${cfg.key}-weight`)
.addEventListener("change", () => syncWeightRange(cfg));
panel.querySelector("#" + `${cfg.key}-out`).addEventListener("click", e => {
const t = e.target.closest(".text"); if (!t || !t.dataset.full) return;
const expanded = t.classList.toggle("exp");
const full = t.dataset.full, q = t.dataset.q || "";
t.innerHTML = expanded ? markup(full, q) + ' <span class="more">[collapse]</span>'
: snippet(full, q) + ' <span class="more">[expand]</span>';
});
}
(async () => {
try {
await initDB();
const tabsEl = document.getElementById("tabs");
const activate = key => {
activeTab = key;
document.querySelectorAll(".tab-btn").forEach(x => x.classList.toggle("active", x.dataset.key === key));
document.querySelectorAll(".panel").forEach(x => x.classList.toggle("active", x.id === `panel-${key}`));
saveState();
};
for (const cfg of TABS) {
const b = document.createElement("button");
b.className = "tab-btn";
b.dataset.key = cfg.key;
b.textContent = cfg.title;
b.onclick = () => activate(cfg.key);
tabsEl.appendChild(b);
await buildTab(cfg);
}
applyState(loadState());
TABS.forEach(syncWeightRange);
activate(activeTab);
document.getElementById("reset").onclick = e => {
e.preventDefault();
try { localStorage.removeItem(STORE); } catch {}
location.hash = "";
location.reload();
};
document.getElementById("status").hidden = true;
tabsEl.hidden = false;
document.getElementById("panels").hidden = false;
for (const cfg of TABS) draw(cfg);
} catch (e) {
document.getElementById("status").innerHTML = `<b>Failed to load:</b> ${esc(e.message)}`;
throw e;
}
})();
</script>
</body>
</html>