351 lines
20 KiB
HTML
351 lines
20 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>{{ document.document_id }}</title>
|
|
<link rel="stylesheet" href="/static/app.css">
|
|
</head>
|
|
<body>
|
|
<div class="app-shell" id="app-shell">
|
|
<aside class="sidebar">
|
|
<div class="sidebar-top">
|
|
<div class="sidebar-toggle" id="menu-toggle" aria-label="Toggle navigation" role="button" tabindex="0">
|
|
<span></span><span></span><span></span>
|
|
</div>
|
|
<div class="brand">Document Processor</div>
|
|
</div>
|
|
|
|
<div class="sidebar-section-title">Workspace</div>
|
|
<nav class="nav-list">
|
|
<a class="nav-link active" href="/documents/" title="Documents"><span class="nav-link-short">D</span><span class="nav-link-text">Documents</span></a>
|
|
<a class="nav-link" href="/queue/" title="Review Queue"><span class="nav-link-short">Q</span><span class="nav-link-text">Review Queue</span></a>
|
|
<a class="nav-link" href="/trash/" title="Trash"><span class="nav-link-short">T</span><span class="nav-link-text">Trash</span></a>
|
|
<a class="nav-link" href="/ingest/" title="Ingest"><span class="nav-link-short">I</span><span class="nav-link-text">Ingest</span></a>
|
|
</nav>
|
|
</aside>
|
|
|
|
<main class="main">
|
|
{% if error == "line_count_mismatch" %}
|
|
<div class="error-box">
|
|
Could not save reviewed OCR because line count did not match OCR layout.
|
|
Expected {{ error_expected }}, got {{ error_actual }}.
|
|
</div>
|
|
{% elif error == "save_ocr_corrected_failed" %}
|
|
<div class="error-box">
|
|
Could not save OCR-corrected PDF. Check that reviewed OCR line count matches raw OCR line count.
|
|
</div>
|
|
{% elif error == "rerun_ocr_failed" %}
|
|
<div class="error-box">OCR rerun failed.</div>
|
|
{% elif error == "save_field_enriched_failed" %}
|
|
<div class="error-box">Could not save field-enriched PDF.</div>
|
|
{% endif %}
|
|
|
|
<div class="detail-sticky-header">
|
|
<div class="topbar">
|
|
<div>
|
|
<h1 class="page-title">{{ document.document_id }}</h1>
|
|
<p class="page-subtitle">{{ document.original_filename or document.canonical_filename or document.document_type }}</p>
|
|
</div>
|
|
<div class="badges">
|
|
<span class="badge {% if document.review_status == 'reviewed' %}reviewed{% else %}pending{% endif %}">{{ document.review_status }}</span>
|
|
<span class="badge">{{ document.document_type }}</span>
|
|
<span class="badge">{{ document.mime_type }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card" style="margin-bottom: 0;">
|
|
<div class="button-row">
|
|
<form method="post" action="/documents/{{ document.document_id }}/rerun-ocr">
|
|
<button type="submit">Re-run OCR</button>
|
|
</form>
|
|
<form method="post" action="/documents/{{ document.document_id }}/save-ocr-corrected-pdf">
|
|
<button class="primary" type="submit">Save OCR-corrected PDF</button>
|
|
</form>
|
|
<form method="post" action="/documents/{{ document.document_id }}/save-field-enriched-pdf">
|
|
<button type="submit">Save field-enriched PDF</button>
|
|
</form>
|
|
<form method="post" action="/documents/{{ document.document_id }}/move-to-trash">
|
|
<button class="danger" type="submit">Move to trash</button>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="queue-nav-row">
|
|
{% if prev_doc %}
|
|
<a class="button-link" href="/documents/{{ prev_doc.document_id }}">← Previous</a>
|
|
{% endif %}
|
|
{% if next_doc %}
|
|
<a class="button-link" href="/documents/{{ next_doc.document_id }}">Next →</a>
|
|
{% endif %}
|
|
{% if next_ocr_doc %}
|
|
<a class="button-link" href="/documents/{{ next_ocr_doc.document_id }}">Next OCR review</a>
|
|
{% endif %}
|
|
{% if next_fields_doc %}
|
|
<a class="button-link" href="/documents/{{ next_fields_doc.document_id }}">Next field extraction</a>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="workspace-grid">
|
|
<section>
|
|
<div class="card preview-card">
|
|
<h2 class="card-title">Document preview</h2>
|
|
{% if file_url %}
|
|
{% if document.mime_type == "application/pdf" %}
|
|
<iframe class="preview-frame" src="{{ file_url }}"></iframe>
|
|
{% elif document.mime_type in ["image/jpeg", "image/png"] %}
|
|
<img class="preview-image" src="{{ file_url }}" alt="Document image">
|
|
{% else %}
|
|
<p><a href="{{ file_url }}" target="_blank">Open file</a></p>
|
|
{% endif %}
|
|
{% else %}
|
|
<p class="empty-state">No preview available.</p>
|
|
{% endif %}
|
|
</div>
|
|
</section>
|
|
|
|
<section>
|
|
<div class="card">
|
|
<div class="right-pane-tabs">
|
|
<button class="tab-button active" type="button" data-tab="ocr-review">OCR Review</button>
|
|
<button class="tab-button" type="button" data-tab="extracted-fields">Extracted Fields</button>
|
|
<button class="tab-button" type="button" data-tab="versions">Versions</button>
|
|
<button class="tab-button" type="button" data-tab="raw-ocr">Raw OCR</button>
|
|
</div>
|
|
|
|
<div class="tab-panel active" data-panel="ocr-review">
|
|
<h2 class="card-title">Reviewed OCR</h2>
|
|
{% if reviewed_ocr %}
|
|
<p>Current reviewed version saved at {{ reviewed_ocr.created_at }} — v{{ reviewed_ocr.version_number }}</p>
|
|
{% else %}
|
|
<p class="empty-state">No reviewed OCR saved yet.</p>
|
|
{% endif %}
|
|
|
|
<p>
|
|
Expected OCR lines: <span id="expected-lines">{{ expected_line_count }}</span><br>
|
|
Current editor lines: <span id="actual-lines">{{ actual_line_count }}</span><br>
|
|
<span id="line-warning" class="line-warning" {% if expected_line_count == actual_line_count %}style="display:none;"{% endif %}>
|
|
Line count mismatch may affect corrected PDF layout.
|
|
</span>
|
|
</p>
|
|
|
|
<form method="post" action="/documents/{{ document.document_id }}/review-text">
|
|
<div class="form-field full">
|
|
<label for="reviewed_text">Edit reviewed OCR text (one line per OCR line)</label>
|
|
<div class="editor-wrap">
|
|
<pre class="line-numbers" id="line-numbers">{% for n in line_numbers %}{{ n }}
|
|
{% endfor %}</pre>
|
|
<textarea id="reviewed_text" name="reviewed_text" rows="34" spellcheck="false">{{ review_text_value }}</textarea>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-field full">
|
|
<label>Quality flags</label>
|
|
<div>
|
|
{% for flag in quality_flag_options %}
|
|
<label style="display:block; margin-bottom: 0.25rem;">
|
|
<input type="checkbox" name="quality_flags" value="{{ flag }}" {% if flag in current_quality_flags %}checked{% endif %}>
|
|
{{ flag }}
|
|
</label>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-field full">
|
|
<label for="quality_note">Quality note</label>
|
|
<textarea id="quality_note" name="quality_note" rows="4">{{ current_quality_note }}</textarea>
|
|
</div>
|
|
|
|
<div class="button-row">
|
|
<button class="primary" type="submit" id="save-reviewed-btn">Save reviewed OCR</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="tab-panel" data-panel="extracted-fields">
|
|
<h2 class="card-title">Extracted fields</h2>
|
|
|
|
{% if current_extracted %}
|
|
<p>Current extracted fields last updated at {{ current_extracted.updated_at }}</p>
|
|
{% else %}
|
|
<p class="empty-state">No extracted fields saved yet.</p>
|
|
{% endif %}
|
|
|
|
<form method="get" action="/documents/{{ document.document_id }}">
|
|
<input type="hidden" name="autofill_extracted" value="1">
|
|
<div class="button-row">
|
|
<button type="submit">Auto-extract fields</button>
|
|
</div>
|
|
</form>
|
|
|
|
<form method="post" action="/documents/{{ document.document_id }}/save-extracted-fields" style="margin-top: 1rem;">
|
|
<div class="form-grid">
|
|
<div class="form-field"><label>Merchant raw</label><input type="text" name="merchant_raw" value="{{ extracted_form.merchant_raw }}"></div>
|
|
<div class="form-field"><label>Merchant normalized</label><input type="text" name="merchant_normalized" value="{{ extracted_form.merchant_normalized }}"></div>
|
|
<div class="form-field"><label>Transaction date</label><input type="date" name="transaction_date" value="{{ extracted_form.transaction_date }}"></div>
|
|
<div class="form-field"><label>Transaction time</label><input type="text" name="transaction_time" value="{{ extracted_form.transaction_time }}"></div>
|
|
<div class="form-field"><label>Subtotal</label><input type="text" name="subtotal" value="{{ extracted_form.subtotal }}"></div>
|
|
<div class="form-field"><label>Tax</label><input type="text" name="tax" value="{{ extracted_form.tax }}"></div>
|
|
<div class="form-field"><label>Total</label><input type="text" name="total" value="{{ extracted_form.total }}"></div>
|
|
<div class="form-field"><label>Currency</label><input type="text" name="currency" value="{{ extracted_form.currency }}"></div>
|
|
<div class="form-field"><label>Payment method</label><input type="text" name="payment_method" value="{{ extracted_form.payment_method }}"></div>
|
|
<div class="form-field"><label>Receipt number</label><input type="text" name="receipt_number" value="{{ extracted_form.receipt_number }}"></div>
|
|
<div class="form-field full"><label>Location</label><input type="text" name="location" value="{{ extracted_form.location }}"></div>
|
|
<div class="form-field full"><label>Counterparty</label><input type="text" name="counterparty" value="{{ extracted_form.counterparty }}"></div>
|
|
<div class="form-field full"><label>Extra JSON</label><textarea name="extra_json" rows="8">{{ extracted_form.extra_json }}</textarea></div>
|
|
</div>
|
|
|
|
<div class="button-row" style="margin-top: 1rem;">
|
|
<button class="primary" type="submit">Save extracted fields</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="tab-panel" data-panel="versions">
|
|
<h2 class="card-title">Document versions</h2>
|
|
{% if document.versions %}
|
|
<div class="table-wrap">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Version</th>
|
|
<th>Type</th>
|
|
<th>Path</th>
|
|
<th>Created</th>
|
|
<th>Notes</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for version in document.versions %}
|
|
<tr>
|
|
<td>v{{ version.version_number }}</td>
|
|
<td>{{ version.version_type }}</td>
|
|
<td>{{ version.file_path }}</td>
|
|
<td>{{ version.created_at }}</td>
|
|
<td>{{ version.notes or "" }}</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% else %}
|
|
<p class="empty-state">No versions found.</p>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div class="tab-panel" data-panel="raw-ocr">
|
|
<h2 class="card-title">Raw OCR</h2>
|
|
{% if raw_ocr %}
|
|
<div class="meta-grid">
|
|
<div class="meta-item"><span class="meta-label">Text version</span>v{{ raw_ocr.version_number }}</div>
|
|
<div class="meta-item"><span class="meta-label">OCR engine</span>{{ raw_ocr.ocr_engine or "unknown" }}</div>
|
|
<div class="meta-item"><span class="meta-label">Engine version</span>{{ raw_ocr.ocr_engine_version or "unknown" }}</div>
|
|
<div class="meta-item"><span class="meta-label">Rerun source</span>{{ raw_ocr.rerun_source or "unknown" }}</div>
|
|
<div class="meta-item"><span class="meta-label">Quality score</span>{{ raw_ocr.quality_score if raw_ocr.quality_score is not none else "not scored yet" }}</div>
|
|
<div class="meta-item"><span class="meta-label">Quality note</span>{{ raw_ocr.quality_note or "" }}</div>
|
|
</div>
|
|
<p><strong>Quality flags:</strong> {{ raw_ocr.quality_flags if raw_ocr and raw_ocr.quality_flags else [] }}</p>
|
|
<pre class="codeblock">{{ raw_ocr.text_content }}</pre>
|
|
{% else %}
|
|
<p class="empty-state">No raw OCR text found.</p>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h2 class="card-title">Metadata</h2>
|
|
<div class="meta-grid">
|
|
<div class="meta-item"><span class="meta-label">Type</span>{{ document.document_type }}</div>
|
|
<div class="meta-item"><span class="meta-label">Review status</span>{{ document.review_status }}</div>
|
|
<div class="meta-item"><span class="meta-label">Source path</span>{{ document.source_path }}</div>
|
|
<div class="meta-item"><span class="meta-label">Current path</span>{{ document.current_path }}</div>
|
|
<div class="meta-item"><span class="meta-label">Original filename</span>{{ document.original_filename }}</div>
|
|
<div class="meta-item"><span class="meta-label">Canonical filename</span>{{ document.canonical_filename }}</div>
|
|
<div class="meta-item"><span class="meta-label">MIME type</span>{{ document.mime_type }}</div>
|
|
<div class="meta-item"><span class="meta-label">File size</span>{{ document.file_size }}</div>
|
|
<div class="meta-item"><span class="meta-label">Page count</span>{{ document.page_count }}</div>
|
|
<div class="meta-item"><span class="meta-label">Share path</span>{{ document.share_path or "" }}</div>
|
|
<div class="meta-item"><span class="meta-label">Created at</span>{{ document.created_at }}</div>
|
|
<div class="meta-item"><span class="meta-label">Updated at</span>{{ document.updated_at }}</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
|
|
<script>
|
|
(function () {
|
|
const appShell = document.getElementById("app-shell");
|
|
const menuToggle = document.getElementById("menu-toggle");
|
|
if (appShell && menuToggle) {
|
|
menuToggle.addEventListener("click", function () {
|
|
appShell.classList.toggle("nav-open");
|
|
});
|
|
menuToggle.addEventListener("keydown", function (e) {
|
|
if (e.key === "Enter" || e.key === " ") {
|
|
e.preventDefault();
|
|
appShell.classList.toggle("nav-open");
|
|
}
|
|
});
|
|
}
|
|
|
|
const tabButtons = document.querySelectorAll("[data-tab]");
|
|
const tabPanels = document.querySelectorAll("[data-panel]");
|
|
tabButtons.forEach(function (btn) {
|
|
btn.addEventListener("click", function () {
|
|
const target = btn.getAttribute("data-tab");
|
|
tabButtons.forEach(function (b) { b.classList.remove("active"); });
|
|
tabPanels.forEach(function (p) { p.classList.remove("active"); });
|
|
btn.classList.add("active");
|
|
const panel = document.querySelector('[data-panel="' + target + '"]');
|
|
if (panel) panel.classList.add("active");
|
|
});
|
|
});
|
|
|
|
const textarea = document.getElementById("reviewed_text");
|
|
const expectedLinesEl = document.getElementById("expected-lines");
|
|
const actualLinesEl = document.getElementById("actual-lines");
|
|
const warningEl = document.getElementById("line-warning");
|
|
const saveBtn = document.getElementById("save-reviewed-btn");
|
|
const lineNumbersEl = document.getElementById("line-numbers");
|
|
|
|
if (textarea && expectedLinesEl && actualLinesEl && warningEl && saveBtn && lineNumbersEl) {
|
|
const expectedLines = parseInt(expectedLinesEl.textContent || "0", 10);
|
|
|
|
function countLines(text) {
|
|
if (text.length === 0) return 0;
|
|
return text.split('\n').length;
|
|
}
|
|
|
|
function rebuildLineNumbers(lineCount) {
|
|
let nums = "";
|
|
for (let i = 1; i <= lineCount; i++) nums += i + "\n";
|
|
lineNumbersEl.textContent = nums;
|
|
}
|
|
|
|
function syncScroll() {
|
|
lineNumbersEl.scrollTop = textarea.scrollTop;
|
|
}
|
|
|
|
function updateEditorState() {
|
|
const actual = countLines(textarea.value);
|
|
actualLinesEl.textContent = actual.toString();
|
|
rebuildLineNumbers(Math.max(actual, expectedLines));
|
|
const mismatch = expectedLines > 0 && actual !== expectedLines;
|
|
warningEl.style.display = mismatch ? "inline" : "none";
|
|
saveBtn.disabled = mismatch;
|
|
syncScroll();
|
|
}
|
|
|
|
textarea.addEventListener("input", updateEditorState);
|
|
textarea.addEventListener("scroll", syncScroll);
|
|
|
|
updateEditorState();
|
|
syncScroll();
|
|
}
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|