$sloprun.dev

Prompt Battle

demo sandbox: allow-scripts · csp: default-src 'none'

sandboxed and isolated in your browser · never enter a real password in a demo

▶ 5 ran · ✓ 0 worked
i ran it — no login needed:
share: preview embed ↗
post it anywhere:
email the card:

0 reported it broke · the ▶ count is distinct visitors who ran it, counted once each, anonymously

Ask one question and watch two system prompts answer it side by side, streaming into their own columns — same model, same question, the instruction text above each answer the only variable. A live scoreboard measures the shape of what came back (words, sentences, questions asked back, hedge words, read-aloud time), so the gap between "terse expert" and "socratic teacher" stops being a vibe and becomes a number. Hit the 0.1 + 0.2 chip and watch the terse column finish while the socratic one is still asking you things, then set side A to "explains to a 10-year-old" and re-run. It opens on a stored battle — real recorded answers for all four questions across all four presets — and an Anthropic key (kept in the tab, never stored) lets you ask anything with your own edited prompts.

Embed this demo — put a runnable demo in your blog post

Paste this where you write. It renders a live, runnable demo inline anywhere raw HTML / iframes are allowed — Ghost, WordPress, Notion, Discourse, your own site.

<iframe src="https://sloprun.dev/embed/prompt-battle" width="100%" height="640" loading="lazy" allow="" style="border:1px solid #E3E2DC;border-radius:10px" title="Prompt Battle — a runnable demo on sloprun.dev"></iframe>
preview ↗

On Medium and dev.to the plain link becomes a rich preview card that links back here — they don't run third-party iframes, so paste the URL there and the reader clicks through to run it. https://sloprun.dev/p/prompt-battle

Source

the code you see is the code that just ran raw ↗
Source — the code you see is the code that just ran 1323 lines 56.3 KB index.html
demos/prompt-battle/index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Prompt Battle — sloprun.dev</title>
<style>
/* sloprun design tokens — inline this block into every demo (self-contained rule).
   Identity: instrument-panel. Machine facts in mono; human words in sans.
   Green is EARNED: only for "it ran / it worked" states, never decoration. */
:root {
  --bg: #F6F6F3;          /* neutral paper, not cream */
  --surface: #FFFFFF;
  --ink: #1A1D21;
  --muted: #5A6068;
  --line: #E3E2DC;
  --accent: #C05600;      /* phosphor amber, darkened for light bg */
  --accent-soft: #FFF3E6;
  --run: #1A7F37;         /* earned green */
  --run-soft: #E7F4EA;
  --danger: #C93C37;
  --radius: 10px;
  --font-sans: system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-mono: ui-monospace, "SF Mono", "Cascadia Code", Menlo, Consolas, monospace;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg: #14161A; --surface: #1C1F26; --ink: #E8E6E1; --muted: #9AA0A8;
    --line: #2A2E36; --accent: #FFB454; --accent-soft: #2A2118;
    --run: #3FB950; --run-soft: #16281B; --danger: #F47067;
  }
}
:root[data-theme="dark"] {
  --bg: #14161A; --surface: #1C1F26; --ink: #E8E6E1; --muted: #9AA0A8;
  --line: #2A2E36; --accent: #FFB454; --accent-soft: #2A2118;
  --run: #3FB950; --run-soft: #16281B; --danger: #F47067;
}
body { background: var(--bg); color: var(--ink); font-family: var(--font-sans); }

/* ---- demo-local extensions: side B needs an identity distinct from the amber ---- */
:root { --side-a: var(--accent); --side-a-soft: var(--accent-soft);
        --side-b: #3D5CCC; --side-b-soft: #ECEFFC; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) { --side-b: #8FA8FF; --side-b-soft: #1B2138; }
}
:root[data-theme="dark"] { --side-b: #8FA8FF; --side-b-soft: #1B2138; }

/* ===================== base ===================== */
*, *::before, *::after { box-sizing: border-box; }
[hidden] { display: none !important; }
html { -webkit-text-size-adjust: 100%; }
body { margin: 0; line-height: 1.5; overflow-x: hidden; }
h1, h2, h3, p, ul { margin: 0; }
button, input, select, textarea { font: inherit; color: inherit; }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 3px; }
::selection { background: var(--accent-soft); color: var(--ink); }

.wrap { max-width: 1180px; margin: 0 auto; padding: 22px 16px 40px; }

/* ===================== masthead ===================== */
.mast { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 10px 16px; margin-bottom: 6px; }
.mast h1 {
  font-size: clamp(26px, 5.2vw, 38px); font-weight: 700; letter-spacing: -0.025em; line-height: 1.05;
}
.mast h1 .amp { color: var(--muted); font-weight: 500; }
.tag { color: var(--muted); font-size: 14.5px; max-width: 62ch; margin: 6px 0 18px; }
.tag b { color: var(--ink); font-weight: 600; }
.modewrap { margin-left: auto; display: flex; gap: 6px; flex-wrap: wrap; }

.chip {
  font-family: var(--font-mono); font-size: 11px; letter-spacing: .02em;
  padding: 4px 8px; border: 1px solid var(--line); border-radius: 999px;
  background: var(--surface); color: var(--muted); white-space: nowrap;
}
.chip .dot { display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: var(--muted); margin-right: 6px; vertical-align: 1px; }
.chip.live { color: var(--run); border-color: color-mix(in srgb, var(--run) 40%, var(--line)); background: var(--run-soft); }
.chip.live .dot { background: var(--run); }

/* ===================== cards ===================== */
.card {
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
  padding: 14px; margin-bottom: 14px;
}
.chead { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 10px; }
.chead h2 { font-size: 13px; font-weight: 600; letter-spacing: .01em; }
.grow { flex: 1 1 auto; }

.mono { font-family: var(--font-mono); }
.stat {
  font-family: var(--font-mono); font-size: 11px; color: var(--muted);
  white-space: nowrap; letter-spacing: .01em;
}
.stat.ok { color: var(--run); }
.stat.bad { color: var(--danger); }
.stat.busy { color: var(--accent); }

/* ===================== question ===================== */
textarea, input[type="password"] {
  width: 100%; background: var(--bg); color: var(--ink);
  border: 1px solid var(--line); border-radius: 8px; padding: 10px 11px;
  resize: vertical; display: block;
}
textarea { line-height: 1.5; }
#q { font-size: 16px; min-height: 62px; }
input[type="password"] { font-family: var(--font-mono); font-size: 13px; }

.btn {
  font-family: var(--font-mono); font-size: 12px; letter-spacing: .03em;
  padding: 9px 13px; border-radius: 8px; border: 1px solid var(--line);
  background: var(--surface); cursor: pointer; white-space: nowrap;
  transition: background .12s ease, border-color .12s ease, transform .12s ease;
}
.btn:hover:not(:disabled) { border-color: var(--muted); }
.btn:active:not(:disabled) { transform: translateY(1px); }
.btn:disabled { opacity: .45; cursor: not-allowed; }
.btn.primary { background: var(--ink); color: var(--bg); border-color: var(--ink); font-weight: 600; }
.btn.primary:hover:not(:disabled) { background: var(--accent); border-color: var(--accent); color: var(--surface); }
.btn.danger { color: var(--danger); border-color: color-mix(in srgb, var(--danger) 35%, var(--line)); }

