Files
AI-Prototype/prompts/ocr-viewer-prompt.md
2026-05-13 10:03:38 -04:00

29 KiB
Raw Blame History

P44 OCR Viewer — Rebuild Prompts

Reference: ocr-viewer-audit.md — read this before starting any step. Each step must be confirmed working before the next step starts. Three files: p44-ocr-viewer.html / p44-ocr-viewer.css / p44-ocr-viewer.js


Before you start

Read ocr-viewer-audit.md in full. It contains the complete audit of the previous codebase, the root causes of every bug we hit, and the rebuild specification. Every architectural decision in these prompts comes from that document.

Key rules that must never be violated in this rebuild:

  • #text-output is display only — never set contentEditable on it
  • #text-editor is the only edit surface — always a <textarea>, always plain text
  • No markdown parser — OCR output is plain text, display with <pre>
  • No confidence scoring — removed by design
  • Each step confirmed working before the next begins

Step 1 — HTML/CSS shell

No JavaScript. Layout and visual design only.

Create three empty files: p44-ocr-viewer.html, p44-ocr-viewer.css, p44-ocr-viewer.js

Build the complete HTML structure and CSS. Nothing should be functional.

HTML structure required:

#app (grid: toolbar / pass2-banner / panels / footer)

  #toolbar
    h1 "⚜ P44 OCR VIEWER"
    #btn-load-ch button "▶ Calgary Highlanders — Sep 44"
    PDF override: label.file-btn > input#input-pdf[type=file accept=.pdf]
    OCR override: label.file-btn > input#input-text[type=file accept=.txt,.md]
    JSON override: label.file-btn > input#input-json[type=file accept=.json]
    #btn-example "Load sample"
    #btn-sync-scroll "Sync Scroll" (class active by default)
    role switcher: label "ROLE" + select#role-select
      option value="pass1" "Pass 1 — Volunteer"
      option value="pass2" "Pass 2 — Senior Editor"
    #btn-clear-all "⚠ Clear all data" (red-tinted border and text)

  #pass2-banner (display:none by default)
    span#pass2-banner-text
    button#btn-pass2-reset "↺ Reset Pass 2"

  #panels (two-column flex)

    Left panel div.panel
      div.panel-header
        span "Source Document — Page Scan"
        span#zoom-label "100%"
        button#btn-zoom-reset "1:1"
        div.page-nav
          button#btn-prev "◀" disabled
          input#page-input type=number value=1 min=1
          button#btn-next "▶" disabled
          span#page-label "/ —"
      div.panel-body#image-panel-body
        div#image-placeholder
          div#img-spinner style=display:none
          div#img-ph-text "War diary scan loads automatically."
        div#pdf-container style=display:none
          canvas#pdf-canvas
          div#overlay

    Right panel div.panel
      div.panel-header
        span "OCR Text Output"
        span#ocr-sync-label
        div#correction-dot style=display:none title="This page has been edited"
        button#btn-flag-verified class="tb-btn flag-btn" "✓ Verified"
        button#btn-flag-review   class="tb-btn flag-btn" "⚑ Review"
        button#btn-flag-error    class="tb-btn flag-btn" "✗ Error"
        button#btn-flag-clear    class="tb-btn flag-btn" "Clear"
        button#btn-edit          class="tb-btn"          "Edit"
      div.panel-body#text-panel-body
        div#text-placeholder
          div#txt-spinner style=display:none
          div#txt-ph-text "OCR transcript loads automatically."
        div#text-output style=display:none
        textarea#text-editor style=display:none

  #footer
    span#status-msg "Ready."
    span#flags-summary
    span#kbd-hint "← → navigate   v/r/e flag   esc close"

CSS variables — use exactly these:

:root {
  --bg:        #1a1a2e;
  --surface:   #16213e;
  --surface2:  #0f3460;
  --gold:      #c8a84b;
  --gold-dim:  #8a6f2e;
  --text:      #ddd4bb;
  --text-dim:  #7a7260;
  --text-muted:#4a4538;
  --border:    #2e2e4e;
  --green:     #4caf70;
  --green-bg:  #1a3326;
  --amber:     #e8a838;
  --amber-bg:  #2e2010;
  --red:       #d05050;
  --red-bg:    #2e1a1a;
  --toolbar-h: 52px;
  --footer-h:  34px;
}

