scratch / prompt-diffs /index.html
dutifulbob's picture
Fix report table header alignment
24e7d3f verified
Raw
History Blame Contribute Delete
22.3 kB
<!doctype html>
<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 => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[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>