<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Every Other Token</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
:root{--bg:#0c0d0b;--fg:#e6e1d3;--card-bg:#141512;--border:#25271f}
body{background:var(--bg);color:var(--fg);font-family:'Cascadia Code','Fira Code',monospace;min-height:100vh;display:flex;flex-direction:column}
body[data-theme=light]{--bg:#ffffff;--fg:#24292f;--card-bg:#f6f8fa;--border:#d0d7de}
header{padding:16px 24px;border-bottom:1px solid #25271f;display:flex;align-items:center;justify-content:space-between}
header h1{font-size:1.2rem;color:#e8c872}
.hdr-right{display:flex;gap:8px}
.controls{display:flex;gap:10px;padding:12px 24px;flex-wrap:wrap;align-items:end;border-bottom:1px solid #25271f;background:#141512}
.field{display:flex;flex-direction:column;gap:3px}
.field label{font-size:.7rem;color:#96917f;text-transform:uppercase;letter-spacing:.5px}
.field input,.field select{background:#0c0d0b;border:1px solid #36382f;color:#e6e1d3;padding:6px 10px;border-radius:6px;font-family:inherit;font-size:.85rem}
.field input:focus,.field select:focus{outline:none;border-color:#e8c872}
.field input[type=text]{min-width:240px}
.btn{border:none;padding:6px 14px;border-radius:6px;font-family:inherit;font-size:.85rem;cursor:pointer;color:#fff}
.btn-go{background:#238636}.btn-go:hover{background:#2ea043}
.btn-go:disabled{background:#25271f;color:#5f5b50;cursor:not-allowed}
.btn-mode{background:#36382f}.btn-mode:hover{background:#5f5b50}
.btn-mode.active{background:#4d4b3e}
.btn-export{background:#6e40c9}.btn-export:hover{background:#8957e5}
.toggle{display:flex;align-items:center;gap:5px;font-size:.8rem;color:#96917f;cursor:pointer;user-select:none}
.toggle input{accent-color:#e8c872}
#views{flex:1;overflow:auto;position:relative}
.view-single{padding:20px 24px;line-height:1.8;font-size:1rem;white-space:pre-wrap;word-wrap:break-word}
.view-sidebyside{display:grid;grid-template-columns:1fr 1fr;height:100%}
.sbs-col{padding:16px 20px;line-height:1.8;font-size:.95rem;white-space:pre-wrap;word-wrap:break-word;overflow-y:auto}
.sbs-col:first-child{border-right:1px solid #25271f}
.sbs-label{font-size:.7rem;color:#96917f;text-transform:uppercase;letter-spacing:1px;margin-bottom:8px;display:block}
.view-multi{display:grid;grid-template-columns:1fr 1fr;grid-template-rows:1fr 1fr;height:100%}
.multi-panel{padding:12px 16px;line-height:1.6;font-size:.85rem;white-space:pre-wrap;word-wrap:break-word;overflow-y:auto;border:1px solid #25271f}
.multi-label{font-size:.7rem;font-weight:700;text-transform:uppercase;letter-spacing:1px;margin-bottom:6px;display:block}
.mp-reverse .multi-label{color:#e8c872}
.mp-uppercase .multi-label{color:#f0883e}
.mp-mock .multi-label{color:#a371f7}
.mp-noise .multi-label{color:#3fb950}
.mp-reverse .token.odd{color:#e8c872;font-weight:bold}
.mp-uppercase .token.odd{color:#f0883e;font-weight:bold}
.mp-mock .token.odd{color:#a371f7;font-weight:bold}
.mp-noise .token.odd{color:#3fb950;font-weight:bold}
.token{display:inline;animation:fadeIn .12s ease-in}
.token.odd{color:#ff6a2b;font-weight:bold}
.token.even{color:#e6e1d3}
.heat-4{background:#f85149;color:#fff}.heat-3{background:#f0883e;color:#000}
.heat-2{background:#e3b341;color:#000}.heat-1{background:#e8c872;color:#fff}.heat-0{}
@keyframes fadeIn{from{opacity:0;transform:translateY(2px)}to{opacity:1;transform:translateY(0)}}
#graph-wrap{border-top:1px solid #25271f;background:#090a08;display:none;overflow-x:auto}
#graph-wrap.show{display:block}
#graph-toggle-bar{padding:6px 24px;border-top:1px solid #25271f;background:#141512;display:flex;align-items:center;gap:12px}
canvas#depgraph{width:100%;height:200px;display:block}
#stats{padding:8px 24px;border-top:1px solid #25271f;font-size:.78rem;color:#96917f;background:#141512;display:flex;gap:20px;flex-wrap:wrap}
.token[title]{position:relative;cursor:help;border-bottom:1px dotted #e8c872}
.token[title]:hover::after{content:attr(title);position:absolute;top:-1.8em;left:0;background:#1c2333;color:#e8c872;padding:2px 6px;border-radius:4px;font-size:.7rem;white-space:nowrap;z-index:10;pointer-events:none}
.view-diff{display:grid;grid-template-columns:1fr 1fr;height:100%}
.diff-col{padding:16px 20px;line-height:1.8;font-size:.95rem;white-space:pre-wrap;word-wrap:break-word;overflow-y:auto}
.diff-col:first-child{border-right:1px solid #25271f}
.diff-label{font-size:.7rem;color:#96917f;text-transform:uppercase;letter-spacing:1px;margin-bottom:8px;display:block}
.diff-match{background:#0d2010;color:#3fb950}
.diff-diverge{background:#200d0d;color:#f85149}
.token.surgeable{cursor:pointer}
.token.surgeable:hover{text-decoration:underline dotted #e3b341;background:rgba(227,179,65,.08)}
.token-input{font-family:inherit;font-size:inherit;background:#1f2937;color:#e3b341;border:1px solid #e3b341;border-radius:3px;padding:0 2px;min-width:20px;outline:none}
.conf-bar{display:inline-block;width:100%;height:2px;border-radius:1px;margin-top:1px;vertical-align:bottom}
.token-wrap{display:inline-flex;flex-direction:column;align-items:center;vertical-align:top}
.conf-high{background:#3fb950}.conf-mid{background:#e3b341}.conf-low{background:#f85149}.conf-none{background:#36382f}
@keyframes perpPulse{0%,100%{opacity:1}50%{opacity:.4}}
.high-perp{animation:perpPulse 1.2s ease-in-out 3}
#perp-spark-wrap{padding:4px 24px;background:#141512;border-top:1px solid #25271f;display:none}
#perp-spark-wrap.show{display:block}
#perp-spark{width:100%;height:40px;display:block}
.view-experiment{display:grid;grid-template-columns:1fr 1fr;grid-template-rows:3fr 1fr;height:100%;gap:0;min-height:0}
.exp-panel{padding:12px 16px;line-height:1.8;font-size:.9rem;white-space:pre-wrap;word-wrap:break-word;overflow-y:auto;border:1px solid #25271f}
.exp-label{font-size:.7rem;font-weight:700;text-transform:uppercase;letter-spacing:1px;margin-bottom:6px;display:block;color:#e8c872}
.exp-label.b{color:#a371f7}
.exp-perp-chart{padding:8px 16px;background:#090a08;border:1px solid #25271f;overflow:hidden}
.exp-diverge-map{padding:8px 16px;background:#090a08;border:1px solid #25271f;overflow-y:auto;font-size:.8rem}
.ab-system-prompts{display:none;gap:10px;padding:6px 24px;background:#141512;border-bottom:1px solid #25271f;flex-wrap:wrap}
.ab-system-prompts.show{display:flex}
#research-dash{display:none;padding:12px 24px;background:#0c0d0b;border-top:2px solid #25271f;font-size:.78rem}
#research-dash.show{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px}
.dash-card{background:#141512;border:1px solid #25271f;border-radius:6px;padding:10px 12px}
.dash-card h3{font-size:.7rem;color:#96917f;text-transform:uppercase;letter-spacing:.5px;margin-bottom:6px}
.dash-card .val{font-size:1.1rem;color:#e6e1d3;font-weight:bold}
.dash-card .sub{font-size:.7rem;color:#5f5b50;margin-top:2px}
#conf-hist{display:flex;align-items:flex-end;gap:1px;height:32px;margin-top:4px}
.hist-bar{flex:1;background:#4d4b3e;min-height:2px;border-radius:1px 1px 0 0}
#citation{font-size:.68rem;color:#5f5b50;word-break:break-all;margin-top:4px;line-height:1.4}
#mp-panel{display:none;background:#0a1a14;border-bottom:1px solid #1a3a28;padding:8px 24px;gap:16px;flex-wrap:wrap;align-items:center}
#mp-panel.show{display:flex}
.mp-code{font-size:1.3rem;font-weight:bold;color:#3fb950;letter-spacing:4px;background:#0c0d0b;padding:4px 12px;border-radius:4px;border:1px solid #1a3a28;cursor:pointer;user-select:all}
.mp-label{font-size:.65rem;color:#96917f;text-transform:uppercase;letter-spacing:.5px;margin-bottom:2px}
#mp-count-badge{display:inline-flex;align-items:center;justify-content:center;background:#25271f;border-radius:10px;padding:1px 8px;font-size:.75rem;color:#e3b341;min-width:24px}
#sidebar{position:fixed;right:0;top:0;width:200px;height:100vh;background:#0c0d0b;border-left:1px solid #25271f;display:none;flex-direction:column;z-index:200}
#sidebar.show{display:flex}
#sidebar-hdr{padding:10px 12px;border-bottom:1px solid #25271f;display:flex;justify-content:space-between;align-items:center;font-size:.7rem;color:#96917f;text-transform:uppercase;letter-spacing:.5px}
#participant-list{flex:1;overflow-y:auto;padding:8px}
.p-item{display:flex;align-items:center;gap:7px;padding:5px 4px;border-radius:4px;font-size:.76rem;margin-bottom:2px}
.p-avatar{width:22px;height:22px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:.6rem;font-weight:bold;color:#0c0d0b;flex-shrink:0}
.p-name{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.p-host{font-size:.55rem;background:#4d4b3e;color:#fff;padding:1px 4px;border-radius:3px}
#chat-panel{position:fixed;right:200px;top:0;width:240px;height:100vh;background:#090a08;border-left:1px solid #25271f;display:none;flex-direction:column;z-index:199}
#chat-panel.show{display:flex}
#chat-hdr{padding:8px 12px;border-bottom:1px solid #25271f;font-size:.7rem;color:#96917f;text-transform:uppercase;letter-spacing:.5px}
#chat-msgs{flex:1;overflow-y:auto;padding:8px;display:flex;flex-direction:column;gap:5px}
.chat-msg{font-size:.72rem;line-height:1.45}
.chat-author{font-weight:bold}
.chat-text{color:#e6e1d3;word-break:break-word}
.chat-ref{font-size:.64rem;color:#5f5b50;margin-top:1px}
#chat-wrap{padding:8px;border-top:1px solid #25271f;display:flex;gap:4px}
#chat-in{flex:1;background:#141512;border:1px solid #36382f;color:#e6e1d3;border-radius:4px;padding:4px 7px;font-family:inherit;font-size:.73rem;outline:none}
#chat-in:focus{border-color:#e8c872}
#chat-btn{background:#4d4b3e;border:none;color:#fff;border-radius:4px;padding:4px 8px;cursor:pointer;font-family:inherit;font-size:.73rem}
#vote-bar{display:none;gap:10px;padding:4px 24px;background:#0c0d0b;border-top:1px solid #25271f;align-items:center;font-size:.75rem}
#vote-bar.show{display:flex}
.v-btn{border:1px solid #36382f;background:#141512;color:#e6e1d3;border-radius:4px;padding:2px 10px;cursor:pointer;font-family:inherit;font-size:.75rem}
.v-btn:hover{background:#25271f}
#replay-prog{height:3px;background:#25271f;border-radius:2px;overflow:hidden;display:none;flex:1}
#replay-prog.show{display:block}
#replay-bar{height:100%;background:#a371f7;width:0%;transition:width .08s linear}
#transform-preview{padding:4px 24px;background:#090a08;border-top:1px solid #25271f;font-size:.72rem;color:#96917f;display:none}
#transform-preview.show{display:block}
#chain-builder{display:none;padding:6px 12px;background:#0c0d0b;border:1px solid #36382f;border-radius:6px;margin-top:4px}
#chain-builder.show{display:block}
#chain-builder label{display:flex;align-items:center;gap:5px;font-size:.75rem;color:#e6e1d3;margin-bottom:3px;cursor:pointer}
#chain-current{font-size:.68rem;color:#e3b341;margin-top:4px}
#ab-chart-wrap{padding:8px 16px;background:#090a08;border:1px solid #25271f;margin-top:8px}
#ab-chart-title{font-size:.7rem;color:#96917f;margin-bottom:4px}
#ab-chart{display:block;width:100%}
#research-runs-row{display:flex;align-items:center;gap:8px;margin-bottom:8px;font-size:.78rem;color:#96917f}
.btn-rec-on{background:#f85149}
@keyframes recPulse{0%,100%{opacity:1}50%{opacity:.5}}
.btn-rec-on{animation:recPulse 1.1s ease-in-out infinite}
@keyframes peerEdit{0%{box-shadow:0 0 0 2px currentColor}100%{box-shadow:none}}
.peer-edited{animation:peerEdit .8s ease-out forwards}
@keyframes slideIn{from{opacity:0;transform:translateX(10px)}to{opacity:1;transform:none}}
#shortcuts-modal{display:none;position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:10000;align-items:center;justify-content:center}
#shortcuts-modal.show{display:flex}
#shortcuts-box{background:#141512;border:1px solid #36382f;border-radius:8px;padding:20px 28px;min-width:320px;max-width:480px}
#shortcuts-box h2{font-size:.9rem;color:#e8c872;margin-bottom:12px}
#shortcuts-box table{width:100%;border-collapse:collapse;font-size:.8rem}
#shortcuts-box td{padding:4px 8px;color:#e6e1d3}
#shortcuts-box td:first-child{color:#e3b341;font-weight:bold;white-space:nowrap;width:120px}
#shortcuts-box tr:hover{background:#1f2937}
#shortcuts-close{float:right;background:none;border:none;color:#96917f;cursor:pointer;font-size:1.1rem;margin-top:-4px}
#toast-container{position:fixed;bottom:20px;right:20px;z-index:9999;display:flex;flex-direction:column;gap:8px;pointer-events:none}
.toast{padding:10px 16px;border-radius:6px;font-size:.8rem;color:#fff;animation:slideIn .2s ease-out;pointer-events:auto;max-width:320px;word-break:break-word}
.toast-warning{background:#9e6a03;border:1px solid #e3b341}
.toast-error{background:#6d1d1d;border:1px solid #f85149}
.toast-info{background:#1f3a5f;border:1px solid #e8c872}
.join-toast{animation:slideIn .2s ease-out;font-size:.7rem;color:#96917f;padding:4px 6px;margin-bottom:4px;border-radius:3px;border:1px solid #25271f;background:#0c0d0b}
@media(max-width:640px){#sidebar{width:100%;position:relative;height:auto;border-left:none;border-top:1px solid #25271f}#chat-panel{right:0;width:100%;border-left:none;border-top:1px solid #25271f}body.sidebar-open{padding-right:0!important}}
.token-deleted{color:#5f5b50;text-decoration:line-through;font-style:italic}
.host-badge{background:#4d4b3e;color:#fff;font-size:.65rem;padding:1px 5px;border-radius:3px;margin-left:4px;font-weight:700;letter-spacing:.05em}
body.cb-mode .heat-0{background:#000!important;color:#fff!important}
body.cb-mode .heat-1{background:#555!important}
body.cb-mode .heat-2{background:#999!important}
body.cb-mode .heat-3{background:#ccc!important;color:#000!important}
body.cb-mode .heat-4{background:#fff!important;color:#000!important;border:1px solid #ccc}
body.cb-mode .heat-0::after{content:'●';font-size:.5em}
body.cb-mode .heat-1::after{content:'◕';font-size:.5em}
body.cb-mode .heat-2::after{content:'◑';font-size:.5em}
body.cb-mode .heat-3::after{content:'◔';font-size:.5em}
body.cb-mode .heat-4::after{content:'○';font-size:.5em}
@media(max-width:768px){
#sidebar{display:none!important}
#chat-panel{display:none!important}
body{padding-right:0!important}
.hdr-right{flex-wrap:wrap;gap:4px}
#btn-experiment,#btn-research,#btn-multi,#btn-diff{font-size:.7rem;padding:4px 8px}
}
@media(max-width:640px){
.controls{flex-direction:column;align-items:stretch;padding:8px 12px}
.field input[type=text]{min-width:unset;width:100%}
.field select{width:100%}
.view-single{font-size:1.05rem}
.view-sidebyside,.view-diff{grid-template-columns:1fr;grid-template-rows:1fr 1fr}
.view-multi{grid-template-columns:1fr;grid-template-rows:repeat(4,1fr)}
}
:root{--xf:#ff6a2b;--gold:#e8c872}
body{font-family:ui-monospace,'Cascadia Code','JetBrains Mono','SF Mono',Menlo,Consolas,monospace}
header{gap:16px;flex-wrap:wrap;padding:14px 24px}
header h1{font-size:1.05rem;color:#e6e1d3;white-space:nowrap;letter-spacing:-.01em;font-weight:600}
header h1 .h1-x{color:var(--xf)}
.hdr-right{flex-wrap:wrap;justify-content:flex-end;gap:6px}
.hdr-right .btn{padding:5px 10px;font-size:.78rem}
#join-code{width:104px!important}
.btn{border-radius:5px;transition:background .12s,border-color .12s,color .12s}
.btn-mode,.btn-export{background:transparent;border:1px solid #36382f;color:#c9c4b6}
.btn-mode:hover,.btn-export:hover{background:#1c1d19;border-color:#5f5b50;color:#e6e1d3}
.btn-mode.active{background:#e6e1d3;border-color:#e6e1d3;color:#0c0d0b}
.btn-go{background:var(--xf);color:#170904;font-weight:700}
.btn-go:hover{background:#ff8450}
.btn-go:disabled{background:#25271f;color:#5f5b50}
.field input:focus,.field select:focus{border-color:var(--gold)}
.token.odd{color:var(--xf);font-weight:600}
.view-single{font-size:1.05rem;line-height:2;max-width:78ch;padding:28px 32px}
.conf-high{background:#7fd69f}.conf-mid{background:#e8c872}.conf-low{background:#ff6a6a}
.token.conf-high:not([class*=heat-]),.token.conf-mid:not([class*=heat-]),.token.conf-low:not([class*=heat-]){background:none;text-decoration-line:underline;text-decoration-thickness:2px;text-underline-offset:7px}
.token.conf-high{text-decoration-color:#7fd69f}.token.conf-mid{text-decoration-color:#e8c872}.token.conf-low{text-decoration-color:#ff5c7a}
.token[data-ws]::before{content:attr(data-ws);display:inline-block;white-space:pre}
.token[title]{border-bottom:none}
@media(max-width:640px){.hdr-right{justify-content:flex-start;gap:6px}.hdr-right .btn{font-size:.72rem!important;padding:5px 9px!important}}
</style>
</head>
<body>
<header>
<h1>every-<span class="h1-x">other</span>-token</h1>
<div class="hdr-right">
<button class="btn btn-mode active" id="btn-single" title="Single stream" aria-label="Single stream view">Single</button>
<button class="btn btn-mode" id="btn-sbs" title="Side-by-side: original vs transformed" aria-label="Side-by-side view: original vs transformed">Split</button>
<button class="btn btn-mode" id="btn-multi" title="All 4 transforms simultaneously" aria-label="Quad view: all 4 transforms simultaneously">Quad</button>
<button class="btn btn-mode" id="btn-diff" title="Live diff: OpenAI vs Anthropic simultaneously" aria-label="Diff view: OpenAI vs Anthropic">Diff</button>
<button class="btn btn-mode" id="btn-experiment" title="A/B experiment: two system prompts, same user prompt" aria-label="A/B experiment mode">Experiment</button>
<button class="btn btn-mode" id="btn-research" title="Research dashboard: stats, perplexity, confidence" aria-label="Research dashboard">Research</button>
<button class="btn btn-export" id="btn-export" title="Export session as JSON" aria-label="Export session as JSON">Export JSON</button>
<button class="btn btn-export" id="btn-export-csv" title="Export session as CSV" aria-label="Export session as CSV">Export CSV</button>
<button class="btn btn-export" id="btn-import" title="Import a previously exported JSON session" aria-label="Import JSON session">Import JSON</button>
<input type="file" id="import-file" accept=".json" style="display:none" aria-label="Select JSON file to import">
<button class="btn btn-mode" id="btn-cb-mode" aria-label="Toggle colorblind-safe mode" title="Colorblind safe" style="font-size:.78rem;padding:4px 10px">◑ CB</button>
<button class="btn btn-mode" id="btn-theme" title="Toggle dark/light theme" style="font-size:1rem;padding:4px 10px" aria-label="Toggle dark/light theme"><span aria-hidden="true">🌙</span></button>
<button class="btn btn-mode" style="font-size:.78rem;padding:5px 12px" id="btn-host" title="Host a collaborative session" aria-label="Host a collaborative session">Host Session</button>
<input type="text" id="join-code" placeholder="Room code" maxlength="6" pattern="[A-Z0-9]+" inputmode="text" autocomplete="off" oninput="this.value=this.value.toUpperCase().replace(/[^A-Z0-9]/g,'')" style="background:#0c0d0b;border:1px solid #36382f;color:#3fb950;border-radius:6px;padding:5px 8px;font-family:inherit;font-size:.78rem;width:88px;text-transform:uppercase;letter-spacing:2px" aria-label="Room code">
<button class="btn btn-mode" style="font-size:.78rem;padding:5px 12px" id="btn-join">Join</button>
</div>
</header>
<div id="ab-prompts" class="ab-system-prompts">
<div class="field"><label>System Prompt A</label><input type="text" id="sysprompt-a" value="You are a creative storyteller." style="min-width:280px"></div>
<div class="field"><label>System Prompt B</label><input type="text" id="sysprompt-b" value="You are a technical writer. Be precise and concise." style="min-width:280px"></div>
</div>
<div class="controls">
<div class="field"><label for="prompt">Prompt</label><input type="text" id="prompt" value="Tell me a story about a robot" placeholder="Enter prompt..."></div>
<div class="field"><label for="transform">Transform</label>
<div style="display:flex;gap:4px;align-items:center">
<select id="transform"><option value="reverse">reverse</option><option value="uppercase">uppercase</option><option value="mock">mock</option><option value="noise">noise</option><option value="chaos">chaos</option></select>
<button id="btn-chain-mode" class="btn btn-mode" style="padding:4px 8px;font-size:.73rem" title="Toggle chain builder">Chain</button>
</div>
<div id="chain-builder">
<label><input type="checkbox" data-tx="reverse"> reverse</label>
<label><input type="checkbox" data-tx="uppercase"> uppercase</label>
<label><input type="checkbox" data-tx="mock"> mock</label>
<label><input type="checkbox" data-tx="noise"> noise</label>
<label><input type="checkbox" data-tx="scramble"> scramble</label>
<label><input type="checkbox" data-tx="delete"> delete</label>
<label><input type="checkbox" data-tx="synonym"> synonym</label>
<div id="chain-current" style="color:#5f5b50">none</div>
</div>
</div>
<div class="field"><label for="provider">Provider</label><select id="provider"><option value="openai">OpenAI</option><option value="anthropic">Anthropic</option><option value="mock">Mock (no API key)</option></select></div>
<div class="field"><label for="model">Model</label><input type="text" id="model" value="" placeholder="auto" style="min-width:160px"></div>
<label class="toggle"><input type="checkbox" id="heatmap"> Heatmap</label>
<label class="toggle"><input type="checkbox" id="graphtoggle"> Graph</label>
<div class="field"><label for="min-confidence">Min Conf</label>
<div style="display:flex;align-items:center;gap:4px">
<input type="range" id="min-confidence" min="0" max="100" value="0" style="width:80px;accent-color:#e8c872">
<span id="conf-label" style="font-size:.78rem;color:#e6e1d3;min-width:28px">0%</span>
</div>
</div>
<button class="btn btn-go" id="start">Stream</button>
<div class="field"><label>Search tokens</label><input type="text" id="token-search" placeholder="Filter…" style="min-width:120px" aria-label="Search token stream"></div>
</div>
<div id="transform-preview"></div>
<div id="views">
<div class="view-single" id="v-single"></div>
<div class="view-sidebyside" id="v-sbs" style="display:none">
<div class="sbs-col" id="sbs-orig"><span class="sbs-label">Original</span></div>
<div class="sbs-col" id="sbs-xform"><span class="sbs-label">Transformed</span></div>
</div>
<div class="view-multi" id="v-multi" style="display:none">
<div class="multi-panel mp-reverse" id="mp-reverse"><span class="multi-label">Reverse</span></div>
<div class="multi-panel mp-uppercase" id="mp-uppercase"><span class="multi-label">Uppercase</span></div>
<div class="multi-panel mp-mock" id="mp-mock"><span class="multi-label">Mock</span></div>
<div class="multi-panel mp-noise" id="mp-noise"><span class="multi-label">Noise</span></div>
</div>
<div class="view-diff" id="v-diff" style="display:none">
<div class="diff-col" id="diff-openai"><span class="diff-label">OpenAI</span></div>
<div class="diff-col" id="diff-anthropic"><span class="diff-label">Anthropic</span></div>
</div>
<div class="view-experiment" id="v-experiment" style="display:none">
<div class="exp-panel" id="exp-a"><span class="exp-label">System Prompt A</span></div>
<div class="exp-panel" id="exp-b"><span class="exp-label b">System Prompt B</span></div>
<div class="exp-perp-chart" id="exp-perp"><span style="font-size:.7rem;color:#96917f">Perplexity comparison (A=gold, B=purple): run streams to populate</span><br><canvas id="exp-perp-canvas" style="width:100%;height:120px;display:block;margin-top:4px"></canvas></div>
<div class="exp-diverge-map" id="exp-diverge"><span style="font-size:.7rem;color:#96917f">Divergence map: run streams to populate</span></div>
</div>
<div id="ab-chart-wrap" style="display:none;padding:8px 16px;background:#090a08;border-top:1px solid #25271f">
<div id="ab-chart-title" style="font-size:.7rem;color:#96917f;margin-bottom:4px">Token Agreement: first 50 positions (green=match, red=diverge)</div>
<canvas id="ab-chart" height="80" style="display:block;width:100%"></canvas>
</div>
<div id="v-research" style="display:none;padding:16px 24px;overflow-y:auto;height:100%">
<h2 style="color:#e8c872;font-size:1rem;margin-bottom:4px">Research Dashboard</h2>
<div id="research-runs-row">
<span>Research runs:</span>
<input type="number" id="research-runs" value="3" min="1" max="20" style="width:45px;background:#0c0d0b;border:1px solid #36382f;color:#e6e1d3;padding:3px 6px;border-radius:4px;font-family:inherit;font-size:.78rem">
<span id="research-progress" style="color:#a371f7"></span>
</div>
<button id="btn-export-research" class="btn btn-export" style="font-size:.73rem;padding:4px 12px;margin-bottom:8px" aria-label="Export research stats as Markdown">Export Stats (MD)</button>
<p style="font-size:.75rem;color:#96917f;margin-bottom:12px">Stats computed from the most recent stream. Stream tokens first.</p>
<div id="research-grid" style="display:grid;grid-template-columns:repeat(3,1fr);gap:12px"></div>
<div style="margin-top:16px">
<div style="font-size:.72rem;color:#96917f;text-transform:uppercase;letter-spacing:.5px;margin-bottom:4px">Top 10 High-Perplexity Tokens</div>
<ol id="research-perp-list" style="list-style:none;font-size:.8rem;color:#e6e1d3"></ol>
</div>
<div style="margin-top:16px">
<div style="font-size:.72rem;color:#96917f;text-transform:uppercase;letter-spacing:.5px;margin-bottom:4px">Perplexity Distribution</div>
<label style="display:flex;align-items:center;gap:6px;font-size:.7rem;color:#96917f;margin-bottom:4px">
Perplexity bins: <input type="range" id="perp-bin-slider" min="5" max="50" value="10" style="width:80px;accent-color:#a371f7">
<span id="perp-bin-count">10</span>
</label>
<div id="research-perp-hist" style="display:flex;gap:2px;align-items:flex-end;height:48px;margin-top:4px"></div>
</div>
<div style="margin-top:16px">
<div style="font-size:.72rem;color:#96917f;text-transform:uppercase;letter-spacing:.5px;margin-bottom:4px">Confidence Distribution</div>
<div id="research-conf-hist" style="display:flex;gap:2px;align-items:flex-end;height:48px;margin-top:4px"></div>
<div style="display:flex;justify-content:space-between;font-size:.6rem;color:#5f5b50;margin-top:2px"><span>0.0</span><span>0.5</span><span>1.0</span></div>
</div>
<div style="margin-top:16px" id="sig-test-section">
<div style="font-size:.72rem;color:#96917f;text-transform:uppercase;letter-spacing:.5px;margin-bottom:4px">A/B Significance Test (Even vs Odd Runs)</div>
<div id="sig-test-result" style="background:#141512;border:1px solid #25271f;border-radius:4px;padding:10px 12px;font-size:.8rem">
<span style="color:#5f5b50">Run at least 4 tokens from two alternating runs to see results.</span>
</div>
</div>
<div style="margin-top:16px">
<div style="font-size:.72rem;color:#96917f;text-transform:uppercase;letter-spacing:.5px;margin-bottom:4px">Citation</div>
<code id="research-citation" style="font-size:.7rem;color:#e3b341;white-space:pre-wrap;display:block;background:#141512;padding:8px;border-radius:4px;border:1px solid #25271f"></code>
</div>
<div style="margin-top:16px" id="undo-history-section">
<div style="font-size:.72rem;color:#96917f;text-transform:uppercase;letter-spacing:.5px;margin-bottom:4px">Surgery Edit History</div>
<div id="surgery-history" style="background:#141512;border:1px solid #25271f;border-radius:4px;padding:8px 12px;font-size:.78rem;max-height:120px;overflow-y:auto;color:#e6e1d3">
<span style="color:#5f5b50">No edits yet.</span>
</div>
<div style="display:flex;gap:6px;margin-top:6px">
<button id="btn-undo-one" class="btn" style="background:#25271f;font-size:.73rem;padding:3px 10px" title="Undo last edit (Ctrl+Z)" disabled>↩ Undo</button>
<button id="btn-undo-all" class="btn" style="background:#25271f;font-size:.73rem;padding:3px 10px" onclick="undoAll()">Undo All Edits</button>
</div>
</div>
</div>
</div>
<div id="graph-wrap"><canvas id="depgraph" role="img" aria-label="Token dependency graph showing transformed vs untransformed token positions"></canvas></div>
<div id="perp-spark-wrap"><div style="font-size:.65rem;color:#96917f;margin:2px 0">Perplexity (last 60 tokens)</div><svg id="perp-spark" viewBox="0 0 600 40" preserveAspectRatio="none" role="img" aria-label="Perplexity sparkline for last 60 tokens"><title>Perplexity over last 60 tokens</title><polyline id="perp-line" points="" fill="none" stroke="#e8c872" stroke-width="1.5"/></svg></div>
<div id="conf-spark-wrap" style="padding:4px 24px;background:#090a08;border-top:1px solid #25271f;display:none">
<div style="font-size:.65rem;color:#96917f;margin-bottom:2px">Confidence (last 60 tokens)</div>
<canvas id="sparkline" width="600" height="60" style="display:block;background:#141512;border-radius:4px;width:100%;max-width:600px" role="img" aria-label="Confidence sparkline for last 60 tokens"></canvas>
</div>
<div id="stats"></div>
<div id="research-dash"></div>
<div id="mp-panel">
<div><div class="mp-label">Room Code</div><div class="mp-code" id="mp-code" title="Click to copy join link">------</div></div>
<div><div class="mp-label">Participants</div><span id="mp-count-badge">1</span></div>
<button class="btn" style="background:#238636;font-size:.75rem;padding:4px 11px" id="btn-copy-link">Copy Link</button>
<button class="btn" style="background:#6e40c9;font-size:.75rem;padding:4px 11px;display:none" id="btn-replay" aria-label="Replay recording">▶ Replay</button>
<div id="replay-prog"><div id="replay-bar"></div></div>
<div style="display:flex;align-items:center;gap:4px;font-size:.72rem;color:#96917f">
<span>Speed:</span>
<input type="range" id="replay-speed" min="1" max="10" value="3" style="width:60px;accent-color:#a371f7">
<span id="speed-label">3×</span>
</div>
<button class="btn" id="btn-rec" style="background:#36382f;font-size:.75rem;padding:4px 11px" aria-label="Record session">⏺ Record</button>
<button class="btn" style="background:#25271f;font-size:.75rem;padding:4px 11px;margin-left:auto" id="btn-leave">Leave</button>
</div>
<div id="vote-bar" role="region" aria-label="Transform voting">
<span style="color:#96917f;font-size:.7rem">Transform vote:</span>
<button class="v-btn" id="btn-vote-up">👍 <span id="vote-up-n">0</span></button>
<button class="v-btn" id="btn-vote-dn">👎 <span id="vote-dn-n">0</span></button>
<span id="vote-label" style="font-size:.7rem;color:#5f5b50"></span>
</div>
<div id="sidebar" role="complementary" aria-label="Participants">
<div id="sidebar-hdr">
<span>Participants</span>
<button onclick="$('#chat-panel').classList.toggle('show')" style="background:none;border:none;color:#96917f;cursor:pointer;font-size:.8rem" title="Toggle chat">💬</button>
</div>
<div id="participant-list"></div>
</div>
<div id="chat-panel" role="complementary" aria-label="Chat">
<div id="chat-hdr">Chat</div>
<div id="chat-msgs"></div>
<div id="chat-wrap">
<input type="text" id="chat-in" placeholder="Message…" autocomplete="off">
<button id="chat-btn">Send</button>
</div>
</div>
<div id="toast-container"></div>
<div id="shortcuts-modal" role="dialog" aria-modal="true" aria-label="Keyboard shortcuts">
<div id="shortcuts-box">
<button id="shortcuts-close" aria-label="Close shortcuts">✕</button>
<h2>Keyboard Shortcuts</h2>
<table>
<tr><td>?</td><td>Show this help</td></tr>
<tr><td>Ctrl+Z</td><td>Undo last token surgery</td></tr>
<tr><td>Enter</td><td>Submit prompt</td></tr>
<tr><td>Escape</td><td>Close modal / cancel edit</td></tr>
<tr><td>Ctrl+E</td><td>Export session as JSON</td></tr>
<tr><td>Ctrl+Shift+E</td><td>Export session as CSV</td></tr>
</table>
</div>
</div>
<script>
const $=s=>document.querySelector(s);
const $$=s=>document.querySelectorAll(s);
function showNotice(msg, type='info') {
const c = document.getElementById('toast-container');
if (!c) return;
const t = document.createElement('div');
t.className = 'toast toast-' + type;
t.textContent = msg;
c.appendChild(t);
setTimeout(() => t.remove(), 4000);
}
const TX={
reverse:s=>s.split('').reverse().join(''),
uppercase:s=>s.toUpperCase(),
mock:s=>s.split('').map((c,i)=>i%2===0?c.toLowerCase():c.toUpperCase()).join(''),
noise:s=>{const n='*+~@#$%';return s+n[Math.floor(Math.random()*n.length)]},
chaos:s=>{const picks=['reverse','uppercase','mock','noise'];const k=picks[Math.floor(Math.random()*4)];return TX[k](s)}
};
let es=null, mode='single';
let allTokens=[], graphNodes=[], surgeryLog=[], undoStack=[];
function renderSurgeryHistory() {
const el = document.getElementById('surgery-history');
if (!el) return;
if (surgeryLog.length === 0) { el.innerHTML = '<span style="color:#5f5b50">No edits yet.</span>'; return; }
const frag=document.createDocumentFragment();
surgeryLog.slice().reverse().forEach(e=>{
const row=document.createElement('div');
row.style.cssText='display:flex;gap:8px;padding:2px 0;border-bottom:1px solid #25271f';
const idx=document.createElement('span');
idx.style.cssText='color:#5f5b50;font-size:.7rem;flex-shrink:0';
idx.textContent='#'+e.index;
const orig=document.createElement('span');
orig.style.cssText='color:#f85149;text-decoration:line-through;flex:1;overflow:hidden;text-overflow:ellipsis';
orig.textContent=e.original;
const repl=document.createElement('span');
repl.style.cssText='color:#3fb950;flex:1;overflow:hidden;text-overflow:ellipsis';
repl.textContent='→ '+e.replacement;
row.appendChild(idx);row.appendChild(orig);row.appendChild(repl);
frag.appendChild(row);
});
el.innerHTML='';
el.appendChild(frag);
}
function undoAll() {
while (undoStack.length) {
const last = undoStack.pop();
last.sp.textContent = last.oldText;
if (last.idx >= 0 && allTokens[last.idx]) allTokens[last.idx].text = last.oldText;
}
surgeryLog.length = 0;
renderSurgeryHistory();
if(typeof updateUndoBtnState==='function')updateUndoBtnState();
}
let perpWindow=[], confWindow=[];
let expATokens=[], expBTokens=[];
let scrollPending=false;
let _streamRetryTimer=null;
let _researchCache={len:-1};
const views={single:$('#v-single'),sbs:$('#v-sbs'),multi:$('#v-multi'),diff:$('#v-diff'),experiment:$('#v-experiment'),research:$('#v-research')};
function setMode(m){
mode=m;
Object.values(views).forEach(v=>{if(v)v.style.display='none'});
if(m==='experiment'){$('#v-experiment').style.display='';$('#ab-prompts').classList.add('show')}
else{if(views[m])views[m].style.display='';$('#ab-prompts').classList.remove('show')}
$$('.btn-mode').forEach(b=>b.classList.remove('active'));
$('#btn-'+m)&&$('#btn-'+m).classList.add('active');
if(m==='research')renderResearch();
}
$('#btn-single').onclick=()=>setMode('single');
$('#btn-sbs').onclick=()=>setMode('sbs');
$('#btn-multi').onclick=()=>setMode('multi');
$('#btn-diff').onclick=()=>{setMode('diff');if(!es)startDiff()};
$('#btn-experiment').onclick=()=>setMode('experiment');
$('#btn-research').onclick=()=>setMode('research');
$('#graphtoggle').onchange=function(){
$('#graph-wrap').classList.toggle('show',this.checked);
if(this.checked)drawGraph();
};
function mkSpan(text,isOdd,importance,extraCls,chaosLabel,confidence,perplexity){
const s=document.createElement('span');
s.className='token '+(isOdd?'odd':'even');
if(extraCls)s.classList.add(extraCls);
if($('#heatmap').checked){
const h=importance>=.8?4:importance>=.6?3:importance>=.4?2:importance>=.2?1:0;
s.classList.add('heat-'+h);
}
if(chaosLabel==='deleted'||text===''){
s.textContent='⌀';
s.classList.add('token-deleted');
s.title='Token deleted';
s.style.opacity='0.4';
}
if(chaosLabel&&isOdd&&chaosLabel!=='deleted')s.title='chaos → '+chaosLabel;
if(confidence!=null){
const cls=confidence>=0.7?'conf-high':confidence>=0.4?'conf-mid':'conf-low';
s.classList.add(cls);
s.title=(s.title?s.title+' | ':'')+('conf: '+(confidence*100).toFixed(0)+'%');
}
if(perplexity!=null&&perplexity>5)s.classList.add('high-perp');
if(chaosLabel!=='deleted'&&text!==''){
const m=/^(\s+)(\S[\s\S]*)$/.exec(text);
if(m){s.dataset.ws=m[1];s.textContent=m[2];}else s.textContent=text;
}
s.addEventListener('contextmenu',function(e){
e.preventDefault();
navigator.clipboard&&navigator.clipboard.writeText(s.textContent).then(()=>{
const orig=s.style.outline;s.style.outline='1px solid #3fb950';
setTimeout(()=>s.style.outline=orig,600);
});
});
return s;
}
function appendTok(container,sp){
const ws=sp.dataset.ws;
if(ws){container.appendChild(document.createTextNode(ws));delete sp.dataset.ws;}
container.appendChild(sp);
}
$('#token-search').addEventListener('input',function(){
const q=this.value.toLowerCase();
$$('#v-single .token').forEach(sp=>{
sp.style.opacity=(!q||sp.textContent.toLowerCase().includes(q))?'':'0.15';
sp.style.outline=(!q||sp.textContent.toLowerCase().includes(q))?'':'1px solid #e3b341';
});
});
function updatePerpSparkline(perplexity){
if(perplexity==null)return;
perpWindow.push(perplexity);
if(perpWindow.length>60)perpWindow.shift();
const spark=$('#perp-spark-wrap');
if(!spark)return;
spark.classList.add('show');
const line=$('#perp-line');
if(!line)return;
const max=Math.max(...perpWindow,1);
const pts=perpWindow.map((v,i)=>{
const x=Math.round(i/(perpWindow.length-1||1)*600);
const y=Math.round((1-v/max)*36+2);
return x+','+y;
}).join(' ');
line.setAttribute('points',pts);
}
(function(){
const svg = document.getElementById('perp-spark');
if (!svg) return;
const tip = document.createElement('div');
tip.id = 'spark-tip';
tip.style.cssText = 'display:none;position:fixed;background:#1c2333;color:#a371f7;padding:2px 8px;border-radius:4px;font-size:.7rem;pointer-events:none;z-index:100;border:1px solid #36382f';
document.body.appendChild(tip);
svg.addEventListener('mousemove', e => {
if (!perpWindow.length) return;
const rect = svg.getBoundingClientRect();
const frac = (e.clientX - rect.left) / rect.width;
const idx = Math.min(perpWindow.length - 1, Math.max(0, Math.round(frac * (perpWindow.length - 1))));
tip.textContent = 'token ' + idx + ': perplexity ' + perpWindow[idx].toFixed(2);
tip.style.display = '';
tip.style.left = (e.clientX + 12) + 'px';
tip.style.top = (e.clientY - 8) + 'px';
});
svg.addEventListener('mouseleave', () => { tip.style.display = 'none'; });
})();
function welchT(a, b) {
if (a.length < 2 || b.length < 2) return null;
const ma = a.reduce((s,x)=>s+x,0)/a.length;
const mb = b.reduce((s,x)=>s+x,0)/b.length;
const va = a.reduce((s,x)=>s+(x-ma)**2,0)/(a.length-1);
const vb = b.reduce((s,x)=>s+(x-mb)**2,0)/(b.length-1);
const se = Math.sqrt(va/a.length + vb/b.length);
if (se === 0) return null;
const t = Math.abs(ma - mb) / se;
const normalCDF = z => {
const t = 1/(1+0.2316419*Math.abs(z));
const poly = t*(0.319381530+t*(-0.356563782+t*(1.781477937+t*(-1.821255978+t*1.330274429))));
const pdf = Math.exp(-z*z/2)/Math.sqrt(2*Math.PI);
const p = 1 - pdf*poly;
return z >= 0 ? p : 1-p;
};
const p = 2*(1-normalCDF(t));
return {t, p, ma, mb, na: a.length, nb: b.length};
}
function updateSigTest(tokens) {
const evenConf = tokens.filter((_,i)=>i%2===0).map(t=>t.confidence).filter(c=>c!=null);
const oddConf = tokens.filter((_,i)=>i%2!==0).map(t=>t.confidence).filter(c=>c!=null);
const el = document.getElementById('sig-test-result');
if (!el) return;
const r = welchT(evenConf, oddConf);
if (!r) {
el.innerHTML = '<span style="color:#5f5b50">Not enough confidence data for t-test (need OpenAI provider).</span>';
return;
}
const sig = r.p < 0.05;
const color = sig ? '#f85149' : '#3fb950';
el.innerHTML = `
<div style="display:flex;gap:16px;flex-wrap:wrap">
<div><span style="color:#96917f">Even tokens mean conf:</span> <strong style="color:#e8c872">${r.ma.toFixed(4)}</strong> (n=${r.na})</div>
<div><span style="color:#96917f">Odd tokens mean conf:</span> <strong style="color:#a371f7">${r.mb.toFixed(4)}</strong> (n=${r.nb})</div>
<div><span style="color:#96917f">t-statistic:</span> <strong style="color:#e3b341">${r.t.toFixed(3)}</strong></div>
<div><span style="color:#96917f">p-value:</span> <strong style="color:${color}">${r.p.toFixed(4)}</strong></div>
<div style="padding:2px 8px;border-radius:3px;background:${sig?'#2d1b1b':'#0d2010'};color:${color};font-size:.75rem">${sig?'⚠ SIGNIFICANT (p<0.05)':'✓ Not significant'}</div>
</div>`;
}
function renderPerpHist(binCount){
const hist=document.getElementById('research-perp-hist');
if(!hist)return;
const withPerp=allTokens.filter(t=>t.perplexity!=null);
if(!withPerp.length)return;
const maxP=Math.max(...withPerp.map(t=>t.perplexity));
const minP=Math.min(...withPerp.map(t=>t.perplexity));
const buckets=Array(binCount).fill(0);
withPerp.forEach(t=>{
const b=Math.min(binCount-1,Math.floor((t.perplexity-minP)/(maxP-minP+1e-9)*binCount));
buckets[b]++;
});
const maxB=Math.max(...buckets,1);
hist.innerHTML=buckets.map((v,i)=>{
const lo=(minP+(maxP-minP)/binCount*i).toFixed(1);
const hi=(minP+(maxP-minP)/binCount*(i+1)).toFixed(1);
return `<div style="flex:1;background:#a371f7;height:${Math.round(v/maxB*44)+2}px;border-radius:2px 2px 0 0" title="${lo}–${hi}: ${v}"></div>`;
}).join('');
}
let _researchRunCurrent = 0, _researchRunTotal = 0;
function setResearchProgress(cur, total) {
_researchRunCurrent = cur; _researchRunTotal = total;
const el = document.getElementById('research-progress');
if (el) el.textContent = total > 0 ? 'Run ' + cur + '/' + total + (cur < total ? '...' : ' ✓') : '';
}
function renderResearch(){
if(allTokens.length===_researchCache.len&&allTokens.length>0&&mode==='research'){
updateSigTest(allTokens);return;
}
_researchCache.len=allTokens.length;
const grid=$('#research-grid');
const perpList=$('#research-perp-list');
const confHist=$('#research-conf-hist');
const citation=$('#research-citation');
if(!grid)return;
if(allTokens.length===0){grid.innerHTML='<div style="color:#96917f;font-size:.8rem">No tokens yet. Run a stream first.</div>';return;}
const unique=new Set(allTokens.map(t=>t.original.toLowerCase())).size;
const diversity=(unique/allTokens.length).toFixed(3);
const avgLen=(allTokens.reduce((s,t)=>s+t.original.length,0)/allTokens.length).toFixed(1);
const withPerp=allTokens.filter(t=>t.perplexity!=null);
const avgPerp=withPerp.length>0?(withPerp.reduce((s,t)=>s+t.perplexity,0)/withPerp.length).toFixed(2):'n/a';
const withConf=allTokens.filter(t=>t.confidence!=null);
const avgConf=withConf.length>0?((withConf.reduce((s,t)=>s+t.confidence,0)/withConf.length)*100).toFixed(0)+'%':'n/a';
const costEst=allTokens.length>0?'$'+(allTokens.length/1000*0.002).toFixed(4):'n/a';
grid.innerHTML=`
<div class="dash-card"><h3>Vocab Diversity</h3><div class="val">${diversity}</div><div class="sub">${unique} unique / ${allTokens.length} total</div></div>
<div class="dash-card"><h3>Avg Token Length</h3><div class="val">${avgLen}</div><div class="sub">characters per token</div></div>
<div class="dash-card"><h3>Avg Perplexity</h3><div class="val">${avgPerp}</div><div class="sub">exp(-logprob); lower=confident</div></div>
<div class="dash-card"><h3>Avg Confidence</h3><div class="val">${avgConf}</div><div class="sub">from top-1 logprob</div></div>
<div class="dash-card"><h3>Token Count</h3><div class="val">${allTokens.length}</div><div class="sub">${allTokens.filter(t=>t.transformed).length} transformed</div></div>
<div class="dash-card"><h3>Est. Cost</h3><div class="val">${costEst}</div><div class="sub">GPT-3.5 rate ($0.002/1K)</div></div>
`.replace(/dash-card/g,'r-card').replace(/class="val"/g,'class="r-stat"').replace(/class="sub"/g,'class="r-sub"');
if(perpList){
const sorted=[...withPerp].sort((a,b)=>b.perplexity-a.perplexity).slice(0,10);
perpList.innerHTML=sorted.map((t,i)=>`<li style="display:flex;justify-content:space-between;padding:3px 0;border-bottom:1px solid #25271f;font-size:.78rem"><span style="color:#e6e1d3">${i+1}. "${t.original}"</span><span style="color:#f85149">${t.perplexity.toFixed(1)}</span></li>`).join('');
}
if(confHist&&withConf.length>0){
const buckets=Array(10).fill(0);
withConf.forEach(t=>{const b=Math.min(9,Math.floor(t.confidence*10));buckets[b]++});
const maxB=Math.max(...buckets,1);
confHist.innerHTML=buckets.map((v,i)=>`<div style="flex:1;background:#4d4b3e;height:${Math.round(v/maxB*44)+2}px;border-radius:2px 2px 0 0;opacity:${0.4+i*0.06}" title="${(i*10)}-${(i+1)*10}%: ${v} tokens"></div>`).join('');
}
if(citation){
const ts=new Date().toISOString().replace('T',' ').replace(/\.\d+Z$/,' UTC');
citation.textContent=`Every Other Token (v4.0.0). Session recorded ${ts}.\nTokens: ${allTokens.length}, Transform: ${$('#transform').value}, Provider: ${$('#provider').value}, Model: ${$('#model').value||'auto'}.\nVocab diversity: ${diversity}, Avg perplexity: ${avgPerp}, Avg confidence: ${avgConf}.`;
}
updateSigTest(allTokens);
renderPerpHist(10);
const perpSlider=document.getElementById('perp-bin-slider');
if(perpSlider){
perpSlider.oninput=function(){
document.getElementById('perp-bin-count').textContent=this.value;
renderPerpHist(parseInt(this.value));
};
}
}
let expATokens2=[], expBTokens2=[];
function startExperiment(){
if(es){es.close();es=null}
$('#exp-a').innerHTML='<span class="exp-label">System Prompt A</span>';
$('#exp-b').innerHTML='<span class="exp-label b">System Prompt B</span>';
$('#exp-diverge').innerHTML='<span style="font-size:.7rem;color:#96917f">Divergence map: streaming...</span>';
expATokens2=[];expBTokens2=[];
const p=encodeURIComponent($('#prompt').value);
const t=$('#transform').value;
const m=encodeURIComponent($('#model').value);
const sa=encodeURIComponent(($('#sysprompt-a')&&$('#sysprompt-a').value)||'You are a creative storyteller.');
const sb=encodeURIComponent(($('#sysprompt-b')&&$('#sysprompt-b').value)||'You are a technical writer. Be precise.');
const prov=$('#provider').value;
const url='/ab-stream?prompt='+p+'&transform='+t+'&provider='+prov+'&model='+m+'&sys_a='+sa+'&sys_b='+sb;
$('#start').disabled=true;$('#start').textContent='Experimenting...';
es=new EventSource(url);
es.onmessage=e=>{
if(e.data==='[DONE]'){
es.close();es=null;
$('#start').disabled=false;$('#start').textContent='Stream';
renderExpDivergence();
return;
}
try{
const tk=JSON.parse(e.data);
if(tk.side==='a'){
expATokens2.push(tk);
const sp=mkSpan(tk.text,tk.transformed,tk.importance,'',tk.chaos_label,tk.confidence,tk.perplexity);
sp.dataset.idx=tk.index;
appendTok($('#exp-a'),sp);$('#exp-a').scrollTop=$('#exp-a').scrollHeight;
updatePerpSparkline(tk.perplexity);
} else if(tk.side==='b'){
expBTokens2.push(tk);
const sp=mkSpan(tk.text,tk.transformed,tk.importance,'',tk.chaos_label,tk.confidence,tk.perplexity);
sp.dataset.idx=tk.index;
appendTok($('#exp-b'),sp);$('#exp-b').scrollTop=$('#exp-b').scrollHeight;
}
$('#stats').textContent='A: '+expATokens2.length+' tokens | B: '+expBTokens2.length+' tokens';
}catch(err){console.warn('[eot] token parse error:', err);}
};
es.onerror=()=>{
es.close();es=null;
$('#start').disabled=false;$('#start').textContent='Stream';
showNotice('Stream interrupted or disconnected.','warning');
};;
}
function renderExpDivergence(){
const el=$('#exp-diverge');if(!el)return;
const minLen=Math.min(expATokens2.length,expBTokens2.length);
if(minLen===0){el.innerHTML='<span style="font-size:.7rem;color:#96917f">No tokens to compare.</span>';return;}
let matches=0;
const rows=[];
for(let i=0;i<minLen;i++){
const match=expATokens2[i].original===expBTokens2[i].original;
if(match)matches++;
rows.push(`<div style="display:flex;gap:8px;font-size:.75rem;padding:1px 0;color:${match?'#3fb950':'#f85149'}"><span style="flex:1;overflow:hidden;text-overflow:ellipsis">${escHtml(expATokens2[i].original)}</span><span style="flex:1;overflow:hidden;text-overflow:ellipsis">${escHtml(expBTokens2[i].original)}</span></div>`);
}
const pct=Math.round(matches/minLen*100);
el.innerHTML=`<div style="font-size:.7rem;color:#96917f;margin-bottom:4px">Similarity: ${pct}% (${matches}/${minLen})</div>`+rows.slice(0,50).join('');
if(typeof _renderExpDivergenceExtra==='function')_renderExpDivergenceExtra(expATokens2,expBTokens2,minLen,matches);
}
function enableSurgery(container){
container.querySelectorAll('.token').forEach(sp=>{
if(sp.classList.contains('surgeable'))return;
sp.classList.add('surgeable');
sp.addEventListener('click',function(){
const orig=sp.textContent;
const inp=document.createElement('input');
inp.className='token-input';
inp.value=orig;
inp.style.width=Math.max(orig.length*0.6+1,2)+'em';
sp.replaceWith(inp);
inp.focus();inp.select();
function commit(){
const newText=inp.value||orig;
sp.textContent=newText;
inp.replaceWith(sp);
if(newText!==orig){
undoStack.push({sp, oldText: orig, newText, idx: parseInt(sp.dataset.idx || '-1')});
surgeryLog.push({index:parseInt(sp.dataset.idx||'0'),original:orig,replacement:newText,timestamp:new Date().toISOString()});
renderSurgeryHistory();
if(typeof updateUndoBtnState==='function')updateUndoBtnState();
if(ws&&ws.readyState===WebSocket.OPEN){try{ws.send(JSON.stringify({type:'surgery',token_index:parseInt(sp.dataset.idx||'0'),old_text:orig,new_text:newText}))}catch(e){console.warn('[eot] ws surgery send failed:',e);}}
}
}
inp.addEventListener('blur',commit);
inp.addEventListener('keydown',e=>{if(e.key==='Enter')inp.blur();if(e.key==='Escape'){inp.value=orig;inp.blur()}});
});
});
}
function heatColor(imp){
if(imp>=.8)return'#f85149';
if(imp>=.6)return'#f0883e';
if(imp>=.4)return'#e3b341';
if(imp>=.2)return'#e8c872';
return'#5f5b50';
}
function drawGraph(){
const canvas=$('#depgraph');
if(!canvas||!$('#graphtoggle').checked)return;
const dpr=window.devicePixelRatio||1;
const rect=canvas.parentElement.getBoundingClientRect();
const nodes=graphNodes;
if(nodes.length===0)return;
const gap=60;
const xs=nodes.map((_,i)=>(i+1)*gap);
const neededW=Math.max((nodes.length+1)*gap,rect.width);
canvas.width=neededW*dpr;
canvas.height=200*dpr;
canvas.style.width=neededW+'px';
canvas.style.height='200px';
const ctx=canvas.getContext('2d');
ctx.scale(dpr,dpr);
ctx.clearRect(0,0,neededW,200);
const y1=50,y2=150;
let lastEvenX=0,lastEvenIdx=-1;
for(let i=0;i<nodes.length;i++){
const n=nodes[i];
const x=xs[i];
const y=n.transformed?y2:y1;
if(n.transformed&&lastEvenIdx>=0){
const ex=(lastEvenIdx+1)*gap;
ctx.beginPath();
ctx.moveTo(ex,y1+8);
ctx.bezierCurveTo(ex,y1+40,x,y2-40,x,y2-8);
ctx.lineWidth=Math.min(n.original.length,6);
ctx.strokeStyle=heatColor(n.importance);
ctx.globalAlpha=0.6;
ctx.stroke();
ctx.globalAlpha=1;
}
if(!n.transformed){lastEvenX=x;lastEvenIdx=i;}
ctx.beginPath();
ctx.arc(x,y,7,0,Math.PI*2);
ctx.fillStyle=n.transformed?'#ff6a2b':'#e6e1d3';
ctx.fill();
ctx.strokeStyle='#36382f';ctx.lineWidth=1;ctx.stroke();
ctx.fillStyle='#96917f';
ctx.font='10px monospace';
ctx.textAlign='center';
const label=n.text.length>6?n.text.slice(0,5)+'…':n.text;
ctx.fillText(label,x,y+(n.transformed?20:-14));
}
}
$('#start').onclick=()=>{
if(es){es.close();es=null}
if(_streamRetryTimer){clearTimeout(_streamRetryTimer);_streamRetryTimer=null;}
if(mode==='diff'){startDiff();return}
if(mode==='experiment'){startExperiment();return}
if(mode==='research'){
const totalRuns = Math.max(1, parseInt(document.getElementById('research-runs')&&document.getElementById('research-runs').value||'1'));
if(totalRuns > 1) {
let runNum = 0;
setResearchProgress(0, totalRuns);
function doResearchRun() {
if(runNum >= totalRuns) { setResearchProgress(totalRuns, totalRuns); return; }
runNum++;
setResearchProgress(runNum, totalRuns);
}
doResearchRun();
}
}
$('#v-single').innerHTML='';
$('#sbs-orig').innerHTML='<span class="sbs-label">Original</span>';
$('#sbs-xform').innerHTML='<span class="sbs-label">Transformed</span>';
['reverse','uppercase','mock','noise','chaos'].forEach(t=>{
const el=$('#mp-'+t);if(el)el.innerHTML='<span class="multi-label">'+t+'</span>';
});
$('#stats').textContent='';
allTokens=[];graphNodes=[];surgeryLog=[];undoStack=[];
perpWindow=[];confWindow=[];_researchCache={len:-1};
_sseQueue.length=0; _stopSseFlush();
_streamStart = Date.now();
if($('#graphtoggle').checked)drawGraph();
const p=encodeURIComponent($('#prompt').value);
const txVal = (_chainModeActive && chainTransforms.length) ? chainTransforms.join(',') : $('#transform').value;
const prov=$('#provider').value;
const m=encodeURIComponent($('#model').value);
const hm=$('#heatmap').checked?'1':'0';
const minConf = parseInt($('#min-confidence').value||'0');
const minConfParam = minConf > 0 ? '&min_confidence='+(minConf/100) : '';
const roomParam=roomCode?'&room='+encodeURIComponent(roomCode):'';
const url='/stream?prompt='+p+'&transform='+encodeURIComponent(txVal)+'&provider='+prov+'&model='+m+'&heatmap='+hm+minConfParam+roomParam;
$('#start').disabled=true;$('#start').textContent='Streaming...';
let count=0,xformed=0,streamDone=false;
const _countRef={count:0}, _xformedRef={xformed:0}, _modeRef={mode};
let _retryCount=0;
const MAX_RETRIES=4;
function attachStreamHandlers(evSrc){
evSrc.onmessage=e=>{
_retryCount=0;
if(e.data==='[DONE]'){
streamDone=true;
evSrc.close();es=null;
_finishSseFlush(()=>{
$('#start').disabled=false;$('#start').textContent='Stream';
if($('#graphtoggle').checked)drawGraph();
enableSurgery($('#v-single'));
enableSurgery($('#sbs-orig'));
enableSurgery($('#sbs-xform'));
renderResearch();
updateUndoBtnState();
});
return;
}
try{
const tk=JSON.parse(e.data);
allTokens.push(tk);
window.tokenLog.push(tk);
_sseQueue.push(tk);
}catch(err){console.warn('[eot] token parse error:', err);}
};
evSrc.onerror=()=>{
if(streamDone)return;
evSrc.close();es=null;
if(_retryCount<MAX_RETRIES){
const delay=Math.min(1000*Math.pow(2,_retryCount),16000);
_retryCount++;
$('#stats').textContent='Connection lost: reconnecting in '+(delay/1000).toFixed(0)+'s… (attempt '+_retryCount+'/'+MAX_RETRIES+')';
_streamRetryTimer=setTimeout(()=>{
es=new EventSource(url);
attachStreamHandlers(es);
},delay);
} else {
$('#start').disabled=false;$('#start').textContent='Stream';
showNotice('Stream disconnected after '+MAX_RETRIES+' retries.','warning');
}
};
}
_countRef.count=0; _xformedRef.xformed=0; _modeRef.mode=mode;
_startSseFlush(_countRef, _xformedRef, _modeRef);
(async()=>{
try{
const probe=await fetch(url,{method:'HEAD',signal:AbortSignal.timeout(3000)});
if(probe.status===429){
_stopSseFlush();
$('#start').disabled=false;$('#start').textContent='Stream';
showNotice('Rate limited (429): please wait 60s','error');
return;
}
}catch(_){}
es=new EventSource(url);
attachStreamHandlers(es);
})();
updateUndoBtnState();
};
let diffOpenaiTokens=[], diffAnthropicTokens=[];
function startDiff(){
if(es){es.close();es=null}
$('#diff-openai').innerHTML='<span class="diff-label">OpenAI</span>';
$('#diff-anthropic').innerHTML='<span class="diff-label">Anthropic</span>';
diffOpenaiTokens=[];diffAnthropicTokens=[];
$('#stats').textContent='';
const p=encodeURIComponent($('#prompt').value);
const t=$('#transform').value;
const m=encodeURIComponent($('#model').value);
const hm=$('#heatmap').checked?'1':'0';
const url='/diff-stream?prompt='+p+'&transform='+t+'&model='+m+'&heatmap='+hm;
$('#start').disabled=true;$('#start').textContent='Diffing...';
es=new EventSource(url);
es.onmessage=e=>{
if(e.data==='[DONE]'){
es.close();es=null;
$('#start').disabled=false;$('#start').textContent='Stream';
applyDiffHighlights();
return;
}
try{
const tk=JSON.parse(e.data);
if(tk.side==='openai'){
diffOpenaiTokens.push(tk);
const sp=mkSpan(tk.text,tk.transformed,tk.importance,'',tk.chaos_label);
sp.dataset.idx=tk.index;
appendTok($('#diff-openai'),sp);
$('#diff-openai').scrollTop=$('#diff-openai').scrollHeight;
} else if(tk.side==='anthropic'){
diffAnthropicTokens.push(tk);
const sp=mkSpan(tk.text,tk.transformed,tk.importance,'',tk.chaos_label);
sp.dataset.idx=tk.index;
appendTok($('#diff-anthropic'),sp);
$('#diff-anthropic').scrollTop=$('#diff-anthropic').scrollHeight;
}
$('#stats').textContent='OpenAI: '+diffOpenaiTokens.length+' tokens | Anthropic: '+diffAnthropicTokens.length+' tokens';
}catch(err){console.warn('[eot] token parse error:', err);}
};
es.onerror=()=>{
es.close();es=null;
$('#start').disabled=false;$('#start').textContent='Stream';
showNotice('Stream interrupted or disconnected.','warning');
};;
}
function applyDiffHighlights(){
const oSpans=Array.from($('#diff-openai').querySelectorAll('.token'));
const aSpans=Array.from($('#diff-anthropic').querySelectorAll('.token'));
let matches=0;
const total=Math.max(oSpans.length,aSpans.length);
for(let i=0;i<total;i++){
const match=oSpans[i]&&aSpans[i]&&oSpans[i].textContent===aSpans[i].textContent;
if(oSpans[i])oSpans[i].classList.add(match?'diff-match':'diff-diverge');
if(aSpans[i])aSpans[i].classList.add(match?'diff-match':'diff-diverge');
if(match)matches++;
}
const pct=total>0?Math.round(matches/total*100):0;
$('#stats').textContent='Match: '+pct+'% ('+matches+'/'+total+') | OpenAI: '+oSpans.length+' tokens | Anthropic: '+aSpans.length+' tokens';
}
function updateConfSparkline(confidence){
confWindow.push(confidence!=null?confidence:0.5);
if(confWindow.length>60)confWindow.shift();
const wrap=$('#conf-spark-wrap');if(wrap)wrap.style.display='';
const canvas=$('#sparkline');if(!canvas)return;
const ctx=canvas.getContext('2d');
const w=canvas.width,h=canvas.height;
ctx.clearRect(0,0,w,h);
ctx.fillStyle='#141512';ctx.fillRect(0,0,w,h);
if(confWindow.length<2)return;
const avg=confWindow.reduce((a,b)=>a+b,0)/confWindow.length;
const color=avg>=0.7?'#7fd69f':avg>=0.4?'#e8c872':'#ff6a6a';
ctx.beginPath();ctx.strokeStyle=color;ctx.lineWidth=2;
confWindow.forEach((v,i)=>{
const x=Math.round(i/(confWindow.length-1)*w);
const y=Math.round((1-v)*h);
i===0?ctx.moveTo(x,y):ctx.lineTo(x,y);
});
ctx.stroke();
}
(function(){
if(localStorage.getItem('eot-cb-mode')==='1')document.body.classList.add('cb-mode');
})();
document.getElementById('btn-cb-mode').onclick=function(){
const on=document.body.classList.toggle('cb-mode');
localStorage.setItem('eot-cb-mode',on?'1':'0');
this.classList.toggle('active',on);
};
(function(){if(document.body.classList.contains('cb-mode'))document.getElementById('btn-cb-mode').classList.add('active');})();
(function(){
const saved=localStorage.getItem('eot-theme');
if(saved==='light'){document.body.setAttribute('data-theme','light');if($('#btn-theme'))$('#btn-theme').textContent='☀️';}
})();
$('#btn-theme').onclick=function(){
const isLight=document.body.getAttribute('data-theme')==='light';
if(isLight){document.body.removeAttribute('data-theme');this.textContent='🌙';localStorage.setItem('eot-theme','dark');}
else{document.body.setAttribute('data-theme','light');this.textContent='☀️';localStorage.setItem('eot-theme','light');}
};
(function(){
const saved=localStorage.getItem('eot-prompt');
if(saved&&$('#prompt')&&$('#prompt').value==='Tell me a story about a robot')$('#prompt').value=saved;
})();
$('#prompt').addEventListener('input',function(){localStorage.setItem('eot-prompt',this.value);});
window.tokenLog=[];
$('#btn-export').onclick=()=>{
if(allTokens.length===0){showNotice('No tokens to export. Run a stream first.', 'warning');return}
const data={
prompt:$('#prompt').value,
provider:$('#provider').value,
model:$('#model').value||'auto',
transform:$('#transform').value,
timestamp:new Date().toISOString(),
exported_at: new Date().toISOString(),
mode: mode,
token_count:allTokens.length,
transformed_count:allTokens.filter(t=>t.transformed).length,
tokens:allTokens.map(t=>({text:t.text,original:t.original,index:t.index,transformed:t.transformed,importance:t.importance,chaos_label:t.chaos_label||null,confidence:t.confidence,perplexity:t.perplexity,alternatives:t.alternatives||[]})),
surgery_log:surgeryLog
};
const blob=new Blob([JSON.stringify(data,null,2)],{type:'application/json'});
const url=URL.createObjectURL(blob);
const a=document.createElement('a');
a.href=url;a.download='every-other-token-'+Date.now()+'.json';
document.body.appendChild(a);a.click();a.remove();
URL.revokeObjectURL(url);
};
$('#btn-export-csv').onclick=()=>{
if(allTokens.length===0){showNotice('No tokens to export. Run a stream first.', 'warning');return;}
const meta=[
'# Every-Other-Token Export',
'# exported_at: '+new Date().toISOString(),
'# prompt: '+JSON.stringify(document.getElementById('prompt')?document.getElementById('prompt').value:''),
'# token_count: '+allTokens.length,
''
].join('\n');
const header='index,original,text,transformed,confidence,perplexity\n';
const rows=allTokens.map(t=>[t.index,JSON.stringify(t.original),JSON.stringify(t.text),t.transformed,t.confidence!=null?t.confidence:'',t.perplexity!=null?t.perplexity:''].join(',')).join('\n');
const blob=new Blob([meta+header+rows],{type:'text/csv'});
const url=URL.createObjectURL(blob);
const a=document.createElement('a');
a.href=url;a.download='eot_session_'+Date.now()+'.csv';
document.body.appendChild(a);a.click();a.remove();
URL.revokeObjectURL(url);
};
$('#btn-import').onclick=()=>$('#import-file').click();
$('#import-file').onchange=function(){
const f=this.files[0];if(!f)return;
const reader=new FileReader();
reader.onload=ev=>{
try{
const data=JSON.parse(ev.target.result);
if(!Array.isArray(data.tokens))throw new Error('Invalid session file');
allTokens=data.tokens;graphNodes=data.tokens;surgeryLog=data.surgery_log||[];undoStack=[];
$('#v-single').innerHTML='';
const frag=document.createDocumentFragment();
allTokens.forEach(tk=>{
const sp=mkSpan(tk.text,tk.transformed,tk.importance,'',tk.chaos_label,tk.confidence,tk.perplexity);
sp.dataset.idx=tk.index;frag.appendChild(sp);
});
$('#v-single').appendChild(frag);
enableSurgery($('#v-single'));
renderResearch();
$('#stats').textContent='Imported: '+allTokens.length+' tokens from '+f.name;
}catch(e){alert('Import failed: '+e.message);}
};
reader.readAsText(f);
this.value='';
};
let _resizeTimer=null;
window.addEventListener('resize',()=>{
clearTimeout(_resizeTimer);
_resizeTimer=setTimeout(()=>{if($('#graphtoggle').checked)drawGraph();},150);
});
document.addEventListener('keydown', function(e){
if((e.ctrlKey||e.metaKey)&&e.key==='z'&&!e.shiftKey){
e.preventDefault();
undoOne();
}
if(e.key==='?'&&!e.ctrlKey&&!e.metaKey){
e.preventDefault();
document.getElementById('shortcuts-modal').classList.toggle('show');
}
if(e.key==='Escape'){
document.getElementById('shortcuts-modal').classList.remove('show');
}
if((e.ctrlKey||e.metaKey)&&e.key==='e'&&!e.shiftKey){
e.preventDefault();
document.getElementById('btn-export').click();
}
if((e.ctrlKey||e.metaKey)&&e.key==='E'&&e.shiftKey){
e.preventDefault();
document.getElementById('btn-export-csv').click();
}
if(e.key==='Enter'&&e.target.id==='prompt'&&!e.shiftKey){
e.preventDefault();
document.getElementById('start').click();
}
});
let ws=null, myId=null, myColor='#e8c872', myName='Guest', amHost=false;
let roomCode=null, isRecording=false, hasReplay=false;
const peerColors={};
function sendWs(obj){if(ws&&ws.readyState===WebSocket.OPEN){try{ws.send(JSON.stringify(obj));}catch(e){console.warn('[eot] ws send failed:',e);}}}
function escHtml(s){return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');}
function safeCssColor(c){return/^#[0-9a-fA-F]{3,8}$|^[a-z]+$/i.test(String(c))?String(c):'#e8c872';}
function initRoom(code,asHost){
roomCode=code; amHost=asHost;
$('#mp-code').textContent=code;
$('#mp-panel').classList.add('show');
$('#sidebar').classList.add('show');
$('#vote-bar').classList.add('show');
if(amHost)$('#btn-rec').style.display='';
document.body.style.paddingRight='200px';
const proto=location.protocol==='https:'?'wss':'ws';
ws=new WebSocket(proto+'://'+location.host+'/ws/'+code);
ws.onopen=()=>{ if(!amHost)sendWs({type:'set_name',name:myName||'Guest'}); };
ws.onmessage=e=>{try{onWsMsg(JSON.parse(e.data));}catch(_){}};
ws.onclose=()=>{
leaveRoom(false);
if(roomCode)$('#stats').textContent='⚠ Multiplayer connection lost.';
};
ws.onerror=e=>{
console.error('WS',e);
showToast('Connection error: check network','#f85149');
};
}
function onWsMsg(m){
switch(m.type){
case 'welcome':
myId=m.participant.id; myColor=m.participant.color; myName=m.participant.name;
renderParticipants(m.room_state.participants||[]);
setParticipantCount(m.room_state.participants?m.room_state.participants.length:1);
{
const existingBadge=$('#mp-panel .host-badge');if(existingBadge)existingBadge.remove();
if(m.is_host||amHost){
const hb=document.createElement('span');hb.className='host-badge';hb.textContent='HOST';
const codeEl=$('#mp-code');if(codeEl&&codeEl.parentElement)codeEl.parentElement.appendChild(hb);
}
}
break;
case 'participant_join':
addPToList(m.participant); incrPCount(1);
showToast(m.participant.name+' joined', m.participant.color);
break;
case 'participant_leave':
$('#pid-'+m.participant_id)&&$('#pid-'+m.participant_id).remove(); incrPCount(-1);
break;
case 'participant_update':
{const el=$('#pid-'+m.participant.id);if(el)el.querySelector('.p-name').textContent=m.participant.name;}
break;
case 'token':
if(!amHost){
allTokens.push(m); graphNodes.push(m);
const sp=mkSpan(m.text,m.transformed,m.importance,'',m.chaos_label,m.confidence,m.perplexity);
sp.dataset.idx=m.index; $('#v-single').appendChild(sp);
updatePerpSparkline(m.perplexity);
}
break;
case 'surgery':
applyPeerSurgery(m); break;
case 'chat':
renderChatMsg(m); break;
case 'vote_update':
if(m.transform===$('#transform').value){$('#vote-up-n').textContent=m.up;$('#vote-dn-n').textContent=m.down;}
$('#vote-label').textContent=m.transform+': +'+m.up+'/-'+m.down;
break;
case 'record_started':
isRecording=true;$('#btn-rec').textContent='⏹ Stop';$('#btn-rec').classList.add('btn-rec-on'); break;
case 'record_stopped':
isRecording=false;$('#btn-rec').textContent='⏺ Record';$('#btn-rec').classList.remove('btn-rec-on');
hasReplay=true;$('#btn-replay').style.display=''; break;
case 'replay_event':
doReplayEvent(m.event,m.offset_ms); break;
case 'replay_done':
$('#replay-prog').classList.remove('show'); break;
case 'stream_done':
$('#start').disabled=false;$('#start').textContent='Stream';
if(!amHost){enableSurgery($('#v-single'));renderResearch();}
break;
case 'error':
showNotice('Room error: '+m.message, 'error'); break;
}
}
function renderParticipants(list){$('#participant-list').innerHTML='';list.forEach(addPToList);}
function addPToList(p){
if($('#pid-'+p.id))return;
peerColors[p.id]=p.color;
const d=document.createElement('div');d.className='p-item';d.id='pid-'+p.id;
const av=document.createElement('div');av.className='p-avatar';av.style.background=safeCssColor(p.color);
av.textContent=(p.name||'?')[0].toUpperCase();
const nm=document.createElement('span');nm.className='p-name';nm.textContent=p.name;
d.appendChild(av);d.appendChild(nm);
if(p.is_host){const h=document.createElement('span');h.className='p-host';h.textContent='HOST';d.appendChild(h);nm.textContent='★ '+nm.textContent;}
$('#participant-list').appendChild(d);
}
function setParticipantCount(n){$('#mp-count-badge').textContent=n;}
function incrPCount(d){$('#mp-count-badge').textContent=Math.max(1,parseInt($('#mp-count-badge').textContent||'1')+d);}
function showToast(text,color){
const t=document.createElement('div');t.className='join-toast';t.style.borderColor=color||'#25271f';t.textContent=text;
$('#participant-list').prepend(t);setTimeout(()=>t.remove(),3000);
}
function applyPeerSurgery(edit){
if(allTokens[edit.token_index])allTokens[edit.token_index].text=edit.new_text;
[$('#v-single'),$('#sbs-xform')].forEach(c=>{
if(!c)return;
const spans=c.querySelectorAll('.token');
if(spans[edit.token_index]){
const sp=spans[edit.token_index];sp.textContent=edit.new_text;
sp.style.color=edit.editor_color;sp.classList.add('peer-edited');
setTimeout(()=>{sp.style.color='';sp.classList.remove('peer-edited');},900);
}
});
}
function renderChatMsg(m){
const d=document.createElement('div');d.className='chat-msg';
const msgs=$('#chat-msgs');
if(msgs.children.length>=200)msgs.removeChild(msgs.firstChild);
const author=document.createElement('span');author.className='chat-author';
author.style.color=safeCssColor(m.author_color);author.textContent=m.author_name;
const sep=document.createTextNode(' ');
const body=document.createElement('span');body.className='chat-text';body.textContent=m.text;
const ts=new Date().toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'});
const tsSpan=document.createElement('span');
tsSpan.title=new Date().toISOString();
tsSpan.style.cssText='font-size:.6rem;color:#5f5b50;cursor:help;margin-left:4px';
tsSpan.textContent=ts;
d.appendChild(author);d.appendChild(sep);d.appendChild(body);d.appendChild(tsSpan);
if(m.token_index!=null){
const ref=document.createElement('div');ref.className='chat-ref';ref.textContent='re: token #'+m.token_index;
d.appendChild(ref);
}
msgs.appendChild(d);msgs.scrollTop=msgs.scrollHeight;
}
function sendChat(){
const t=$('#chat-in').value.trim();if(!t||!roomCode)return;
sendWs({type:'chat',text:t,token_index:null});$('#chat-in').value='';
}
$('#chat-btn').onclick=sendChat;
$('#chat-in').addEventListener('keydown',e=>{if(e.key==='Enter')sendChat();});
function doReplayEvent(ev,offsetMs){
if(!ev)return;
if(ev.type==='token'||ev.index!=null){
allTokens.push(ev);
const sp=mkSpan(ev.text,ev.transformed,ev.importance,'',ev.chaos_label,ev.confidence,ev.perplexity);
sp.dataset.idx=ev.index;$('#v-single').appendChild(sp);
} else if(ev.type==='surgery'){applyPeerSurgery(ev);}
else if(ev.type==='chat'){renderChatMsg(ev);}
}
$('#btn-host').onclick=async()=>{
const btn=$('#btn-host');
btn.disabled=true;btn.textContent='Creating...';
try{
const r=await fetch('/room/create',{method:'POST'});
if(!r.ok)throw new Error('Server error '+r.status);
const d=await r.json();
amHost=true; myName='Host';
initRoom(d.code,true);
}catch(e){
showNotice('Could not create room: '+e.message, 'error');
}finally{
btn.disabled=false;btn.textContent='Host Session';
}
};
$('#btn-join').onclick=()=>{
const c=$('#join-code').value.trim().toUpperCase().replace(/[^A-Z0-9]/g,'');
if(c.length!==6){showNotice('Room code must be 6 characters.', 'warning'); return;}
myName='Guest'; initRoom(c,false);
};
(function(){
const m=location.pathname.match(/\/join\/([A-Z0-9]{6})/i);
if(m){const code=m[1].toUpperCase();myName='Guest';setTimeout(()=>initRoom(code,false),100);}
})();
$('#btn-copy-link').onclick=()=>{
if(!roomCode)return;
const url=location.origin+'/join/'+roomCode;
navigator.clipboard.writeText(url).then(()=>{
$('#btn-copy-link').textContent='Copied!';setTimeout(()=>$('#btn-copy-link').textContent='Copy Link',1500);
}).catch(()=>{$('#btn-copy-link').textContent=roomCode;});
};
$('#mp-code').onclick=()=>$('#btn-copy-link').click();
$('#btn-leave').onclick=()=>{if(ws)ws.close();leaveRoom(true);};
function leaveRoom(explicit){
ws=null;roomCode=null;amHost=false;isRecording=false;hasReplay=false;
$('#mp-panel').classList.remove('show');
$('#sidebar').classList.remove('show');
$('#chat-panel').classList.remove('show');
$('#vote-bar').classList.remove('show');
document.body.style.paddingRight='';
$('#participant-list').innerHTML='';
}
$('#btn-vote-up').onclick=()=>sendWs({type:'vote',transform:$('#transform').value,dir:'up'});
$('#btn-vote-dn').onclick=()=>sendWs({type:'vote',transform:$('#transform').value,dir:'down'});
$('#btn-rec').onclick=()=>{
if(!roomCode)return;
sendWs(isRecording?{type:'record_stop'}:{type:'record_start'});
};
$('#btn-replay').onclick=()=>{
if(!roomCode)return;
$('#v-single').innerHTML='';allTokens=[];graphNodes=[];
$('#replay-prog').classList.add('show');$('#replay-bar').style.width='0%';
sendWs({type:'replay_request'});
};
const _baseStartClick=$('#start').onclick;
$('#start').onclick=function(){
_baseStartClick&&_baseStartClick.call(this);
if(!roomCode||!amHost)return;
setTimeout(()=>{
if(!es)return;
const orig=es.onmessage;
es.onmessage=function(ev){
orig&&orig.call(this,ev);
if(ev.data==='[DONE]'){sendWs({type:'stream_done'});return;}
try{
const tk=JSON.parse(ev.data);
sendWs({type:'token',...tk});
if(isRecording)sendWs({type:'_record_token',...tk}); }catch(_){}
};
},30);
};
document.getElementById('shortcuts-close').onclick = () =>
document.getElementById('shortcuts-modal').classList.remove('show');
document.getElementById('shortcuts-modal').onclick = e => {
if(e.target === document.getElementById('shortcuts-modal'))
document.getElementById('shortcuts-modal').classList.remove('show');
};
let _sseQueue = [];
let _sseFlushRunning = false;
function _startSseFlush(countRef, xformedRef, modeRef) {
if (_sseFlushRunning) return;
_sseFlushRunning = true;
function flush() {
if (!_sseFlushRunning) return;
const MAX_DOM_TOKENS = 500;
const batch = _sseQueue.splice(0, 30);
if (batch.length > 0) {
const vsingle = $('#v-single');
batch.forEach(tk => {
const domToks = vsingle.querySelectorAll('.token');
if (domToks.length >= MAX_DOM_TOKENS) domToks[0].remove();
countRef.count++; if (tk.transformed) xformedRef.xformed++;
const singleSp = mkSpan(tk.text, tk.transformed, tk.importance, '', tk.chaos_label, tk.confidence, tk.perplexity);
singleSp.dataset.idx = tk.index;
appendTok(vsingle, singleSp);
updatePerpSparkline(tk.perplexity);
updateConfSparkline(tk.confidence);
const origSp = mkSpan(tk.original, false, tk.importance, null, null, tk.confidence, tk.perplexity); origSp.dataset.idx = tk.index;
appendTok($('#sbs-orig'), origSp);
const xformSp = mkSpan(tk.text, tk.transformed, tk.importance, '', tk.chaos_label, tk.confidence, tk.perplexity); xformSp.dataset.idx = tk.index;
appendTok($('#sbs-xform'), xformSp);
['reverse','uppercase','mock','noise','chaos'].forEach(txName => {
const panel = $('#mp-' + txName);
if (!panel) return;
appendTok(panel, mkSpan(tk.transformed ? TX[txName](tk.original) : tk.original, tk.transformed, tk.importance));
});
graphNodes.push(tk);
});
if (!scrollPending) {
scrollPending = true;
requestAnimationFrame(() => {
scrollPending = false;
const active = views[mode];
if (active) active.scrollTop = active.scrollHeight;
if (mode === 'multi') ['reverse','uppercase','mock','noise'].forEach(tn => { const pp = $('#mp-' + tn); if (pp) pp.scrollTop = pp.scrollHeight; });
});
}
if ($('#graphtoggle').checked && !window._graphRafPending) {
window._graphRafPending = true;
requestAnimationFrame(() => { window._graphRafPending = false; drawGraph(); });
}
const elapsedMs = Date.now() - (_streamStart || Date.now());
const elapsedSec = Math.max(elapsedMs / 1000, 0.01);
const tokSec = (countRef.count / elapsedSec).toFixed(1);
const elapsedStr = (elapsedSec).toFixed(1);
$('#stats').textContent = 'Tokens: ' + countRef.count + ' | Transformed: ' + xformedRef.xformed + ' | Mode: ' + modeRef.mode + ' | ' + elapsedStr + 's | ' + tokSec + ' tok/s';
}
if (_sseOnDrained && _sseQueue.length === 0) {
const done = _sseOnDrained;
_sseOnDrained = null;
_sseFlushRunning = false;
done();
return;
}
setTimeout(flush, 100);
}
setTimeout(flush, 100);
}
let _sseOnDrained = null;
function _stopSseFlush() { _sseFlushRunning = false; _sseOnDrained = null; }
function _finishSseFlush(onDone) {
if (!_sseFlushRunning) { onDone(); return; }
_sseOnDrained = onDone;
}
let _streamStart = null;
(function(){
const SAMPLE = 'The quick brown fox jumps';
function updatePreview() {
const sel = $('#transform');
const chainMode = typeof chainTransforms !== 'undefined' && chainTransforms.length > 0;
const txName = sel ? sel.value : 'reverse';
let transformed;
if (chainMode) {
transformed = chainTransforms.reduce((s, tx) => TX[tx] ? TX[tx](s) : s, SAMPLE);
} else {
transformed = TX[txName] ? TX[txName](SAMPLE) : SAMPLE;
}
const prev = $('#transform-preview');
if (!prev) return;
prev.classList.add('show');
prev.innerHTML = '<span style="color:#5f5b50">Preview: </span><span style="color:#96917f">' + escHtml(SAMPLE) + '</span><span style="color:#5f5b50"> → </span><span style="color:#e3b341">' + escHtml(transformed) + '</span>';
}
const txSel = $('#transform');
if (txSel) txSel.addEventListener('input', updatePreview);
window._updateTransformPreview = updatePreview;
setTimeout(updatePreview, 0);
})();
$('#min-confidence').oninput = function() { $('#conf-label').textContent = this.value + '%'; };
let chainTransforms = [];
let _chainModeActive = false;
$('#btn-chain-mode').onclick = function() {
_chainModeActive = !_chainModeActive;
const panel = $('#chain-builder');
if (panel) { if (_chainModeActive) panel.classList.add('show'); else panel.classList.remove('show'); }
this.classList.toggle('active', _chainModeActive);
if (!_chainModeActive) {
const errEl=document.getElementById('chain-error');if(errEl){errEl.textContent='';errEl.style.display='none';}
const prevEl=document.getElementById('chain-preview');if(prevEl)prevEl.textContent='';
$('#start').disabled=false;
}
};
(function(){
const builder = $('#chain-builder');
if (!builder) return;
{
const errSpan=document.createElement('span');errSpan.id='chain-error';errSpan.style.cssText='color:#f85149;font-size:.7rem;display:none';builder.appendChild(errSpan);
const previewSpan=document.createElement('span');previewSpan.id='chain-preview';previewSpan.style.cssText='color:#3fb950;font-size:.75rem;font-family:monospace;display:block;margin-top:2px';builder.appendChild(previewSpan);
}
function updateChain() {
const checks = builder.querySelectorAll('input[data-tx]');
chainTransforms = [];
checks.forEach(cb => { if (cb.checked) chainTransforms.push(cb.dataset.tx); });
const cur = $('#chain-current');
if (cur) cur.textContent = chainTransforms.length ? chainTransforms.join(',') : 'none';
const errEl=document.getElementById('chain-error');
const prevEl=document.getElementById('chain-preview');
const startBtn=$('#start');
if(errEl){
if(chainTransforms.length===0&&_chainModeActive){
errEl.textContent='Select at least one transform';errEl.style.display='';
if(prevEl)prevEl.textContent='';
if(startBtn&&_chainModeActive)startBtn.disabled=true;
} else {
const seen={};const dups=[];
chainTransforms.forEach(tx=>{seen[tx]=(seen[tx]||0)+1;if(seen[tx]===2)dups.push(tx);});
if(dups.length>0){errEl.textContent='⚠ Duplicate: '+dups.join(', ');errEl.style.display='';}
else{errEl.textContent='';errEl.style.display='none';}
if(prevEl)prevEl.textContent=chainTransforms.length?'Chain: '+chainTransforms.join(' → '):'';
if(startBtn)startBtn.disabled=false;
}
}
if (window._updateTransformPreview) window._updateTransformPreview();
}
builder.querySelectorAll('input[data-tx]').forEach(cb => cb.addEventListener('change', updateChain));
})();
$('#replay-speed').oninput = function() {
$('#speed-label').textContent = this.value + '×';
};
function getReplaySpeedMultiplier() {
const s = $('#replay-speed');
return s ? Math.max(1, parseInt(s.value)) : 3;
}
(function(){
const _origDoReplayEvent = doReplayEvent;
window.doReplayEvent = function(ev, offsetMs) {
const speed = getReplaySpeedMultiplier();
const delay = Math.max(0, (offsetMs || 0) / speed);
setTimeout(() => _origDoReplayEvent(ev, 0), delay);
};
})();
function undoOne() {
const last = undoStack.pop();
if (!last) return;
last.sp.textContent = last.oldText;
if (last.idx >= 0 && allTokens[last.idx]) allTokens[last.idx].text = last.oldText;
if (typeof ws !== 'undefined' && ws && ws.readyState === WebSocket.OPEN) {
try { ws.send(JSON.stringify({type:'surgery', token_index: last.idx, old_text: last.newText, new_text: last.oldText})); } catch(e) { console.warn('[eot] ws undo send failed:', e); }
}
updateUndoBtnState();
}
function updateUndoBtnState() {
const btn = $('#btn-undo-one');
if (btn) btn.disabled = undoStack.length === 0;
}
$('#btn-undo-one').onclick = undoOne;
(function(){
const _origPush = Array.prototype.push;
const _origPop = Array.prototype.pop;
})();
function _renderExpDivergenceExtra(tokA, tokB, minLen, matches) {
const el = $('#exp-diverge');
if (minLen === 0) { const cw=$('#ab-chart-wrap');if(cw)cw.style.display='none'; return; }
const agreements = [];
for (let i = 0; i < minLen; i++) {
agreements.push(tokA[i].original === tokB[i].original);
}
const pct = Math.round(matches / minLen * 100);
const summaryDiv = document.createElement('div');
summaryDiv.style.cssText = 'font-size:.72rem;color:#96917f;margin-bottom:4px;margin-top:4px';
summaryDiv.innerHTML = '<strong style="color:#e6e1d3">Agreement: ' + pct + '%</strong> | Matching: <span style="color:#3fb950">' + matches + '</span> | Diverging: <span style="color:#f85149">' + (minLen - matches) + '</span>';
if (el) el.appendChild(summaryDiv);
const chartWrap = $('#ab-chart-wrap');
if (!chartWrap) return;
chartWrap.style.display = '';
const canvas = $('#ab-chart');
if (!canvas) return;
const positions = Math.min(50, agreements.length);
const dpr = window.devicePixelRatio || 1;
const w = canvas.parentElement.clientWidth || 600;
canvas.width = w * dpr;
canvas.height = 80 * dpr;
canvas.style.width = w + 'px';
canvas.style.height = '80px';
const ctx = canvas.getContext('2d');
ctx.scale(dpr, dpr);
ctx.clearRect(0, 0, w, 80);
const barW = Math.max(1, (w - 40) / positions);
const chartH = 55;
const xOff = 30;
ctx.strokeStyle = '#36382f'; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(xOff, 5); ctx.lineTo(xOff, chartH + 5); ctx.lineTo(w - 5, chartH + 5); ctx.stroke();
ctx.fillStyle = '#96917f'; ctx.font = '10px monospace'; ctx.textAlign = 'center';
ctx.fillText('Token Agreement', w / 2, 12);
ctx.fillStyle = '#5f5b50'; ctx.font = '9px monospace';
ctx.fillText('Token position', w / 2, 76);
for (let i = 0; i < positions; i++) {
ctx.fillStyle = agreements[i] ? '#3fb950' : '#f85149';
const x = xOff + i * barW + 1;
ctx.fillRect(x, 10, Math.max(1, barW - 2), chartH - 5);
}
ctx.fillStyle = '#5f5b50'; ctx.font = '8px monospace'; ctx.textAlign = 'center';
[0, Math.floor(positions / 2), positions - 1].forEach(i => {
if (i < positions) ctx.fillText(i + 1, xOff + i * barW + barW / 2, chartH + 14);
});
};
document.getElementById('btn-export-research').onclick = () => {
if (!allTokens.length) { showNotice('No tokens yet. Run a stream first.', 'warning'); return; }
const withConf = allTokens.filter(t => t.confidence != null);
const withPerp = allTokens.filter(t => t.perplexity != null);
const avgConf = withConf.length ? (withConf.reduce((s,t)=>s+t.confidence,0)/withConf.length*100).toFixed(1)+'%' : 'n/a';
const avgPerp = withPerp.length ? (withPerp.reduce((s,t)=>s+t.perplexity,0)/withPerp.length).toFixed(3) : 'n/a';
const unique = new Set(allTokens.map(t=>t.original.toLowerCase())).size;
const diversity = (unique/allTokens.length).toFixed(3);
const ts = new Date().toISOString();
const md = [
'# Every-Other-Token Research Export',
'',
`**Exported:** ${ts}`,
`**Prompt:** ${document.getElementById('prompt').value}`,
`**Provider:** ${document.getElementById('provider').value}`,
`**Model:** ${document.getElementById('model').value || 'auto'}`,
`**Transform:** ${document.getElementById('transform').value}`,
'',
'## Aggregate Stats',
'',
`| Metric | Value |`,
`|--------|-------|`,
`| Token count | ${allTokens.length} |`,
`| Transformed | ${allTokens.filter(t=>t.transformed).length} |`,
`| Vocab diversity | ${diversity} |`,
`| Avg confidence | ${avgConf} |`,
`| Avg perplexity | ${avgPerp} |`,
'',
'## Top 10 High-Perplexity Tokens',
'',
...withPerp.sort((a,b)=>b.perplexity-a.perplexity).slice(0,10).map((t,i)=>
`${i+1}. \`${t.original}\`: perplexity ${t.perplexity.toFixed(2)}`
),
'',
'## Surgery Log',
'',
surgeryLog.length ? surgeryLog.map(e=>`- token #${e.index}: \`${e.original}\` → \`${e.replacement}\``).join('\n') : '_(no edits)_',
].join('\n');
const blob = new Blob([md], {type:'text/markdown'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = 'eot-research-' + Date.now() + '.md';
document.body.appendChild(a); a.click(); a.remove();
URL.revokeObjectURL(url);
};</script>
</body>
</html>