CSS layout rules:

  • #app: CSS grid grid-template-rows: var(--toolbar-h) auto 1fr var(--footer-h), height: 100vh, overflow: hidden
  • #panels: flex row, each .panel takes width: 50%, display: flex, flex-direction: column, overflow: hidden
  • .panel-body: flex: 1, overflow-y: auto, overflow-x: hidden
  • #pass2-banner: display: none, slim flex row, background: #1a1200, border-bottom: 1px solid var(--gold-dim), padding: 6px 14px, font-size: 11px, color: var(--amber)
  • #text-editor: display: none, width: 100%, height: 100%, resize: none, font-family: Georgia serif, font-size: 12.5px, line-height: 1.75, background: var(--surface2), color: var(--text), border: none, border-top: 1px solid var(--gold-dim), padding: 16px 20px, box-sizing: border-box, outline: none
  • #text-editor:focus: border-top-color: var(--gold)
  • .tb-btn: padding: 3px 9px, background: transparent, border: 1px solid var(--gold-dim), border-radius: 3px, color: var(--text-dim), font-family: Georgia serif, font-size: 10px, cursor: pointer, letter-spacing: 0.04em
  • .tb-btn:hover: border-color: var(--gold), color: var(--gold)
  • .tb-btn.active: border-color: var(--gold), color: var(--gold)
  • .file-btn: same as .tb-btn but as a <label> with hidden <input type="file"> inside
  • #btn-clear-all: border-color: #d05050, color: #d05050
  • #btn-clear-all:hover: border-color: #e07070, color: #e07070
  • .flag-btn.active: border-color: var(--gold), color: var(--gold)
  • #correction-dot: width: 8px, height: 8px, border-radius: 50%, background: var(--gold), display: none, flex-shrink: 0
  • .panel-header: display: flex, align-items: center, gap: 6px, padding: 0 10px, height: 38px, background: var(--surface), border-bottom: 1px solid var(--border), flex-shrink: 0, font-size: 9px, letter-spacing: 0.1em, color: var(--text-dim), text-transform: uppercase
  • #toolbar: display: flex, align-items: center, gap: 8px, padding: 0 12px, background: var(--surface), border-bottom: 2px solid var(--gold-dim)
  • #toolbar h1: font-size: 13px, font-weight: normal, letter-spacing: 0.1em, color: var(--gold), white-space: nowrap
  • .divider: width: 1px, height: 26px, background: var(--border), flex-shrink: 0
  • #footer: display: flex, align-items: center, justify-content: space-between, padding: 0 14px, background: var(--surface), border-top: 1px solid var(--border), font-size: 10px, color: var(--text-dim)
  • #pdf-container: position: relative, display: inline-block
  • #overlay: position: absolute, top: 0, left: 0, pointer-events: none
  • .word-box: position: absolute, cursor: crosshair, border: 1px solid transparent, border-radius: 2px
  • .word-box:hover, .word-box.highlight: background: rgba(200,168,75,0.18), border-color: var(--gold-dim)
  • .panel-placeholder: display: flex, flex-direction: column, align-items: center, justify-content: center, height: 100%, color: var(--text-dim), font-size: 12px, text-align: center, gap: 10px
  • .spinner: width: 24px, height: 24px, border: 2px solid var(--border), border-top-color: var(--gold), border-radius: 50%, animation: spin 0.8s linear infinite
  • @keyframes spin: to { transform: rotate(360deg) }
  • #page-input: width: 48px, text-align: center, background: var(--surface2), border: 1px solid var(--border), color: var(--text), font-family: Georgia serif, font-size: 12px, padding: 2px 4px, border-radius: 3px
  • select#role-select: same styling as .tb-btn but as a select element, background: var(--surface2)
  • #ocr-sync-label: font-size: 9px, color: var(--gold-dim), letter-spacing: 0.06em

Do not write any JavaScript yet.

Confirm when complete by listing every id and every CSS class defined.


Step 2 — PDF loading and page navigation

Add to p44-ocr-viewer.js only. Do not change HTML or CSS.

Add the following in this order:

  1. PDF.js worker setup:
if (typeof pdfjsLib !== 'undefined') {
  pdfjsLib.GlobalWorkerOptions.workerSrc =
    'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
}
  1. Data paths:
const REAL_PDF_PATH  = './Inputs/Calgary-Highlanders_War-Diary_Sep44.pdf';
const REAL_TEXT_PATH = './Inputs/ocr-output/Calgary-Highlanders_War-Diary_Sep44_olmocr.md';
  1. State object — only these properties to start:
const state = {
  mode:        'none',  // 'pdf' | 'image' | 'none'
  pdfDoc:      null,
  currentPage: 1,
  totalPages:  0,
  zoomScale:   1.0,     // Ctrl+scroll zoom multiplier (0.54.0)
  wordData:    [],
  rendering:   false,
  lastRenderTime: 0,
  ocrPages:    {},      // { [pageNum]: string } — populated in Step 3
  flags:       {},      // populated in Step 5
  corrections: {},      // populated in Step 6
  editMode:    false,   // populated in Step 6
};
  1. All DOM refs (getElementById for every id in the HTML)

  2. escH(s) utility function

  3. setStatus(msg, warn) utility function

  4. showImgSpinner(on), showTxtSpinner(on), showImgPlaceholder(html), showTxtPlaceholder(html) utility functions

  5. loadPDFBuffer(arrayBuffer) — loads PDF into PDF.js, sets state.pdfDoc, state.totalPages, state.mode = 'pdf', calls renderCurrentPage()

  6. renderCurrentPage() — fits page to panel width, applies state.zoomScale, uses DPR for HiDPI, renders to #pdf-canvas, calls buildPDFWordOverlay(). Wrap showOCRPage() call in try/catch (showOCRPage is added in Step 3). Include _pendingPage queue and debounce guard.

  7. buildPDFWordOverlay(page, vp, dpr) — builds word boxes from state.wordData if loaded, otherwise leaves overlay blank

  8. makeWordBox(id, x, y, w, h, title) — creates positioned div with hover highlight

  9. goToPage(n) — validates range, guards against re-render of same page, calls renderCurrentPage(). No edit mode save yet (added in Step 6).

  10. _pendingPage = null module variable

  11. updateZoomLabel() — updates #zoom-label text

  12. Event listeners:

    • #btn-prev click → goToPage(state.currentPage - 1)
    • #btn-next click → goToPage(state.currentPage + 1)
    • #page-input change → goToPage(parseInt(value))
    • #btn-zoom-reset click → reset state.zoomScale to 1.0, re-render
    • window resize → re-render if PDF loaded and not rendering
    • #input-pdf change → read file, call loadPDFBuffer()
    • #input-json change → read file, parse JSON, set state.wordData, re-render
  13. Ctrl+scroll zoom on #image-panel-body:

    • MUST check e.ctrlKey — only zoom when Ctrl is held
    • Only call e.preventDefault() when e.ctrlKey is true
    • Without Ctrl: let the event pass through normally for scrolling
    • With Ctrl: adjust state.zoomScale by ±0.1, clamp to 0.54.0, re-render
    • After re-render, scroll to keep cursor anchor point in view
  14. DOMContentLoaded auto-load:

window.addEventListener('DOMContentLoaded', () => {
  if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
    setTimeout(loadCalgaryHighlanders, 200);
  }
});
  1. loadCalgaryHighlanders() — fetches PDF only for now (OCR text fetch added in Step 3). Uses Promise.allSettled.

Test: PDF loads, pages navigate with buttons and keyboard input, zoom works with Ctrl+scroll, normal scroll works without Ctrl.


Step 3 — OCR text loading and plain text display

Add to p44-ocr-viewer.js only. Do not change HTML or CSS.

  1. parseOCRByPage(rawText):

    • Splits on ## Page N headings
    • Stores content between headings in state.ocrPages[N]
    • Does NOT include the ## Page N heading line in the content
    • buf = [] not buf = [line]
  2. loadOCRText(rawText):

    • Calls parseOCRByPage(rawText)
    • Does NOT call showOCRPage() — only calls it if state.pdfDoc is already loaded
    • This prevents double-render on startup
  3. showOCRPage(pageNum):

    • Clears textOutput.innerHTML = ''
    • Sets textOutput.dataset.page = pageNum
    • Hides placeholder, shows textOutput
    • If no OCR pages loaded: shows "OCR text not loaded yet" message
    • If no content for this page: shows "No OCR text for page N" message
    • Gets display text: corrections[pageNum]?.pass1 || state.ocrPages[pageNum] (corrections object added in Step 6, guard with optional chaining for now)
    • Renders as: <pre style="white-space:pre-wrap;font-family:inherit;font-size:12.5px;line-height:1.75;padding:16px 20px;">{escH(displayText)}</pre>
    • Scrolls text panel to top
    • Updates #ocr-sync-label text
  4. Update loadCalgaryHighlanders() to also fetch REAL_TEXT_PATH and call loadOCRText()

  5. Add #input-text change event listener → loadOCRText()

  6. Add showOCRPage(state.currentPage) call inside renderCurrentPage() after PDF page renders

