PDF-Extractor / static /index.html
Archie0099's picture
Add knowledge graph search over extracted text
0992cee verified
Raw
History Blame Contribute Delete
20.1 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Local PDF Extractor</title>
<script>
/* Set the theme before first paint (no flash), then wire the header toggle.
Self-contained so app.js is untouched. */
(function () {
var root = document.documentElement;
try {
var t = localStorage.getItem("theme");
if (t !== "light" && t !== "dark") t = "light";
root.setAttribute("data-theme", t);
} catch (e) {
root.setAttribute("data-theme", "light");
}
document.addEventListener("DOMContentLoaded", function () {
var btn = document.getElementById("themeToggle");
if (btn) {
btn.addEventListener("click", function () {
var next = root.getAttribute("data-theme") === "light" ? "dark" : "light";
root.setAttribute("data-theme", next);
try { localStorage.setItem("theme", next); } catch (e) {}
});
}
/* Disable local-handwriting gracefully where its packages aren't
installed (e.g. the lean hosted build) and point users to Gemini,
instead of letting a page fail mid-extraction. */
fetch("/api/capabilities")
.then(function (r) { return r.json(); })
.then(function (caps) {
if (!caps) return;
if (caps.handwriting === false) {
var hw = document.getElementById("handwritingToggle");
if (hw) { hw.checked = false; hw.disabled = true; }
var row = document.getElementById("handwritingRow");
if (row) row.classList.add("is-unavailable");
var note = document.getElementById("hwUnavailable");
if (note) note.hidden = false;
}
if (caps.online_demo === true) {
var on = document.getElementById("onlineDemoNote");
if (on) on.hidden = false;
}
})
.catch(function () {});
});
})();
</script>
<link rel="stylesheet" href="/static/styles.css" />
</head>
<body>
<header class="app-header">
<div class="brand">
<div class="brand-mark" aria-hidden="true">
<svg viewBox="0 0 32 32" width="32" height="32" role="img" aria-label="logo">
<rect x="5" y="3" width="18" height="26" rx="3" fill="currentColor" opacity="0.15"></rect>
<rect x="5" y="3" width="18" height="26" rx="3" fill="none" stroke="currentColor" stroke-width="2"></rect>
<line x1="9" y1="11" x2="19" y2="11" stroke="currentColor" stroke-width="2" stroke-linecap="round"></line>
<line x1="9" y1="16" x2="19" y2="16" stroke="currentColor" stroke-width="2" stroke-linecap="round"></line>
<line x1="9" y1="21" x2="15" y2="21" stroke="currentColor" stroke-width="2" stroke-linecap="round"></line>
</svg>
</div>
<div class="brand-text">
<h1>Local PDF Extractor</h1>
<p class="tagline">Extract text from typed, scanned &amp; handwritten PDFs</p>
</div>
</div>
<div class="header-actions">
<button type="button" class="theme-toggle" id="themeToggle" aria-label="Switch light / dark theme" title="Switch light / dark theme">
<svg class="icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<circle cx="12" cy="12" r="4"></circle>
<path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"></path>
</svg>
<svg class="icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
</svg>
</button>
</div>
</header>
<main class="layout">
<!-- Sidebar: settings only -->
<aside class="sidebar">
<section class="panel">
<h2 class="panel-title">Settings</h2>
<div class="setting">
<label class="setting-label">Accuracy</label>
<div class="segmented" id="modeToggle" role="radiogroup" aria-label="Accuracy">
<button type="button" class="seg active" data-mode="max" role="radio" aria-checked="true">Max accuracy</button>
<button type="button" class="seg" data-mode="fast" role="radio" aria-checked="false">Faster</button>
</div>
<p class="hint" id="modeHint">Max accuracy is slower but renders at higher DPI and uses angle correction.</p>
</div>
<div class="setting">
<label class="setting-label" for="langSelect">Language</label>
<select id="langSelect" class="select">
<option value="en" selected>English</option>
<option value="hi">Hindi (Devanagari)</option>
</select>
</div>
<details class="accordion">
<summary class="accordion-summary">
<span>Advanced preprocessing</span>
<svg class="accordion-chevron" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 9l6 6 6-6"></path></svg>
</summary>
<div class="accordion-body">
<div class="setting setting-row">
<div>
<label class="setting-label" for="preprocessToggle">Preprocessing</label>
<p class="hint">Deskew, contrast-boost &amp; denoise before OCR. Best left on.</p>
</div>
<label class="switch">
<input type="checkbox" id="preprocessToggle" checked />
<span class="slider" aria-hidden="true"></span>
</label>
</div>
<div class="setting setting-row">
<div>
<label class="setting-label" for="binarizeToggle">Binarize</label>
<p class="hint">Force black-and-white. Helps some clean/high-contrast scans; usually leave off (grayscale is more accurate on most documents).</p>
</div>
<label class="switch">
<input type="checkbox" id="binarizeToggle" />
<span class="slider" aria-hidden="true"></span>
</label>
</div>
<div class="setting setting-row">
<div>
<label class="setting-label" for="forceOcrToggle">Force OCR</label>
<p class="hint">Re-read every page as an image, even pages that already have selectable text. Use only when the embedded text is wrong/garbled, or text is trapped inside images. Slower; off by default.</p>
</div>
<label class="switch">
<input type="checkbox" id="forceOcrToggle" />
<span class="slider" aria-hidden="true"></span>
</label>
</div>
<div class="setting setting-row">
<div>
<label class="setting-label" for="removeHeadersToggle">Remove headers &amp; footers</label>
<p class="hint">Strip repeating page headers, footers &amp; page numbers from multi-page documents (3+ pages). On by default; turn off to keep every line exactly as on the page.</p>
</div>
<label class="switch">
<input type="checkbox" id="removeHeadersToggle" checked />
<span class="slider" aria-hidden="true"></span>
</label>
</div>
</div>
</details>
<details class="accordion">
<summary class="accordion-summary">
<span>Handwriting &amp; online OCR</span>
<svg class="accordion-chevron" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 9l6 6 6-6"></path></svg>
</summary>
<div class="accordion-body">
<div class="setting setting-row" id="handwritingRow">
<div>
<label class="setting-label" for="handwritingToggle">Handwriting (local) <span class="beta-tag">beta</span></label>
<p class="hint">Read handwritten pages with a local model (English, ~73% on real cursive). Much slower; first use downloads a model once. For far better handwriting, use Online Gemini below instead &mdash; you don't need both.</p>
</div>
<label class="switch">
<input type="checkbox" id="handwritingToggle" />
<span class="slider" aria-hidden="true"></span>
</label>
</div>
<p class="hint hw-note" id="hwUnavailable" hidden>Local handwriting isn't installed in this build &mdash; use <strong>Online vision OCR (Gemini)</strong> below for handwriting.</p>
<div class="setting setting-row">
<div>
<label class="setting-label" for="onlineToggle">Online vision OCR (Gemini)</label>
<p class="hint">Far higher accuracy on handwriting &amp; hard scans, via Google Gemini.
<strong>Sends your page images to Google</strong> &mdash; off by default. Needs a free API key.</p>
</div>
<label class="switch">
<input type="checkbox" id="onlineToggle" />
<span class="slider" aria-hidden="true"></span>
</label>
</div>
<p class="hint hw-note" id="onlineDemoNote" hidden>This demo includes a Gemini key &mdash; just turn this on and leave the key field blank. Or paste your own key to use that instead.</p>
<div class="setting setting-sub" id="onlineRow" hidden>
<label class="setting-label" for="onlineKey">Gemini API key</label>
<input type="password" id="onlineKey" class="select online-input" placeholder="paste key from aistudio.google.com" autocomplete="off" spellcheck="false" />
<div class="online-key-actions">
<button type="button" class="btn btn-ghost btn-sm" id="onlineCheck">Check key</button>
<span class="online-status" id="onlineStatus" data-role="onlineStatus"></span>
</div>
<label class="setting-label" for="onlineModel" style="margin-top:8px;">Model</label>
<select id="onlineModel" class="select"></select>
<p class="hint">Your key is stored only in this browser and sent with each upload &mdash; never saved on the server.
Pages with a real text layer are still read locally (not sent).</p>
</div>
</div>
</details>
</section>
</aside>
<!-- Main work area: dropzone becomes the results pane after upload -->
<section class="work">
<div class="toolbar" id="toolbar" hidden>
<div class="search-wrap">
<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true" class="search-icon">
<circle cx="11" cy="11" r="7" fill="none" stroke="currentColor" stroke-width="2"></circle>
<line x1="16.5" y1="16.5" x2="21" y2="21" stroke="currentColor" stroke-width="2" stroke-linecap="round"></line>
</svg>
<input type="search" id="searchInput" class="search-input" placeholder="Search across all extracted text…" autocomplete="off" />
<span class="search-count" id="searchCount" hidden></span>
</div>
<div class="toolbar-actions">
<label for="fileInput" class="btn btn-ghost add-more" title="Add more PDFs">+ Add PDF</label>
<button type="button" class="btn btn-ghost" id="copyAllBtn">Copy all</button>
<div class="export-group">
<button type="button" class="btn btn-ghost" data-export="txt">.txt</button>
<button type="button" class="btn btn-ghost" data-export="md">.md</button>
<button type="button" class="btn btn-ghost" data-export="docx">.docx</button>
<button type="button" class="btn btn-ghost" data-export="json">.json</button>
</div>
</div>
</div>
<!-- Empty state == the single central dropzone + auto-detect -->
<div class="empty-state" id="emptyState">
<section
class="dropzone"
id="dropzone"
tabindex="0"
role="button"
aria-label="Drop PDF files here or click to browse">
<div class="dz-icon-wrap">
<svg viewBox="0 0 24 24" width="34" height="34" aria-hidden="true" class="dz-icon">
<path d="M12 16V4m0 0l-4 4m4-4l4 4" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
<path d="M4 16v2a2 2 0 002 2h12a2 2 0 002-2v-2" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>
</div>
<p class="dz-primary">Drop your PDF here</p>
<p class="dz-secondary">or <span class="dz-link">browse files</span> &middot; multiple files supported</p>
</section>
<div class="dz-extra">
<button type="button" class="btn btn-ghost" id="suggestBtn">Auto-detect best settings</button>
<p class="hint dz-extra-hint">Samples 1&ndash;2 pages of a file and recommends settings &middot; can take up to a minute on the CPU &middot; nothing is applied until you click Apply.</p>
<div class="setting" id="suggestResult" hidden>
<div class="suggest-card">
<p class="suggest-rationale" data-role="suggestRationale"></p>
<div class="suggest-evidence" data-role="suggestEvidence"></div>
<button type="button" class="btn btn-primary btn-sm" id="suggestApply">Apply recommended</button>
</div>
</div>
</div>
</div>
<!-- File pickers live here (always present, hidden) so the dropzone and the
toolbar "Add PDF" label can both trigger them regardless of view state. -->
<input type="file" id="fileInput" accept="application/pdf,.pdf" multiple hidden />
<input type="file" id="suggestInput" accept="application/pdf,.pdf" hidden />
<div id="filesContainer" class="files-container"></div>
</section>
</main>
<footer class="app-footer">
<details class="footer-tips">
<summary>
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"></circle><path d="M12 16v-4M12 8h.01"></path></svg>
Good to know
</summary>
<ul class="footer-tips-list">
<li>Typed / born-digital pages are extracted instantly and exactly &mdash; no OCR.</li>
<li>OCR accuracy depends on scan quality; handwriting is the hardest.</li>
<li>For the best handwriting, use Online Gemini; everything else runs on your machine.</li>
<li>The first local-OCR run downloads model weights once, then caches them.</li>
</ul>
</details>
</footer>
<!-- Templates -->
<template id="fileCardTemplate">
<article class="file-card">
<div class="file-head">
<div class="file-meta">
<span class="status-dot" data-role="statusDot"></span>
<h3 class="file-name" data-role="fileName"></h3>
</div>
<div class="file-head-right">
<span class="status-text" data-role="statusText">Pending</span>
<button type="button" class="btn btn-danger btn-sm" data-role="cancelBtn">Cancel</button>
<button type="button" class="btn btn-ghost btn-sm btn-remove" data-role="removeBtn" title="Remove this file" aria-label="Remove this file">&times;</button>
</div>
</div>
<div class="progress-row">
<div class="progress-track"><div class="progress-fill" data-role="progressFill"></div></div>
<span class="progress-label" data-role="progressLabel">0 / 0</span>
</div>
<div class="file-error" data-role="fileError" hidden></div>
<details class="kg-panel" data-role="kgPanel" hidden>
<summary class="kg-summary">
<svg class="kg-summary-icon" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<circle cx="5" cy="6" r="2.4"></circle><circle cx="19" cy="6" r="2.4"></circle><circle cx="12" cy="18" r="2.4"></circle>
<line x1="6.9" y1="7.4" x2="10.4" y2="16.2"></line><line x1="17.1" y1="7.4" x2="13.6" y2="16.2"></line><line x1="7" y1="6" x2="17" y2="6"></line>
</svg>
<span>Knowledge graph &amp; search</span>
<span class="kg-summary-status" data-role="kgStatus"></span>
<svg class="accordion-chevron kg-chevron" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 9l6 6 6-6"></path></svg>
</summary>
<div class="kg-body">
<p class="hint kg-intro">Turn this document into a searchable graph of its entities &amp; facts, then ask questions and get answers traced back to the exact page. Uses <strong>Google Gemini</strong> (online &mdash; same key as Online OCR).</p>
<div class="kg-build-row">
<button type="button" class="btn btn-primary btn-sm" data-role="kgBuildBtn">Build knowledge graph</button>
<span class="kg-build-note hint" data-role="kgBuildNote"></span>
</div>
<div class="kg-query-wrap" data-role="kgQueryWrap" hidden>
<div class="kg-ask">
<input type="search" class="kg-query-input" data-role="kgQueryInput" placeholder="Ask about this document…" autocomplete="off" />
<button type="button" class="btn btn-primary btn-sm" data-role="kgAskBtn">Ask</button>
</div>
<div class="kg-results" data-role="kgResults"></div>
<div class="kg-tools">
<button type="button" class="btn btn-ghost btn-sm" data-role="kgVizToggle">Show graph view</button>
<button type="button" class="btn btn-ghost btn-sm" data-role="kgExportBtn">Export graph (.json)</button>
<button type="button" class="btn btn-ghost btn-sm" data-role="kgRebuildBtn">Rebuild</button>
</div>
<div class="kg-viz-wrap" data-role="kgVizWrap" hidden>
<canvas class="kg-canvas" data-role="kgCanvas" width="640" height="400"></canvas>
<p class="hint kg-viz-note" data-role="kgVizNote"></p>
</div>
</div>
</div>
</details>
<div class="pages" data-role="pages"></div>
</article>
</template>
<template id="pageCardTemplate">
<div class="page-card" data-role="pageCard">
<div class="page-head">
<span class="page-num" data-role="pageNum"></span>
<span class="badge" data-role="badge"></span>
<span class="to-check" data-role="toCheck" hidden></span>
<span class="page-spacer"></span>
<button type="button" class="btn btn-ghost btn-sm" data-role="toggleImg">Show original page</button>
<button type="button" class="btn btn-ghost btn-sm" data-role="editBtn">Edit</button>
<button type="button" class="btn btn-ghost btn-sm" data-role="copyPage">Copy</button>
</div>
<pre class="page-text" data-role="pageText"></pre>
<textarea class="page-edit" data-role="pageEdit" spellcheck="false" hidden></textarea>
<div class="page-image-wrap" data-role="imageWrap" hidden></div>
</div>
</template>
<div class="toast" id="toast" role="status" aria-live="polite" hidden></div>
<script src="/static/app.js"></script>
</body>
</html>