PACWIN2027/Form_Summary_Extraction
0
1<!doctype html>
2<html lang="en">
3<head>
4<meta charset="utf-8">
5<meta name="viewport" content="width=device-width, initial-scale=1">
6<title>Electoral PDF Extractor</title>
7<style>
8 :root {
9 --bg: #f5f6f8;
10 --panel: #ffffff;
11 --ink: #16202c;
12 --muted: #63718a;
13 --line: #dfe4ec;
14 --accent: #1f4e79;
15 --accent-soft: #e8f0f9;
16 --ok: #1a7f52;
17 --warn: #8a5a00;
18 --warn-bg: #fdf4e2;
19 --err: #b3261e;
20 --radius: 10px;
21 }
22 @media (prefers-color-scheme: dark) {
23 :root {
24 --bg: #12161c; --panel: #1a2029; --ink: #e8edf5; --muted: #97a3b8;
25 --line: #2b3441; --accent: #6ea8dc; --accent-soft: #1d2c3d;
26 --ok: #59c795; --warn: #e0b055; --warn-bg: #2e2716; --err: #f08078;
27 }
28 }
29 * { box-sizing: border-box; }
30 body {
31 margin: 0; background: var(--bg); color: var(--ink);
32 font: 15px/1.5 -apple-system, "Segoe UI", Roboto, sans-serif;
33 }
34 header {
35 background: var(--panel); border-bottom: 1px solid var(--line);
36 padding: 18px 24px;
37 }
38 header h1 { margin: 0; font-size: 19px; letter-spacing: -.01em; }
39 header p { margin: 4px 0 0; color: var(--muted); font-size: 13.5px; }
40 main { max-width: 1080px; margin: 0 auto; padding: 24px; }
41 .panel {
42 background: var(--panel); border: 1px solid var(--line);
43 border-radius: var(--radius); padding: 20px; margin-bottom: 18px;
44 }
45 #drop {
46 border: 2px dashed var(--line); border-radius: var(--radius);
47 padding: 34px 20px; text-align: center; cursor: pointer;
48 transition: border-color .15s, background .15s;
49 }
50 #drop.over, #drop:hover { border-color: var(--accent); background: var(--accent-soft); }
51 #drop strong { display: block; font-size: 16px; margin-bottom: 4px; }
52 #drop span { color: var(--muted); font-size: 13.5px; }
53 .opts { display: flex; flex-wrap: wrap; gap: 18px; margin-top: 18px; }
54 .opt { display: flex; flex-direction: column; gap: 5px; font-size: 13px; }
55 .opt label { color: var(--muted); font-weight: 600; letter-spacing: .02em; }
56 select, input[type=number] {
57 font: inherit; padding: 7px 9px; border: 1px solid var(--line);
58 border-radius: 7px; background: var(--bg); color: var(--ink); min-width: 165px;
59 }
60 .check { flex-direction: row; align-items: center; gap: 8px; align-self: end;
61 padding-bottom: 8px; }
62 .check label { color: var(--ink); font-weight: 400; }
63 button {
64 font: inherit; font-weight: 600; padding: 10px 20px; border: 0;
65 border-radius: 7px; background: var(--accent); color: #fff; cursor: pointer;
66 }
67 button:disabled { opacity: .5; cursor: default; }
68 button.ghost { background: transparent; color: var(--accent);
69 border: 1px solid var(--line); }
70 .row { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; margin-top: 18px; }
71 .hidden { display: none !important; }
72 .status { display: flex; align-items: center; gap: 10px; font-size: 14px; }
73 .spinner {
74 width: 15px; height: 15px; border: 2px solid var(--line);
75 border-top-color: var(--accent); border-radius: 50%;
76 animation: spin .8s linear infinite; flex: none;
77 }
78 @keyframes spin { to { transform: rotate(360deg); } }
79 .log {
80 margin-top: 12px; font-size: 12.5px; color: var(--muted);
81 font-family: ui-monospace, Consolas, monospace; max-height: 130px;
82 overflow-y: auto; white-space: pre-wrap;
83 }
84 .stats { display: flex; gap: 26px; flex-wrap: wrap; margin-bottom: 16px; }
85 .stat b { display: block; font-size: 22px; letter-spacing: -.02em; }
86 .stat span { color: var(--muted); font-size: 12px; text-transform: uppercase;
87 letter-spacing: .05em; }
88 .warn {
89 background: var(--warn-bg); border: 1px solid var(--warn); color: var(--warn);
90 border-radius: 8px; padding: 10px 12px; font-size: 13px; margin-bottom: 10px;
91 }
92 .err { color: var(--err); font-size: 14px; }
93 .tablewrap { overflow-x: auto; border: 1px solid var(--line); border-radius: 8px; }
94 table { border-collapse: collapse; width: 100%; font-size: 12.5px; }
95 th, td { text-align: left; padding: 7px 10px; border-bottom: 1px solid var(--line);
96 vertical-align: top; }
97 th { background: var(--accent-soft); position: sticky; top: 0;
98 font-size: 11px; text-transform: uppercase; letter-spacing: .04em; }
99 td:nth-child(3) { min-width: 280px; }
100 tr:last-child td { border-bottom: 0; }
101 .note { color: var(--muted); font-size: 12.5px; margin-top: 10px; }
102</style>
103</head>
104<body>
105<header>
106 <h1>Electoral PDF Extractor</h1>
107 <p>Upload a Form 20, electoral roll or summary sheet — get a formatted Excel workbook back.</p>
108</header>
109
110<main>
111 <div class="panel">
112 <div id="drop">
113 <strong>Drop a PDF here, or click to choose</strong>
114 <span id="picked">Punjabi and English documents are both supported</span>
115 <input id="file" type="file" accept="application/pdf" class="hidden">
116 </div>
117
118 <div class="opts">
119 <div class="opt">
120 <label for="doctype">Document type</label>
121 <select id="doctype"><option value="">Detect automatically</option></select>
122 </div>
123 <div class="opt">
124 <label for="startpage">Start from page</label>
125 <input id="startpage" type="number" min="1" placeholder="4 (summary sheets)">
126 </div>
127 <div class="opt">
128 <label for="sep">Areas in one cell</label>
129 <select id="sep">
130 <option value="semicolon">1. A; 2. B; 3. C</option>
131 <option value="newline">One per line</option>
132 </select>
133 </div>
134 <div class="opt check">
135 <input id="translate" type="checkbox" checked>
136 <label for="translate">Translate Punjabi → English</label>
137 </div>
138 </div>
139
140 <div class="row">
141 <button id="go" disabled>Extract</button>
142 <button id="reset" class="ghost hidden">Start over</button>
143 </div>
144 </div>
145
146 <div id="progress" class="panel hidden">
147 <div class="status"><div class="spinner"></div><span id="msg">Working…</span></div>
148 <div class="log" id="log"></div>
149 </div>
150
151 <div id="result" class="panel hidden">
152 <div class="stats">
153 <div class="stat"><b id="s-records">0</b><span>Records</span></div>
154 <div class="stat"><b id="s-type">—</b><span>Document</span></div>
155 <div class="stat"><b id="s-pages">0</b><span>Pages read</span></div>
156 </div>
157 <div id="warnings"></div>
158 <div class="row" style="margin-top:0">
159 <button id="download">Download Excel workbook</button>
160 </div>
161 <p class="note">Preview of the first rows — the workbook contains every record.</p>
162 <div class="tablewrap"><table><thead id="thead"></thead><tbody id="tbody"></tbody></table></div>
163 </div>
164</main>
165
166<script>
167const $ = id => document.getElementById(id);
168let chosen = null, jobId = null, poller = null;
169
170fetch('/api/health').then(r => r.json()).then(h => {
171 h.doc_types.forEach(t => {
172 const o = document.createElement('option');
173 o.value = t.value; o.textContent = t.label;
174 $('doctype').appendChild(o);
175 });
176 if (!h.ocr_available) {
177 $('picked').textContent =
178 'Text-layer PDFs only — Tesseract is not installed, so scanned pages will be skipped.';
179 }
180});
181
182const drop = $('drop');
183drop.addEventListener('click', () => $('file').click());
184drop.addEventListener('dragover', e => { e.preventDefault(); drop.classList.add('over'); });
185drop.addEventListener('dragleave', () => drop.classList.remove('over'));
186drop.addEventListener('drop', e => {
187 e.preventDefault(); drop.classList.remove('over');
188 if (e.dataTransfer.files.length) pick(e.dataTransfer.files[0]);
189});
190$('file').addEventListener('change', e => { if (e.target.files.length) pick(e.target.files[0]); });
191
192function pick(f) {
193 if (!f.name.toLowerCase().endsWith('.pdf')) { alert('Please choose a PDF file.'); return; }
194 chosen = f;
195 $('picked').textContent = f.name + ' (' + (f.size / 1048576).toFixed(1) + ' MB)';
196 $('go').disabled = false;
197}
198
199$('go').addEventListener('click', async () => {
200 if (!chosen) return;
201 $('go').disabled = true;
202 $('result').classList.add('hidden');
203 $('progress').classList.remove('hidden');
204 $('log').textContent = '';
205 $('msg').textContent = 'Uploading…';
206
207 const body = new FormData();
208 body.append('file', chosen);
209 body.append('doc_type', $('doctype').value);
210 body.append('translate', $('translate').checked ? 'true' : 'false');
211 body.append('start_page', $('startpage').value || '0');
212 body.append('area_separator', $('sep').value);
213
214 try {
215 const res = await fetch('/api/extract', { method: 'POST', body });
216 const data = await res.json();
217 if (!res.ok) throw new Error(data.detail || 'Upload failed');
218 jobId = data.job_id;
219 poller = setInterval(poll, 900);
220 poll();
221 } catch (err) {
222 fail(err.message);
223 }
224});
225
226async function poll() {
227 const res = await fetch('/api/jobs/' + jobId);
228 if (!res.ok) return;
229 const job = await res.json();
230 $('msg').textContent = job.message || job.status;
231 $('log').textContent = (job.log || []).join('\n');
232 $('log').scrollTop = $('log').scrollHeight;
233
234 if (job.status === 'done') { clearInterval(poller); show(job); }
235 else if (job.status === 'error') { clearInterval(poller); fail(job.message); }
236}
237
238function show(job) {
239 $('progress').classList.add('hidden');
240 $('result').classList.remove('hidden');
241 $('reset').classList.remove('hidden');
242 $('s-records').textContent = job.record_count;
243 $('s-type').textContent = job.doc_type_label;
244 $('s-pages').textContent = job.pages;
245
246 $('warnings').innerHTML = '';
247 (job.warnings || []).forEach(w => {
248 const d = document.createElement('div');
249 d.className = 'warn'; d.textContent = w;
250 $('warnings').appendChild(d);
251 });
252
253 $('thead').innerHTML = '<tr>' +
254 job.columns.map(c => '<th>' + esc(c) + '</th>').join('') + '</tr>';
255 $('tbody').innerHTML = job.preview.map(
256 r => '<tr>' + r.map(c => '<td>' + esc(c) + '</td>').join('') + '</tr>').join('');
257}
258
259function fail(message) {
260 $('progress').classList.add('hidden');
261 $('result').classList.remove('hidden');
262 $('reset').classList.remove('hidden');
263 $('warnings').innerHTML = '<div class="warn err">' + esc(message) + '</div>';
264 $('thead').innerHTML = ''; $('tbody').innerHTML = '';
265 $('download').classList.add('hidden');
266}
267
268$('download').addEventListener('click', () => {
269 window.location = '/api/jobs/' + jobId + '/download';
270});
271
272$('reset').addEventListener('click', () => {
273 chosen = null; jobId = null;
274 $('file').value = '';
275 $('picked').textContent = 'Punjabi and English documents are both supported';
276 $('go').disabled = true;
277 $('download').classList.remove('hidden');
278 $('result').classList.add('hidden');
279 $('reset').classList.add('hidden');
280});
281
282function esc(v) {
283 return String(v === null || v === undefined ? '' : v)
284 .replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
285}
286</script>
287</body>
288</html>
289 