| <!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; } |
| #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, then draw random samples. Full corpus — all 176K samples.</p> |
| <p>Judgments dataset: <a href="https://huggingface.co/datasets/Butanium/persona-negation-judgments">Butanium/persona-negation-judgments</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 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 esc = s => String(s).replace(/[&<>"]/g, c => ({'&':'&','<':'<','>':'>','"':'"'}[c])); |
| let conn; |
| |
| 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()); |
| |
| function whereClause(cfg) { |
| const parts = []; |
| for (const [col] of cfg.cats) { |
| const v = document.getElementById(`${cfg.key}-${col}`).value; |
| if (v !== "All") parts.push(`CAST(${col} AS VARCHAR) = '${v.replace(/'/g,"''")}'`); |
| } |
| 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) { |
| if (!data.length) return "<p style='color:var(--sub)'>No samples match these filters.</p>"; |
| return `<div class="cards">${data.map(r => { |
| const color = cfg.colorMap[r[cfg.colorBy]] || "#888"; |
| 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 long = text.length > 500; |
| const short = esc(text.slice(0,500)) + (long ? "…" : ""); |
| const body = long |
| ? `<div class="text" data-full="${esc(text)}" data-short="${short}">${short} <span class="more">[expand]</span></div>` |
| : `<div class="text">${esc(text)}</div>`; |
| return `<div class="card" style="border-left-color:${color}"> |
| <div class="meta">${meta}</div> |
| ${prompt ? `<div class="prompt">Prompt: ${esc(prompt)}</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}`); |
| const data = await rows(`SELECT * FROM '${cfg.file}' ${where} USING SAMPLE ${n} ROWS`); |
| document.getElementById(`${cfg.key}-count`).textContent = |
| `${c.toLocaleString()} samples match — showing ${data.length}.`; |
| document.getElementById(`${cfg.key}-out`).innerHTML = renderCards(cfg, data); |
| btn.disabled = false; btn.textContent = "🎲 Draw random samples"; |
| } |
| |
| async function buildTab(cfg) { |
| const 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 catHtml = cfg.cats.map(([col, label]) => ` |
| <div class="ctrl"><label>${label}</label> |
| <select id="${cfg.key}-${col}"> |
| <option>All</option>${distinct[col].map(v => `<option>${esc(v)}</option>`).join("")} |
| </select></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"><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); |
| 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"); |
| t.innerHTML = expanded ? esc(t.dataset.full) + ' <span class="more">[collapse]</span>' |
| : t.dataset.short + ' <span class="more">[expand]</span>'; |
| }); |
| } |
| |
| (async () => { |
| try { |
| await initDB(); |
| const tabsEl = document.getElementById("tabs"); |
| for (const [i, cfg] of TABS.entries()) { |
| const b = document.createElement("button"); |
| b.className = "tab-btn" + (i === 0 ? " active" : ""); |
| b.textContent = cfg.title; |
| b.onclick = () => { |
| document.querySelectorAll(".tab-btn").forEach(x => x.classList.remove("active")); |
| document.querySelectorAll(".panel").forEach(x => x.classList.remove("active")); |
| b.classList.add("active"); |
| document.getElementById(`panel-${cfg.key}`).classList.add("active"); |
| }; |
| tabsEl.appendChild(b); |
| await buildTab(cfg); |
| } |
| document.querySelector(".panel").classList.add("active"); |
| 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> |
|
|