P44 OCR Viewer — Pass 2 Diff View + Edit Mode Reference Pane

Scope: p44-ocr-viewer.js, p44-ocr-viewer.css, p44-ocr-viewer.html. Do not touch PDF rendering, zoom, Pass 1 flag logic, or the corrections storage model.

BACKGROUND — how corrections are stored

Each page's corrections live in state.corrections[pageNum] with this shape:
javascript

{
original: "raw OCR text as loaded",
pass1:    "volunteer-corrected text",
pass2:    "senior editor text"
}

The diff always compares original against pass1. Never use state.currentPage inside diff functions — always pass pageNum explicitly to avoid page-bleed bugs.

PART 1 — HTML (p44-ocr-viewer.html)

Inside #text-panel-body, replace:
html

<div id="pass2-revert-bar" style="display:none;"></div>
<div id="pass2-change-summary" style="display:none;"></div>
<div id="text-output" style="display:none;"></div>
<div id="editor-wrap" style="display:none;">
  <div id="line-numbers"></div>
  <textarea id="text-editor" aria-label="OCR text editor"></textarea>
</div>

With:
html

<div id="pass2-revert-bar" style="display:none;"></div>
<div id="diff-toolbar" style="display:none;"></div>
<div id="text-output" style="display:none;"></div>
<div id="editor-wrap" style="display:none;">
  <div id="diff-reference" style="display:none;"></div>
  <div id="editor-bottom">
    <div id="line-numbers"></div>
    <textarea id="text-editor" aria-label="OCR text editor"></textarea>
  </div>
</div>

In the right panel header, add a diff toggle button immediately before #btn-edit:
html

<button id="btn-diff" class="tb-btn" title="Toggle diff view — shows Pass 1 changes highlighted">⇄ Diff</button>

Remove <div id="pass2-change-summary" style="display:none;"></div> entirely — it is replaced by the new diff system.

PART 2 — CSS (p44-ocr-viewer.css)

Add at the end of the file:
css

/* ── Diff toolbar ─────────────────────────────────────────────── */
#diff-toolbar {
display: none;
align-items: center;
gap: 10px;
padding: 5px 16px;
background: var(--surface);
border-bottom: 1px solid var(--border);
font-size: 10px;
color: var(--text-dim);
flex-shrink: 0;
}
#diff-toolbar .diff-legend {
display: flex;
align-items: center;
gap: 10px;
flex: 1;
}
#diff-toolbar .diff-count {
font-size: 10px;
color: var(--gold-dim);
letter-spacing: 0.04em;
}
.diff-swatch {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 10px;
color: var(--text-dim);
}
.diff-swatch-del {
width: 24px; height: 10px;
background: rgba(208,80,80,0.22);
border-radius: 2px;
}
.diff-swatch-ins {
width: 24px; height: 10px;
background: rgba(92,184,92,0.18);
border-radius: 2px;
}

/* ── Diff inline highlights ───────────────────────────────────── */
.diff-del {
background: rgba(208,80,80,0.22);
color: #e07070;
border-radius: 2px;
padding: 0 2px;
text-decoration: line-through;
font-size: 11px;
}
.diff-ins {
background: rgba(92,184,92,0.18);
color: #7ed87e;
border-radius: 2px;
padding: 0 2px;
}

/* ── Editor wrap — vertical split ─────────────────────────────── */
#editor-wrap {
display: none;
flex-direction: column;
flex: 1;
min-height: 0;
overflow: hidden;
border-top: 1px solid var(--gold-dim);
}

/* ── Diff reference pane (top half of editor split) ───────────── */
#diff-reference {
display: none;
flex: 1;
min-height: 0;
overflow-y: auto;
border-bottom: 1px solid var(--border);
padding: 12px 20px;
font-size: 12.5px;
line-height: 1.75;
color: var(--text);
background: var(--surface);
white-space: pre-wrap;
font-family: 'Georgia', 'Times New Roman', serif;
}
#diff-reference-label {
font-size: 9px;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--gold-dim);
margin-bottom: 8px;
padding-bottom: 5px;
border-bottom: 1px solid var(--border);
}

