OCR-Viewer (#1)

Co-authored-by: nathan <nathan.kehler@gmail.com>
Reviewed-on: #1
This commit is contained in:
2026-05-19 22:03:28 +00:00
parent 727f4fcc57
commit 330b703539
104 changed files with 144649 additions and 2 deletions

View File

@@ -0,0 +1,409 @@
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