CoolFace
Apppublic

PACWIN2027/Form_Summary_Extraction

sourceHugging Faceupdated 1mo agoView on Hugging Face
0likes
index.html289 linesDownload Raw Back to static
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 &mdash; 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 &rarr; 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">&mdash;</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 &mdash; 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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
285}
286</script>
287</body>
288</html>
289