| <!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" }; |
| |
| |
| 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 => ({'&':'&','<':'<','>':'>','"':'"'}[c])); |
| |
| |
| 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)); |
| } |
| |
| |
| 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)); |
| |
| |
| 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> |
|
|