Test: OCR text loads alongside PDF, correct page shown for each page number, no ## Page N heading visible in text panel, plain text renders correctly.


Step 4 — Synchronized scrolling

Add to p44-ocr-viewer.js only. Do not change HTML or CSS.

  1. Module variables:
let _syncScrolling = false;
let _syncActive    = true;
  1. onImgScroll() — when left panel scrolls, scroll right panel to same proportional position. Use requestAnimationFrame to reset _syncScrolling flag.

  2. onTxtScroll() — when right panel scrolls, scroll left panel to same proportional position.

  3. Attach scroll listeners to #image-panel-body and #text-panel-body

  4. #btn-sync-scroll click → toggle _syncActive, toggle .active class on button

Test: Scrolling either panel scrolls the other proportionally. Toggle button disables sync. Normal mouse wheel scroll works on left panel (no zoom without Ctrl).


Step 5 — Flag system

Add to p44-ocr-viewer.js only. Do not change HTML or CSS.

  1. updateFlagButtons(pageNum) — toggles .active class on flag buttons based on state.flags[pageNum]

  2. updateFlagsSummary() — counts verified/review/error flags, updates #flags-summary text

  3. setFlag(type) — sets or clears state.flags[state.currentPage], calls updateFlagButtons(), updateFlagsSummary(), saveFlags()

  4. saveFlags():

    • POST to /save-flags with state.flags as JSON body
    • On failure: fallback to localStorage.setItem('p44-flags', ...)
  5. loadFlagsOnStartup():

    • Fetch /flags.json
    • On failure: fallback to localStorage.getItem('p44-flags')
    • Call updateFlagsSummary() after loading
  6. clearPass1Queue() — removes all verified flags, keeps review/error, confirm dialog required

  7. Event listeners:

    • #btn-flag-verifiedsetFlag('verified')
    • #btn-flag-reviewsetFlag('review')
    • #btn-flag-errorsetFlag('error')
    • #btn-flag-clearsetFlag(null)
    • #btn-clear-p1-queueclearPass1Queue()
  8. Call loadFlagsOnStartup() on DOMContentLoaded

  9. Call updateFlagButtons(pageNum) at the end of showOCRPage()

Test: Flags save and persist across refresh. Footer summary updates. Clear queue removes only verified flags.


Step 6 — Edit mode (Pass 1 only)

This is the most critical step. Read the architecture rules before starting.

Architecture rules — must not be violated:

  • #text-output is NEVER set to contentEditable. It is display only.
  • #text-editor (<textarea>) is the ONLY edit surface.
  • When edit mode is active: hide #text-output, show #text-editor
  • When edit mode is inactive: hide #text-editor, show #text-output
  • #text-editor always contains plain text — never HTML

Correction data model:

state.corrections[pageNum] = {
  original: string,  // raw OCR snapshot, taken ONCE on first edit-open, never overwritten
  pass1:    string,  // Pass 1 saved text (absent if unchanged from original)
  pass2:    string,  // Pass 2 saved text (added in Step 8, absent for now)
}

