Files
gh-christianlouis-docuelevate/frontend/templates/file_view.html
T

1575 lines
60 KiB
HTML

{% extends "base.html" %}
{% block title %}{{ file.document_title or file.original_filename or 'Document' }} - DocuElevate{% endblock %}
{% block head_extra %}
<script src="/static/js/common.js"></script>
<!-- PDF.js library (Apache 2.0 License - compatible) -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
<script>
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
</script>
<style>
.doc-container { max-width: 1200px; margin: 0 auto; }
/* ── header ── */
.doc-header {
display: flex; align-items: flex-start; justify-content: space-between;
gap: 1rem; margin-bottom: 1.5rem; flex-wrap: wrap;
}
.doc-title { font-size: 1.75rem; font-weight: 700; color: #1f2937; line-height: 1.25; }
.doc-subtitle { font-size: 0.9rem; color: #6b7280; margin-top: 0.25rem; font-family: monospace; }
/* ── status pill ── */
.status-pill {
display: inline-flex; align-items: center; gap: 0.4rem;
padding: 0.35rem 0.85rem; border-radius: 9999px;
font-size: 0.8rem; font-weight: 600; white-space: nowrap;
}
.pill-completed { background: #d1fae5; color: #065f46; }
.pill-failed { background: #fee2e2; color: #991b1b; }
.pill-processing { background: #dbeafe; color: #1e3a8a; }
.pill-pending { background: #fef3c7; color: #92400e; }
.pill-duplicate { background: #e5e7eb; color: #374151; }
/* ── cards ── */
.doc-card {
background: white; border-radius: 0.5rem;
box-shadow: 0 1px 3px rgba(0,0,0,.08);
padding: 1.5rem; margin-bottom: 1.25rem;
}
.doc-card-title {
font-size: 1rem; font-weight: 700; color: #374151;
text-transform: uppercase; letter-spacing: 0.05em;
margin-bottom: 1rem;
}
/* ── two-column layout ── */
.doc-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1.25rem; }
@media (max-width: 768px) { .doc-grid { grid-template-columns: 1fr; } }
/* ── metadata table ── */
.meta-row { display: flex; gap: 0.5rem; padding: 0.5rem 0; border-bottom: 1px solid #f3f4f6; align-items: baseline; }
.meta-row:last-child { border-bottom: none; }
.meta-label { font-size: 0.75rem; font-weight: 600; color: #6b7280; text-transform: uppercase; letter-spacing: 0.05em; min-width: 110px; flex-shrink: 0; }
.meta-value { font-size: 0.9rem; color: #1f2937; word-break: break-word; }
/* ── tags ── */
.tag-pill { display: inline-block; padding: 0.15rem 0.6rem; border-radius: 9999px; font-size: 0.75rem; font-weight: 500; background: #f0fdf4; color: #15803d; margin: 0.1rem; }
/* ── pdf.js viewer ── */
.pdf-viewer { position: relative; background: #525659; border-radius: 0.375rem; overflow: hidden; }
.pdf-controls {
display: flex; align-items: center; justify-content: center; gap: 0.75rem;
padding: 0.5rem; background: #f1f5f9; border-radius: 0.375rem 0.375rem 0 0;
}
.pdf-nav-btn {
padding: 0.35rem 0.75rem; border: 1px solid #cbd5e1; border-radius: 0.25rem;
background: white; color: #374151; font-size: 0.8rem; cursor: pointer;
min-height: 36px; min-width: 36px;
}
.pdf-nav-btn:disabled { opacity: 0.4; cursor: default; }
.pdf-nav-btn:hover:not(:disabled) { background: #e2e8f0; }
.pdf-page-info { font-size: 0.8rem; color: #374151; font-weight: 600; min-width: 100px; text-align: center; }
.pdf-canvas-wrap { display: flex; justify-content: center; padding: 0.75rem; min-height: 400px; max-height: 700px; overflow: auto; }
.pdf-canvas-wrap canvas { max-width: 100%; height: auto; }
/* ── image viewer with zoom/pan ── */
.img-viewer { position: relative; background: #f1f5f9; border-radius: 0.375rem; overflow: hidden; }
.img-controls {
display: flex; align-items: center; justify-content: center; gap: 0.5rem;
padding: 0.5rem; background: #f1f5f9; border-radius: 0.375rem 0.375rem 0 0;
}
.img-controls button {
padding: 0.35rem 0.6rem; border: 1px solid #cbd5e1; border-radius: 0.25rem;
background: white; color: #374151; font-size: 0.85rem; cursor: pointer;
min-height: 36px; min-width: 36px;
}
.img-controls button:hover { background: #e2e8f0; }
.img-wrap {
display: flex; justify-content: center; align-items: center;
overflow: auto; max-height: 650px; min-height: 300px; cursor: grab;
padding: 0.5rem; background: #525659;
}
.img-wrap.dragging { cursor: grabbing; }
.img-wrap img { transition: transform 0.15s ease; transform-origin: center center; }
/* ── text viewer ── */
.text-viewer { border-radius: 0.375rem; overflow: hidden; }
.text-viewer-header {
display: flex; align-items: center; justify-content: space-between;
padding: 0.5rem 0.75rem; background: #f1f5f9; border-radius: 0.375rem 0.375rem 0 0;
}
.text-viewer-header span { font-size: 0.8rem; color: #374151; font-weight: 600; }
.text-content {
background: #1e293b; color: #e2e8f0; padding: 1rem; font-family: 'SFMono-Regular', Consolas, monospace;
font-size: 0.82rem; line-height: 1.65; max-height: 600px; overflow: auto; white-space: pre-wrap; word-break: break-word;
tab-size: 4;
}
.text-content .line-number {
display: inline-block; width: 3.5em; color: #64748b; text-align: right;
margin-right: 1em; user-select: none; font-size: 0.75rem;
}
/* ── preview iframe fallback ── */
.preview-frame { width: 100%; height: 600px; border: none; border-radius: 0.375rem; background: #f1f5f9; display: block; }
/* ── text box ── */
.ocr-text {
background: #1a202c; color: #e2e8f0; padding: 1rem 1.25rem;
border-radius: 0.375rem; font-size: 0.8rem; font-family: monospace;
line-height: 1.6; max-height: 400px; overflow-y: auto;
white-space: pre-wrap; word-break: break-word;
}
.text-toggle {
cursor: pointer; color: #3b82f6; font-size: 0.875rem;
font-weight: 600; display: inline-flex; align-items: center; gap: 0.4rem;
background: none; border: none; padding: 0;
}
.text-toggle:hover { color: #2563eb; }
/* ── action buttons ── */
.action-btn {
display: inline-flex; align-items: center; gap: 0.4rem;
padding: 0.5rem 1.1rem; border-radius: 0.375rem;
font-size: 0.875rem; font-weight: 600; text-decoration: none;
cursor: pointer; border: none; transition: filter 0.1s;
}
.action-btn:hover { filter: brightness(0.92); }
.btn-primary { background: #3b82f6; color: white; }
.btn-secondary { background: #f3f4f6; color: #374151; }
.btn-green { background: #10b981; color: white; }
.btn-process { background: #6366f1; color: white; }
/* ── detail rows ── */
.info-row { display: flex; justify-content: space-between; padding: 0.4rem 0; border-bottom: 1px solid #f3f4f6; font-size: 0.875rem; }
.info-row:last-child { border-bottom: none; }
.info-key { color: #6b7280; }
.info-val { color: #1f2937; font-family: monospace; text-align: right; word-break: break-all; max-width: 60%; }
/* ── error ── */
.error-box { background: #fee2e2; border: 1px solid #f87171; color: #b91c1c; padding: 1rem; border-radius: 0.375rem; margin-bottom: 1rem; }
/* ── Comments panel ───────────────────────────────────────────────────── */
.comments-panel, .annotations-panel {
margin-top: 0;
}
.panel-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
}
.panel-header h3 {
margin-bottom: 0;
}
.comments-empty, .annotations-empty {
text-align: center;
padding: 2rem;
color: #718096;
}
.comments-loading, .annotations-loading {
text-align: center;
padding: 1.5rem;
color: #718096;
}
.comments-error {
text-align: center;
padding: 1rem;
color: #991B1B;
}
/* Individual comment */
.comment-item {
background-color: #f7fafc;
border-radius: 0.5rem;
padding: 1rem;
margin-bottom: 0.75rem;
border-left: 3px solid #4299e1;
}
.dark .comment-item {
background-color: #2d3748;
border-left-color: #63b3ed;
}
.comment-item.comment-reply {
margin-left: 1.5rem;
border-left-color: #a0aec0;
background-color: #edf2f7;
}
.dark .comment-item.comment-reply {
background-color: #1a202c;
border-left-color: #4a5568;
}
.comment-item.comment-resolved {
opacity: 0.75;
border-left-color: #48bb78;
}
.comment-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
flex-wrap: wrap;
}
.comment-author {
font-weight: 600;
color: #2d3748;
font-size: 0.875rem;
}
.dark .comment-author {
color: #e2e8f0;
}
.comment-time {
font-size: 0.75rem;
color: #718096;
}
.comment-resolved-badge {
font-size: 0.75rem;
color: #065F46;
background-color: #D1FAE5;
padding: 0.125rem 0.5rem;
border-radius: 9999px;
font-weight: 600;
}
.dark .comment-resolved-badge {
background-color: #065F46;
color: #D1FAE5;
}
.comment-body {
color: #4a5568;
font-size: 0.9375rem;
line-height: 1.5;
margin-bottom: 0.5rem;
word-break: break-word;
}
.dark .comment-body {
color: #cbd5e0;
}
.comment-mention {
color: #3182ce;
font-weight: 600;
}
.dark .comment-mention {
color: #63b3ed;
}
.comment-actions {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.comment-action-btn {
background: none;
border: 1px solid #e2e8f0;
color: #718096;
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 0.25rem;
min-height: 28px;
}
.comment-action-btn:hover {
background-color: #edf2f7;
color: #2d3748;
}
.dark .comment-action-btn {
border-color: #4a5568;
color: #a0aec0;
}
.dark .comment-action-btn:hover {
background-color: #4a5568;
color: #e2e8f0;
}
.comment-action-btn--danger:hover {
background-color: #FEE2E2;
color: #991B1B;
border-color: #f56565;
}
.dark .comment-action-btn--danger:hover {
background-color: #742a2a;
color: #feb2b2;
border-color: #f56565;
}
.comment-replies {
margin-top: 0.75rem;
}
/* Comment form */
.comment-form-wrapper {
margin-top: 1rem;
}
.comment-textarea {
width: 100%;
border: 1px solid #e2e8f0;
border-radius: 0.375rem;
padding: 0.75rem;
font-size: 0.875rem;
resize: vertical;
min-height: 60px;
font-family: inherit;
}
.dark .comment-textarea {
background-color: #2d3748;
border-color: #4a5568;
color: #e2e8f0;
}
.comment-textarea:focus {
outline: none;
border-color: #4299e1;
box-shadow: 0 0 0 2px rgba(66, 153, 225, 0.3);
}
.comment-submit-btn {
background-color: #4299e1;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 0.375rem;
font-size: 0.875rem;
cursor: pointer;
font-weight: 600;
margin-top: 0.5rem;
min-height: 36px;
}
.comment-submit-btn:hover {
background-color: #3182ce;
}
.comment-cancel-btn {
background-color: #e2e8f0;
color: #4a5568;
border: none;
padding: 0.5rem 1rem;
border-radius: 0.375rem;
font-size: 0.875rem;
cursor: pointer;
margin-top: 0.5rem;
margin-left: 0.5rem;
min-height: 36px;
}
.dark .comment-cancel-btn {
background-color: #4a5568;
color: #e2e8f0;
}
.comment-edit-btns, .annotation-edit-btns {
display: flex;
gap: 0.5rem;
margin-top: 0.5rem;
}
.comment-reply-form {
margin-top: 0.75rem;
padding-top: 0.75rem;
border-top: 1px solid #e2e8f0;
}
.dark .comment-reply-form {
border-top-color: #4a5568;
}
/* @mention dropdown */
.mention-dropdown-wrapper {
position: relative;
}
#mention-dropdown {
position: absolute;
bottom: 100%;
left: 0;
width: 280px;
max-height: 200px;
overflow-y: auto;
background-color: white;
border: 1px solid #e2e8f0;
border-radius: 0.375rem;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 50;
margin-bottom: 0.25rem;
}
.dark #mention-dropdown {
background-color: #2d3748;
border-color: #4a5568;
}
.mention-item {
display: flex;
flex-direction: column;
width: 100%;
text-align: left;
padding: 0.5rem 0.75rem;
border: none;
background: none;
cursor: pointer;
font-size: 0.875rem;
min-height: 44px;
justify-content: center;
}
.mention-item:hover, .mention-item:focus {
background-color: #edf2f7;
outline: none;
}
.dark .mention-item:hover, .dark .mention-item:focus {
background-color: #4a5568;
}
.mention-user-id {
font-weight: 600;
color: #2d3748;
}
.dark .mention-user-id {
color: #e2e8f0;
}
.mention-display-name {
font-size: 0.75rem;
color: #718096;
}
/* ── Annotations panel ────────────────────────────────────────────────── */
.annotation-item {
background-color: #f7fafc;
border-radius: 0.5rem;
padding: 1rem;
margin-bottom: 0.75rem;
border-left: 3px solid #ecc94b;
}
.dark .annotation-item {
background-color: #2d3748;
border-left-color: #d69e2e;
}
.annotation-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
flex-wrap: wrap;
}
.annotation-type {
font-size: 0.75rem;
font-weight: 600;
padding: 0.125rem 0.5rem;
border-radius: 9999px;
text-transform: capitalize;
}
.annotation-type--note {
background-color: #DBEAFE;
color: #1E3A8A;
}
.annotation-type--highlight {
background-color: #FEF3C7;
color: #92400E;
}
.annotation-type--underline {
background-color: #D1FAE5;
color: #065F46;
}
.annotation-type--strikethrough {
background-color: #FEE2E2;
color: #991B1B;
}
.dark .annotation-type--note {
background-color: #1E3A8A;
color: #DBEAFE;
}
.dark .annotation-type--highlight {
background-color: #92400E;
color: #FEF3C7;
}
.dark .annotation-type--underline {
background-color: #065F46;
color: #D1FAE5;
}
.dark .annotation-type--strikethrough {
background-color: #991B1B;
color: #FEE2E2;
}
.annotation-color-dot {
display: inline-block;
width: 12px;
height: 12px;
border-radius: 50%;
border: 1px solid rgba(0, 0, 0, 0.2);
}
.annotation-page {
font-size: 0.75rem;
color: #718096;
}
.annotation-content {
color: #4a5568;
font-size: 0.9375rem;
line-height: 1.5;
margin-bottom: 0.5rem;
word-break: break-word;
}
.dark .annotation-content {
color: #cbd5e0;
}
.annotation-meta {
display: flex;
gap: 0.75rem;
font-size: 0.75rem;
color: #718096;
margin-bottom: 0.5rem;
}
.annotation-author {
font-weight: 600;
}
.annotation-actions {
display: flex;
gap: 0.5rem;
}
.annotation-action-btn {
background: none;
border: 1px solid #e2e8f0;
color: #718096;
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
cursor: pointer;
min-height: 28px;
}
.annotation-action-btn:hover {
background-color: #edf2f7;
color: #2d3748;
}
.dark .annotation-action-btn {
border-color: #4a5568;
color: #a0aec0;
}
.dark .annotation-action-btn:hover {
background-color: #4a5568;
color: #e2e8f0;
}
.annotation-action-btn--danger:hover {
background-color: #FEE2E2;
color: #991B1B;
border-color: #f56565;
}
.dark .annotation-action-btn--danger:hover {
background-color: #742a2a;
color: #feb2b2;
}
.annotation-textarea {
width: 100%;
border: 1px solid #e2e8f0;
border-radius: 0.375rem;
padding: 0.75rem;
font-size: 0.875rem;
resize: vertical;
min-height: 60px;
font-family: inherit;
}
.dark .annotation-textarea {
background-color: #2d3748;
border-color: #4a5568;
color: #e2e8f0;
}
.annotation-textarea:focus {
outline: none;
border-color: #ecc94b;
box-shadow: 0 0 0 2px rgba(236, 201, 75, 0.3);
}
.annotation-select {
border: 1px solid #e2e8f0;
border-radius: 0.375rem;
padding: 0.5rem;
font-size: 0.875rem;
margin-top: 0.5rem;
width: 100%;
}
.dark .annotation-select {
background-color: #2d3748;
border-color: #4a5568;
color: #e2e8f0;
}
.annotation-submit-btn {
background-color: #ecc94b;
color: #744210;
border: none;
padding: 0.5rem 1rem;
border-radius: 0.375rem;
font-size: 0.875rem;
cursor: pointer;
font-weight: 600;
margin-top: 0.5rem;
min-height: 36px;
}
.annotation-submit-btn:hover {
background-color: #d69e2e;
}
.annotation-cancel-btn {
background-color: #e2e8f0;
color: #4a5568;
border: none;
padding: 0.5rem 1rem;
border-radius: 0.375rem;
font-size: 0.875rem;
cursor: pointer;
margin-top: 0.5rem;
margin-left: 0.5rem;
min-height: 36px;
}
.dark .annotation-cancel-btn {
background-color: #4a5568;
color: #e2e8f0;
}
/* Annotation form layout */
.annotation-form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.75rem;
margin-top: 0.75rem;
}
.annotation-form-grid .form-group {
display: flex;
flex-direction: column;
}
.annotation-form-grid label {
font-size: 0.75rem;
font-weight: 600;
color: #4a5568;
margin-bottom: 0.25rem;
}
.dark .annotation-form-grid label {
color: #a0aec0;
}
.annotation-form-grid input,
.annotation-form-grid select {
border: 1px solid #e2e8f0;
border-radius: 0.375rem;
padding: 0.5rem;
font-size: 0.875rem;
}
.dark .annotation-form-grid input,
.dark .annotation-form-grid select {
background-color: #2d3748;
border-color: #4a5568;
color: #e2e8f0;
}
/* ── Collaboration panels grid ────────────────────────────────────────── */
.collab-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.5rem;
}
@media (max-width: 768px) {
.collab-grid {
grid-template-columns: 1fr;
}
}
</style>
{% endblock %}
{% block content %}
<div class="doc-container">
{% if error %}
<div class="error-box"><strong>Error:</strong> {{ error }}</div>
{% elif file %}
<!-- ── Back + header ── -->
<a href="/files" style="display:inline-flex;align-items:center;gap:0.4rem;color:#3b82f6;font-weight:600;text-decoration:none;margin-bottom:1.25rem;" aria-label="Back to Files">
<i class="fas fa-arrow-left" aria-hidden="true"></i> Back to Files
</a>
<div class="doc-header">
<div>
<div class="doc-title">
{% if gpt_metadata and gpt_metadata.filename %}
{{ gpt_metadata.filename | replace('.pdf','') | replace('_',' ') }}
{% elif file.document_title %}
{{ file.document_title }}
{% else %}
{{ file.original_filename or '(untitled)' }}
{% endif %}
</div>
<div class="doc-subtitle">{{ file.original_filename }}</div>
</div>
<div style="display:flex;align-items:center;gap:0.75rem;flex-wrap:wrap;">
<!-- Status pill -->
{% if file.is_duplicate %}
<span class="status-pill pill-duplicate"><i class="fas fa-copy" aria-hidden="true"></i> Duplicate</span>
{% elif step_summary %}
{% set main_completed = step_summary.main.success + step_summary.main.skipped %}
{% if step_summary.main.failure > 0 or step_summary.uploads.failure > 0 %}
<span class="status-pill pill-failed"><i class="fas fa-times-circle" aria-hidden="true"></i> Failed</span>
{% elif step_summary.main["in_progress"] > 0 or step_summary.uploads["in_progress"] > 0 %}
<span class="status-pill pill-processing"><i class="fas fa-circle-notch fa-spin" aria-hidden="true"></i> Processing</span>
{% elif step_summary.total_main_steps > 0 and main_completed == step_summary.total_main_steps and step_summary.main.failure == 0 %}
<span class="status-pill pill-completed"><i class="fas fa-check-circle" aria-hidden="true"></i> Completed</span>
{% else %}
<span class="status-pill pill-pending"><i class="fas fa-pause-circle" aria-hidden="true"></i> Pending</span>
{% endif %}
{% endif %}
<!-- Process detail link -->
<a href="/files/{{ file.id }}/detail" class="action-btn btn-process">
<i class="fas fa-cogs" aria-hidden="true"></i> Processing Details
</a>
</div>
</div>
<!-- ── Main two-column grid ── -->
<div class="doc-grid">
<!-- LEFT: Metadata -->
<div>
{% if gpt_metadata %}
<div class="doc-card">
<div class="doc-card-title"><i class="fas fa-tags" aria-hidden="true" style="color:#3b82f6;margin-right:0.4rem;"></i>Document Metadata</div>
{% if gpt_metadata.document_type %}
<div class="meta-row">
<span class="meta-label">Type</span>
<span class="meta-value">{{ gpt_metadata.document_type }}</span>
</div>
{% endif %}
{% if gpt_metadata.date %}
<div class="meta-row">
<span class="meta-label">Date</span>
<span class="meta-value">{{ gpt_metadata.date }}</span>
</div>
{% endif %}
{% if gpt_metadata.absender %}
<div class="meta-row">
<span class="meta-label">Sender</span>
<span class="meta-value">{{ gpt_metadata.absender }}</span>
</div>
{% endif %}
{% if gpt_metadata.empfaenger %}
<div class="meta-row">
<span class="meta-label">Recipient</span>
<span class="meta-value">{{ gpt_metadata.empfaenger }}</span>
</div>
{% endif %}
{% if gpt_metadata.betrag %}
<div class="meta-row">
<span class="meta-label">Amount</span>
<span class="meta-value">{{ gpt_metadata.betrag }}</span>
</div>
{% endif %}
{% if gpt_metadata.kontonummer %}
<div class="meta-row">
<span class="meta-label">Account</span>
<span class="meta-value" style="font-family:monospace;">{{ gpt_metadata.kontonummer }}</span>
</div>
{% endif %}
{% if gpt_metadata.tags %}
<div class="meta-row" style="align-items:flex-start;">
<span class="meta-label" style="margin-top:0.2rem;">Tags</span>
<span class="meta-value">
{% if gpt_metadata.tags is string %}
{% for t in gpt_metadata.tags.split(',') %}
<span class="tag-pill">{{ t.strip() }}</span>
{% endfor %}
{% else %}
{% for t in gpt_metadata.tags %}
<span class="tag-pill">{{ t }}</span>
{% endfor %}
{% endif %}
</span>
</div>
{% endif %}
{% if gpt_metadata.filename %}
<div class="meta-row">
<span class="meta-label">Suggested name</span>
<span class="meta-value" style="font-family:monospace;font-size:0.8rem;">{{ gpt_metadata.filename }}</span>
</div>
{% endif %}
</div>
{% endif %}
<!-- File info -->
<div class="doc-card">
<div class="doc-card-title"><i class="fas fa-info-circle" aria-hidden="true" style="color:#6b7280;margin-right:0.4rem;"></i>File Info</div>
<div class="info-row">
<span class="info-key">Uploaded</span>
<span class="info-val">{{ file.created_at.strftime('%Y-%m-%d %H:%M') if file.created_at else 'N/A' }}</span>
</div>
<div class="info-row">
<span class="info-key">Size</span>
<span class="info-val">{{ (file.file_size / 1024) | round(1) }} KB</span>
</div>
<div class="info-row">
<span class="info-key">MIME type</span>
<span class="info-val">{{ file.mime_type or 'N/A' }}</span>
</div>
<div class="info-row">
<span class="info-key">ID</span>
<span class="info-val">{{ file.id }}</span>
</div>
<div class="info-row">
<span class="info-key">Hash</span>
<span class="info-val" style="font-size:0.7rem;">{{ file.filehash[:32] }}…</span>
</div>
<div class="info-row">
<span class="info-key">Pipeline</span>
<span class="info-val">
{% if pipeline_info %}
<a href="/pipelines" style="text-decoration:none;color:inherit;font-weight:500;" aria-label="View pipeline management for {{ pipeline_info.name }}">{{ pipeline_info.name }}</a>
{% if not pipeline_info.is_explicit %}
<span style="font-size:0.7em;color:#7c3aed;" title="System default pipeline applied automatically">(default)</span>
{% elif not pipeline_info.is_system %}
<span style="font-size:0.7em;color:#059669;" title="Custom user-defined pipeline">(custom)</span>
{% endif %}
{% else %}
<span style="color:#9ca3af;font-style:italic;">Standard</span>
{% endif %}
</span>
</div>
</div>
<!-- Actions -->
<div class="doc-card">
<div class="doc-card-title"><i class="fas fa-bolt" aria-hidden="true" style="color:#f59e0b;margin-right:0.4rem;"></i>Actions</div>
<div style="display:flex;flex-wrap:wrap;gap:0.6rem;">
{% if processed_file_exists %}
<a href="/api/files/{{ file.id }}/download?version=processed" class="action-btn btn-primary">
<i class="fas fa-download" aria-hidden="true"></i> Download (processed)
</a>
{% endif %}
{% if original_file_exists %}
<a href="/api/files/{{ file.id }}/download?version=original" class="action-btn btn-secondary">
<i class="fas fa-download" aria-hidden="true"></i> Download (original)
</a>
{% endif %}
{% if processed_file_exists %}
<a href="/api/files/{{ file.id }}/preview?version=processed" target="_blank" class="action-btn btn-secondary">
<i class="fas fa-external-link-alt" aria-hidden="true"></i> Open processed
</a>
{% elif original_file_exists %}
<a href="/api/files/{{ file.id }}/preview?version=original" target="_blank" class="action-btn btn-secondary">
<i class="fas fa-external-link-alt" aria-hidden="true"></i> Open original
</a>
{% endif %}
<a href="/shared-links?file_id={{ file.id }}" class="action-btn btn-secondary">
<i class="fas fa-share-alt" aria-hidden="true"></i> Share
</a>
</div>
</div>
</div>
<!-- RIGHT: Document preview -->
<div>
<div class="doc-card" style="padding:1rem;">
{% set preview_mime = file.mime_type or 'application/pdf' %}
{% set preview_version = 'processed' if processed_file_exists else 'original' %}
{% set has_preview = processed_file_exists or original_file_exists %}
{% if preview_mime.startswith('image/') %}
<div class="doc-card-title"><i class="fas fa-image" aria-hidden="true" style="color:#8b5cf6;margin-right:0.4rem;"></i>Preview</div>
{% elif preview_mime.startswith('text/') %}
<div class="doc-card-title"><i class="fas fa-file-code" aria-hidden="true" style="color:#059669;margin-right:0.4rem;"></i>Preview</div>
{% else %}
<div class="doc-card-title"><i class="fas fa-file-pdf" aria-hidden="true" style="color:#ef4444;margin-right:0.4rem;"></i>Preview</div>
{% endif %}
{% if has_preview %}
{% if preview_mime.startswith('image/') %}
<!-- ── Image viewer with zoom / pan ── -->
<div class="img-viewer" id="img-viewer">
<div class="img-controls">
<button onclick="imgZoom(1.25)" title="Zoom in" aria-label="Zoom in"><i class="fas fa-search-plus" aria-hidden="true"></i></button>
<button onclick="imgZoom(0.8)" title="Zoom out" aria-label="Zoom out"><i class="fas fa-search-minus" aria-hidden="true"></i></button>
<button onclick="imgReset()" title="Reset zoom" aria-label="Reset zoom"><i class="fas fa-expand" aria-hidden="true"></i></button>
<span id="img-zoom-level" style="font-size:0.8rem;color:#6b7280;min-width:3em;text-align:center;">100%</span>
</div>
<div class="img-wrap" id="img-wrap">
<img
id="preview-img"
src="/api/files/{{ file.id }}/preview?version={{ preview_version }}"
alt="{{ file.original_filename }}"
draggable="false"
style="max-width:100%;height:auto;">
</div>
</div>
{% elif preview_mime.startswith('text/') %}
<!-- ── Text file viewer with line numbers ── -->
<div class="text-viewer" id="text-viewer">
<div class="text-viewer-header">
<span><i class="fas fa-file-code" aria-hidden="true" style="margin-right:0.3rem;"></i>{{ file.original_filename }}</span>
<button class="text-toggle" onclick="copyTextPreview()" id="text-copy-btn" aria-label="Copy file text">
<i class="fas fa-copy" aria-hidden="true"></i> Copy
</button>
</div>
<div class="text-content" id="text-preview-content" aria-live="polite">
<div style="text-align:center;padding:2rem;color:#94a3b8;"><i class="fas fa-spinner fa-spin" aria-hidden="true"></i> Loading…</div>
</div>
</div>
{% else %}
<!-- ── PDF viewer (pdf.js) ── -->
<div class="pdf-viewer" id="pdf-viewer">
<div class="pdf-controls">
<button class="pdf-nav-btn" onclick="pdfChangePage(-1)" id="pdf-prev-btn" aria-label="Previous page"><i class="fas fa-chevron-left" aria-hidden="true"></i></button>
<span class="pdf-page-info" id="pdf-page-info" aria-live="polite">Loading…</span>
<button class="pdf-nav-btn" onclick="pdfChangePage(1)" id="pdf-next-btn" aria-label="Next page"><i class="fas fa-chevron-right" aria-hidden="true"></i></button>
</div>
<div class="pdf-canvas-wrap" id="pdf-canvas-wrap">
<div style="text-align:center;padding:3rem;color:#94a3b8;">
<i class="fas fa-spinner fa-spin" aria-hidden="true" style="font-size:2rem;margin-bottom:0.75rem;"></i>
<p>Loading PDF…</p>
</div>
</div>
</div>
{% endif %}
<div style="font-size:0.75rem;color:#9ca3af;margin-top:0.4rem;text-align:right;">{{ 'Processed' if processed_file_exists else 'Original' }} version</div>
{% else %}
<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:300px;background:#f9fafb;border-radius:0.375rem;color:#9ca3af;">
<i class="fas fa-file" aria-hidden="true" style="font-size:3rem;margin-bottom:1rem;opacity:0.4;"></i>
<p>No file available for preview</p>
</div>
{% endif %}
</div>
</div>
</div>
<!-- ── Extracted text ── -->
{% if file.ocr_text %}
<div class="doc-card">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.75rem;">
<div class="doc-card-title" style="margin:0;"><i class="fas fa-align-left" aria-hidden="true" style="color:#8b5cf6;margin-right:0.4rem;"></i>Extracted Text</div>
<div style="display:flex;gap:0.5rem;">
<button class="text-toggle" onclick="toggleOcrText()" aria-expanded="false" aria-controls="ocr-text-block">
<i id="ocr-toggle-icon" class="fas fa-chevron-down" aria-hidden="true"></i>
<span id="ocr-toggle-label">Show text</span>
</button>
<button class="text-toggle" onclick="copyOcrText()" id="ocr-copy-btn" style="color:#10b981;" aria-label="Copy extracted text">
<i class="fas fa-copy" aria-hidden="true"></i> Copy
</button>
</div>
</div>
{% if file.detected_language %}
<div style="font-size:0.75rem;color:#6b7280;margin-bottom:0.5rem;">
<i class="fas fa-globe" aria-hidden="true" style="margin-right:0.25rem;"></i>
Detected language: <strong>{{ file.detected_language }}</strong>
</div>
{% endif %}
<div id="ocr-text-block" style="display:none;">
<pre class="ocr-text" id="ocr-text-content">{{ file.ocr_text }}</pre>
</div>
</div>
<!-- ── Default-language translation ── -->
{% if file.default_language_text %}
<div class="doc-card">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.75rem;">
<div class="doc-card-title" style="margin:0;">
<i class="fas fa-language" aria-hidden="true" style="color:#3b82f6;margin-right:0.4rem;"></i>
Default Language Version
{% if file.default_language_code %}
<span style="font-size:0.75rem;color:#6b7280;font-weight:normal;margin-left:0.5rem;">({{ file.default_language_code }})</span>
{% endif %}
</div>
<div style="display:flex;gap:0.5rem;">
<button class="text-toggle" onclick="toggleDefaultLangText()" aria-expanded="false" aria-controls="default-lang-text-block">
<i id="default-lang-toggle-icon" class="fas fa-chevron-down" aria-hidden="true"></i>
<span id="default-lang-toggle-label">Show text</span>
</button>
<button class="text-toggle" onclick="copyDefaultLangText()" id="default-lang-copy-btn" style="color:#10b981;" aria-label="Copy default language text">
<i class="fas fa-copy" aria-hidden="true"></i> Copy
</button>
</div>
</div>
<div id="default-lang-text-block" style="display:none;">
<pre class="ocr-text" id="default-lang-text-content">{{ file.default_language_text }}</pre>
</div>
</div>
{% elif file.detected_language %}
<!-- Translation pending or document already in default language -->
<div class="doc-card">
<div style="display:flex;justify-content:space-between;align-items:center;">
<div class="doc-card-title" style="margin:0;">
<i class="fas fa-language" aria-hidden="true" style="color:#3b82f6;margin-right:0.4rem;"></i>
Default Language Version
</div>
<button class="action-btn btn-secondary" onclick="loadDefaultLangText({{ file.id }})">
<i class="fas fa-language" aria-hidden="true"></i> Load translation
</button>
</div>
<div id="default-lang-load-area" style="margin-top:0.75rem;"></div>
</div>
{% endif %}
<!-- ── On-the-fly translation ── -->
{% if file.ocr_text %}
<div class="doc-card">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.75rem;">
<div class="doc-card-title" style="margin:0;">
<i class="fas fa-exchange-alt" aria-hidden="true" style="color:#f59e0b;margin-right:0.4rem;"></i>
Translate to Another Language
</div>
</div>
<div style="display:flex;gap:0.5rem;align-items:center;flex-wrap:wrap;">
<label for="translate-lang-select" class="sr-only">Target language</label>
<select id="translate-lang-select" style="padding:0.4rem 0.6rem;border:1px solid #d1d5db;border-radius:0.375rem;font-size:0.85rem;min-width:160px;" aria-label="Select target language for translation">
<option value="">Select language…</option>
<option value="en">English</option>
<option value="de">Deutsch</option>
<option value="fr">Français</option>
<option value="es">Español</option>
<option value="it">Italiano</option>
<option value="pt">Português</option>
<option value="nl">Nederlands</option>
<option value="pl">Polski</option>
<option value="ru">Русский</option>
<option value="zh">中文</option>
<option value="ja">日本語</option>
<option value="ko">한국어</option>
<option value="ar">العربية</option>
<option value="hi">हिन्दी</option>
<option value="tr">Türkçe</option>
<option value="sv">Svenska</option>
<option value="da">Dansk</option>
<option value="no">Norsk</option>
<option value="fi">Suomi</option>
<option value="cs">Čeština</option>
<option value="ro">Română</option>
<option value="uk">Українська</option>
</select>
<button class="action-btn btn-secondary" onclick="translateOnTheFly({{ file.id }})" id="translate-btn" style="min-height:44px;min-width:44px;">
<i class="fas fa-language" aria-hidden="true"></i> Translate
</button>
<button class="text-toggle" onclick="copyTranslatedText()" id="translate-copy-btn" style="color:#10b981;display:none;" aria-label="Copy translated text">
<i class="fas fa-copy" aria-hidden="true"></i> Copy
</button>
</div>
<div id="translate-result-area" style="margin-top:0.75rem;"></div>
</div>
{% endif %}
{% elif processed_file_exists or original_file_exists %}
<div class="doc-card">
<div style="display:flex;justify-content:space-between;align-items:center;">
<div class="doc-card-title" style="margin:0;"><i class="fas fa-align-left" aria-hidden="true" style="color:#8b5cf6;margin-right:0.4rem;"></i>Extracted Text</div>
<button class="action-btn btn-secondary" onclick="loadText({{ file.id }})">
<i class="fas fa-file-alt" aria-hidden="true"></i> Extract &amp; show text
</button>
</div>
<div id="text-load-area" style="margin-top:0.75rem;"></div>
</div>
{% endif %}
<!-- ── Comments & Annotations ──────────────────────────────────────────── -->
<div class="doc-card">
<div class="collab-grid">
<!-- Comments Panel -->
<section class="comments-panel" aria-label="{{ _('comments.heading') }}">
<div class="panel-header">
<h3><i class="fas fa-comments" aria-hidden="true"></i> {{ _("comments.heading") }}</h3>
</div>
<div id="comments-list" aria-live="polite"></div>
<!-- New comment form -->
<div class="comment-form-wrapper">
<form id="comment-form" aria-label="{{ _('comments.add_comment') }}">
<div class="mention-dropdown-wrapper">
<div id="mention-dropdown" class="hidden" role="listbox" aria-label="{{ _('comments.mention_users') }}"></div>
<textarea
id="comment-input"
class="comment-textarea"
placeholder="{{ _('comments.body_placeholder') }}"
rows="3"
aria-label="{{ _('comments.body_placeholder') }}"
maxlength="10000"
></textarea>
</div>
<button type="submit" class="comment-submit-btn">
<i class="fas fa-paper-plane" aria-hidden="true"></i> {{ _("comments.add_comment") }}
</button>
</form>
</div>
</section>
<!-- Annotations Panel -->
<section class="annotations-panel" aria-label="{{ _('annotations.heading') }}">
<div class="panel-header">
<h3><i class="fas fa-sticky-note" aria-hidden="true"></i> {{ _("annotations.heading") }}</h3>
</div>
<div id="annotations-list" aria-live="polite"></div>
<!-- New annotation form -->
<div class="comment-form-wrapper">
<form id="annotation-form" aria-label="{{ _('annotations.add') }}">
<textarea
id="annotation-content-input"
class="annotation-textarea"
placeholder="{{ _('annotations.content_placeholder') }}"
rows="2"
aria-label="{{ _('annotations.content_placeholder') }}"
maxlength="5000"
></textarea>
<div class="annotation-form-grid">
<div class="form-group">
<label for="annotation-page-input">{{ _("annotations.page") }}</label>
<input type="number" id="annotation-page-input" min="1" value="1" aria-label="{{ _('annotations.page') }}">
</div>
<div class="form-group">
<label for="annotation-type-input">{{ _("annotations.type") }}</label>
<select id="annotation-type-input" aria-label="Annotation type">
<option value="note">{{ _("annotations.type_note") }}</option>
<option value="highlight">{{ _("annotations.type_highlight") }}</option>
<option value="underline">{{ _("annotations.type_underline") }}</option>
<option value="strikethrough">{{ _("annotations.type_strikethrough") }}</option>
</select>
</div>
<div class="form-group">
<label for="annotation-color-input">{{ _("annotations.color") }}</label>
<input type="color" id="annotation-color-input" value="#ffff00" aria-label="{{ _('annotations.color') }}">
</div>
</div>
<button type="submit" class="annotation-submit-btn">
<i class="fas fa-plus" aria-hidden="true"></i> {{ _("annotations.add") }}
</button>
</form>
</div>
</section>
</div>
</div>
{% else %}
<!-- file is None without error -->
<div class="error-box">Document not found.</div>
{% endif %}
</div>
<!-- Comments & Annotations JS -->
<script src="{{ url_for('static', path='js/comments.js') }}" defer></script>
<script src="{{ url_for('static', path='js/annotations.js') }}" defer></script>
<script>
// ── Toggle inline OCR text visibility ──
function toggleOcrText() {
var block = document.getElementById('ocr-text-block');
var icon = document.getElementById('ocr-toggle-icon');
var label = document.getElementById('ocr-toggle-label');
var btn = icon.closest('button');
if (block.style.display === 'none') {
block.style.display = 'block';
icon.className = 'fas fa-chevron-up';
label.textContent = 'Hide text';
if (btn) btn.setAttribute('aria-expanded', 'true');
} else {
block.style.display = 'none';
icon.className = 'fas fa-chevron-down';
label.textContent = 'Show text';
if (btn) btn.setAttribute('aria-expanded', 'false');
}
}
// ── Copy OCR text to clipboard ──
function copyOcrText() {
var content = document.getElementById('ocr-text-content');
if (!content) return;
var text = content.textContent;
var btn = document.getElementById('ocr-copy-btn');
var orig = btn.innerHTML;
navigator.clipboard.writeText(text).then(function() {
btn.innerHTML = '<i class="fas fa-check"></i> Copied!';
setTimeout(function() { btn.innerHTML = orig; }, 2000);
}).catch(function() {
try {
var ta = document.createElement('textarea');
ta.value = text;
ta.style.cssText = 'position:fixed;opacity:0;';
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
btn.innerHTML = '<i class="fas fa-check"></i> Copied!';
setTimeout(function() { btn.innerHTML = orig; }, 2000);
} catch(e) {}
});
}
// ── On-demand text extraction (when ocr_text not in DB) ──
var _textCache = null;
function loadText(fileId) {
var area = document.getElementById('text-load-area');
if (_textCache) {
renderText(area, _textCache);
return;
}
area.innerHTML = '<div style="text-align:center;padding:1.5rem;color:#6b7280;"><i class="fas fa-spinner fa-spin fa-2x"></i><p style="margin-top:0.5rem;">Extracting text…</p></div>';
var url = {% if processed_file_exists %}'/files/' + fileId + '/text/processed'{% else %}'/files/' + fileId + '/text/original'{% endif %};
fetch(url)
.then(function(r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
.then(function(data) {
_textCache = data.text;
renderText(area, data.text);
})
.catch(function(err) {
area.innerHTML = '<div style="color:#dc2626;padding:0.75rem;background:#fee2e2;border-radius:0.375rem;font-size:0.875rem;"><i class="fas fa-exclamation-triangle"></i> Failed to extract text: ' + err.message + '</div>';
});
}
function renderText(container, text) {
container.innerHTML = '';
var pre = document.createElement('pre');
pre.className = 'ocr-text';
pre.textContent = text;
container.appendChild(pre);
}
// ── PDF.js viewer ──
var _pdfState = { doc: null, page: 1, total: 0 };
function pdfInit(url) {
var wrap = document.getElementById('pdf-canvas-wrap');
if (!wrap) return;
pdfjsLib.getDocument(url).promise.then(function(pdf) {
_pdfState.doc = pdf;
_pdfState.total = pdf.numPages;
_pdfState.page = 1;
pdfRenderPage();
}).catch(function() {
wrap.innerHTML = '<div style="text-align:center;padding:2rem;color:#f87171;"><i class="fas fa-exclamation-triangle" style="font-size:2rem;margin-bottom:0.5rem;"></i><p>Failed to load PDF</p></div>';
});
}
function pdfRenderPage() {
var wrap = document.getElementById('pdf-canvas-wrap');
if (!_pdfState.doc || !wrap) return;
_pdfState.doc.getPage(_pdfState.page).then(function(page) {
var vp = page.getViewport({ scale: 1.0 });
var scale = Math.min(wrap.clientWidth / vp.width, 2.0);
var svp = page.getViewport({ scale: scale });
var canvas = document.createElement('canvas');
canvas.width = svp.width;
canvas.height = svp.height;
canvas.style.maxWidth = '100%';
canvas.style.height = 'auto';
wrap.innerHTML = '';
wrap.appendChild(canvas);
page.render({ canvasContext: canvas.getContext('2d'), viewport: svp });
pdfUpdateInfo();
});
}
function pdfChangePage(delta) {
var np = _pdfState.page + delta;
if (np >= 1 && np <= _pdfState.total) {
_pdfState.page = np;
pdfRenderPage();
}
}
function pdfUpdateInfo() {
var info = document.getElementById('pdf-page-info');
if (info) info.textContent = 'Page ' + _pdfState.page + ' of ' + _pdfState.total;
var prev = document.getElementById('pdf-prev-btn');
var next = document.getElementById('pdf-next-btn');
if (prev) prev.disabled = _pdfState.page <= 1;
if (next) next.disabled = _pdfState.page >= _pdfState.total;
}
// ── Image viewer with zoom / pan ──
var _imgScale = 1;
function imgZoom(factor) {
_imgScale = Math.max(0.25, Math.min(5, _imgScale * factor));
var img = document.getElementById('preview-img');
if (img) img.style.transform = 'scale(' + _imgScale + ')';
var label = document.getElementById('img-zoom-level');
if (label) label.textContent = Math.round(_imgScale * 100) + '%';
}
function imgReset() {
_imgScale = 1;
var img = document.getElementById('preview-img');
if (img) img.style.transform = 'scale(1)';
var label = document.getElementById('img-zoom-level');
if (label) label.textContent = '100%';
}
// Mouse-drag panning for image viewer
(function() {
var wrap = document.getElementById('img-wrap');
if (!wrap) return;
var dragging = false, startX, startY, scrollL, scrollT;
wrap.addEventListener('mousedown', function(e) {
dragging = true; wrap.classList.add('dragging');
startX = e.pageX - wrap.offsetLeft; startY = e.pageY - wrap.offsetTop;
scrollL = wrap.scrollLeft; scrollT = wrap.scrollTop;
});
wrap.addEventListener('mousemove', function(e) {
if (!dragging) return; e.preventDefault();
wrap.scrollLeft = scrollL - (e.pageX - wrap.offsetLeft - startX);
wrap.scrollTop = scrollT - (e.pageY - wrap.offsetTop - startY);
});
wrap.addEventListener('mouseup', function() { dragging = false; wrap.classList.remove('dragging'); });
wrap.addEventListener('mouseleave', function() { dragging = false; wrap.classList.remove('dragging'); });
// Mouse wheel zoom
wrap.addEventListener('wheel', function(e) {
e.preventDefault();
imgZoom(e.deltaY < 0 ? 1.1 : 0.9);
}, { passive: false });
})();
// ── Text file viewer ──
function loadTextPreview(url) {
var container = document.getElementById('text-preview-content');
if (!container) return;
fetch(url)
.then(function(r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.text(); })
.then(function(text) {
container.innerHTML = '';
var lines = text.split('\n');
for (var i = 0; i < lines.length; i++) {
var lineEl = document.createElement('div');
var numSpan = document.createElement('span');
numSpan.className = 'line-number';
numSpan.textContent = (i + 1) + '';
lineEl.appendChild(numSpan);
lineEl.appendChild(document.createTextNode(lines[i]));
container.appendChild(lineEl);
}
})
.catch(function() {
container.innerHTML = '<div style="color:#f87171;padding:1rem;">Failed to load file content</div>';
});
}
function copyTextPreview() {
var container = document.getElementById('text-preview-content');
if (!container) return;
var lines = container.querySelectorAll('div');
var text = [];
lines.forEach(function(line) {
var clone = line.cloneNode(true);
var num = clone.querySelector('.line-number');
if (num) num.remove();
text.push(clone.textContent);
});
var btn = document.getElementById('text-copy-btn');
var orig = btn.innerHTML;
navigator.clipboard.writeText(text.join('\n')).then(function() {
btn.innerHTML = '<i class="fas fa-check"></i> Copied!';
setTimeout(function() { btn.innerHTML = orig; }, 2000);
}).catch(function() {
try {
var ta = document.createElement('textarea');
ta.value = text.join('\n');
ta.style.cssText = 'position:fixed;opacity:0;';
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
btn.innerHTML = '<i class="fas fa-check"></i> Copied!';
setTimeout(function() { btn.innerHTML = orig; }, 2000);
} catch(e) {}
});
}
// ── Default-language text toggle ──
function toggleDefaultLangText() {
var block = document.getElementById('default-lang-text-block');
var icon = document.getElementById('default-lang-toggle-icon');
var label = document.getElementById('default-lang-toggle-label');
var btn = icon ? icon.closest('button') : null;
if (!block) return;
if (block.style.display === 'none') {
block.style.display = 'block';
if (icon) icon.className = 'fas fa-chevron-up';
if (label) label.textContent = 'Hide text';
if (btn) btn.setAttribute('aria-expanded', 'true');
} else {
block.style.display = 'none';
if (icon) icon.className = 'fas fa-chevron-down';
if (label) label.textContent = 'Show text';
if (btn) btn.setAttribute('aria-expanded', 'false');
}
}
// ── Copy default-language text ──
function copyDefaultLangText() {
var content = document.getElementById('default-lang-text-content');
if (!content) return;
var text = content.textContent;
var btn = document.getElementById('default-lang-copy-btn');
if (!btn) return;
var orig = btn.innerHTML;
navigator.clipboard.writeText(text).then(function() {
btn.innerHTML = '<i class="fas fa-check"></i> Copied!';
setTimeout(function() { btn.innerHTML = orig; }, 2000);
}).catch(function() {
try {
var ta = document.createElement('textarea');
ta.value = text;
ta.style.cssText = 'position:fixed;opacity:0;';
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
btn.innerHTML = '<i class="fas fa-check"></i> Copied!';
setTimeout(function() { btn.innerHTML = orig; }, 2000);
} catch(e) {}
});
}
// ── Load default-language text on demand ──
function loadDefaultLangText(fileId) {
var area = document.getElementById('default-lang-load-area');
if (!area) return;
area.innerHTML = '<div style="text-align:center;padding:1.5rem;color:#6b7280;"><i class="fas fa-spinner fa-spin fa-2x" aria-hidden="true"></i><p style="margin-top:0.5rem;">Loading translation…</p></div>';
fetch('/files/' + fileId + '/text/default-language')
.then(function(r) {
if (r.status === 404) throw new Error('No translation available yet — it may still be processing');
if (!r.ok) throw new Error('HTTP ' + r.status);
return r.json();
})
.then(function(data) {
area.innerHTML = '';
var info = document.createElement('div');
info.style.cssText = 'font-size:0.75rem;color:#6b7280;margin-bottom:0.5rem;';
info.textContent = 'Translated to: ' + data.language_code + (data.detected_language ? ' (from ' + data.detected_language + ')' : '');
area.appendChild(info);
var pre = document.createElement('pre');
pre.className = 'ocr-text';
pre.textContent = data.text;
area.appendChild(pre);
})
.catch(function(err) {
area.innerHTML = '<div style="color:#92400e;padding:0.75rem;background:#fef3c7;border-radius:0.375rem;font-size:0.875rem;"><i class="fas fa-info-circle" aria-hidden="true"></i> ' + err.message + '</div>';
});
}
// ── On-the-fly translation ──
var _translateCache = {};
function translateOnTheFly(fileId) {
var select = document.getElementById('translate-lang-select');
var area = document.getElementById('translate-result-area');
var copyBtn = document.getElementById('translate-copy-btn');
if (!select || !area) return;
var lang = select.value;
if (!lang) { area.innerHTML = '<div style="color:#92400e;padding:0.75rem;background:#fef3c7;border-radius:0.375rem;font-size:0.875rem;">Please select a target language.</div>'; return; }
// Check cache
if (_translateCache[lang]) {
renderTranslation(area, _translateCache[lang], lang);
if (copyBtn) copyBtn.style.display = '';
return;
}
area.innerHTML = '<div style="text-align:center;padding:1.5rem;color:#6b7280;" aria-live="polite"><i class="fas fa-spinner fa-spin fa-2x" aria-hidden="true"></i><p style="margin-top:0.5rem;">Translating…</p></div>';
if (copyBtn) copyBtn.style.display = 'none';
fetch('/api/files/' + fileId + '/translate?lang=' + encodeURIComponent(lang))
.then(function(r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
.then(function(data) {
_translateCache[lang] = data.text;
renderTranslation(area, data.text, lang);
if (copyBtn) copyBtn.style.display = '';
})
.catch(function(err) {
area.innerHTML = '<div style="color:#dc2626;padding:0.75rem;background:#fee2e2;border-radius:0.375rem;font-size:0.875rem;"><i class="fas fa-exclamation-triangle" aria-hidden="true"></i> Translation failed: ' + err.message + '</div>';
});
}
function renderTranslation(container, text, lang) {
container.innerHTML = '';
var info = document.createElement('div');
info.style.cssText = 'font-size:0.75rem;color:#6b7280;margin-bottom:0.5rem;';
info.textContent = 'Translated to: ' + lang;
container.appendChild(info);
var pre = document.createElement('pre');
pre.className = 'ocr-text';
pre.id = 'translated-text-content';
pre.textContent = text;
container.appendChild(pre);
}
function copyTranslatedText() {
var content = document.getElementById('translated-text-content');
if (!content) return;
var text = content.textContent;
var btn = document.getElementById('translate-copy-btn');
if (!btn) return;
var orig = btn.innerHTML;
navigator.clipboard.writeText(text).then(function() {
btn.innerHTML = '<i class="fas fa-check"></i> Copied!';
setTimeout(function() { btn.innerHTML = orig; }, 2000);
}).catch(function() {
try {
var ta = document.createElement('textarea');
ta.value = text;
ta.style.cssText = 'position:fixed;opacity:0;';
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
btn.innerHTML = '<i class="fas fa-check"></i> Copied!';
setTimeout(function() { btn.innerHTML = orig; }, 2000);
} catch(e) {}
});
}
// ── Initialise on load ──
document.addEventListener('DOMContentLoaded', function() {
{% if (processed_file_exists or original_file_exists) and file %}
{% set pv = 'processed' if processed_file_exists else 'original' %}
{% set pm = file.mime_type or 'application/pdf' %}
{% if pm.startswith('text/') %}
loadTextPreview('/api/files/{{ file.id }}/preview?version={{ pv }}');
{% elif not pm.startswith('image/') %}
pdfInit('/api/files/{{ file.id }}/preview?version={{ pv }}');
{% endif %}
{% endif %}
{% if file %}
// ── Initialise comments & annotations ──
var fileId = {{ file.id | tojson }};
fetch('/api/auth/whoami')
.then(function (r) { return r.json(); })
.then(function (data) {
var userId = (data && (data.email || data.preferred_username)) || '';
var commentsI18n = {
empty: {{ _("comments.empty") | tojson }},
add_comment: {{ _("comments.add_comment") | tojson }},
add_reply: {{ _("comments.add_reply") | tojson }},
edit: {{ _("comments.edit") | tojson }},
save: {{ _("comments.save") | tojson }},
resolve: {{ _("comments.resolve") | tojson }},
resolved: {{ _("comments.resolved") | tojson }},
unresolve: {{ _("comments.unresolve") | tojson }},
delete_confirm: {{ _("comments.delete_confirm") | tojson }},
reply_placeholder: {{ _("comments.reply_placeholder") | tojson }},
body_placeholder: {{ _("comments.body_placeholder") | tojson }},
mention_users: {{ _("comments.mention_users") | tojson }},
cancel: {{ _("common.cancel") | tojson }}
};
var annotationsI18n = {
empty: {{ _("annotations.empty") | tojson }},
add: {{ _("annotations.add") | tojson }},
save: {{ _("annotations.save") | tojson }},
delete_confirm: {{ _("annotations.delete_confirm") | tojson }},
page: {{ _("annotations.page") | tojson }},
color: {{ _("annotations.color") | tojson }},
type_note: {{ _("annotations.type_note") | tojson }},
type_highlight: {{ _("annotations.type_highlight") | tojson }},
type_underline: {{ _("annotations.type_underline") | tojson }},
type_strikethrough: {{ _("annotations.type_strikethrough") | tojson }},
cancel: {{ _("common.cancel") | tojson }}
};
if (typeof initComments === 'function') {
initComments(fileId, userId, commentsI18n);
}
if (typeof initAnnotations === 'function') {
initAnnotations(fileId, userId, annotationsI18n);
}
})
.catch(function () {
if (typeof initComments === 'function') initComments(fileId, '', {});
if (typeof initAnnotations === 'function') initAnnotations(fileId, '', {});
});
{% endif %}
});
</script>
{% endblock %}