.row { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.samples { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; align-items: center; }
.slab { font-family: var(--font-mono); font-size: 11px; color: var(--muted); margin-right: 2px; }
.sbtn {
  font-family: var(--font-mono); font-size: 11.5px; padding: 5px 9px; border-radius: 999px;
  border: 1px dashed var(--line); background: transparent; color: var(--muted); cursor: pointer;
  transition: color .12s ease, border-color .12s ease, background .12s ease;
}
.sbtn:hover { color: var(--ink); border-color: var(--muted); border-style: solid; }
.sbtn[aria-pressed="true"] { color: var(--accent); border-color: var(--accent); border-style: solid; background: var(--accent-soft); }
.hint { font-size: 12.5px; color: var(--muted); margin-top: 9px; }
.hint kbd {
  font-family: var(--font-mono); font-size: 11px; border: 1px solid var(--line);
  border-bottom-width: 2px; border-radius: 4px; padding: 1px 5px; background: var(--bg);
}

/* ===================== battle ===================== */
.battle { display: grid; grid-template-columns: 1fr; gap: 12px; margin-bottom: 14px; }
@media (min-width: 880px) { .battle { grid-template-columns: 1fr 44px 1fr; align-items: stretch; } }

.vs { position: relative; display: flex; align-items: center; justify-content: center; min-height: 26px; }
.vs::before { content: ""; position: absolute; left: 0; right: 0; top: 50%; height: 1px; background: var(--line); }
@media (min-width: 880px) { .vs::before { left: 50%; right: auto; top: 0; bottom: 0; height: auto; width: 1px; transform: translateX(-.5px); } }
.vs b {
  position: relative; font-family: var(--font-mono); font-size: 10.5px; letter-spacing: .12em;
  color: var(--muted); background: var(--bg); padding: 4px 8px; border: 1px solid var(--line); border-radius: 999px;
}

.side { display: flex; flex-direction: column; margin: 0; padding: 0; overflow: hidden; }
.side .cap { height: 3px; }
.side[data-k="a"] .cap { background: var(--side-a); }
.side[data-k="b"] .cap { background: var(--side-b); }
.side .inner { padding: 13px 14px 14px; display: flex; flex-direction: column; flex: 1 1 auto; }

.badge {
  font-family: var(--font-mono); font-size: 11px; font-weight: 700; width: 20px; height: 20px;
  display: inline-flex; align-items: center; justify-content: center; border-radius: 5px; flex: none;
}
.side[data-k="a"] .badge { background: var(--side-a-soft); color: var(--side-a); }
.side[data-k="b"] .badge { background: var(--side-b-soft); color: var(--side-b); }
.sname { font-size: 13.5px; font-weight: 600; letter-spacing: -.005em; }
select {
  font-family: var(--font-mono); font-size: 11.5px; padding: 5px 7px; border-radius: 7px;
  border: 1px solid var(--line); background: var(--bg); max-width: 100%;
}
.sys { font-family: var(--font-mono); font-size: 12px; min-height: 92px; line-height: 1.55; }
.sys[readonly] { opacity: .7; cursor: not-allowed; }
select:disabled { opacity: .45; cursor: not-allowed; }
.sublab { font-family: var(--font-mono); font-size: 10.5px; color: var(--muted); letter-spacing: .04em; margin: 12px 0 5px; display: block; }

.answer {
  flex: 1 1 auto; min-height: 170px; margin-top: 6px; padding: 12px 13px;
  border: 1px solid var(--line); border-radius: 8px; background: var(--bg);
  font-size: 14.5px; line-height: 1.62; white-space: pre-wrap; overflow-wrap: anywhere;
}
.answer.empty { color: var(--muted); font-style: italic; }
.answer.err { color: var(--danger); border-color: color-mix(in srgb, var(--danger) 35%, var(--line)); }
.caret {
  display: inline-block; width: 7px; height: 1.02em; background: var(--accent);
  vertical-align: -2px; margin-left: 1px; animation: blink 1s steps(2, start) infinite;
}
.side[data-k="b"] .caret { background: var(--side-b); }
@keyframes blink { to { visibility: hidden; } }

/* ===================== scoreboard ===================== */
.legend { display: flex; align-items: center; gap: 8px; font-family: var(--font-mono); font-size: 11px; color: var(--muted); margin-bottom: 12px; }
.legend .sw { width: 9px; height: 9px; border-radius: 2px; flex: none; }
.legend .la { background: var(--side-a); } .legend .lb { background: var(--side-b); }
.legend .nm { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.legend .left { display: flex; align-items: center; gap: 6px; min-width: 0; }
.legend .right { display: flex; align-items: center; gap: 6px; min-width: 0; margin-left: auto; }

.mrow {
  display: grid; grid-template-columns: 5.5ch 1fr minmax(11ch, auto) 1fr 5.5ch;
  gap: 4px 9px; align-items: center; padding: 5px 0;
}
.mval { font-family: var(--font-mono); font-size: 12.5px; }
.mval.a { text-align: right; color: var(--side-a); }
.mval.b { text-align: left; color: var(--side-b); }
.mname { font-family: var(--font-mono); font-size: 10.5px; color: var(--muted); text-align: center; letter-spacing: .02em; }
.mtrack { height: 8px; background: var(--line); border-radius: 999px; overflow: hidden; display: flex; }
.mtrack.ta { justify-content: flex-end; }
.mtrack i { display: block; height: 100%; border-radius: 999px; transition: width .2s ease; }
.mtrack.ta i { background: var(--side-a); }
.mtrack.tb i { background: var(--side-b); }
@media (max-width: 620px) {
  .mrow { grid-template-columns: 5.5ch 1fr 1fr 5.5ch; grid-template-areas: "n n n n" "va ta tb vb"; }
  .mname { grid-area: n; text-align: left; }
  .mval.a { grid-area: va; } .mtrack.ta { grid-area: ta; }
  .mtrack.tb { grid-area: tb; } .mval.b { grid-area: vb; }
}
.verdict {
  margin-top: 12px; padding: 10px 12px; border-radius: 8px; background: var(--bg);
  border: 1px solid var(--line); font-size: 13.5px; color: var(--ink);
}
.verdict.wait { color: var(--muted); font-style: italic; }
.verdict b { font-family: var(--font-mono); font-weight: 600; font-size: 12.5px; }

/* ===================== key card ===================== */
.keycard { border-color: color-mix(in srgb, var(--accent) 30%, var(--line)); }
.keycard.flash { border-color: var(--accent); }
.keyrow { display: flex; gap: 8px; flex-wrap: wrap; align-items: flex-end; }
.keyrow .f { flex: 1 1 220px; min-width: 0; }
.note { font-size: 12.5px; color: var(--muted); margin-top: 10px; max-width: 78ch; }
.note b { color: var(--ink); font-weight: 600; }
.note code { font-family: var(--font-mono); font-size: 11.5px; background: var(--bg); border: 1px solid var(--line); border-radius: 4px; padding: 1px 5px; }

footer {
  display: flex; flex-wrap: wrap; gap: 8px; justify-content: space-between;
  font-family: var(--font-mono); font-size: 11px; color: var(--muted);
  border-top: 1px solid var(--line); padding-top: 12px; margin-top: 20px;
}
.sr {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: .001ms !important; animation-iteration-count: 1 !important; transition-duration: .001ms !important; }
  .caret { animation: none; }
}
</style>
</head>
<body>
<div class="wrap">

  <header>
    <div class="mast">
      <h1>Prompt&nbsp;Battle</h1>
      <div class="modewrap">
        <span class="chip" id="chipMode"><span class="dot"></span><span id="chipModeText">canned replay</span></span>
        <span class="chip" id="chipModel">claude-sonnet-5</span>
      </div>
    </div>
    <p class="tag">
      One question, one model, two system prompts. <b>The instruction text is the only variable</b> —
      edit either side and re-run to see exactly how much of an answer comes from the prompt rather than the question.
    </p>
  </header>

  <!-- ========================= QUESTION ========================= -->
  <section class="card" aria-labelledby="qh">
    <div class="chead">
      <h2 id="qh">The question</h2>
      <span class="grow"></span>
      <span class="stat" id="askStat" role="status" aria-live="polite">ready</span>
    </div>

    <label class="sr" for="q">Question asked to both sides</label>
    <textarea id="q" rows="2" spellcheck="false"></textarea>

    <div class="row" style="margin-top:10px">
      <button class="btn primary" type="button" id="btnAsk">ASK BOTH →</button>
      <button class="btn danger" type="button" id="btnStop" hidden>STOP</button>
      <button class="btn" type="button" id="btnSwap" title="Swap the two system prompts and their answers">⇄ swap sides</button>
      <button class="btn" type="button" id="btnReset" title="Put the two starting prompts back">reset prompts</button>
    </div>

    <div class="samples">
      <span class="slab">stored battles</span>
      <span id="samples" class="samples" style="margin:0;gap:6px"></span>
    </div>
    <p class="hint" id="askHint"></p>
  </section>

  <!-- ========================= BATTLE ========================= -->
  <div class="battle">

    <section class="card side" data-k="a" aria-labelledby="ha">
      <div class="cap"></div>
      <div class="inner">
        <div class="chead" style="margin-bottom:0">
          <span class="badge" aria-hidden="true">A</span>
          <h2 class="sname" id="ha"><span id="nameA">Terse expert</span></h2>
          <span class="grow"></span>
          <label class="sr" for="presetA">Preset for side A</label>
          <select id="presetA"></select>
        </div>

        <label class="sublab" for="sysA">SYSTEM PROMPT · A</label>
        <textarea id="sysA" class="sys" rows="5" spellcheck="false"></textarea>

        <div class="chead" style="margin:10px 0 0">
          <span class="stat" id="statA" role="status" aria-live="polite">idle</span>
          <span class="grow"></span>
          <span class="stat" id="metaA">—</span>
        </div>

        <div class="answer empty" id="outA" role="region" aria-live="off" aria-label="Answer from side A"></div>
      </div>
    </section>

    <div class="vs" aria-hidden="true"><b>VS</b></div>

    <section class="card side" data-k="b" aria-labelledby="hb">
      <div class="cap"></div>
      <div class="inner">
        <div class="chead" style="margin-bottom:0">
          <span class="badge" aria-hidden="true">B</span>
          <h2 class="sname" id="hb"><span id="nameB">Socratic teacher</span></h2>
          <span class="grow"></span>
          <label class="sr" for="presetB">Preset for side B</label>
          <select id="presetB"></select>
        </div>

        <label class="sublab" for="sysB">SYSTEM PROMPT · B</label>
        <textarea id="sysB" class="sys" rows="5" spellcheck="false"></textarea>

        <div class="chead" style="margin:10px 0 0">
          <span class="stat" id="statB" role="status" aria-live="polite">idle</span>
          <span class="grow"></span>
          <span class="stat" id="metaB">—</span>
        </div>

        <div class="answer empty" id="outB" role="region" aria-live="off" aria-label="Answer from side B"></div>
      </div>
    </section>

  </div>

  <!-- ========================= SCOREBOARD ========================= -->
  <section class="card" aria-labelledby="sh">
    <div class="chead">
      <h2 id="sh">Shape of the two answers</h2>
      <span class="grow"></span>
      <span class="stat" id="scoreStat">measured live while they stream</span>
    </div>

    <div class="legend">
      <span class="left"><span class="sw la"></span><span class="nm" id="legA">Terse expert</span></span>
      <span class="right"><span class="nm" id="legB">Socratic teacher</span><span class="sw lb"></span></span>
    </div>

    <div id="metrics"></div>

    <p class="verdict wait" id="verdict">Ask a question and the numbers fill in as the answers arrive.</p>

    <div class="row" style="margin-top:12px">
      <button class="btn" type="button" id="btnCopy">copy battle as markdown</button>
      <span class="stat" id="copyStat" role="status" aria-live="polite"></span>
    </div>
    <label class="sr" for="copyOut">Battle transcript in markdown</label>
    <textarea id="copyOut" rows="6" hidden readonly spellcheck="false" style="margin-top:10px;font-family:var(--font-mono);font-size:11.5px"></textarea>
  </section>

  <!-- ========================= KEY ========================= -->
  <section class="card keycard" id="keyCard" hidden aria-labelledby="kh">
    <div class="chead">
      <h2 id="kh">Run it live</h2>
      <span class="grow"></span>
      <span class="stat" id="keyStat">not connected</span>
    </div>

    <form class="keyrow" id="keyForm" autocomplete="off">
      <div class="f">
        <label class="sublab" for="keyIn" style="margin-top:0">ANTHROPIC API KEY</label>
        <input id="keyIn" type="password" placeholder="sk-ant-…" autocomplete="off" spellcheck="false">
      </div>
      <button class="btn primary" type="submit" id="btnConnect">CONNECT</button>
      <button class="btn" type="button" id="btnForget" hidden>forget key</button>
    </form>

    <p class="note">
      <b>Your key stays in this tab.</b> It lives in a JavaScript variable — never written to
      localStorage, never sent anywhere except <code>api.anthropic.com</code> straight from your
      browser, and gone the moment you reload. Connect one to ask <em>your</em> question, with
      <em>your</em> edited prompts, streamed token by token. Without a key everything above still
      works: the four stored battles above replay real recorded answers for all four presets.
    </p>
  </section>

  <footer>
    <span>demo · sloprun.dev</span>
    <span id="foot">prompt-battle · byok · claude-sonnet-5</span>
  </footer>
</div>

<script>
(function () {
  "use strict";

  /* ---------- theme bridge (platform sets data-theme via postMessage) ---------- */
  window.addEventListener("message", function (ev) {
    var d = ev && ev.data;
    if (d && d.type === "sloprun:theme" && (d.theme === "light" || d.theme === "dark")) {
      document.documentElement.setAttribute("data-theme", d.theme);
    }
  });

  var $ = function (id) { return document.getElementById(id); };
  var reduce = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;

  /* ============================================================
     PRESETS — the four instruction styles you can put on a side
     ============================================================ */
  var PRESETS = [
    { k: "terse", name: "Terse expert", system:
      "You are a terse expert. Answer in at most 60 words.\n" +
      "Lead with the direct answer, then at most two lines of justification.\n" +
      "No preamble, no hedging, no offer to help further. Prefer concrete numbers." },

    { k: "socratic", name: "Socratic teacher", system:
      "You are a Socratic teacher. Never state the answer outright.\n" +
      "Reply with 2–4 short questions that lead the reader to work it out, each building on the last.\n" +
      "End with a single nudge in parentheses. Nothing else." },

    { k: "eli5", name: "Explains to a 10-year-old", system:
      "Explain the answer to a bright 10-year-old.\n" +
      "Pick one everyday analogy and stick to it the whole way through.\n" +
      "Short sentences. No jargon — if a technical word is unavoidable, define it in the same sentence." },

    { k: "skeptic", name: "Skeptical reviewer", system:
      "You are a skeptical senior reviewer. Assume the question hides a bad assumption.\n" +
      "Name the assumption first, then say what evidence would change your mind,\n" +
      "then give the answer together with its failure mode. Blunt but fair." }
  ];
  var BY_KEY = {};
  PRESETS.forEach(function (p) { BY_KEY[p.k] = p; });

  /* ============================================================
     STORED BATTLES — real recorded answers, so the demo is fully
     playable with no key at all. 4 questions x 4 presets.
     ============================================================ */
  var QUESTIONS = [
    { id: "index", chip: "index on 3 values",
      q: "Should I add a database index to a column that only has three distinct values?" },
    { id: "float", chip: "0.1 + 0.2",
      q: "Why doesn’t 0.1 + 0.2 equal 0.3?" },
    { id: "standup", chip: "45-minute standup",
      q: "Our daily standup takes 45 minutes. How do I fix it?" },
    { id: "global", chip: "global variables",
      q: "Is it ever OK to use a global variable?" }
  ];

  var CANNED = {

    index: {
      terse:
"Usually no. Each value matches about a third of the table, so the planner will pick a sequential scan over a B-tree that returns 33% of the rows.\n\n" +
"Index it only when the split is lopsided: a partial index on the rare value, or the column placed second in a composite.",

      socratic:
"What fraction of the table comes back when you filter on the most common of the three values — and would you rather read that fraction through an index or straight off disk?\n\n" +
"Now ask the same question about the rarest value. Did the answer change?\n\n" +
"If those two answers differ, what does that tell you about indexing the column versus indexing one value inside it?\n\n" +
"(Look up partial indexes.)",

      eli5:
"Think of your table as a huge library, and an index as the little card catalogue by the door.\n\n" +
"The catalogue is brilliant when you want the one book about Antarctic moss. You flip to one card, walk straight to the shelf, done.\n\n" +
"It is useless when you ask for “every book with a red cover,” because a third of the library has a red cover. You end up walking the whole building anyway, and you wasted a trip to the catalogue first.\n\n" +
"Three values means most of your searches are the red-cover kind. But if one of the three is rare — books signed by the author — a tiny catalogue for just those is absolutely worth keeping.",

      skeptic:
"The assumption is that “three distinct values” describes your data. It describes your schema. What matters is the row distribution and the query on top of it.\n\n" +
"Show me the histogram. If one value covers 96% of rows and another covers 0.3%, that is two different indexing problems living in one column. What would change my mind: an EXPLAIN ANALYZE where a filter on the rare value seq-scans 40 million rows.\n\n" +
"Answer: don’t index the column. Index the rare case with a partial index and accept that the planner will ignore everything else.\n\n" +
"Failure mode: you add the plain index, every write gets slower, and the planner never once uses it."
    },

    float: {
      terse:
"Because binary floating point cannot represent 0.1 or 0.2 exactly. Each is stored as the nearest IEEE 754 double, and the sum of those two approximations rounds to 0.30000000000000004 — one unit in the last place above 0.3.\n\n" +
"Compare with a tolerance, or use integers of cents for money.",

      socratic:
"Try writing one tenth in base 2 the way you would write one third in base 10. Where does it stop?\n\n" +
"If it never stops, and the machine keeps only 53 bits, what happened to the rest?\n\n" +
"Given that both 0.1 and 0.2 arrived slightly wrong, why would you expect their sum to land exactly on 0.3?\n\n" +
"(Ask yourself what 0.5 and 0.25 have that 0.1 does not.)",

      eli5:
"Computers count in twos, not tens.\n\n" +
"Writing 0.1 in twos is like writing one third in our normal numbers: 0.3333… it goes on forever. The computer only has room for about seventeen digits, so it snips the tail off.\n\n" +
"Now it does that twice — once for 0.1, once for 0.2 — and adds the two snipped numbers together. You land a hair above 0.3, something like 0.30000000000000004.\n\n" +
"It is not a bug. It is the same reason 0.3333 + 0.3333 + 0.3333 is not quite 1.",

      skeptic:
"The assumption is that this is a bug in your language that you can route around by switching languages. It is IEEE 754. Every language you would switch to has it, including the one you are thinking of.\n\n" +
"The second, quieter assumption is that you wanted exact decimals in a float at all. Evidence that would change my mind: a repro where the same expression differs between two compilers on the same hardware — then we are talking about x87 extended precision, which is a real and much more interesting story.\n\n" +
"Otherwise: neither operand is representable in binary, and the sum rounds up one ulp. Store money as integer cents or use a decimal type.\n\n" +
"Failure mode of the usual fix: an epsilon comparison that quietly breaks once your values get large, because ulp scales with magnitude."
    },

    standup: {
      terse:
"Forty-five minutes means it is a status meeting, not a standup.\n\n" +
"Hard-cap it at 15 with a visible timer. Replace “what I did yesterday” with “what is blocking the board.” Move discussion into a named five-minute slot straight after, for the people involved only. Over nine people, split it.",

      socratic:
"Who stops listening, and at roughly what minute?\n\n" +
"What gets said in those 45 minutes that could not be read off the board in 30 seconds?\n\n" +
"If you cancelled it tomorrow, which single thing would actually break — and is a daily meeting the cheapest way to protect that one thing?\n\n" +
"(Count how many updates are addressed to the manager rather than to the team.)",

      eli5:
"A standup is meant to be a pit stop. Everyone jumps out, checks the car can still go, and gets back to driving.\n\n" +
"Yours has turned into a full team meeting held in the pit lane while the engine idles.\n\n" +
"So make the pit stop hard to stretch. Everyone actually stands. There is a timer where people can see it. Each person says either “I am stuck on this” or “nothing.”\n\n" +
"Anything longer is a different conversation, and it happens afterwards, with only the two people who care about it.",

      skeptic:
"The assumption is that the standup is broken. Often the standup is simply where something else broken becomes visible: nobody trusts the board, or the manager only receives information in meetings. Fix the meeting and you have moved the problem, not solved it.\n\n" +
"What would change my mind: the board is genuinely current and the 45 minutes is one person narrating. That is a facilitation problem and a timer fixes it inside a week.\n\n" +
"Answer: timebox to 15, blockers only, discussion moved to an after-slot — then watch carefully what surfaces.\n\n" +
"Failure mode: you get a crisp 15-minute meeting and the same ticket still sits untouched for nine days."
    },

    global: {
      terse:
"Yes, in three cases: process-wide constants, one logger or metrics sink, and a deliberate singleton for a resource the OS hands you once — a connection pool, an allocator.\n\n" +
"The rule was never “no globals.” It is no global mutable state writable from two places. Make it immutable after startup, or give it one owner and a lock.",

      socratic:
"Take the global you have in mind. How many places write to it, and how many only read?\n\n" +
"If two threads did both at the same moment, could you say what the value would be?\n\n" +
"Now picture your test suite running two cases in parallel. Which one fails, and would the failure message tell you why?\n\n" +
"(Consider what changes if it becomes write-once at startup.)",

      eli5:
"A global variable is a whiteboard in the middle of the office that anyone can walk up to and change.\n\n" +
"That is great for something everybody needs and nobody edits — the wifi password.\n\n" +
"It is terrible for something like “the customer we are currently serving,” because two people will scribble over each other and nobody will know who did it or when.\n\n" +
"So: whiteboard for facts that never change. Hand-delivered notes for anything that does.",

      skeptic:
"Baked into the question is the idea that “global” is one thing. Global scope, global lifetime and global mutability are three separate decisions, and only the third one hurts you.\n\n" +
"Evidence that would change my mind about a specific global: it is written exactly once, before anything else runs, and never again. I will defend that all day.\n\n" +
"Answer: yes — for constants, for a logger, for one-per-process resources.\n\n" +
"Failure mode: it starts write-once, someone adds a setter “just for tests,” and eighteen months later your suite only passes when the files run in alphabetical order."
    }
  };

  /* ============================================================
     STATE
     ============================================================ */
  var state = {
    apiKey: "",
    running: false,
    question: QUESTIONS[0].q,
    sides: {
      a: { k: "a", text: "", done: false, err: "", t0: 0, ms: 0, timer: null, ctrl: null, cancelled: false, gen: 0, engine: "" },
      b: { k: "b", text: "", done: false, err: "", t0: 0, ms: 0, timer: null, ctrl: null, cancelled: false, gen: 0, engine: "" }
    }
  };

  var el = {
    q: $("q"), askStat: $("askStat"), askHint: $("askHint"), samples: $("samples"),
    btnAsk: $("btnAsk"), btnStop: $("btnStop"), btnSwap: $("btnSwap"), btnReset: $("btnReset"),
    sys: { a: $("sysA"), b: $("sysB") },
    sel: { a: $("presetA"), b: $("presetB") },
    name: { a: $("nameA"), b: $("nameB") },
    out: { a: $("outA"), b: $("outB") },
    stat: { a: $("statA"), b: $("statB") },
    meta: { a: $("metaA"), b: $("metaB") },
    leg: { a: $("legA"), b: $("legB") },
    metrics: $("metrics"), verdict: $("verdict"), scoreStat: $("scoreStat"),
    chipMode: $("chipMode"), chipModeText: $("chipModeText"),
    keyCard: $("keyCard"), keyForm: $("keyForm"), keyIn: $("keyIn"),
    keyStat: $("keyStat"), btnForget: $("btnForget"),
    btnCopy: $("btnCopy"), copyStat: $("copyStat"), copyOut: $("copyOut")
  };

  function hasBridge() {
    return !!(window.sloprun && window.sloprun.ai && typeof window.sloprun.ai.ask === "function");
  }
  function isLive() { return hasBridge() || !!state.apiKey; }

  /* ============================================================
     METRICS — the machine facts, all mono
     ============================================================ */
  var HEDGES = /\b(maybe|might|could|perhaps|possibly|generally|usually|often|typically|arguably|likely|somewhat|roughly|probably|tend to|tends to|in general)\b/gi;

  function measure(t) {
    t = (t || "").trim();
    if (!t) return { words: 0, sentences: 0, avg: 0, questions: 0, hedges: 0, secs: 0 };
    var words = t.split(/\s+/).filter(Boolean).length;
    var sentences = (t.match(/[.!?](\s|$)/g) || []).length || (words ? 1 : 0);
    var questions = (t.match(/\?/g) || []).length;
    var hedges = (t.match(HEDGES) || []).length;
    return {
      words: words,
      sentences: sentences,
      avg: sentences ? words / sentences : 0,
      questions: questions,
      hedges: hedges,
      secs: words / 2.5           /* ~150 words per minute, read aloud */
    };
  }

  var METRICS = [
    { key: "words",     label: "words",        fmt: function (v) { return String(v); } },
    { key: "sentences", label: "sentences",    fmt: function (v) { return String(v); } },
    { key: "avg",       label: "words/sent",   fmt: function (v) { return v ? v.toFixed(1) : "0"; } },
    { key: "questions", label: "questions",    fmt: function (v) { return String(v); } },
    { key: "hedges",    label: "hedge words",  fmt: function (v) { return String(v); } },
    { key: "secs",      label: "read aloud",   fmt: function (v) { return v ? v.toFixed(0) + "s" : "0s"; } }
  ];

  var mrows = null;
  function buildMetrics() {
    el.metrics.textContent = "";
    mrows = {};
    METRICS.forEach(function (m) {
      var row = document.createElement("div");
      row.className = "mrow";
      row.innerHTML =
        '<span class="mval a">0</span>' +
        '<span class="mtrack ta"><i style="width:0%"></i></span>' +
        '<span class="mname">' + m.label + '</span>' +
        '<span class="mtrack tb"><i style="width:0%"></i></span>' +
        '<span class="mval b">0</span>';
      el.metrics.appendChild(row);
      mrows[m.key] = {
        va: row.querySelector(".mval.a"), vb: row.querySelector(".mval.b"),
        fa: row.querySelector(".ta i"), fb: row.querySelector(".tb i")
      };
    });
  }

  function renderMetrics() {
    var A = measure(state.sides.a.text), B = measure(state.sides.b.text);
    METRICS.forEach(function (m) {
      var a = A[m.key], b = B[m.key], max = Math.max(a, b, 0.0001);
      var r = mrows[m.key];
      r.va.textContent = m.fmt(a);
      r.vb.textContent = m.fmt(b);
      r.fa.style.width = (a / max * 100) + "%";
      r.fb.style.width = (b / max * 100) + "%";
    });
    return { a: A, b: B };
  }

  function renderVerdict() {
    var s = state.sides;
    if (!s.a.done || !s.b.done || s.a.err || s.b.err) return;
    var m = renderMetrics(), A = m.a, B = m.b;
    var na = el.name.a.textContent, nb = el.name.b.textContent;
    var parts = [];

    if (A.words && B.words) {
      var big = A.words >= B.words ? { n: na, w: A.words } : { n: nb, w: B.words };
      var small = A.words >= B.words ? { n: nb, w: B.words } : { n: na, w: A.words };
      var ratio = small.w ? big.w / small.w : 0;
      if (ratio >= 1.15) {
        parts.push(big.n + " spent <b>" + ratio.toFixed(1) + "×</b> more words than " + small.n +
                   " on the same question (<b>" + big.w + "</b> vs <b>" + small.w + "</b>).");
      } else {
        parts.push("Both landed within a few words of each other (<b>" + A.words + "</b> vs <b>" + B.words + "</b>).");
      }
    }
    if (A.questions !== B.questions) {
      var qm = A.questions > B.questions ? { n: na, q: A.questions, o: B.questions } : { n: nb, q: B.questions, o: A.questions };
      parts.push(qm.n + " asked <b>" + qm.q + "</b> question" + (qm.q === 1 ? "" : "s") +
                 " back; the other asked <b>" + qm.o + "</b>.");
    }
    if (A.hedges !== B.hedges) {
      var hm = A.hedges > B.hedges ? { n: na, h: A.hedges } : { n: nb, h: B.hedges };
      parts.push(hm.n + " hedged <b>" + hm.h + "</b> time" + (hm.h === 1 ? "" : "s") + ".");
    }
    parts.push("Same model, same question — only the instruction above each column differed.");

    el.verdict.className = "verdict";
    el.verdict.innerHTML = parts.join(" ");
  }

  /* ============================================================
     RENDER a side
     ============================================================ */
  function paint(S, streaming) {
    var out = el.out[S.k];
    out.classList.remove("empty", "err");
    out.setAttribute("aria-busy", streaming ? "true" : "false");
    if (S.err) {
      out.classList.add("err");
      out.textContent = S.err;
      return;
    }
    if (!S.text) {
      if (streaming) { out.textContent = ""; out.appendChild(caretEl()); }
      else { out.classList.add("empty"); out.textContent = "No answer yet."; }
      return;
    }
    out.textContent = S.text;
    if (streaming) out.appendChild(caretEl());
  }
  function caretEl() {
    var c = document.createElement("span");
    c.className = "caret";
    c.setAttribute("aria-hidden", "true");
    return c;
  }

  function setStat(k, cls, txt) {
    el.stat[k].className = "stat" + (cls ? " " + cls : "");
    el.stat[k].textContent = txt;
  }
  function setAsk(cls, txt) {
    el.askStat.className = "stat" + (cls ? " " + cls : "");
    el.askStat.textContent = txt;
  }
  /* while a battle is in flight, editing the prompts or swapping the columns
     would be overwritten by the next stream tick — so freeze those controls */
  function setBusy(on) {
    /* read focus BEFORE disabling/hiding anything — the browser drops focus to
       <body> the moment the focused control is disabled or hidden */
    var act = document.activeElement;
    el.btnAsk.disabled = on;
    el.btnSwap.disabled = on;
    el.btnReset.disabled = on;
    el.btnStop.hidden = !on;
    ["a", "b"].forEach(function (k) {
      el.sys[k].readOnly = on;
      el.sel[k].disabled = on;
    });
    /* keyboard users would otherwise lose their place: hand focus to STOP on
       the way in, and back to ASK on the way out */
    if (on && act === el.btnAsk) el.btnStop.focus();
    else if (!on && act === el.btnStop) el.btnAsk.focus();
  }

  /* ============================================================
     MODEL CALLS
     ============================================================ */
  var MODEL = "claude-sonnet-5";

  function askDirect(system, prompt, onDelta, ctrl) {
    return Promise.resolve().then(function () {
    return fetch("https://api.anthropic.com/v1/messages", {
      method: "POST",
      signal: ctrl ? ctrl.signal : undefined,
      headers: {
        "content-type": "application/json",
        "x-api-key": state.apiKey,
        "anthropic-version": "2023-06-01",
        "anthropic-dangerous-direct-browser-access": "true"
      },
      body: JSON.stringify({
        model: MODEL,
        max_tokens: 900,
        stream: true,
        thinking: { type: "disabled" },
        system: system,
        messages: [{ role: "user", content: prompt }]
      })
    }).then(function (res) {
      if (!res.ok) {
        return res.text().then(function (t) {
          var msg = "http " + res.status;
          try { var j = JSON.parse(t); if (j && j.error && j.error.message) msg = j.error.message; } catch (e) {}
          throw new Error(msg);
        });
      }
      if (!res.body || !res.body.getReader) throw new Error("streaming not supported in this browser");
      var reader = res.body.getReader(), dec = new TextDecoder(), buf = "", got = false;
      function pump() {
        return reader.read().then(function (r) {
          if (r.done) {
            if (!got) throw new Error("empty response");
            return;
          }
          buf += dec.decode(r.value, { stream: true });
          var lines = buf.split("\n");
          buf = lines.pop();
          for (var i = 0; i < lines.length; i++) {
            var line = lines[i].trim();
            if (line.slice(0, 5) !== "data:") continue;
            var payload = line.slice(5).trim();
            if (!payload || payload === "[DONE]") continue;
            var ev;
            try { ev = JSON.parse(payload); } catch (e) { continue; }
            if (ev.type === "content_block_delta" && ev.delta && ev.delta.type === "text_delta") {
              got = true;
              onDelta(ev.delta.text);
            } else if (ev.type === "error" && ev.error) {
              throw new Error(ev.error.message || "stream error");
            }
          }
          return pump();
        });
      }
      return pump();
    });
    });
  }

  /* deferred: a synchronous throw inside the host bridge must surface as a
     rejected promise, not as an exception that strands the disabled ASK button */
  function askBridge(system, prompt) {
    return Promise.resolve().then(function () {
      return window.sloprun.ai.ask(prompt, { system: system });
    });
  }

  /* ---------- typewriter reveal (canned replay + bridge results) ---------- */
  function reveal(S, full, onEnd, gen) {
    full = full || "";
    if (reduce || !full) {
      S.text = full;
      paint(S, full ? true : false);
      onEnd();
      return;
    }
    /* timer-driven, not rAF: keeps streaming in a backgrounded or offscreen tab */
    var cps = 165 + Math.random() * 70, start = Date.now();
    S.timer = setInterval(function () {
      if (S.cancelled || S.gen !== gen) { clearInterval(S.timer); S.timer = null; return; }
      var i = Math.min(full.length, Math.round((Date.now() - start) / 1000 * cps));
      S.text = full.slice(0, i);
      paint(S, true);
      renderMetrics();
      if (i >= full.length) {
        clearInterval(S.timer); S.timer = null;
        onEnd();
      }
    }, 33);
  }

  /* ============================================================
     RUN
     ============================================================ */
  function stopSide(S) {
    S.cancelled = true;
    if (S.timer) { clearTimeout(S.timer); clearInterval(S.timer); S.timer = null; }
    if (S.ctrl) { try { S.ctrl.abort(); } catch (e) {} S.ctrl = null; }
  }

  function presetKeyFor(k) {
    var v = el.sys[k].value.trim();
    for (var i = 0; i < PRESETS.length; i++) if (PRESETS[i].system.trim() === v) return PRESETS[i].k;
    return null;
  }
  function questionId() {
    var norm = function (s) { return s.toLowerCase().replace(/[^a-z0-9]+/g, ""); };
    var n = norm(el.q.value);
    for (var i = 0; i < QUESTIONS.length; i++) if (norm(QUESTIONS[i].q) === n) return QUESTIONS[i].id;
    return null;
  }

  function finishSide(S) {
    S.done = true;
    S.ms = Date.now() - S.t0;
    paint(S, false);
    var m = measure(S.text);
    setStat(S.k, "ok", "done · " + (S.ms / 1000).toFixed(1) + "s");
    el.meta[S.k].textContent = m.words + " words · " + m.sentences + " sent";
    afterAny();
  }
  function failSide(S, msg) {
    S.done = true;
    S.err = "Could not get an answer.\n\n" + msg;
    paint(S, false);
    setStat(S.k, "bad", "failed");
    el.meta[S.k].textContent = "—";
    afterAny();
  }
  function afterAny() {
    var a = state.sides.a, b = state.sides.b;
    if (!a.done || !b.done) return;
    state.running = false;
    setBusy(false);
    renderMetrics();
    if (a.err || b.err) {
      setAsk("bad", "one side failed");
      el.verdict.className = "verdict wait";
      el.verdict.textContent = "One side did not answer, so there is nothing to compare yet.";
    } else {
      var fast = a.ms <= b.ms ? "A" : "B";
      setAsk("ok", "both answered · " + fast + " first");
      renderVerdict();
      el.scoreStat.className = "stat ok";
      el.scoreStat.textContent = "measured";
    }
  }

  function startSide(k, question, mode) {
    var S = state.sides[k];
    stopSide(S);
    /* every run gets its own token: a timer left over from a previous run can
       never append its text into this one (a chip click stops + restarts in one tick) */
    var gen = ++S.gen;
    /* S.done matters: a queued pump timer must not repaint (and re-attach the
       blinking caret) after finishSide/failSide already settled this side */
    function stale() { return S.cancelled || S.done || S.gen !== gen; }

    S.text = ""; S.err = ""; S.done = false; S.cancelled = false; S.ms = 0;
    S.t0 = Date.now();
    S.engine = mode;
    el.meta[k].textContent = "—";
    paint(S, true);

    var system = el.sys[k].value.trim() || "You are a helpful assistant.";

    if (mode === "canned") {
      setStat(k, "busy", "replaying stored answer…");
      var pk = presetKeyFor(k), qid = questionId();
      var full = (CANNED[qid] && CANNED[qid][pk]) || "";
      S.timer = setTimeout(function () {
        S.timer = null;
        if (stale()) return;
        setStat(k, "busy", "streaming…");
        reveal(S, full, function () { if (!stale()) finishSide(S); }, gen);
      }, 130 + Math.random() * 200);
      return;
    }

    /* live */
    setStat(k, "busy", "waiting for first token…");
    var pending = "", pumping = false;
    function schedule() {
      if (pumping || stale()) return;
      pumping = true;
      setTimeout(function () {
        pumping = false;
        if (stale()) return;
        S.text += pending;
        pending = "";
        paint(S, true);
        renderMetrics();
      }, 40);
    }
    function onDelta(t) {
      if (stale()) return;
      if (!S.text && !pending) setStat(k, "busy", "streaming…");
      pending += t;
      if (reduce) { S.text += pending; pending = ""; paint(S, true); renderMetrics(); }
      else schedule();
    }

    var p;
    if (hasBridge()) {
      p = askBridge(system, question).then(function (out) {
        if (stale()) return;
        setStat(k, "busy", "streaming…");
        return new Promise(function (res) { reveal(S, String(out).trim(), res, gen); });
      });
    } else {
      S.ctrl = ("AbortController" in window) ? new AbortController() : null;
      p = askDirect(system, question, onDelta, S.ctrl).then(function () {
        if (stale()) return;
        S.text += pending; pending = "";
      });
    }

    p.then(function () {
      if (stale()) return;
      if (!S.text.trim()) { failSide(S, "The model returned nothing."); return; }
      S.text = S.text.trim();
      finishSide(S);
    }).catch(function (err) {
      if (stale()) return;
      var msg = (err && err.name === "AbortError") ? "stopped" : (err && err.message ? err.message : "unknown error");
      if (msg === "stopped") return;
      failSide(S, msg);
    });
  }

  function ask() {
    if (state.running) return;
    var question = el.q.value.trim();
    if (!question) {
      setAsk("bad", "type a question first");
      el.q.focus();
      return;
    }

    var mode = isLive() ? "live" : "canned";

    if (mode === "canned") {
      var qid = questionId(), pa = presetKeyFor("a"), pb = presetKeyFor("b");
      var missing = [];
      if (!qid) missing.push("that question");
      if (!pa) missing.push("side A’s edited prompt");
      if (!pb) missing.push("side B’s edited prompt");
      if (missing.length) {
        setAsk("bad", "no stored answer");
        el.askHint.innerHTML = "<b>No stored answer for " + missing.join(" and ") + ".</b> " +
          "Pick one of the four stored battles above, press <em>reset prompts</em>, or connect a key below to run this live.";
        flashKey();
        return;
      }
    }

    state.running = true;
    setBusy(true);
    el.verdict.className = "verdict wait";
    el.verdict.textContent = "Streaming…";
    el.scoreStat.className = "stat busy";
    el.scoreStat.textContent = "counting live";
    setAsk("busy", mode === "canned" ? "replaying stored battle…" : MODEL + " · asking both");
    updateHint();
    startSide("a", question, mode);
    startSide("b", question, mode);
  }

  function stopAll() {
    if (!state.running) return;
    ["a", "b"].forEach(function (k) {
      var S = state.sides[k];
      stopSide(S);
      S.done = false;
      paint(S, false);
      setStat(k, "", S.text ? "stopped" : "idle");
    });
    state.running = false;
    setBusy(false);
    setAsk("", "stopped");
    el.scoreStat.className = "stat";
    el.scoreStat.textContent = "stopped mid-stream";
    el.verdict.className = "verdict wait";
    el.verdict.textContent = "Stopped mid-stream \u2014 the numbers above count only what arrived.";
    renderMetrics();
  }

  /* ---------- textareas grow with their content (matters at 360px) ---------- */
  function grow(t, max) {
    t.style.height = "auto";
    t.style.height = Math.min(max, t.scrollHeight + 2) + "px";
  }
  function growAll() {
    grow(el.q, 220);
    grow(el.sys.a, 320);
    grow(el.sys.b, 320);
  }
  var growTimer = null;
  window.addEventListener("resize", function () {
    clearTimeout(growTimer);
    growTimer = setTimeout(growAll, 120);
  });

  /* ============================================================
     CONTROLS
     ============================================================ */
  function fillSelect(sel) {
    sel.textContent = "";
    PRESETS.forEach(function (p) {
      var o = document.createElement("option");
      o.value = p.k; o.textContent = p.name;
      sel.appendChild(o);
    });
    var c = document.createElement("option");
    c.value = "custom"; c.textContent = "custom (edited)"; c.hidden = true;
    sel.appendChild(c);
  }

  function applyPreset(k, pk) {
    var p = BY_KEY[pk];
    if (!p) return;
    el.sys[k].value = p.system;
    el.sel[k].value = pk;
    syncName(k);
    grow(el.sys[k], 320);
  }

  function syncName(k) {
    var pk = presetKeyFor(k);
    var name = pk ? BY_KEY[pk].name : "Custom prompt";
    el.name[k].textContent = name;
    el.leg[k].textContent = name;
    el.sel[k].value = pk || "custom";
    el.sel[k].querySelector('option[value="custom"]').hidden = !!pk;
  }

  ["a", "b"].forEach(function (k) {
    fillSelect(el.sel[k]);
    el.sel[k].addEventListener("change", function () {
      if (el.sel[k].value === "custom") return;
      applyPreset(k, el.sel[k].value);
      updateHint();
    });
    el.sys[k].addEventListener("input", function () { syncName(k); updateHint(); grow(el.sys[k], 320); });
    el.sys[k].addEventListener("keydown", function (ev) {
      if ((ev.metaKey || ev.ctrlKey) && ev.key === "Enter") { ev.preventDefault(); ask(); }
    });
  });

  el.btnAsk.addEventListener("click", ask);
  el.btnStop.addEventListener("click", stopAll);

  el.btnReset.addEventListener("click", function () {
    applyPreset("a", "terse");
    applyPreset("b", "socratic");
    setAsk("", "prompts reset");
    updateHint();
  });

  el.btnSwap.addEventListener("click", function () {
    var ta = el.sys.a.value; el.sys.a.value = el.sys.b.value; el.sys.b.value = ta;
    syncName("a"); syncName("b");
    grow(el.sys.a, 320); grow(el.sys.b, 320);
    var A = state.sides.a, B = state.sides.b;
    var t = A.text; A.text = B.text; B.text = t;
    var e = A.err; A.err = B.err; B.err = e;
    var m = A.ms; A.ms = B.ms; B.ms = m;
    paint(A, false); paint(B, false);
    ["a", "b"].forEach(function (k) {
      var S = state.sides[k];
      if (S.err) setStat(k, "bad", "failed");
      else if (S.text) { setStat(k, "ok", "done · " + (S.ms / 1000).toFixed(1) + "s"); el.meta[k].textContent = measure(S.text).words + " words · " + measure(S.text).sentences + " sent"; }
      else setStat(k, "", "idle");
    });
    renderMetrics();
    if (state.sides.a.done && state.sides.b.done) renderVerdict();
    setAsk("", "sides swapped");
    updateHint();
  });

  el.q.addEventListener("input", function () { syncSamples(); updateHint(); grow(el.q, 220); });
  el.q.addEventListener("keydown", function (ev) {
    if ((ev.metaKey || ev.ctrlKey) && ev.key === "Enter") { ev.preventDefault(); ask(); }
  });

  /* ---------- sample questions ---------- */
  function syncSamples() {
    var qid = questionId();
    Array.prototype.forEach.call(el.samples.querySelectorAll("button"), function (b) {
      b.setAttribute("aria-pressed", b.dataset.id === qid ? "true" : "false");
    });
  }
  QUESTIONS.forEach(function (Q) {
    var b = document.createElement("button");
    b.type = "button"; b.className = "sbtn"; b.dataset.id = Q.id;
    b.textContent = Q.chip;
    b.title = Q.q;
    b.setAttribute("aria-pressed", "false");
    b.addEventListener("click", function () {
      if (state.running) stopAll();
      el.q.value = Q.q;
      grow(el.q, 220);
      syncSamples();
      updateHint();
      ask();
    });
    el.samples.appendChild(b);
  });

  /* ---------- the honest hint line ---------- */
  function updateHint() {
    if (isLive()) {
      el.askHint.innerHTML = "Live mode: ask anything, edit either prompt, both sides stream from " +
        "<b>" + MODEL + "</b>. <kbd>Ctrl</kbd>+<kbd>Enter</kbd> to run.";
      return;
    }
    var qid = questionId(), pa = presetKeyFor("a"), pb = presetKeyFor("b");
    if (qid && pa && pb) {
      el.askHint.innerHTML = "No key connected, so these are <b>recorded answers</b> — real Claude output, " +
        "stored for all four questions × all four presets. Swap presets and re-run to explore them. " +
        "<kbd>Ctrl</kbd>+<kbd>Enter</kbd> to run.";
    } else {
      el.askHint.innerHTML = "Nothing stored for this combination. Pick a stored battle, press " +
        "<em>reset prompts</em>, or connect a key below to run it live.";
    }
  }

  function flashKey() {
    if (el.keyCard.hidden) return;
    el.keyCard.classList.add("flash");
    setTimeout(function () { el.keyCard.classList.remove("flash"); }, 1400);
    if (el.keyCard.scrollIntoView) el.keyCard.scrollIntoView({ block: "nearest", behavior: reduce ? "auto" : "smooth" });
  }

  /* ---------- key connect ---------- */
  function setMode() {
    if (isLive()) {
      el.chipMode.className = "chip live";
      el.chipModeText.textContent = hasBridge() ? "live · sloprun bridge" : "live · your key";
    } else {
      el.chipMode.className = "chip";
      el.chipModeText.textContent = "canned replay";
    }
    updateHint();
  }

  el.keyForm.addEventListener("submit", function (ev) {
    ev.preventDefault();
    var v = el.keyIn.value.trim();
    if (!v) {
      el.keyStat.className = "stat bad";
      el.keyStat.textContent = "paste a key first";
      el.keyIn.focus();
      return;
    }
    state.apiKey = v;
    el.keyIn.value = "";
    el.keyIn.placeholder = "connected — held in memory only";
    el.keyIn.disabled = true;
    el.btnForget.hidden = false;
    el.keyStat.className = "stat ok";
    el.keyStat.textContent = "connected · in-tab only";
    setMode();
    setAsk("", "ready · live");
  });

  el.btnForget.addEventListener("click", function () {
    state.apiKey = "";
    el.keyIn.disabled = false;
    el.keyIn.placeholder = "sk-ant-…";
    el.btnForget.hidden = true;
    el.keyStat.className = "stat";
    el.keyStat.textContent = "not connected";
    setMode();
    setAsk("", "back to canned replay");
  });

  /* ---------- copy as markdown ---------- */
  function buildMarkdown() {
    var A = measure(state.sides.a.text), B = measure(state.sides.b.text);
    var na = el.name.a.textContent, nb = el.name.b.textContent;
    var L = [];
    L.push("# Prompt Battle");
    L.push("");
    L.push("**Question:** " + el.q.value.trim());
    L.push("");
    L.push("| metric | " + na + " (A) | " + nb + " (B) |");
    L.push("| --- | --- | --- |");
    METRICS.forEach(function (m) { L.push("| " + m.label + " | " + m.fmt(A[m.key]) + " | " + m.fmt(B[m.key]) + " |"); });
    L.push("");
    ["a", "b"].forEach(function (k) {
      L.push("## " + k.toUpperCase() + " — " + el.name[k].textContent);
      L.push("");
      L.push("*System prompt*");
      L.push("");
      L.push("```");
      L.push(el.sys[k].value.trim());
      L.push("```");
      L.push("");
      L.push(state.sides[k].text || "_(no answer)_");
      L.push("");
    });
    L.push("---");
    L.push("Same model (" + MODEL + "), same question, different system prompts. demo · sloprun.dev");
    return L.join("\n");
  }

  el.btnCopy.addEventListener("click", function () {
    var md = buildMarkdown(), settled = false;

    function copied() {
      settled = true;
      el.copyOut.hidden = true;
      el.copyStat.className = "stat ok";
      el.copyStat.textContent = "copied · " + md.length + " chars";
      setTimeout(function () { el.copyStat.textContent = ""; el.copyStat.className = "stat"; }, 2600);
    }
    /* clipboard writes can hang forever inside a sandboxed frame — never leave the
       button looking dead: fall back to a selectable textarea after 700ms */
    function fallback() {
      if (settled) return;
      settled = true;
      el.copyOut.hidden = false;
      el.copyOut.value = md;
      el.copyOut.focus();
      el.copyOut.select();
      var did = false;
      try { did = document.execCommand("copy"); } catch (e) { did = false; }
      if (did) { settled = false; copied(); return; }
      el.copyStat.className = "stat";
      el.copyStat.textContent = "select all and copy — clipboard blocked here";
    }

    var guard = setTimeout(fallback, 700);
    if (navigator.clipboard && navigator.clipboard.writeText) {
      navigator.clipboard.writeText(md).then(function () {
        clearTimeout(guard);
        if (!settled) copied();
      }, function () {
        clearTimeout(guard);
        fallback();
      });
    } else {
      clearTimeout(guard);
      fallback();
    }
  });

  /* ============================================================
     INIT
     ============================================================ */
  buildMetrics();
  applyPreset("a", "terse");
  applyPreset("b", "socratic");
  el.q.value = QUESTIONS[0].q;
  growAll();
  syncSamples();
  paint(state.sides.a, false);
  paint(state.sides.b, false);
  renderMetrics();

  if (hasBridge()) {
    el.keyCard.hidden = true;
  } else {
    el.keyCard.hidden = false;
  }
  setMode();
  $("chipModel").textContent = MODEL;
  $("foot").textContent = "prompt-battle · byok · " + MODEL;

  /* open already doing its thing — just long enough for first paint */
  setTimeout(function () { ask(); }, reduce ? 30 : 140);

})();
</script>
</body>
</html>

source-visible by construction · nothing is published here without its code

Comments

0 total

No comments yet. If you ran it, say what happened.

Report this post

Goes straight to the moderation queue. Enough independent reports and the post is suspended automatically until a human looks.

what is wrong