Add to p44-ocr-viewer.js:

  1. updateCorrectionIndicator(pageNum) — shows/hides #correction-dot based on whether corrections[pageNum]?.pass1 exists

  2. postCorrection(pageNum, corrObj):

    • POST to /save-corrections with { page: pageNum, correction: corrObj }
    • On failure: fallback to localStorage p44-corrections
  3. loadCorrectionsOnStartup():

    • Fetch /corrections.json
    • On failure: fallback to localStorage
    • Remove any entries where both pass1 and pass2 are absent
    • Set state.corrections
  4. enterEditMode():

    - Get pageNum = state.currentPage
    - If corrections[pageNum] does not exist: create { original: state.ocrPages[pageNum].trim() }
    - If corrections[pageNum].original does not exist: set it to state.ocrPages[pageNum].trim()
    - Set textEditor.value = corrections[pageNum].pass1 || corrections[pageNum].original
    - Set textEditor.dataset.page = pageNum
    - Hide textOutput (display:none)
    - Show textEditor (display:block)
    - Focus textEditor
    - Set state.editMode = true
    - Update btnEdit text to "Editing", add .active class
    
  5. exitEditMode():

    - If !state.editMode: return
    - Get pageNum = parseInt(textEditor.dataset.page)
    - Get editedText = textEditor.value.trim()
    - Get originalText = corrections[pageNum]?.original || state.ocrPages[pageNum]?.trim() || ''
    - If editedText !== originalText AND editedText is not empty:
        corrections[pageNum].pass1 = editedText
      Else:
        delete corrections[pageNum].pass1
    - If neither pass1 nor pass2 exist: delete corrections[pageNum] entirely
    - Call postCorrection(pageNum, corrections[pageNum] || null)
    - Call updateCorrectionIndicator(pageNum)
    - Hide textEditor (display:none), clear textEditor.value
    - Show textOutput (display:block)
    - Set state.editMode = false
    - Update btnEdit text to "Edit", remove .active class
    - Call showOCRPage(pageNum) to re-render display with saved correction
    
  6. Update goToPage() — call exitEditMode() at the very top before any other logic

  7. #btn-edit click → toggle edit mode

  8. Update showOCRPage() display text logic:

    const corr = state.corrections[pageNum];
    const displayText = (corr?.pass1) || state.ocrPages[pageNum] || '';
    
  9. Call loadCorrectionsOnStartup() on DOMContentLoaded

  10. Call updateCorrectionIndicator(pageNum) at end of showOCRPage()

Test thoroughly before proceeding:

  • Enter edit mode: textarea appears, display hidden
  • Make a change and exit: display re-renders with correction, gold dot appears
  • Navigate away and back: correction persists, dot still shows
  • Enter and exit without changing: no correction saved, no dot
  • Refresh page: correction loads from localStorage/server, dot shows correctly
  • Confirm ## Page N never appears in the textarea

Step 7 — Corrections persistence in launch_viewer.py

Add to launch_viewer.py only. Do not change any viewer files.

Add a do_POST method to QuietHandler that handles these endpoints:

  1. POST /save-flags — read JSON body, write to flags.json in ROOT

  2. POST /save-corrections — read JSON body ({ page, correction }), load existing corrections.json, update the entry for page, write back

  3. POST /save-pass2-flags — read JSON body, write to pass2_flags.json in ROOT

  4. POST /clear-all-data — delete flags.json, corrections.json, pass2_flags.json if they exist, respond 200

All endpoints:

  • Read body with self.rfile.read(int(self.headers['Content-Length']))
  • Parse with json.loads()
  • Respond 200 OK on success
  • Respond 400 on parse error
  • Never crash the server on bad input — wrap in try/except

Test: Save a flag, restart the server, reload viewer — flag persists from file not just localStorage.


Step 8 — Role switcher and Pass 2 mode

Add to p44-ocr-viewer.js only. Do not change HTML or CSS.

  1. Module variables:
let currentRole = 'pass1';
let pass2Flags  = {};
const PASS1_LABELS = { verified: '✓ Verified',       review: '⚑ Review',    error: '✗ Error'   };
const PASS2_LABELS = { verified: '✓ Pass 2 approved', review: '⚑ Needs work', error: '✕ Rejected' };
  1. getVerifiedPages() — returns sorted array of page numbers where state.flags[n] === 'verified'

  2. updatePass2NavButtons() — in Pass 2 mode, disables prev/next if no verified pages before/after current

  3. updatePass2Banner() — updates banner text with count of verified pages ready for review

  4. applyRole(role):

    • Sets currentRole
    • Saves to localStorage('p44-role')
    • Shows/hides #pass2-banner
    • Updates flag button labels to PASS1 or PASS2 labels
    • Calls updatePass2Banner() if pass2
    • Re-renders current page via showOCRPage(state.currentPage)
  5. Update #btn-prev / #btn-next click handlers — in Pass 2 mode, jump between verified pages only

  6. Update showOCRPage() — at the end, if currentRole === 'pass2', call enterEditMode() if not already in edit mode. Also show pass2-specific display text: corr?.pass2 || corr?.pass1 || state.ocrPages[pageNum]

  7. savePass2Flags() — POST to /save-pass2-flags, fallback to localStorage p44-pass2-flags

  8. loadPass2FlagsOnStartup() — fetch /pass2_flags.json, fallback to localStorage

  9. Update setFlag() — in Pass 2 mode, write to pass2Flags and call savePass2Flags() instead of state.flags

  10. Update updateFlagButtons() — read from pass2Flags in Pass 2 mode

  11. resetPass2() — clears pass2Flags and all corrections, confirm dialog, cannot be undone

  12. #btn-pass2-reset click → resetPass2()

  13. #role-select change → applyRole(value)

  14. On DOMContentLoaded, restore role: applyRole(localStorage.getItem('p44-role') || 'pass1')

  15. Call loadPass2FlagsOnStartup() on DOMContentLoaded

Test: Switch roles, Pass 2 nav jumps between verified pages, Pass 2 flags stored separately, role persists on refresh.


Step 9 — Pass 2 change summary panel

Add to HTML, CSS, and JS.

HTML — add inside #text-panel-body after #text-placeholder, before #text-output:

<div id="pass2-change-summary" style="display:none;"></div>

CSS — add:

#pass2-change-summary {
  display: none;
  padding: 10px 20px;
  background: var(--surface2);
  border-bottom: 1px solid var(--gold-dim);
  flex-shrink: 0;
  font-size: 11px;
}
#pass2-change-summary .cs-title {
  font-size: 10px;
  color: var(--gold-dim);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  margin-bottom: 6px;
}
#pass2-change-summary .cs-list {
  color: var(--text-dim);
  line-height: 1.8;
  max-height: 100px;
  overflow-y: auto;
  margin-bottom: 8px;
  font-family: Georgia, serif;
}
#pass2-change-summary .cs-changed { color: var(--amber); }
#pass2-change-summary .cs-added   { color: var(--green); }
#pass2-change-summary .cs-deleted { color: var(--red);   }
#pass2-change-summary .cs-actions { display: flex; gap: 8px; }
.cs-revert-btn {
  font-size: 10px;
  font-family: Georgia, serif;
  padding: 3px 10px;
  border-radius: 3px;
  border: 1px solid var(--border);
  background: transparent;
  color: var(--text-dim);
  cursor: pointer;
  letter-spacing: 0.04em;
}
.cs-revert-btn:hover { border-color: var(--gold-dim); color: var(--gold); }
.cs-revert-btn.danger { border-color: var(--red); color: var(--red); }

JS — add:

  1. buildChangeSummary(originalText, pass1Text):

    • Split both strings on whitespace: str.trim().split(/\s+/)
    • Walk both arrays simultaneously
    • Produce array of { type: 'changed'|'added'|'deleted', from?, to?, word? }
    • Cap at 50 changes, add { type: 'truncated' } if over
    • Return empty array if strings are identical
  2. showPass2ChangeSummary(pageNum):

    • Get corr = state.corrections[pageNum]
    • If no corr?.pass1 or currentRole !== 'pass2': hide panel, return
    • Build changes from corr.original vs corr.pass1
    • If no changes: hide panel, return
    • Render cs-title with change count
    • Render cs-list with each change coloured by type
    • Render two action buttons: "↺ Revert to Pass 1" and "⚠ Revert to original OCR"
    • Show panel
    • Wire revert buttons:
      • Revert to Pass 1: confirm() dialog, set textEditor.value = corr.pass1, status message
      • Revert to original: prompt('Type REVERT to confirm'), if confirmed delete corrections[pageNum], call postCorrection(pageNum, null), update indicator, hide summary, status message
  3. Call showPass2ChangeSummary(state.currentPage) at end of enterEditMode()

  4. Hide #pass2-change-summary at start of exitEditMode()

Test: Switch to Pass 2, navigate to a page with Pass 1 corrections — change summary appears above textarea showing exactly what was changed. Revert to Pass 1 restores Pass 1 text. Revert to original requires typing REVERT and clears all corrections.


Step 10 — Keyboard navigation

