OCR-Viewer #1

Merged
nathan merged 8 commits from OCR-Viewer into main 2026-05-19 22:03:29 +00:00
5 changed files with 11 additions and 2250 deletions
Showing only changes of commit 95ca7efa44 - Show all commits

View File

@@ -12,10 +12,11 @@ import socket
import os
import sys
import json
revfrom urllib.parse import urlparse, parse_qs
from urllib.parse import urlparse, parse_qs
PORT = 5044
ROOT = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(os.path.abspath(__file__))
DATA_DIR = os.path.join(ROOT, 'viewer', 'data')
class QuietHandler(http.server.SimpleHTTPRequestHandler):
@@ -53,7 +54,8 @@ class QuietHandler(http.server.SimpleHTTPRequestHandler):
return
if parsed.path == '/save-flags':
dest = os.path.join(ROOT, 'flags-' + diary + '.json')
os.makedirs(DATA_DIR, exist_ok=True)
dest = os.path.join(DATA_DIR, 'flags-' + diary + '.json')
with open(dest, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2)
self.send_response(200)
@@ -62,7 +64,8 @@ class QuietHandler(http.server.SimpleHTTPRequestHandler):
self.wfile.write(b'{"ok":true}')
elif parsed.path == '/save-corrections':
dest = os.path.join(ROOT, 'corrections-' + diary + '.json')
os.makedirs(DATA_DIR, exist_ok=True)
dest = os.path.join(DATA_DIR, 'corrections-' + diary + '.json')
existing = {}
if os.path.exists(dest):
try:
@@ -84,7 +87,8 @@ class QuietHandler(http.server.SimpleHTTPRequestHandler):
self.wfile.write(b'{"ok":true}')
elif parsed.path == '/save-pass2-flags':
dest = os.path.join(ROOT, 'pass2_flags-' + diary + '.json')
os.makedirs(DATA_DIR, exist_ok=True)
dest = os.path.join(DATA_DIR, 'pass2_flags-' + diary + '.json')
try:
with open(dest, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2)
@@ -98,7 +102,7 @@ class QuietHandler(http.server.SimpleHTTPRequestHandler):
elif parsed.path == '/clear-all-data':
for prefix in ['flags-', 'corrections-', 'pass2_flags-']:
fpath = os.path.join(ROOT, prefix + diary + '.json')
fpath = os.path.join(DATA_DIR, prefix + diary + '.json')
if os.path.exists(fpath):
try:
os.remove(fpath)
@@ -124,7 +128,7 @@ def start_server():
if __name__ == '__main__':
url = 'http://localhost:{}/p44-landing.html'.format(PORT)
url = 'http://localhost:{}/viewer/p44-landing.html'.format(PORT)
if port_in_use(PORT):
print('\n P44 OCR Viewer (server already running)')

View File