/* ── Editor bottom half ───────────────────────────────────────── */
#editor-bottom {
display: flex;
flex: 1;
min-height: 0;
overflow: hidden;
}

Also update the existing #editor-wrap rule — remove flex-direction and border-top from it since those are now set above. The existing rule in the file should become:
css

#editor-wrap {
flex: 1;
min-height: 0;
overflow: hidden;
}

The new rule added above replaces it fully — delete the old one to avoid conflict.

PART 3 — JS (p44-ocr-viewer.js)

3a — New state flag

Inside the state object, add:
javascript

diffMode: false,

3b — DOM refs

Add after the existing editor DOM refs:
javascript

const btnDiff       = document.getElementById('btn-diff');
const diffToolbar   = document.getElementById('diff-toolbar');
const diffReference = document.getElementById('diff-reference');
const editorBottom  = document.getElementById('editor-bottom');

3c — Word-level diff engine

Add this new function. Place it directly above buildChangeSummary():
javascript

function buildInlineDiff(originalText, pass1Text) {
const orig  = (originalText || '').split(/(\s+)/);
const edit  = (pass1Text   || '').split(/(\s+)/);
const result = [];

let i = 0, j = 0;
while (i < orig.length || j < edit.length) {
if (i >= orig.length) {
result.push({ type: 'ins', text: edit[j++] });
} else if (j >= edit.length) {
result.push({ type: 'del', text: orig[i++] });
} else if (orig[i] === edit[j]) {
result.push({ type: 'same', text: orig[i] });
i++; j++;
} else {
result.push({ type: 'del', text: orig[i++] });
result.push({ type: 'ins', text: edit[j++] });
}
}
return result;
}

3d — Diff renderer

Add this new function directly below buildInlineDiff():
javascript

function renderInlineDiff(pageNum) {
const corr         = state.corrections && state.corrections[pageNum];
const originalText = (corr && corr.original) || state.ocrPages[pageNum] || '';
const pass1Text    = corr && corr.pass1;

if (!pass1Text) return null;

const tokens   = buildInlineDiff(originalText, pass1Text);
const delCount = tokens.filter(t => t.type === 'del' && t.text.trim()).length;
const insCount = tokens.filter(t => t.type === 'ins' && t.text.trim()).length;
const total    = Math.max(delCount, insCount);

const html = tokens.map(t => {
if (t.type === 'same') return escH(t.text);
if (t.type === 'del')  return '<span class="diff-del">'  + escH(t.text) + '</span>';
if (t.type === 'ins')  return '<span class="diff-ins">'  + escH(t.text) + '</span>';
return '';
}).join('');

return { html, total };
}

3e — Diff toolbar renderer

Add this new function:
javascript

function showDiffToolbar(pageNum) {
const result = renderInlineDiff(pageNum);
if (!result) {
diffToolbar.style.display = 'none';
return;
}
diffToolbar.innerHTML =
'<div class="diff-legend">' +
'<span class="diff-swatch"><span class="diff-swatch-del"></span> removed</span>' +
'<span class="diff-swatch"><span class="diff-swatch-ins"></span> added</span>' +
'</div>' +
'<span class="diff-count">' + result.total + ' change' + (result.total === 1 ? '' : 's') + ' from Pass 1</span>';
diffToolbar.style.display = 'flex';
}

3f — Apply diff to display panel

Add this new function:
javascript

function applyDiffToDisplay(pageNum) {
if (!state.diffMode) return;
const result = renderInlineDiff(pageNum);
if (!result) return;
textOutput.innerHTML =
'<pre style="white-space:pre-wrap;font-family:inherit;font-size:12.5px;' +
'line-height:1.75;color:var(--text);padding:16px 20px;">' +
result.html + '</pre>';
}

3g — Apply diff to reference pane (edit mode)

Add this new function:
javascript

function applyDiffToReference(pageNum) {
const result = renderInlineDiff(pageNum);
if (!result || !state.diffMode) {
diffReference.style.display = 'none';
return;
}
diffReference.innerHTML =
'<div id="diff-reference-label">Pass 1 corrections — reference</div>' +
'<pre style="white-space:pre-wrap;font-family:inherit;font-size:12.5px;' +
'line-height:1.75;color:var(--text);margin:0;">' +
result.html + '</pre>';
diffReference.style.display = 'block';
}

3h — Toggle diff mode

Add this new function:
javascript

function toggleDiff(pageNum) {
const corr    = state.corrections && state.corrections[pageNum];
const hasPass1 = corr && corr.pass1;

if (!hasPass1) {
setStatus('No Pass 1 corrections on this page — nothing to diff.');
return;
}

state.diffMode = !state.diffMode;
btnDiff.classList.toggle('active', state.diffMode);

if (state.editMode) {
applyDiffToReference(pageNum);
} else {
if (state.diffMode) {
applyDiffToDisplay(pageNum);
showDiffToolbar(pageNum);
} else {
showOCRPage(pageNum);
}
}
}

Add the event listener immediately after the function:
javascript

btnDiff.addEventListener('click', () => toggleDiff(state.currentPage));

3i — Update showOCRPage()

At the very end of showOCRPage(), after updateCorrectionIndicator(pageNum) and before the Pass 2 auto-edit block, add:
javascript

// Show diff toolbar if page has Pass 1 corrections
const corrForPage = state.corrections && state.corrections[pageNum];
const hasPass1    = corrForPage && corrForPage.pass1;
if (hasPass1) {
showDiffToolbar(pageNum);
} else {
diffToolbar.style.display = 'none';
state.diffMode = false;
btnDiff.classList.remove('active');
}

// Auto-enable diff in Pass 2 if Pass 1 corrections exist
if (currentRole === 'pass2' && hasPass1 && !state.editMode) {
state.diffMode = true;
btnDiff.classList.add('active');
applyDiffToDisplay(pageNum);
}

Replace the existing Pass 2 auto-edit block at the bottom of showOCRPage():
javascript

if (currentRole === 'pass2') {
if (!state.editMode) enterEditMode();
updatePass2NavButtons();
}

With:
javascript

if (currentRole === 'pass2') {
updatePass2NavButtons();
}

Note: Pass 2 no longer auto-enters edit mode on page load. The editor opens edit mode manually via the Edit button. This is intentional — the senior editor reviews the diff first, then decides to edit.

3j — Update enterEditMode()

At the end of enterEditMode(), after textEditor.focus(), add:
javascript

applyDiffToReference(pageNum);

3k — Update exitEditMode()

At the start of exitEditMode(), before any existing logic, add:
javascript

diffReference.style.display = 'none';

After the existing line textOutput.style.display = 'block';, add:
javascript

if (state.diffMode) applyDiffToDisplay(pageNum);

3l — Update goToPage()

At the start of goToPage(), before if (state.editMode) exitEditMode();, add:
javascript

state.diffMode = false;
btnDiff.classList.remove('active');
diffToolbar.style.display = 'none';
diffReference.style.display = 'none';

This ensures diff state is always reset on navigation — prevents any possibility of page-bleed.

VERIFICATION CHECKLIST:

    Pass 2 lands on a page with Pass 1 corrections: diff toolbar appears automatically, ⇄ Diff button is active, inline highlights visible
    Pass 2 lands on a page with no Pass 1 corrections: diff toolbar hidden, ⇄ Diff button inactive, clicking it shows status message
    Clicking ⇄ Diff toggles highlights on/off without changing page or losing position
    Clicking Edit while diff is on: reference pane appears above textarea showing highlighted diff, editor opens below
    Clicking Edit while diff is off: reference pane hidden, editor fills full height
    Toggling diff while in edit mode: reference pane appears/disappears above the open textarea
    Navigating to any new page: diff mode resets to off, no highlights carry over from previous page
    Pass 1 role: ⇄ Diff button present but only activates on pages with Pass 1 corrections
