Spaces:
Configuration error
Configuration error
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <title>Prompt Diff Explorer</title> | |
| <style> | |
| :root { | |
| --bg:#f6f8fb; | |
| --panel:#fff; | |
| --ink:#172033; | |
| --muted:#667085; | |
| --line:#d8dee9; | |
| --blue:#0b57d0; | |
| --add:#eaf7ef; | |
| --del:#fff0f0; | |
| --chg:#fff8db; | |
| --hunk:#eef4ff; | |
| } | |
| * { box-sizing:border-box; } | |
| body { margin:0; background:var(--bg); color:var(--ink); font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Arial,sans-serif; line-height:1.45; } | |
| header { background:#fff; border-bottom:1px solid var(--line); padding:28px 32px 20px; position:sticky; top:0; z-index:5; } | |
| main { max-width:1500px; margin:0 auto; padding:24px 32px 52px; } | |
| h1 { margin:0 0 8px; font-size:28px; } | |
| h2 { margin:0 0 14px; font-size:20px; } | |
| h3 { margin:18px 0 10px; font-size:16px; } | |
| section { background:var(--panel); border:1px solid var(--line); border-radius:8px; padding:20px; margin-bottom:20px; } | |
| nav { display:flex; gap:10px; flex-wrap:wrap; margin-top:14px; } | |
| a { color:var(--blue); text-decoration:none; } | |
| nav a,.button { border:1px solid #c9dcff; background:#eef4ff; border-radius:7px; padding:7px 10px; cursor:pointer; color:#0b57d0; } | |
| button.button { font:inherit; } | |
| .muted { color:var(--muted); } | |
| code { font-family:ui-monospace,SFMono-Regular,Consolas,monospace; font-size:12px; overflow-wrap:anywhere; } | |
| .controls { display:grid; grid-template-columns:repeat(12, minmax(0, 1fr)); gap:12px; align-items:end; } | |
| .field { display:flex; flex-direction:column; gap:5px; } | |
| .field label { color:#475467; font-size:12px; font-weight:700; text-transform:uppercase; letter-spacing:.04em; } | |
| .field select,.field input { width:100%; min-height:36px; border:1px solid #cfd7e3; border-radius:7px; background:#fff; color:var(--ink); padding:6px 8px; font:inherit; } | |
| .span3 { grid-column:span 3; } .span2 { grid-column:span 2; } .span4 { grid-column:span 4; } .span6 { grid-column:span 6; } .span12 { grid-column:1 / -1; } | |
| .actions { display:flex; gap:8px; flex-wrap:wrap; align-items:center; } | |
| .summary-line { margin-top:12px; color:#344054; } | |
| .badge { display:inline-block; border:1px solid var(--line); background:#f8fafc; border-radius:999px; padding:2px 8px; margin-right:6px; font-size:12px; } | |
| .scroll { overflow:auto; border:1px solid var(--line); border-radius:8px; } | |
| table { width:100%; border-collapse:collapse; font-size:13px; } | |
| th,td { border-bottom:1px solid var(--line); padding:8px 9px; text-align:left; vertical-align:top; } | |
| th { background:#f0f3f8; color:#384152; font-weight:700; line-height:1.2; white-space:nowrap; vertical-align:middle; } | |
| .diff-table { --line-col:56px; table-layout:fixed; font-family:ui-monospace,SFMono-Regular,Consolas,monospace; font-size:12px; line-height:1.4; } | |
| .diff-table col.line-col { width:var(--line-col); } | |
| .diff-table col.code-col { width:calc((100% - (var(--line-col) * 2)) / 2); } | |
| .diff-table th { position:sticky; top:0; z-index:1; vertical-align:middle; height:34px; } | |
| .diff-table th.group-head { padding-left:calc(var(--line-col) + 8px); } | |
| .diff-table td { padding:0; vertical-align:top; border-bottom:0; } | |
| .diff-table .num { width:var(--line-col); color:#667085; text-align:right; padding:1px 8px; user-select:none; background:#f8fafc; border-right:1px solid var(--line); } | |
| .diff-table .code { white-space:pre-wrap; overflow-wrap:anywhere; padding:1px 8px; min-height:20px; } | |
| .diff-table .same .code { background:#fff; } | |
| .diff-table .add .right-code { background:var(--add); } | |
| .diff-table .del .left-code { background:var(--del); } | |
| .diff-table .replace .left-code { background:var(--del); } | |
| .diff-table .replace .right-code { background:var(--add); } | |
| .diff-table .empty { background:#fbfcfe; color:#98a2b3; } | |
| .unified { margin:0; padding:12px 0; overflow:auto; font-family:ui-monospace,SFMono-Regular,Consolas,monospace; font-size:12px; line-height:1.4; background:#fff; border:1px solid var(--line); border-radius:8px; } | |
| .unified span { display:block; white-space:pre; padding:0 12px; } | |
| .unified .add { background:var(--add); } .unified .del { background:var(--del); } .unified .ctx { color:#1f2937; } .unified .meta { background:var(--hunk); color:#174ea6; font-weight:700; } | |
| .raw-view { margin:0; min-height:360px; max-height:720px; overflow:auto; border:1px solid var(--line); border-radius:8px; background:#fff; padding:12px; font-family:ui-monospace,SFMono-Regular,Consolas,monospace; font-size:12px; line-height:1.45; white-space:pre-wrap; overflow-wrap:anywhere; } | |
| .hidden { display:none; } | |
| .note { border-left:4px solid #ad5f00; padding:10px 12px; background:#fff8eb; color:#4a2d00; } | |
| @media (max-width:900px) { header { position:static; } main,header { padding-left:18px; padding-right:18px; } .span2,.span3,.span4,.span6 { grid-column:1 / -1; } } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <h1>Prompt Diff Explorer</h1> | |
| <div class="muted">Choose any prompt version on the left and right. View either the editable routing policy or the full assembled prompt.</div> | |
| <nav><a href="../final-cardinality-report.html">Final Report</a><a href="#compare">Compare</a><a href="#single">View One Version</a><a href="#versions">Version Files</a><a href="prompt-diff-summary.json">JSON Summary</a></nav> | |
| </header> | |
| <main> | |
| <section id="compare"> | |
| <h2>Compare Two Versions</h2> | |
| <div class="controls"> | |
| <div class="field span3"><label for="leftVersion">Left version</label><select id="leftVersion"></select></div> | |
| <div class="field span3"><label for="rightVersion">Right version</label><select id="rightVersion"></select></div> | |
| <div class="field span2"><label for="contentKind">Content</label><select id="contentKind"><option value="routing_policy">Routing policy</option><option value="prompt">Full prompt</option></select></div> | |
| <div class="field span2"><label for="viewMode">View</label><select id="viewMode"><option value="side">Side by side</option><option value="unified">Unified</option></select></div> | |
| <div class="field span2"><label for="contextMode">Lines</label><select id="contextMode"><option value="all">Show all</option><option value="changed">Only changed</option></select></div> | |
| <div class="actions span12"><button class="button" id="swapBtn">Swap left/right</button><a class="button" id="leftRaw" href="#">Open left raw</a><a class="button" id="rightRaw" href="#">Open right raw</a><button class="button" id="copyUnified">Copy unified diff</button></div> | |
| </div> | |
| <div class="summary-line" id="diffSummary">Loading...</div> | |
| <div class="scroll" id="sideBySide"></div> | |
| <pre class="unified hidden" id="unifiedDiff"></pre> | |
| </section> | |
| <section id="single"> | |
| <h2>View One Version</h2> | |
| <div class="controls"> | |
| <div class="field span4"><label for="singleVersion">Version</label><select id="singleVersion"></select></div> | |
| <div class="field span2"><label for="singleKind">Content</label><select id="singleKind"><option value="routing_policy">Routing policy</option><option value="prompt">Full prompt</option></select></div> | |
| <div class="actions span6"><a class="button" id="singleRaw" href="#">Open raw file</a><button class="button" id="singleToLeft">Use as left</button><button class="button" id="singleToRight">Use as right</button></div> | |
| </div> | |
| <p class="muted" id="singleMeta"></p> | |
| <pre class="raw-view" id="singleText">Loading...</pre> | |
| </section> | |
| <section id="versions"> | |
| <h2>Version Files</h2> | |
| <p class="note">The routing-policy files are usually the best thing to diff. The full prompt files include the scaffold around the editable policy.</p> | |
| <div class="scroll"><table><thead><tr><th>Version</th><th>Note</th><th>Raw prompt</th><th>Raw routing policy</th><th>Routing policy SHA-256</th></tr></thead><tbody><tr><td><b>v9.1 seed</b><br><code>00-v9-1-seed</code></td><td>Baseline OpenClaw classification prompt, normalized to the optimizer placeholder format.</td><td><a href='raw/00-v9-1-seed.prompt.md'>prompt</a><br><span class='muted'>241 lines</span></td><td><a href='raw/00-v9-1-seed.routing_policy.md'>routing policy</a><br><span class='muted'>185 lines</span></td><td><code>3d84a551239a9c26865a44758f0602548c1361482eef087c194d878a2d92ad37</code></td></tr><tr><td><b>GEPA six</b><br><code>01-gepa-six</code></td><td>Early 12B GEPA winner from the first six-row run.</td><td><a href='raw/01-gepa-six.prompt.md'>prompt</a><br><span class='muted'>100 lines</span></td><td><a href='raw/01-gepa-six.routing_policy.md'>routing policy</a><br><span class='muted'>45 lines</span></td><td><code>f4b161bb9bbaf366f1d4f1841243d73544bbd3c553ca6be5eb2818e757007187</code></td></tr><tr><td><b>Previous proper best</b><br><code>02-previous-proper-best</code></td><td>Earlier proper GEPA run before the prop20 continuation.</td><td><a href='raw/02-previous-proper-best.prompt.md'>prompt</a><br><span class='muted'>206 lines</span></td><td><a href='raw/02-previous-proper-best.routing_policy.md'>routing policy</a><br><span class='muted'>151 lines</span></td><td><code>288c716272a98543b9c5d7c191ac9389173adddefd17a7fba409a2e9d6b9ff65</code></td></tr><tr><td><b>Prop20 best</b><br><code>03-prop20-best</code></td><td>Best candidate from the 20-proposal GEPA continuation.</td><td><a href='raw/03-prop20-best.prompt.md'>prompt</a><br><span class='muted'>203 lines</span></td><td><a href='raw/03-prop20-best.routing_policy.md'>routing policy</a><br><span class='muted'>148 lines</span></td><td><code>5ff17ce59da5ff6c98c7241f10a12e7ebb908b415c51a6aad631ad9d83a686e5</code></td></tr><tr><td><b>Hardcase repair v2</b><br><code>04-hardcase-repair-v2</code></td><td>Manual hardcase repair that scored high but failed cardinality.</td><td><a href='raw/04-hardcase-repair-v2.prompt.md'>prompt</a><br><span class='muted'>227 lines</span></td><td><a href='raw/04-hardcase-repair-v2.routing_policy.md'>routing policy</a><br><span class='muted'>171 lines</span></td><td><code>39be82b8f2e94199ed5c832e6ddd8716e886bbe95874f6afad7ff5fd89798abe</code></td></tr><tr><td><b>Cardinality repair v3</b><br><code>05-cardinality-repair-v3</code></td><td>Promoted cardinality-safe repair with a strict two-label cap.</td><td><a href='raw/05-cardinality-repair-v3.prompt.md'>prompt</a><br><span class='muted'>233 lines</span></td><td><a href='raw/05-cardinality-repair-v3.routing_policy.md'>routing policy</a><br><span class='muted'>177 lines</span></td><td><code>b2576ca027148e109a1e72029c192ea9e26be486508671ec7c11025ac80f948b</code></td></tr></tbody></table></div> | |
| </section> | |
| </main> | |
| <script> | |
| const versions = [{"slug": "00-v9-1-seed", "label": "v9.1 seed", "note": "Baseline OpenClaw classification prompt, normalized to the optimizer placeholder format.", "prompt_path": "raw/00-v9-1-seed.prompt.md", "routing_policy_path": "raw/00-v9-1-seed.routing_policy.md", "prompt_sha256": "2d1d28761157c1e749e695f46109886322763dabc4a48877867d9b4807dacbbe", "routing_policy_sha256": "3d84a551239a9c26865a44758f0602548c1361482eef087c194d878a2d92ad37", "prompt_lines": 241, "routing_policy_lines": 185}, {"slug": "01-gepa-six", "label": "GEPA six", "note": "Early 12B GEPA winner from the first six-row run.", "prompt_path": "raw/01-gepa-six.prompt.md", "routing_policy_path": "raw/01-gepa-six.routing_policy.md", "prompt_sha256": "16f7127a6a2f8df2c7aa6d48eee5c4997c34553e70f3a5e1e83c57925453623e", "routing_policy_sha256": "f4b161bb9bbaf366f1d4f1841243d73544bbd3c553ca6be5eb2818e757007187", "prompt_lines": 100, "routing_policy_lines": 45}, {"slug": "02-previous-proper-best", "label": "Previous proper best", "note": "Earlier proper GEPA run before the prop20 continuation.", "prompt_path": "raw/02-previous-proper-best.prompt.md", "routing_policy_path": "raw/02-previous-proper-best.routing_policy.md", "prompt_sha256": "8112eefe3c9e69b3fb8a9525e24a83e259d82f4ef3742041a296439d9f89e435", "routing_policy_sha256": "288c716272a98543b9c5d7c191ac9389173adddefd17a7fba409a2e9d6b9ff65", "prompt_lines": 206, "routing_policy_lines": 151}, {"slug": "03-prop20-best", "label": "Prop20 best", "note": "Best candidate from the 20-proposal GEPA continuation.", "prompt_path": "raw/03-prop20-best.prompt.md", "routing_policy_path": "raw/03-prop20-best.routing_policy.md", "prompt_sha256": "e7394ba8222d0604066d9b014a419bf0f6ae153c08c51aebdb6755b7be1651a5", "routing_policy_sha256": "5ff17ce59da5ff6c98c7241f10a12e7ebb908b415c51a6aad631ad9d83a686e5", "prompt_lines": 203, "routing_policy_lines": 148}, {"slug": "04-hardcase-repair-v2", "label": "Hardcase repair v2", "note": "Manual hardcase repair that scored high but failed cardinality.", "prompt_path": "raw/04-hardcase-repair-v2.prompt.md", "routing_policy_path": "raw/04-hardcase-repair-v2.routing_policy.md", "prompt_sha256": "59332116ec04e8e7b57bcc5f9c2d3f7d4e9b0d60578f0a6559b40359a76ff66f", "routing_policy_sha256": "39be82b8f2e94199ed5c832e6ddd8716e886bbe95874f6afad7ff5fd89798abe", "prompt_lines": 227, "routing_policy_lines": 171}, {"slug": "05-cardinality-repair-v3", "label": "Cardinality repair v3", "note": "Promoted cardinality-safe repair with a strict two-label cap.", "prompt_path": "raw/05-cardinality-repair-v3.prompt.md", "routing_policy_path": "raw/05-cardinality-repair-v3.routing_policy.md", "prompt_sha256": "535b147ca0ba9a8afdd3c658f6395aeb23328663f3de0d2042dc183341b0a912", "routing_policy_sha256": "b2576ca027148e109a1e72029c192ea9e26be486508671ec7c11025ac80f948b", "prompt_lines": 233, "routing_policy_lines": 177}]; | |
| const bySlug = Object.fromEntries(versions.map(v => [v.slug, v])); | |
| const cache = new Map(); | |
| const ids = ['leftVersion','rightVersion','contentKind','viewMode','contextMode','singleVersion','singleKind']; | |
| const el = Object.fromEntries(ids.map(id => [id, document.getElementById(id)])); | |
| function esc(s) { return String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); } | |
| function pathFor(version, kind) { return kind === 'prompt' ? version.prompt_path : version.routing_policy_path; } | |
| function labelFor(v) { return `${v.label} (${v.slug})`; } | |
| function fillSelect(select, selectedSlug) { | |
| select.innerHTML = versions.map(v => `<option value="${esc(v.slug)}">${esc(labelFor(v))}</option>`).join(''); | |
| select.value = selectedSlug; | |
| } | |
| async function loadText(version, kind) { | |
| const path = pathFor(version, kind); | |
| if (!cache.has(path)) { | |
| const res = await fetch(path, {cache:'no-store'}); | |
| if (!res.ok) throw new Error(`${res.status} ${res.statusText} for ${path}`); | |
| cache.set(path, await res.text()); | |
| } | |
| return cache.get(path); | |
| } | |
| function splitLines(text) { return text.replace(/\r\n/g, '\n').replace(/\r/g, '\n').split('\n'); } | |
| function lcsOps(leftLines, rightLines) { | |
| const n = leftLines.length, m = rightLines.length; | |
| const dp = Array.from({length:n+1}, () => new Uint16Array(m+1)); | |
| for (let i=n-1; i>=0; i--) { | |
| for (let j=m-1; j>=0; j--) { | |
| dp[i][j] = leftLines[i] === rightLines[j] ? dp[i+1][j+1] + 1 : Math.max(dp[i+1][j], dp[i][j+1]); | |
| } | |
| } | |
| const ops = []; | |
| let i = 0, j = 0; | |
| while (i < n && j < m) { | |
| if (leftLines[i] === rightLines[j]) { ops.push({type:'same', left:leftLines[i++], right:rightLines[j++]}); } | |
| else if (dp[i+1][j] >= dp[i][j+1]) { ops.push({type:'del', left:leftLines[i++]}); } | |
| else { ops.push({type:'add', right:rightLines[j++]}); } | |
| } | |
| while (i < n) ops.push({type:'del', left:leftLines[i++]}); | |
| while (j < m) ops.push({type:'add', right:rightLines[j++]}); | |
| return ops; | |
| } | |
| function pairOps(ops) { | |
| const rows = []; | |
| let li = 1, ri = 1; | |
| for (let idx = 0; idx < ops.length;) { | |
| if (ops[idx].type === 'same') { | |
| rows.push({type:'same', leftNo:li++, rightNo:ri++, left:ops[idx].left, right:ops[idx].right}); | |
| idx++; | |
| continue; | |
| } | |
| const dels = [], adds = []; | |
| while (idx < ops.length && ops[idx].type !== 'same') { | |
| if (ops[idx].type === 'del') dels.push(ops[idx].left); | |
| else adds.push(ops[idx].right); | |
| idx++; | |
| } | |
| const max = Math.max(dels.length, adds.length); | |
| for (let k=0; k<max; k++) { | |
| const hasLeft = k < dels.length; | |
| const hasRight = k < adds.length; | |
| rows.push({ | |
| type: hasLeft && hasRight ? 'replace' : (hasLeft ? 'del' : 'add'), | |
| leftNo: hasLeft ? li++ : '', | |
| rightNo: hasRight ? ri++ : '', | |
| left: hasLeft ? dels[k] : '', | |
| right: hasRight ? adds[k] : '', | |
| }); | |
| } | |
| } | |
| return rows; | |
| } | |
| function filteredRows(rows, contextMode) { | |
| if (contextMode === 'all') return rows; | |
| const keep = new Set(); | |
| rows.forEach((row, idx) => { if (row.type !== 'same') for (let j=Math.max(0, idx-2); j<=Math.min(rows.length-1, idx+2); j++) keep.add(j); }); | |
| const out = []; | |
| let last = -1; | |
| for (const idx of [...keep].sort((a,b) => a-b)) { | |
| if (last >= 0 && idx > last + 1) out.push({type:'gap', leftNo:'...', rightNo:'...', left:'...', right:'...'}); | |
| out.push(rows[idx]); | |
| last = idx; | |
| } | |
| return out; | |
| } | |
| function makeSideBySide(rows, leftLabel, rightLabel, contextMode) { | |
| const shown = filteredRows(rows, contextMode); | |
| const body = shown.map(row => { | |
| if (row.type === 'gap') return `<tr class="same"><td class="num">...</td><td class="code muted">...</td><td class="num">...</td><td class="code muted">...</td></tr>`; | |
| const leftEmpty = row.leftNo === '' ? ' empty' : ''; | |
| const rightEmpty = row.rightNo === '' ? ' empty' : ''; | |
| return `<tr class="${row.type}"><td class="num">${row.leftNo}</td><td class="code left-code${leftEmpty}">${esc(row.left)}</td><td class="num">${row.rightNo}</td><td class="code right-code${rightEmpty}">${esc(row.right)}</td></tr>`; | |
| }).join(''); | |
| return `<table class="diff-table"><colgroup><col class="line-col"><col class="code-col"><col class="line-col"><col class="code-col"></colgroup><thead><tr><th class="group-head" colspan="2">${esc(leftLabel)}</th><th class="group-head" colspan="2">${esc(rightLabel)}</th></tr></thead><tbody>${body}</tbody></table>`; | |
| } | |
| function makeUnified(rows, leftLabel, rightLabel, contextMode) { | |
| const shown = filteredRows(rows, contextMode); | |
| const lines = [`--- ${leftLabel}`, `+++ ${rightLabel}`]; | |
| for (const row of shown) { | |
| if (row.type === 'gap') { lines.push('@@ ... @@'); continue; } | |
| if (row.type === 'same') lines.push(' ' + row.left); | |
| else if (row.type === 'del') lines.push('-' + row.left); | |
| else if (row.type === 'add') lines.push('+' + row.right); | |
| else { lines.push('-' + row.left); lines.push('+' + row.right); } | |
| } | |
| return lines; | |
| } | |
| function renderUnified(lines) { | |
| return lines.map(line => { | |
| let klass = 'ctx'; | |
| if (line.startsWith('+++') || line.startsWith('---') || line.startsWith('@@')) klass = 'meta'; | |
| else if (line.startsWith('+')) klass = 'add'; | |
| else if (line.startsWith('-')) klass = 'del'; | |
| return `<span class="${klass}">${esc(line)}</span>`; | |
| }).join('\n'); | |
| } | |
| async function updateDiff() { | |
| const left = bySlug[el.leftVersion.value]; | |
| const right = bySlug[el.rightVersion.value]; | |
| const kind = el.contentKind.value; | |
| const [leftText, rightText] = await Promise.all([loadText(left, kind), loadText(right, kind)]); | |
| const rows = pairOps(lcsOps(splitLines(leftText), splitLines(rightText))); | |
| const adds = rows.filter(r => r.type === 'add').length; | |
| const dels = rows.filter(r => r.type === 'del').length; | |
| const reps = rows.filter(r => r.type === 'replace').length; | |
| const same = rows.filter(r => r.type === 'same').length; | |
| const kindLabel = kind === 'prompt' ? 'full prompt' : 'routing policy'; | |
| document.getElementById('diffSummary').innerHTML = `<span class="badge">${esc(kindLabel)}</span><span class="badge">same ${same}</span><span class="badge">changed ${reps}</span><span class="badge">added ${adds}</span><span class="badge">deleted ${dels}</span>`; | |
| document.getElementById('leftRaw').href = pathFor(left, kind); | |
| document.getElementById('rightRaw').href = pathFor(right, kind); | |
| const leftLabel = `${left.slug}.${kind}.md`; | |
| const rightLabel = `${right.slug}.${kind}.md`; | |
| const unifiedLines = makeUnified(rows, leftLabel, rightLabel, el.contextMode.value); | |
| window.currentUnifiedDiff = unifiedLines.join('\n') + '\n'; | |
| document.getElementById('sideBySide').innerHTML = makeSideBySide(rows, leftLabel, rightLabel, el.contextMode.value); | |
| document.getElementById('unifiedDiff').innerHTML = renderUnified(unifiedLines); | |
| const unified = el.viewMode.value === 'unified'; | |
| document.getElementById('sideBySide').classList.toggle('hidden', unified); | |
| document.getElementById('unifiedDiff').classList.toggle('hidden', !unified); | |
| } | |
| async function updateSingle() { | |
| const version = bySlug[el.singleVersion.value]; | |
| const kind = el.singleKind.value; | |
| const text = await loadText(version, kind); | |
| const path = pathFor(version, kind); | |
| document.getElementById('singleRaw').href = path; | |
| document.getElementById('singleMeta').innerHTML = `<b>${esc(version.label)}</b> - ${esc(kind === 'prompt' ? 'full prompt' : 'routing policy')} - <code>${esc(path)}</code>`; | |
| document.getElementById('singleText').textContent = text; | |
| } | |
| function bind() { | |
| fillSelect(el.leftVersion, versions[Math.max(0, versions.length - 2)].slug); | |
| fillSelect(el.rightVersion, versions[versions.length - 1].slug); | |
| fillSelect(el.singleVersion, versions[versions.length - 1].slug); | |
| for (const id of ['leftVersion','rightVersion','contentKind','viewMode','contextMode']) el[id].addEventListener('change', () => updateDiff().catch(showError)); | |
| for (const id of ['singleVersion','singleKind']) el[id].addEventListener('change', () => updateSingle().catch(showError)); | |
| document.getElementById('swapBtn').addEventListener('click', () => { const old = el.leftVersion.value; el.leftVersion.value = el.rightVersion.value; el.rightVersion.value = old; updateDiff().catch(showError); }); | |
| document.getElementById('copyUnified').addEventListener('click', async () => { await navigator.clipboard.writeText(window.currentUnifiedDiff || ''); }); | |
| document.getElementById('singleToLeft').addEventListener('click', () => { el.leftVersion.value = el.singleVersion.value; updateDiff().catch(showError); }); | |
| document.getElementById('singleToRight').addEventListener('click', () => { el.rightVersion.value = el.singleVersion.value; updateDiff().catch(showError); }); | |
| } | |
| function showError(error) { | |
| document.getElementById('diffSummary').textContent = `Error: ${error.message || error}`; | |
| console.error(error); | |
| } | |
| bind(); | |
| updateDiff().catch(showError); | |
| updateSingle().catch(showError); | |
| </script> | |
| </body> | |
| </html> | |