@@ -1,337 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>P44 OCR — War Diary Transcription</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: #111120;
font-family: 'Georgia', 'Times New Roman', serif;
color: #ddd4bb;
min-height: 100vh;
}
#topbar {
background: #16213e;
border-bottom: 1px solid #2e2e4e;
padding: 10px 24px;
display: flex;
align-items: center;
gap: 12px;
}
#topbar h1 {
font-size: 13px;
letter-spacing: .12em;
color: #c8a84b;
text-transform: uppercase;
}
#role-badge {
font-size: 9px;
padding: 2px 8px;
border: 1px solid #8a6f2e;
border-radius: 3px;
color: #8a6f2e;
letter-spacing: .06em;
margin-left: auto;
}
#content {
max-width: 720px;
margin: 0 auto;
padding: 28px 24px;
}
.section-label {
font-size: 9px;
letter-spacing: .12em;
text-transform: uppercase;
color: #8a6f2e;
margin-bottom: 16px;
}
#cards {
display: flex;
flex-direction: column;
gap: 12px;
}
.card {
background: #16213e;
border: 0.5px solid #2e2e4e;
border-radius: 6px;
padding: 16px 18px;
cursor: pointer;
transition: border-color .15s;
}
.card:hover { border-color: #c8a84b; }
.card-top {
display: flex;
align-items: flex-start;
gap: 14px;
}
.thumb {
width: 56px;
height: 72px;
background: #0f2a0f;
border: 0.5px solid #2e4e2e;
border-radius: 3px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
}
.thumb-lines { width: 40px; }
.thumb-line {
height: 2px;
background: #2e4e2e;
border-radius: 1px;
margin-bottom: 3px;
}
.thumb-line.dim { background: #1e3e1e; }
.card-meta { flex: 1; min-width: 0; }
.card-title {
font-size: 13px;
color: #e8d9b0;
letter-spacing: .03em;
margin-bottom: 4px;
}
.card-sub {
font-size: 10px;
color: #8a6f2e;
letter-spacing: .05em;
margin-bottom: 10px;
}
.card-tags { display: flex; gap: 6px; flex-wrap: wrap; }
.tag {
font-size: 9px;
padding: 2px 7px;
border-radius: 3px;
letter-spacing: .05em;
background: #0f1a3e;
color: #6a8fd8;
border: 0.5px solid #2e3e6e;
}
.card-progress {
margin-top: 14px;
border-top: 0.5px solid #2e2e4e;
padding-top: 12px;
}
.prog-row {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 7px;
}
.prog-row:last-child { margin-bottom: 0; }
.prog-label {
font-size: 9px;
letter-spacing: .08em;
text-transform: uppercase;
width: 52px;
flex-shrink: 0;
}
.prog-label.p1 { color: #c8a84b; }
.prog-label.p2 { color: #6a8fd8; }
.prog-track {
flex: 1;
height: 5px;
background: #0f1a0f;
border-radius: 3px;
overflow: hidden;
}
.prog-fill { height: 100%; border-radius: 3px; transition: width .3s ease; }
.prog-fill.p1 { background: #c8a84b; }
.prog-fill.p2 { background: #378ADD; }
.prog-pct {
font-size: 10px;
width: 32px;
text-align: right;
flex-shrink: 0;
}
.prog-pct.p1 { color: #c8a84b; }
.prog-pct.p2 { color: #6a8fd8; }
.card-action { margin-top: 12px; }
.btn-open, .btn-view {
padding: 4px 14px;
border-radius: 3px;
font-size: 10px;
font-family: 'Georgia', serif;
letter-spacing: .05em;
cursor: pointer;
background: transparent;
}
.btn-open {
border: 1px solid #c8a84b;
color: #c8a84b;
}
.btn-open:hover { background: rgba(200,168,75,.08); }
.btn-view {
border: 1px solid #5f5e5a;
color: #888780;
}
.btn-view:hover { background: rgba(136,135,128,.08); }
#footer {
text-align: center;
padding: 20px;
font-size: 10px;
color: #5f5e5a;
letter-spacing: .06em;
}
</style>
</head>
<body>
<div id="topbar">
<h1>⚜ P44 War Diary Transcription</h1>
<span id="role-badge">Pass 1 — Volunteer</span>
</div>
<div id="content">
<div class="section-label">War diaries available for transcription</div>
<div id="cards"></div>
</div>
<div id="footer">Project '44 — Canadian Research and Mapping Association</div>
<script>
const DIARIES = [
{
id: 'calgary-highlanders',
title: 'Calgary Highlanders',
subtitle: 'War Diary — September 1944',
tags: ['Infantry', 'Sep 1944', '343 pages'],
totalPages: 343,
viewerUrl: 'p44-ocr-viewer.html',
flagsKey: 'p44-flags-calgary-highlanders',
pass2FlagsKey: 'p44-pass2-flags-calgary-highlanders',
},
{
id: 'black-watch',
title: 'Black Watch (RHR) of Canada',
subtitle: 'War Diary — September 1944',
tags: ['Infantry', 'Sep 1944', '211 pages'],
totalPages: 211,
viewerUrl: 'p44-ocr-viewer.html',
flagsKey: 'p44-flags-black-watch',
pass2FlagsKey: 'p44-pass2-flags-black-watch',
},
{
id: '5cib',
title: '5th Canadian Infantry Brigade',
subtitle: 'War Diary — September 1944',
tags: ['Brigade HQ', 'Sep 1944', '178 pages'],
totalPages: 178,
viewerUrl: 'p44-ocr-viewer.html',
flagsKey: 'p44-flags-5cib',
pass2FlagsKey: 'p44-pass2-flags-5cib',
},
];
function getCurrentRole() {
return localStorage.getItem('p44-role') || 'pass1';
}
function getProgress(diary) {
let pass1Done = 0, pass2Done = 0;
try {
const flags = JSON.parse(localStorage.getItem(diary.flagsKey) || '{}');
pass1Done = Object.values(flags).filter(f => f === 'done').length;
} catch (_) {}
try {
const pass2Flags = JSON.parse(localStorage.getItem(diary.pass2FlagsKey) || '{}');
pass2Done = Object.values(pass2Flags).filter(f => f === 'done').length;
} catch (_) {}
return {
pass1Pct: Math.round((pass1Done / diary.totalPages) * 100),
pass2Pct: Math.round((pass2Done / diary.totalPages) * 100),
};
}
function openDiary(diaryId) {
const diary = DIARIES.find(d => d.id === diaryId);
if (diary) window.location.href = diary.viewerUrl + '?diary=' + diaryId;
}
function renderCard(diary) {
const role = getCurrentRole();
const prog = getProgress(diary);
const isViewOnly = role === 'pass1' && prog.pass1Pct === 100;
const tagHtml = diary.tags.map(t => '<span class="tag">' + t + '</span>').join('');
const thumbLines = Array(10).fill(0).map((_, i) =>
'<div class="thumb-line' + (i % 2 ? ' dim' : '') + '"></div>'
).join('');
const actionBtn = isViewOnly
? '<button class="btn-view" onclick="openDiary(\'' + diary.id + '\')">View only</button>'
: '<button class="btn-open" onclick="openDiary(\'' + diary.id + '\')">Open diary</button>';
return '<div class="card">' +
'<div class="card-top">' +
'<div class="thumb" aria-hidden="true"><div class="thumb-lines">' + thumbLines + '</div></div>' +
'<div class="card-meta">' +
'<div class="card-title">' + diary.title + '</div>' +
'<div class="card-sub">' + diary.subtitle + '</div>' +
'<div class="card-tags">' + tagHtml + '</div>' +
'</div>' +
'</div>' +
'<div class="card-progress">' +
'<div class="prog-row">' +
'<span class="prog-label p1">Pass 1</span>' +
'<div class="prog-track"><div class="prog-fill p1" style="width:' + prog.pass1Pct + '%"></div></div>' +
'<span class="prog-pct p1">' + prog.pass1Pct + '%</span>' +
'</div>' +
'<div class="prog-row">' +
'<span class="prog-label p2">Pass 2</span>' +
'<div class="prog-track"><div class="prog-fill p2" style="width:' + prog.pass2Pct + '%"></div></div>' +
'<span class="prog-pct p2">' + prog.pass2Pct + '%</span>' +
'</div>' +
'</div>' +
'<div class="card-action">' + actionBtn + '</div>' +
'</div>';
}
function init() {
const role = getCurrentRole();
document.getElementById('role-badge').textContent =
role === 'pass2' ? 'Pass 2 — Senior Editor' : 'Pass 1 — Volunteer';
document.getElementById('cards').innerHTML = DIARIES.map(renderCard).join('');
}
document.addEventListener('DOMContentLoaded', init);
</script>
</body>
</html>

View File

@@ -1,513 +0,0 @@
/* ── Reset & base ─────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #1a1a2e;
--surface: #16213e;
--surface2: #0f3460;
--gold: #c8a84b;
--gold-dim: #8a6f2e;
--text: #ddd4bb;
--text-dim: #7a7260;
--highlight: #ffe066;
--border: #2e2e4e;
--toolbar-h: 52px;
--footer-h: 34px;
}
html, body {
height: 100%;
background: var(--bg);
color: var(--text);
font-family: 'Georgia', 'Times New Roman', serif;
font-size: 14px;
overflow: hidden;
}
/* ── App shell ────────────────────────────────────────────────── */
#app {
display: grid;
grid-template-rows: var(--toolbar-h) auto 1fr var(--footer-h);
height: 100vh;
}
/* ── Override dropdown ────────────────────────────────────────── */
#override-wrap {
position: relative;
flex-shrink: 0;
}
#override-menu {
display: none;
position: absolute;
top: calc(100% + 4px);
left: 0;
z-index: 500;
background: var(--surface);
border: 1px solid var(--gold-dim);
border-radius: 4px;
min-width: 160px;
padding: 4px 0;
box-shadow: 0 4px 14px rgba(0,0,0,0.45);
}
#override-menu.open { display: block; }
.override-item {
display: flex;
align-items: center;
gap: 8px;
padding: 7px 14px;
font-family: 'Georgia', serif;
font-size: 11px;
color: var(--text-dim);
cursor: pointer;
white-space: nowrap;
letter-spacing: 0.03em;
transition: background 0.1s, color 0.1s;
}
.override-item:hover { background: var(--surface2); color: var(--gold); }
.override-item input[type="file"] { display: none; }
/* ── Toolbar ──────────────────────────────────────────────────── */
#toolbar {
display: flex;
align-items: center;
gap: 8px;
padding: 0 12px;
background: var(--surface);
border-bottom: 2px solid var(--gold-dim);
overflow: visible; /* must be visible so the Override dropdown can escape the toolbar */
flex-wrap: nowrap;
position: relative;
z-index: 100; /* sit above pass2-banner and panel headers */
}
#toolbar h1 {
font-size: 13px;
font-weight: normal;
letter-spacing: 0.1em;
color: var(--gold);
white-space: nowrap;
flex-shrink: 0;
}
.divider { width: 1px; height: 26px; background: var(--border); flex-shrink: 0; margin: 0 2px; }
.toolbar-group {
display: flex;
align-items: center;
gap: 4px;
flex-shrink: 0;
}
.toolbar-group .lbl {
font-size: 9px;
color: var(--text-dim);
letter-spacing: 0.06em;
white-space: nowrap;
}
.file-btn {
display: inline-block;
padding: 3px 8px;
background: var(--surface2);
border: 1px solid var(--gold-dim);
border-radius: 3px;
color: var(--text);
font-size: 10px;
cursor: pointer;
letter-spacing: 0.03em;
transition: border-color 0.15s, background 0.15s;
white-space: nowrap;
}
.file-btn:hover { border-color: var(--gold); background: #1a2a50; }
.file-btn input[type="file"] { display: none; }
.tb-btn {
padding: 3px 9px;
background: transparent;
border: 1px solid var(--gold-dim);
border-radius: 3px;
color: var(--text-dim);
font-size: 10px;
font-family: inherit;
letter-spacing: 0.05em;
cursor: pointer;
transition: border-color 0.15s, color 0.15s, background 0.15s;
white-space: nowrap;
flex-shrink: 0;
}
.tb-btn:hover { border-color: var(--gold); color: var(--gold); background: rgba(200,168,75,0.08); }
a.tb-btn { text-decoration: none; display: inline-flex; align-items: center; }
#scale-select {
background: var(--surface2);
border: 1px solid var(--gold-dim);
border-radius: 3px;
color: var(--text);
font-size: 10px;
font-family: inherit;
padding: 2px 4px;
cursor: pointer;
}
/* ── Main panels ──────────────────────────────────────────────── */
#panels {
display: grid;
grid-template-columns: 1fr 1fr;
overflow: hidden;
}
.panel {
display: flex;
flex-direction: column;
overflow: hidden;
border-right: 1px solid var(--border);
}
.panel:last-child { border-right: none; }
.panel-header {
padding: 5px 10px;
font-size: 9px;
letter-spacing: 0.12em;
color: var(--gold-dim);
text-transform: uppercase;
background: var(--surface);
border-bottom: 1px solid var(--border);
flex-shrink: 0;
display: flex;
align-items: center;
gap: 8px;
}
.panel-header .ph-title { flex: 1; }
/* page navigation */
.page-nav { display: flex; align-items: center; gap: 4px; }
.page-nav button {
background: var(--surface2); border: 1px solid var(--gold-dim);
border-radius: 2px; color: var(--text); font-size: 10px;
padding: 1px 6px; cursor: pointer; line-height: 1.5;
}
.page-nav button:hover { border-color: var(--gold); color: var(--gold); }
.page-nav button:disabled { opacity: 0.3; cursor: default; }
.page-nav input[type="number"] {
width: 42px; text-align: center;
background: var(--surface2); border: 1px solid var(--gold-dim);
border-radius: 2px; color: var(--text); font-size: 9px; padding: 2px 3px;
}
#page-label { font-size: 9px; color: var(--text-dim); white-space: nowrap; }
.panel-body {
flex: 1;
overflow: auto;
position: relative;
}
/* ── Left panel: PDF canvas + overlay ────────────────────────── */
#image-panel-body {
position: relative;
scrollbar-gutter: stable; /* reserve scrollbar space always — prevents layout shift on zoom */
}
#pdf-container {
position: relative;
display: block; /* block instead of inline-block — fills full panel width cleanly */
width: 100%;
min-height: 100%; /* always occupies the full panel height so nothing else renders below */
}
#pdf-canvas {
display: block;
user-select: none;
background: #e8e0cc;
}
#overlay {
position: absolute;
top: 0; left: 0;
pointer-events: none;
}
/* noinspection CssUnusedSymbol */
.word-box {
position: absolute;
border: 1px solid transparent;
border-radius: 1px;
cursor: crosshair;
pointer-events: all;
transition: background 0.07s, border-color 0.07s;
}
/* noinspection CssUnusedSymbol */
.word-box:hover,
/* noinspection CssUnusedSymbol */
.word-box.highlight {
background: rgba(255, 224, 102, 0.38);
border-color: var(--highlight);
}
.panel-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
gap: 12px;
color: var(--text-dim);
font-size: 13px;
font-style: italic;
padding: 40px;
text-align: center;
}
.panel-placeholder em {
color: var(--gold-dim);
font-size: 11px;
font-style: normal;
letter-spacing: 0.04em;
}
.spinner {
width: 22px; height: 22px;
border: 2px solid var(--surface2);
border-top-color: var(--gold);
border-radius: 50%;
animation: spin 0.7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* ── Footer ───────────────────────────────────────────────────── */
#footer {
display: flex;
align-items: center;
padding: 0 12px;
gap: 10px;
background: var(--surface);
border-top: 1px solid var(--border);
font-size: 10px;
color: var(--text-dim);
}
#status-msg { flex: 1; font-style: italic; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
/* ── Scrollbars ───────────────────────────────────────────────── */
::-webkit-scrollbar { width: 7px; height: 7px; }
::-webkit-scrollbar-track { background: var(--bg); }
::-webkit-scrollbar-thumb { background: var(--surface2); border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: var(--gold-dim); }
/* ── Active toggle button state (sync-scroll, edit mode) ─────── */
.tb-btn.active { border-color: var(--gold); color: var(--gold); }
/* ── Flag buttons ─────────────────────────────────────────────── */
.flag-btn { font-size: 9px; padding: 2px 7px; }
.flag-btn.active { border-color: var(--gold); color: var(--gold); }
/* ── Footer extras ────────────────────────────────────────────── */
#flags-summary { color: var(--gold-dim); letter-spacing: 0.04em; white-space: nowrap; font-size: 10px; }
#kbd-hint { color: var(--text-dim); font-size: 9px; white-space: nowrap; opacity: 0.7; }
/* ── Zoom indicator in left panel header ─────────────────────────── */
#zoom-label {
font-size: 9px; color: var(--gold-dim); letter-spacing: 0.06em;
white-space: nowrap; min-width: 34px; text-align: right;
}
#btn-zoom-reset {
padding: 1px 6px; font-size: 9px; font-family: inherit;
background: transparent; border: 1px solid var(--gold-dim);
border-radius: 2px; color: var(--text-dim); cursor: pointer;
letter-spacing: 0.04em; white-space: nowrap;
transition: border-color 0.12s, color 0.12s;
}
#btn-zoom-reset:hover { border-color: var(--gold); color: var(--gold); }
/* ── Role switcher ────────────────────────────────────────────────── */
#role-select {
background: var(--surface2);
border: 1px solid var(--gold-dim);
border-radius: 3px;
color: var(--text);
font-size: 10px;
font-family: inherit;
padding: 2px 4px;
cursor: pointer;
}
/* ── Pass 2 review banner ─────────────────────────────────────────── */
#pass2-banner {
display: none;
align-items: center;
padding: 0 14px;
background: #2e2010;
border-bottom: 1px solid #c08020;
font-size: 11px;
color: #e8b848;
letter-spacing: 0.04em;
height: 26px;
overflow: hidden;
white-space: nowrap;
flex-shrink: 0;
}
/* ── Pass 2 revert bar ────────────────────────────────────────── */
#pass2-revert-bar {
display: none;
align-items: center;
gap: 8px;
padding: 6px 16px;
background: var(--surface);
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
#pass2-revert-bar .rv-label {
font-size: 10px;
color: var(--text-dim);
letter-spacing: 0.04em;
flex: 1;
}
#pass2-revert-bar .rv-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;
transition: border-color 0.15s, color 0.15s;
}
#pass2-revert-bar .rv-btn:hover { border-color: var(--gold-dim); color: var(--gold); }
#pass2-revert-bar .rv-btn.danger { border-color: var(--red, #d05050); color: var(--red, #d05050); }
#pass2-revert-bar .rv-btn.danger:hover { border-color: var(--red, #d05050); color: var(--text); }
/* ── Pass 2 change summary panel ─────────────────────────────── */
#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: #e8a838; }
#pass2-change-summary .cs-added { color: #4caf70; }
#pass2-change-summary .cs-deleted { color: #d05050; }
#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: #d05050; color: #d05050; }
.cs-revert-btn.danger:hover { border-color: #e07070; color: #e07070; }
/* ── Plain-text edit surface ──────────────────────────────────── */
#text-panel-body {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
overflow: hidden;
}
#editor-wrap {
display: flex;
flex-direction: row;
flex: 1;
min-height: 0;
overflow: hidden;
}
#line-numbers {
width: 48px;
flex-shrink: 0;
background: transparent;
border-right: 1px solid var(--border);
color: var(--gold-dim);
font-family: 'Courier New', monospace;
font-size: 11px;
line-height: 1.75;
padding: 0 8px 0 12px;
text-align: right;
overflow: hidden;
user-select: none;
white-space: pre;
box-sizing: border-box;
margin-right: 12px;
}
#text-editor {
display: block; /* always block — visibility via #editor-wrap */
flex: 1;
min-width: 0;
min-height: 0;
width: 100%;
height: 100%;
background: var(--surface2);
color: var(--text);
font-family: 'Georgia', 'Times New Roman', serif;
font-size: 12.5px;
line-height: 1.75;
padding: 0 16px 0 0;
border: none;
border-radius: 0;
resize: none;
outline: none;
box-sizing: border-box;
overflow-y: auto;
overflow-x: hidden;
}
/* ── Done-page read-only banner (Pass 1) ─────────────────────── */
#done-banner {
display: none;
align-items: center;
gap: 8px;
padding: 7px 16px;
background: #0f2a0f;
border-bottom: 1px solid #2e6e2e;
font-size: 10px;
color: #5cb85c;
letter-spacing: 0.05em;
flex-shrink: 0;
}
/* ── Problem flag indicator on page nav input ─────────────────── */
#page-input.problem { border-color: #c08020; }
/* ── OCR overlay panel (replaces raster in left panel) ───────── */
#ocr-overlay-panel {
display: none;
position: absolute;
inset: 0;
overflow-y: auto;
background: var(--surface);
padding: 20px 24px;
font-size: 13px;
line-height: 1.8;
color: var(--text);
font-family: 'Georgia', 'Times New Roman', serif;
white-space: pre-wrap;
word-break: break-word;
z-index: 10;
}