Add to p44-ocr-viewer.js only.

  1. document.addEventListener('keydown', e => {...})

  2. Guard: if state.editMode is true, return immediately (let all keys go to textarea)

  3. Guard: if focus is on any INPUT, TEXTAREA, or SELECT, return

  4. Key mappings:

    • ArrowRight or lgoToPage(state.currentPage + 1)
    • ArrowLeft or jgoToPage(state.currentPage - 1)
    • vsetFlag('verified')
    • rsetFlag('review')
    • esetFlag('error')
    • Escape → if edit mode active: exitEditMode(). Otherwise: nothing. Do NOT reference zoomLens — that element does not exist.

Test: Arrow keys navigate pages, v/r/e set flags, Escape exits edit mode, no errors thrown.


Step 11 — Clear all data

Add to p44-ocr-viewer.js and launch_viewer.py.

JS — add clearAllLocalData():

function clearAllLocalData() {
  if (!confirm(
    'Clear ALL saved data?\n\n' +
    'This will wipe:\n' +
    '  • All Pass 1 and Pass 2 flags\n' +
    '  • All text corrections\n' +
    '  • Role setting\n\n' +
    'This cannot be undone.'
  )) return;

  localStorage.removeItem('p44-corrections');
  localStorage.removeItem('p44-flags');
  localStorage.removeItem('p44-pass2-flags');
  localStorage.removeItem('p44-role');

  state.corrections = {};
  state.flags       = {};
  pass2Flags        = {};

  applyRole('pass1');
  updateFlagButtons(state.currentPage);
  updateFlagsSummary();
  updateCorrectionIndicator(state.currentPage);
  if (state.pdfDoc) showOCRPage(state.currentPage);

  fetch('/clear-all-data', { method: 'POST' }).catch(() => {});
  setStatus('All saved data cleared — fresh start.');
}

Wire #btn-clear-all click → clearAllLocalData()

launch_viewer.py — confirm /clear-all-data endpoint exists from Step 7. If not added yet, add it now.

Test: Click clear, confirm, all flags gone, all corrections gone, role reset, page re-renders as raw OCR, server files deleted.


Step 12 — Sample data

Add to p44-ocr-viewer.js only.

  1. Add SAMPLE_TEXT constant (plain prose — no markdown)

  2. Add SAMPLE_JSON constant (word bbox data)

  3. buildSampleImageDataURL() — canvas-drawn sample page image

  4. loadSampleData():

    • Sets state.mode = 'image'
    • Draws sample image to canvas
    • Loads sample JSON word data
    • Calls loadOCRText(SAMPLE_TEXT)
    • Status: 'Sample data loaded.'do not reference any removed features
  5. #btn-example click → loadSampleData()

Test: Sample button loads image and OCR text, page displays correctly.


Step 13 — Final cleanup and launch_viewer.py review

Review all three files against this checklist:

JS cleanup:

  • No reference to zoomLens anywhere
  • No contentEditable set on #text-output anywhere
  • No convertMarkdown() or inlineMd() function
  • No sanitizeCorrection() function
  • No stripHTML() function
  • No word-level diff functions (wordDiff, lcsWords, tokenizeWords, renderDiff)
  • No diff legend DOM refs or event listeners
  • state.scale removed (it was dead — there is no scale dropdown)
  • state.ocrRawFull removed (dead — never read)
  • p44_diff_legend_dismissed NOT in clearAllLocalData() (key was never written)
  • localStorage keys are consistent: p44-flags, p44-pass2-flags, p44-corrections, p44-role

launch_viewer.py review:

  • /save-flags POST handler exists
  • /save-corrections POST handler exists
  • /save-pass2-flags POST handler exists
  • /clear-all-data POST handler exists
  • All handlers wrapped in try/except
  • Cache-Control: no-store headers present on all responses

Final test — full workflow:

  1. Load Calgary Highlanders
  2. Navigate to page 10
  3. Flag as Verified (Pass 1)
  4. Enter edit mode, make a correction, exit
  5. Refresh — correction and flag persist
  6. Switch to Pass 2
  7. Navigate to page 10 (only verified page)
  8. Change summary shows Pass 1 edits
  9. Make a Pass 2 correction
  10. Revert to original (type REVERT)
  11. Clear all data
  12. Refresh — everything is gone

End of rebuild prompts.