View File

@@ -1,132 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>P44 OCR Viewer</title>
<!-- PDF.js (CDN — needs internet access; served via launch_viewer.py) -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
<link rel="stylesheet" href="p44-ocr-viewer.css" />
</head>
<body>
<div id="app">
<!-- ── Toolbar ─────────────────────────────────────────────── -->
<div id="toolbar">
<a id="btn-back" href="p44-landing.html" class="tb-btn" title="Back to diary list">← Diaries</a>
<div class="divider"></div>
<h1>⚜ P44 OCR VIEWER</h1>
<span id="diary-label" style="font-size:10px;color:var(--gold-dim);letter-spacing:.06em;margin-left:6px;"></span>
<div class="divider"></div>
<!-- Single override dropdown — wraps all three hidden file inputs -->
<div id="override-wrap">
<button id="btn-override" class="tb-btn" type="button">Override ▾</button>
<div id="override-menu">
<label class="override-item">📄 Load PDF <input type="file" id="input-pdf" accept=".pdf,application/pdf" /></label>
<label class="override-item">📝 Load OCR text<input type="file" id="input-text" accept=".txt,.md" /></label>
<label class="override-item">🔲 Load JSON <input type="file" id="input-json" accept=".json" /></label>
</div>
</div>
<div class="divider"></div>
<button id="btn-clear-p1-queue" class="tb-btn" title="Clear all Pass 1 done flags — empties the Pass 2 queue">↺ Clear P1 queue</button>
<div style="flex:1;min-width:0;"></div>
<div class="divider"></div>
<div class="toolbar-group">
<span class="lbl">ROLE</span>
<select id="role-select" title="Switch review pass">
<option value="pass1">Pass 1 — Volunteer</option>
<option value="pass2">Pass 2 — Senior Editor</option>
</select>
</div>
<div class="divider"></div>
<button id="btn-clear-all" class="tb-btn" title="Wipe all saved flags, corrections and role — cannot be undone"
style="border-color:#d05050;color:#d05050;">⚠ Clear all data</button>
</div>
<!-- ── Pass 2 review banner ──────────────────────────────── -->
<div id="pass2-banner">
<span id="pass2-banner-text">Pass 2 mode · 0 pages ready for review</span>
<span style="flex:1;"></span>
<button id="btn-pass2-reset" title="Wipe all Pass 2 flags and corrections — start fresh"
style="background:transparent;border:1px solid #c08020;border-radius:3px;color:#e8b848;
font-size:10px;font-family:inherit;padding:2px 9px;cursor:pointer;letter-spacing:.04em;">
↺ Reset Pass 2
</button>
</div>
<!-- ── Panels ──────────────────────────────────────────────── -->
<div id="panels">
<!-- Left: PDF / image -->
<div class="panel">
<div class="panel-header">
<span class="ph-title">Source Document — Page Scan</span>
<span id="zoom-label">100%</span>
<button id="btn-zoom-reset" title="Reset zoom to 100%">1:1</button>
<button id="btn-ocr-toggle" class="tb-btn" title="Toggle between page scan and original OCR text" style="display:none;">📄 OCR</button>
<div class="page-nav">
<button id="btn-prev" disabled></button>
<input type="number" id="page-input" value="1" min="1" aria-label="Page number" />
<button id="btn-next" disabled></button>
<span id="page-label">/ —</span>
</div>
</div>
<div class="panel-body" id="image-panel-body">
<div class="panel-placeholder" id="image-placeholder">
<div class="spinner" id="img-spinner" style="display:none;"></div>
<div id="img-ph-text">
War diary scan loads automatically.
</div>
</div>
<div id="pdf-container" style="display:none;">
<canvas id="pdf-canvas"></canvas>
<div id="overlay"></div>
</div>
<div id="ocr-overlay-panel" style="display:none;"></div>
</div>
</div>
<!-- Right: OCR text -->
<div class="panel">
<div class="panel-header">
<span class="ph-title">OCR Text Output</span>
<span id="ocr-sync-label" style="font-size:9px;color:var(--gold-dim);"></span>
<button id="btn-flag-done" class="tb-btn flag-btn" title="Mark page as done — sends to Pass 2 queue. Cannot be undone.">✓ Done</button>
<button id="btn-flag-problem" class="tb-btn flag-btn" title="Flag page as a problem — OCR too bad to fix. Still goes to Pass 2.">⚠ Problem</button>
</div>
<div class="panel-body" id="text-panel-body">
<div class="panel-placeholder" id="text-placeholder">
<div class="spinner" id="txt-spinner" style="display:none;"></div>
<div id="txt-ph-text">
OCR transcript loads automatically.
</div>
</div>
<div id="done-banner" style="display:none;"></div>
<div id="pass2-revert-bar" style="display:none;"></div>
<div id="pass2-change-summary" style="display:none;"></div>
<div id="editor-wrap">
<div id="line-numbers"></div>
<textarea id="text-editor" aria-label="OCR text editor" wrap="soft"></textarea>
</div>
</div>
</div>
</div>
<!-- ── Footer ──────────────────────────────────────────────── -->
<div id="footer">
<span id="status-msg">Ready — run launch_viewer.py to auto-load Calgary Highlanders data.</span>
<span id="flags-summary"></span>
<span id="kbd-hint">← → navigate &nbsp; v done &nbsp; p problem &nbsp; esc close</span>
</div>
</div>
<script src="p44-ocr-viewer.js"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff