From c70b607939dc1d99929cc5382d633d1fd7d982fc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Mar 2026 11:06:26 +0000 Subject: [PATCH 1/4] Initial plan From 5a3ddcc1f0d9e2daa674d5098ab1bafe4d2e0bc8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Mar 2026 11:28:27 +0000 Subject: [PATCH 2/4] refactor: initial step - remove comments/annotations from file_detail and file_view templates Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com> Agent-Logs-Url: https://github.com/christianlouis/DocuElevate/sessions/12276514-bd3d-4e3e-84d9-5977d1f82b19 --- frontend/templates/file_detail.html | 637 ---------------------------- frontend/templates/file_view.html | 632 +++++++++++++++++++++++++++ tests/test_comments_ui.py | 76 ++-- 3 files changed, 679 insertions(+), 666 deletions(-) diff --git a/frontend/templates/file_detail.html b/frontend/templates/file_detail.html index 2c1242bc..1642313b 100644 --- a/frontend/templates/file_detail.html +++ b/frontend/templates/file_detail.html @@ -1092,512 +1092,6 @@ {% endif %} - - {% endblock %} {% block content %} @@ -2284,137 +1778,6 @@ {% endif %} - -
-
- -
-
-

{{ _("comments.heading") }}

-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-

{{ _("annotations.heading") }}

-
-
- - -
-
- -
-
- - -
-
- - -
-
- - -
-
- -
-
-
-
-
- - - - - - {% endif %} {% endblock %} diff --git a/frontend/templates/file_view.html b/frontend/templates/file_view.html index 01083b78..3c1fd0ec 100644 --- a/frontend/templates/file_view.html +++ b/frontend/templates/file_view.html @@ -148,6 +148,510 @@ /* ── 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; + } + } {% endblock %} @@ -550,6 +1054,83 @@ {% endif %} + +
+
+ +
+
+

{{ _("comments.heading") }}

+
+
+ + +
+
+
+ + +
+ +
+
+
+ + +
+
+

{{ _("annotations.heading") }}

+
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+
+
+ {% else %}
Document not found.
@@ -557,6 +1138,9 @@ + + + {% endblock %} diff --git a/tests/test_comments_ui.py b/tests/test_comments_ui.py index 50d9e9cf..7a44a7b0 100644 --- a/tests/test_comments_ui.py +++ b/tests/test_comments_ui.py @@ -1,4 +1,4 @@ -"""Tests for the comments and annotations UI on the file detail page.""" +"""Tests for the comments and annotations UI on the file view page.""" import pytest from fastapi.testclient import TestClient @@ -7,7 +7,7 @@ from app.models import FileRecord def _create_file(db_session, tmp_path) -> FileRecord: - """Create a minimal FileRecord with a real file path for the detail page.""" + """Create a minimal FileRecord with a real file path for the view page.""" file_path = tmp_path / "test.pdf" file_path.write_bytes(b"%PDF-1.4") f = FileRecord( @@ -26,77 +26,77 @@ def _create_file(db_session, tmp_path) -> FileRecord: @pytest.mark.unit class TestCommentsUIRendering: - """Verify the file detail page includes the comments panel HTML.""" + """Verify the file view page includes the comments panel HTML.""" - def test_detail_page_contains_comments_section(self, client: TestClient, db_session, tmp_path): - """The detail page should render the comments panel container.""" + def test_view_page_contains_comments_section(self, client: TestClient, db_session, tmp_path): + """The view page should render the comments panel container.""" f = _create_file(db_session, tmp_path) - resp = client.get(f"/files/{f.id}/detail") + resp = client.get(f"/files/{f.id}") assert resp.status_code == 200 html = resp.text assert 'id="comments-list"' in html assert 'id="comment-form"' in html assert 'id="comment-input"' in html - def test_detail_page_contains_annotations_section(self, client: TestClient, db_session, tmp_path): - """The detail page should render the annotations panel container.""" + def test_view_page_contains_annotations_section(self, client: TestClient, db_session, tmp_path): + """The view page should render the annotations panel container.""" f = _create_file(db_session, tmp_path) - resp = client.get(f"/files/{f.id}/detail") + resp = client.get(f"/files/{f.id}") assert resp.status_code == 200 html = resp.text assert 'id="annotations-list"' in html assert 'id="annotation-form"' in html assert 'id="annotation-content-input"' in html - def test_detail_page_loads_comments_js(self, client: TestClient, db_session, tmp_path): - """The detail page should include the comments JavaScript file.""" + def test_view_page_loads_comments_js(self, client: TestClient, db_session, tmp_path): + """The view page should include the comments JavaScript file.""" f = _create_file(db_session, tmp_path) - resp = client.get(f"/files/{f.id}/detail") + resp = client.get(f"/files/{f.id}") assert resp.status_code == 200 assert "js/comments.js" in resp.text - def test_detail_page_loads_annotations_js(self, client: TestClient, db_session, tmp_path): - """The detail page should include the annotations JavaScript file.""" + def test_view_page_loads_annotations_js(self, client: TestClient, db_session, tmp_path): + """The view page should include the annotations JavaScript file.""" f = _create_file(db_session, tmp_path) - resp = client.get(f"/files/{f.id}/detail") + resp = client.get(f"/files/{f.id}") assert resp.status_code == 200 assert "js/annotations.js" in resp.text - def test_detail_page_has_mention_dropdown(self, client: TestClient, db_session, tmp_path): + def test_view_page_has_mention_dropdown(self, client: TestClient, db_session, tmp_path): """The mention autocomplete dropdown should be present.""" f = _create_file(db_session, tmp_path) - resp = client.get(f"/files/{f.id}/detail") + resp = client.get(f"/files/{f.id}") assert resp.status_code == 200 assert 'id="mention-dropdown"' in resp.text - def test_detail_page_has_annotation_form_fields(self, client: TestClient, db_session, tmp_path): + def test_view_page_has_annotation_form_fields(self, client: TestClient, db_session, tmp_path): """Annotation form should have page, type, and color inputs.""" f = _create_file(db_session, tmp_path) - resp = client.get(f"/files/{f.id}/detail") + resp = client.get(f"/files/{f.id}") assert resp.status_code == 200 html = resp.text assert 'id="annotation-page-input"' in html assert 'id="annotation-type-input"' in html assert 'id="annotation-color-input"' in html - def test_detail_page_has_collab_grid(self, client: TestClient, db_session, tmp_path): + def test_view_page_has_collab_grid(self, client: TestClient, db_session, tmp_path): """Comments and annotations should be in a side-by-side grid layout.""" f = _create_file(db_session, tmp_path) - resp = client.get(f"/files/{f.id}/detail") + resp = client.get(f"/files/{f.id}") assert resp.status_code == 200 assert "collab-grid" in resp.text - def test_detail_page_no_comments_for_missing_file(self, client: TestClient): + def test_view_page_no_comments_for_missing_file(self, client: TestClient): """When file is not found, no comments section should appear.""" - resp = client.get("/files/99999/detail") + resp = client.get("/files/99999") assert resp.status_code == 200 # The error block is shown, not the main content assert 'id="comments-list"' not in resp.text - def test_detail_page_annotation_type_options(self, client: TestClient, db_session, tmp_path): + def test_view_page_annotation_type_options(self, client: TestClient, db_session, tmp_path): """Annotation type selector should include all four types.""" f = _create_file(db_session, tmp_path) - resp = client.get(f"/files/{f.id}/detail") + resp = client.get(f"/files/{f.id}") assert resp.status_code == 200 html = resp.text assert 'value="note"' in html @@ -104,20 +104,38 @@ class TestCommentsUIRendering: assert 'value="underline"' in html assert 'value="strikethrough"' in html - def test_detail_page_comments_panel_accessibility(self, client: TestClient, db_session, tmp_path): + def test_view_page_comments_panel_accessibility(self, client: TestClient, db_session, tmp_path): """Comments panel should have proper ARIA attributes.""" f = _create_file(db_session, tmp_path) - resp = client.get(f"/files/{f.id}/detail") + resp = client.get(f"/files/{f.id}") assert resp.status_code == 200 html = resp.text assert 'aria-live="polite"' in html assert 'role="listbox"' in html - def test_detail_page_init_script(self, client: TestClient, db_session, tmp_path): + def test_view_page_init_script(self, client: TestClient, db_session, tmp_path): """The init script should call initComments and initAnnotations.""" f = _create_file(db_session, tmp_path) - resp = client.get(f"/files/{f.id}/detail") + resp = client.get(f"/files/{f.id}") assert resp.status_code == 200 html = resp.text assert "initComments" in html assert "initAnnotations" in html + + def test_detail_page_no_comments_section(self, client: TestClient, db_session, tmp_path): + """The detail page should NOT render the comments panel (moved to view page).""" + f = _create_file(db_session, tmp_path) + resp = client.get(f"/files/{f.id}/detail") + assert resp.status_code == 200 + html = resp.text + assert 'id="comments-list"' not in html + assert 'id="comment-form"' not in html + + def test_detail_page_no_annotations_section(self, client: TestClient, db_session, tmp_path): + """The detail page should NOT render the annotations panel (moved to view page).""" + f = _create_file(db_session, tmp_path) + resp = client.get(f"/files/{f.id}/detail") + assert resp.status_code == 200 + html = resp.text + assert 'id="annotations-list"' not in html + assert 'id="annotation-form"' not in html From f852ba978343934631f70aa12aec49ec79da8288 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Mar 2026 11:48:57 +0000 Subject: [PATCH 3/4] refactor(views): split file views into summary, detail, process, and annotations pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - /files/ → new summary page with navigation cards - /files//detail → document detail with metadata, preview, text - /files//process → processing pipeline status and history - /files//annotations → comments & annotations with EmbedPDF viewer - /files//comments → redirects to /annotations - Added embed-pdf-viewer as git submodule for PDF annotation viewer - Updated all navigation links across templates - Updated all tests to use new URL structure Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com> Agent-Logs-Url: https://github.com/christianlouis/DocuElevate/sessions/12276514-bd3d-4e3e-84d9-5977d1f82b19 --- .gitmodules | 3 + app/views/files.py | 156 ++++- frontend/templates/file_annotations.html | 746 +++++++++++++++++++++++ frontend/templates/file_detail.html | 10 +- frontend/templates/file_summary.html | 196 ++++++ frontend/templates/file_view.html | 640 +------------------ frontend/templates/files.html | 2 +- tests/test_comments_ui.py | 96 +-- tests/test_document_preview.py | 36 +- tests/test_file_detail_endpoints.py | 6 +- tests/test_file_detail_enhancements.py | 6 +- tests/test_files_view_extended.py | 8 +- tests/test_views_files_comprehensive.py | 32 +- vendor/embed-pdf-viewer | 1 + 14 files changed, 1213 insertions(+), 725 deletions(-) create mode 100644 .gitmodules create mode 100644 frontend/templates/file_annotations.html create mode 100644 frontend/templates/file_summary.html create mode 160000 vendor/embed-pdf-viewer diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..bc644a9f --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "vendor/embed-pdf-viewer"] + path = vendor/embed-pdf-viewer + url = https://github.com/embedpdf/embed-pdf-viewer.git diff --git a/app/views/files.py b/app/views/files.py index 2f9bc3c4..af9e2e5d 100644 --- a/app/views/files.py +++ b/app/views/files.py @@ -206,10 +206,91 @@ def files_page( @router.get("/files/{file_id}") @require_login +def file_summary_page(request: Request, file_id: int, db: Session = Depends(get_db)): + """ + Return the file summary page — a concise overview with links to detail, processing, and annotations views. + """ + try: + import json + import os + + from app.models import FileRecord + + file_record = db.query(FileRecord).filter(FileRecord.id == file_id).first() + + if not file_record: + return templates.TemplateResponse( + "file_summary.html", + {"request": request, "file": None, "error": f"File with ID {file_id} not found"}, + ) + + from app.config import settings + + workdir = os.path.realpath(settings.workdir) + + def _safe_exists(path: str | None) -> bool: + """Return True only when *path* exists and resides within workdir.""" + if not path: + return False + resolved = os.path.realpath(path) + try: + common = os.path.commonpath([resolved, workdir]) + except ValueError: + return False + return common == workdir and os.path.exists(resolved) + + original_file_exists = _safe_exists(file_record.original_file_path) + processed_file_exists = _safe_exists(file_record.processed_file_path) + + # Load AI metadata — JSON sidecar file first, then DB column + gpt_metadata = None + if file_record.processed_file_path: + metadata_path = os.path.splitext(os.path.realpath(file_record.processed_file_path))[0] + ".json" + if _safe_exists(metadata_path): + try: + with open(metadata_path, "r", encoding="utf-8") as f: + gpt_metadata = json.load(f) + except Exception as e: + logger.warning(f"Failed to load metadata sidecar for file {file_id}: {e}") + + if gpt_metadata is None and file_record.ai_metadata: + try: + gpt_metadata = json.loads(file_record.ai_metadata) + except Exception as e: + logger.warning(f"Failed to parse ai_metadata for file {file_id}: {e}") + + # Quick processing status + try: + from app.utils.step_manager import get_step_summary as _get_step_summary + + step_summary = _get_step_summary(db, file_id) + except Exception: + step_summary = None + + pipeline_info = _resolve_pipeline(db, file_record) + + return templates.TemplateResponse( + "file_summary.html", + { + "request": request, + "file": file_record, + "gpt_metadata": gpt_metadata, + "original_file_exists": original_file_exists, + "processed_file_exists": processed_file_exists, + "step_summary": step_summary, + "pipeline_info": pipeline_info, + }, + ) + except Exception as e: + logger.error(f"Error retrieving file summary {file_id}: {str(e)}") + return templates.TemplateResponse("file_summary.html", {"request": request, "file": None, "error": str(e)}) + + +@router.get("/files/{file_id}/detail") +@require_login def file_view_page(request: Request, file_id: int, db: Session = Depends(get_db)): """ - Return the document view page — document-centric view with metadata, preview, and extracted text. - Process-oriented details are available via /files/{file_id}/detail. + Return the document detail page — document-centric view with metadata, preview, and extracted text. """ try: import json @@ -290,11 +371,11 @@ def file_view_page(request: Request, file_id: int, db: Session = Depends(get_db) return templates.TemplateResponse("file_view.html", {"request": request, "file": None, "error": str(e)}) -@router.get("/files/{file_id}/detail") +@router.get("/files/{file_id}/process") @require_login def file_detail_page(request: Request, file_id: int, db: Session = Depends(get_db)): """ - Return the file detail page showing processing history and file information + Return the file processing page showing processing history and pipeline information. """ try: import json @@ -375,6 +456,73 @@ def file_detail_page(request: Request, file_id: int, db: Session = Depends(get_d return templates.TemplateResponse("file_detail.html", {"request": request, "file": None, "error": str(e)}) +@router.get("/files/{file_id}/annotations") +@require_login +def file_annotations_page(request: Request, file_id: int, db: Session = Depends(get_db)): + """ + Return the comments & annotations page for a file. + """ + try: + import os + + from app.models import FileRecord + + file_record = db.query(FileRecord).filter(FileRecord.id == file_id).first() + + if not file_record: + return templates.TemplateResponse( + "file_annotations.html", + {"request": request, "file": None, "error": f"File with ID {file_id} not found"}, + ) + + from app.config import settings + + workdir = os.path.realpath(settings.workdir) + + def _safe_exists(path: str | None) -> bool: + """Return True only when *path* exists and resides within workdir.""" + if not path: + return False + resolved = os.path.realpath(path) + try: + common = os.path.commonpath([resolved, workdir]) + except ValueError: + return False + return common == workdir and os.path.exists(resolved) + + original_file_exists = _safe_exists(file_record.original_file_path) + processed_file_exists = _safe_exists(file_record.processed_file_path) + + # Determine whether the file is a PDF (for EmbedPDF viewer) + mime = file_record.mime_type or "" + is_pdf = mime == "application/pdf" or (file_record.original_filename or "").lower().endswith(".pdf") + + return templates.TemplateResponse( + "file_annotations.html", + { + "request": request, + "file": file_record, + "original_file_exists": original_file_exists, + "processed_file_exists": processed_file_exists, + "is_pdf": is_pdf, + }, + ) + except Exception as e: + logger.error(f"Error retrieving annotations for file {file_id}: {str(e)}") + return templates.TemplateResponse("file_annotations.html", {"request": request, "file": None, "error": str(e)}) + + +@router.get("/files/{file_id}/comments") +@require_login +def file_comments_redirect(request: Request, file_id: int): + """ + Redirect /files/{file_id}/comments to /files/{file_id}/annotations. + """ + from starlette.responses import RedirectResponse + + return RedirectResponse(url=f"/files/{file_id}/annotations", status_code=302) + + # --------------------------------------------------------------------------- # Pipeline ↔ Celery-log stage mapping # --------------------------------------------------------------------------- diff --git a/frontend/templates/file_annotations.html b/frontend/templates/file_annotations.html new file mode 100644 index 00000000..299ba942 --- /dev/null +++ b/frontend/templates/file_annotations.html @@ -0,0 +1,746 @@ +{% extends "base.html" %} +{% block title %}Comments & Annotations - {{ file.original_filename or 'Document' }} - DocuElevate{% endblock %} + +{% block head_extra %} + + +{% endblock %} + +{% block content %} +
+ + {% if error %} +
Error: {{ error }}
+ {% elif file %} + + + + +
+
+
+ + Comments & Annotations +
+
{{ file.original_filename }}
+
+
+ + + {% if is_pdf and (processed_file_exists or original_file_exists) %} +
+
+ + Document Viewer +
+
+
+ {% endif %} + + +
+
+ +
+
+

{{ _("comments.heading") }}

+
+
+ + +
+
+
+ + +
+ +
+
+
+ + +
+
+

{{ _("annotations.heading") }}

+
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+
+
+ + {% else %} + +
Document not found.
+ {% endif %} + +
+ + + + + + +{% if file and is_pdf and (processed_file_exists or original_file_exists) %} + +{% endif %} +{% endblock %} diff --git a/frontend/templates/file_detail.html b/frontend/templates/file_detail.html index 1642313b..7c00f2b0 100644 --- a/frontend/templates/file_detail.html +++ b/frontend/templates/file_detail.html @@ -1056,7 +1056,7 @@ html += `
- +
{% endif %} - -
-
- -
-
-

{{ _("comments.heading") }}

-
-
- - -
-
-
- - -
- -
-
-
- - -
-
-

{{ _("annotations.heading") }}

-
-
- - -
-
- -
-
- - -
-
- - -
-
- - -
-
- -
-
-
-
-
- {% else %}
Document not found.
@@ -1138,9 +557,6 @@
- - - {% endblock %} diff --git a/frontend/templates/files.html b/frontend/templates/files.html index 1d142a03..10892093 100644 --- a/frontend/templates/files.html +++ b/frontend/templates/files.html @@ -904,7 +904,7 @@ function viewFileDetail(fileId, event) { if (event) event.stopPropagation(); - window.location.href = `/files/${fileId}/detail`; + window.location.href = `/files/${fileId}`; } // ── Preview modal ── diff --git a/tests/test_comments_ui.py b/tests/test_comments_ui.py index 7a44a7b0..059610a5 100644 --- a/tests/test_comments_ui.py +++ b/tests/test_comments_ui.py @@ -1,4 +1,4 @@ -"""Tests for the comments and annotations UI on the file view page.""" +"""Tests for the comments and annotations UI on the file annotations page.""" import pytest from fastapi.testclient import TestClient @@ -7,7 +7,7 @@ from app.models import FileRecord def _create_file(db_session, tmp_path) -> FileRecord: - """Create a minimal FileRecord with a real file path for the view page.""" + """Create a minimal FileRecord with a real file path for the annotations page.""" file_path = tmp_path / "test.pdf" file_path.write_bytes(b"%PDF-1.4") f = FileRecord( @@ -26,77 +26,77 @@ def _create_file(db_session, tmp_path) -> FileRecord: @pytest.mark.unit class TestCommentsUIRendering: - """Verify the file view page includes the comments panel HTML.""" + """Verify the file annotations page includes the comments panel HTML.""" - def test_view_page_contains_comments_section(self, client: TestClient, db_session, tmp_path): - """The view page should render the comments panel container.""" + def test_annotations_page_contains_comments_section(self, client: TestClient, db_session, tmp_path): + """The annotations page should render the comments panel container.""" f = _create_file(db_session, tmp_path) - resp = client.get(f"/files/{f.id}") + resp = client.get(f"/files/{f.id}/annotations") assert resp.status_code == 200 html = resp.text assert 'id="comments-list"' in html assert 'id="comment-form"' in html assert 'id="comment-input"' in html - def test_view_page_contains_annotations_section(self, client: TestClient, db_session, tmp_path): - """The view page should render the annotations panel container.""" + def test_annotations_page_contains_annotations_section(self, client: TestClient, db_session, tmp_path): + """The annotations page should render the annotations panel container.""" f = _create_file(db_session, tmp_path) - resp = client.get(f"/files/{f.id}") + resp = client.get(f"/files/{f.id}/annotations") assert resp.status_code == 200 html = resp.text assert 'id="annotations-list"' in html assert 'id="annotation-form"' in html assert 'id="annotation-content-input"' in html - def test_view_page_loads_comments_js(self, client: TestClient, db_session, tmp_path): - """The view page should include the comments JavaScript file.""" + def test_annotations_page_loads_comments_js(self, client: TestClient, db_session, tmp_path): + """The annotations page should include the comments JavaScript file.""" f = _create_file(db_session, tmp_path) - resp = client.get(f"/files/{f.id}") + resp = client.get(f"/files/{f.id}/annotations") assert resp.status_code == 200 assert "js/comments.js" in resp.text - def test_view_page_loads_annotations_js(self, client: TestClient, db_session, tmp_path): - """The view page should include the annotations JavaScript file.""" + def test_annotations_page_loads_annotations_js(self, client: TestClient, db_session, tmp_path): + """The annotations page should include the annotations JavaScript file.""" f = _create_file(db_session, tmp_path) - resp = client.get(f"/files/{f.id}") + resp = client.get(f"/files/{f.id}/annotations") assert resp.status_code == 200 assert "js/annotations.js" in resp.text - def test_view_page_has_mention_dropdown(self, client: TestClient, db_session, tmp_path): + def test_annotations_page_has_mention_dropdown(self, client: TestClient, db_session, tmp_path): """The mention autocomplete dropdown should be present.""" f = _create_file(db_session, tmp_path) - resp = client.get(f"/files/{f.id}") + resp = client.get(f"/files/{f.id}/annotations") assert resp.status_code == 200 assert 'id="mention-dropdown"' in resp.text - def test_view_page_has_annotation_form_fields(self, client: TestClient, db_session, tmp_path): + def test_annotations_page_has_annotation_form_fields(self, client: TestClient, db_session, tmp_path): """Annotation form should have page, type, and color inputs.""" f = _create_file(db_session, tmp_path) - resp = client.get(f"/files/{f.id}") + resp = client.get(f"/files/{f.id}/annotations") assert resp.status_code == 200 html = resp.text assert 'id="annotation-page-input"' in html assert 'id="annotation-type-input"' in html assert 'id="annotation-color-input"' in html - def test_view_page_has_collab_grid(self, client: TestClient, db_session, tmp_path): + def test_annotations_page_has_collab_grid(self, client: TestClient, db_session, tmp_path): """Comments and annotations should be in a side-by-side grid layout.""" f = _create_file(db_session, tmp_path) - resp = client.get(f"/files/{f.id}") + resp = client.get(f"/files/{f.id}/annotations") assert resp.status_code == 200 assert "collab-grid" in resp.text - def test_view_page_no_comments_for_missing_file(self, client: TestClient): + def test_annotations_page_no_comments_for_missing_file(self, client: TestClient): """When file is not found, no comments section should appear.""" - resp = client.get("/files/99999") + resp = client.get("/files/99999/annotations") assert resp.status_code == 200 # The error block is shown, not the main content assert 'id="comments-list"' not in resp.text - def test_view_page_annotation_type_options(self, client: TestClient, db_session, tmp_path): + def test_annotations_page_annotation_type_options(self, client: TestClient, db_session, tmp_path): """Annotation type selector should include all four types.""" f = _create_file(db_session, tmp_path) - resp = client.get(f"/files/{f.id}") + resp = client.get(f"/files/{f.id}/annotations") assert resp.status_code == 200 html = resp.text assert 'value="note"' in html @@ -104,38 +104,64 @@ class TestCommentsUIRendering: assert 'value="underline"' in html assert 'value="strikethrough"' in html - def test_view_page_comments_panel_accessibility(self, client: TestClient, db_session, tmp_path): + def test_annotations_page_comments_panel_accessibility(self, client: TestClient, db_session, tmp_path): """Comments panel should have proper ARIA attributes.""" f = _create_file(db_session, tmp_path) - resp = client.get(f"/files/{f.id}") + resp = client.get(f"/files/{f.id}/annotations") assert resp.status_code == 200 html = resp.text assert 'aria-live="polite"' in html assert 'role="listbox"' in html - def test_view_page_init_script(self, client: TestClient, db_session, tmp_path): + def test_annotations_page_init_script(self, client: TestClient, db_session, tmp_path): """The init script should call initComments and initAnnotations.""" f = _create_file(db_session, tmp_path) - resp = client.get(f"/files/{f.id}") + resp = client.get(f"/files/{f.id}/annotations") assert resp.status_code == 200 html = resp.text assert "initComments" in html assert "initAnnotations" in html - def test_detail_page_no_comments_section(self, client: TestClient, db_session, tmp_path): - """The detail page should NOT render the comments panel (moved to view page).""" + def test_comments_url_redirects_to_annotations(self, client: TestClient, db_session, tmp_path): + """The /comments URL should redirect to /annotations.""" f = _create_file(db_session, tmp_path) - resp = client.get(f"/files/{f.id}/detail") + resp = client.get(f"/files/{f.id}/comments", follow_redirects=False) + assert resp.status_code == 302 + assert f"/files/{f.id}/annotations" in resp.headers["location"] + + def test_process_page_no_comments_section(self, client: TestClient, db_session, tmp_path): + """The process page should NOT render the comments panel.""" + f = _create_file(db_session, tmp_path) + resp = client.get(f"/files/{f.id}/process") assert resp.status_code == 200 html = resp.text assert 'id="comments-list"' not in html assert 'id="comment-form"' not in html - def test_detail_page_no_annotations_section(self, client: TestClient, db_session, tmp_path): - """The detail page should NOT render the annotations panel (moved to view page).""" + def test_detail_page_no_comments_section(self, client: TestClient, db_session, tmp_path): + """The detail page should NOT render the comments panel.""" f = _create_file(db_session, tmp_path) resp = client.get(f"/files/{f.id}/detail") assert resp.status_code == 200 html = resp.text - assert 'id="annotations-list"' not in html + assert 'id="comments-list"' not in html assert 'id="annotation-form"' not in html + + def test_annotations_page_has_embedpdf_viewer_for_pdf(self, client: TestClient, db_session, tmp_path): + """The annotations page should include the EmbedPDF viewer for PDF files.""" + f = _create_file(db_session, tmp_path) + resp = client.get(f"/files/{f.id}/annotations") + assert resp.status_code == 200 + html = resp.text + assert 'id="embedpdf-viewer"' in html + assert "@embedpdf/snippet" in html + + def test_summary_page_renders(self, client: TestClient, db_session, tmp_path): + """The summary page at /files/{id} should render correctly.""" + f = _create_file(db_session, tmp_path) + resp = client.get(f"/files/{f.id}") + assert resp.status_code == 200 + html = resp.text + assert "Document Detail" in html + assert "Processing" in html + assert "Comments" in html or "Annotations" in html diff --git a/tests/test_document_preview.py b/tests/test_document_preview.py index 45e46c3c..1a9dfa52 100644 --- a/tests/test_document_preview.py +++ b/tests/test_document_preview.py @@ -57,7 +57,7 @@ class TestFileViewPdfJs: pdf.write_bytes(b"%PDF-1.4 test") rec = _create_file_record(db_session, file_path=str(pdf), mime_type="application/pdf") - response = client.get(f"/files/{rec.id}") + response = client.get(f"/files/{rec.id}/detail") assert response.status_code == 200 html = response.text @@ -80,7 +80,7 @@ class TestFileViewPdfJs: pdf.write_bytes(b"%PDF-1.4 test") rec = _create_file_record(db_session, file_path=str(pdf), mime_type="application/pdf") - response = client.get(f"/files/{rec.id}") + response = client.get(f"/files/{rec.id}/detail") html = response.text # The preview section should use pdf-viewer, not iframe @@ -93,7 +93,7 @@ class TestFileViewPdfJs: pdf.write_bytes(b"%PDF-1.4 test") rec = _create_file_record(db_session, file_path=str(pdf), mime_type="application/pdf") - response = client.get(f"/files/{rec.id}") + response = client.get(f"/files/{rec.id}/detail") html = response.text assert 'id="pdf-prev-btn"' in html @@ -116,7 +116,7 @@ class TestFileViewImagePreview: img.write_bytes(b"\xff\xd8\xff\xe0" + b"\x00" * 100) # minimal JPEG header rec = _create_file_record(db_session, filename="photo.jpg", mime_type="image/jpeg", file_path=str(img)) - response = client.get(f"/files/{rec.id}") + response = client.get(f"/files/{rec.id}/detail") assert response.status_code == 200 html = response.text @@ -132,7 +132,7 @@ class TestFileViewImagePreview: img.write_bytes(b"\x89PNG\r\n\x1a\n" + b"\x00" * 50) rec = _create_file_record(db_session, filename="photo.png", mime_type="image/png", file_path=str(img)) - response = client.get(f"/files/{rec.id}") + response = client.get(f"/files/{rec.id}/detail") html = response.text assert 'aria-label="Zoom in"' in html @@ -146,7 +146,7 @@ class TestFileViewImagePreview: img.write_bytes(b"RIFF" + b"\x00" * 50) rec = _create_file_record(db_session, filename="wide.webp", mime_type="image/webp", file_path=str(img)) - response = client.get(f"/files/{rec.id}") + response = client.get(f"/files/{rec.id}/detail") html = response.text # Pan support is implemented via JavaScript on img-wrap @@ -170,7 +170,7 @@ class TestFileViewTextPreview: txt.write_text("Hello world\nSecond line\n") rec = _create_file_record(db_session, filename="readme.txt", mime_type="text/plain", file_path=str(txt)) - response = client.get(f"/files/{rec.id}") + response = client.get(f"/files/{rec.id}/detail") assert response.status_code == 200 html = response.text @@ -184,7 +184,7 @@ class TestFileViewTextPreview: txt.write_text("print('hello')\n") rec = _create_file_record(db_session, filename="code.py", mime_type="text/x-python", file_path=str(txt)) - response = client.get(f"/files/{rec.id}") + response = client.get(f"/files/{rec.id}/detail") html = response.text assert "copyTextPreview" in html @@ -196,7 +196,7 @@ class TestFileViewTextPreview: txt.write_text("a,b,c\n1,2,3\n") rec = _create_file_record(db_session, filename="data.csv", mime_type="text/csv", file_path=str(txt)) - response = client.get(f"/files/{rec.id}") + response = client.get(f"/files/{rec.id}/detail") html = response.text # JS builds line-number spans @@ -218,7 +218,7 @@ class TestFileViewPreviewIcon: pdf.write_bytes(b"%PDF-1.4") rec = _create_file_record(db_session, file_path=str(pdf), mime_type="application/pdf") - html = client.get(f"/files/{rec.id}").text + html = client.get(f"/files/{rec.id}/detail").text assert "fa-file-pdf" in html def test_image_icon(self, client: TestClient, db_session, tmp_path): @@ -227,7 +227,7 @@ class TestFileViewPreviewIcon: img.write_bytes(b"\xff\xd8\xff\xe0" + b"\x00" * 10) rec = _create_file_record(db_session, filename="p.jpg", mime_type="image/jpeg", file_path=str(img)) - html = client.get(f"/files/{rec.id}").text + html = client.get(f"/files/{rec.id}/detail").text assert "fa-image" in html def test_text_icon(self, client: TestClient, db_session, tmp_path): @@ -236,7 +236,7 @@ class TestFileViewPreviewIcon: txt.write_text("hello") rec = _create_file_record(db_session, filename="t.txt", mime_type="text/plain", file_path=str(txt)) - html = client.get(f"/files/{rec.id}").text + html = client.get(f"/files/{rec.id}/detail").text assert "fa-file-code" in html @@ -321,7 +321,7 @@ class TestFileDetailBottomPreview: pdf.write_bytes(b"%PDF-1.4") rec = _create_file_record(db_session, file_path=str(pdf), processed_path=str(pdf)) - response = client.get(f"/files/{rec.id}/detail") + response = client.get(f"/files/{rec.id}/process") assert response.status_code == 200 html = response.text @@ -335,7 +335,7 @@ class TestFileDetailBottomPreview: pdf.write_bytes(b"%PDF-1.4") rec = _create_file_record(db_session, file_path=str(pdf), processed_path=str(pdf)) - response = client.get(f"/files/{rec.id}/detail") + response = client.get(f"/files/{rec.id}/process") html = response.text assert f"/api/files/{rec.id}/download" in html @@ -350,7 +350,7 @@ class TestFileDetailBottomPreview: file_path=str(img), ) - response = client.get(f"/files/{rec.id}/detail") + response = client.get(f"/files/{rec.id}/process") html = response.text assert f"/api/files/{rec.id}/preview?version=original" in html @@ -372,7 +372,7 @@ class TestFileViewOcrText: rec.ocr_text = "Sample extracted OCR text content" db_session.commit() - html = client.get(f"/files/{rec.id}").text + html = client.get(f"/files/{rec.id}/detail").text assert "toggleOcrText" in html assert "ocr-text-block" in html assert "Sample extracted OCR text content" in html @@ -383,7 +383,7 @@ class TestFileViewOcrText: pdf.write_bytes(b"%PDF-1.4") rec = _create_file_record(db_session, file_path=str(pdf)) - html = client.get(f"/files/{rec.id}").text + html = client.get(f"/files/{rec.id}/detail").text assert "loadText" in html or "Extract" in html @@ -409,5 +409,5 @@ class TestFileViewNoFile: db_session.commit() db_session.refresh(rec) - html = client.get(f"/files/{rec.id}").text + html = client.get(f"/files/{rec.id}/detail").text assert "No file available for preview" in html diff --git a/tests/test_file_detail_endpoints.py b/tests/test_file_detail_endpoints.py index f5536116..a519dc8c 100644 --- a/tests/test_file_detail_endpoints.py +++ b/tests/test_file_detail_endpoints.py @@ -447,7 +447,7 @@ class TestFileDetailView: db_session.commit() # Test detail view - response = client.get(f"/files/{file_record.id}/detail") + response = client.get(f"/files/{file_record.id}/process") assert response.status_code == 200 # Check that response contains HTML with file information assert b"File Information" in response.content @@ -504,7 +504,7 @@ class TestFileDetailView: db_session.commit() # Test detail view - response = client.get(f"/files/{file_record.id}/detail") + response = client.get(f"/files/{file_record.id}/process") assert response.status_code == 200 # Check that response contains branching visualization elements assert b"Process Flow Visualization" in response.content @@ -514,7 +514,7 @@ class TestFileDetailView: def test_file_detail_view_nonexistent(self, client: TestClient): """Test file detail view for nonexistent file.""" - response = client.get("/files/99999/detail") + response = client.get("/files/99999/process") assert response.status_code == 200 # Returns page with error message assert b"not found" in response.content.lower() diff --git a/tests/test_file_detail_enhancements.py b/tests/test_file_detail_enhancements.py index c6645628..e0a4312f 100644 --- a/tests/test_file_detail_enhancements.py +++ b/tests/test_file_detail_enhancements.py @@ -50,7 +50,7 @@ def test_file_detail_page_with_metadata(client: TestClient, db_session, sample_p db_session.refresh(file_record) # Get detail page - response = client.get(f"/files/{file_record.id}/detail") + response = client.get(f"/files/{file_record.id}/process") assert response.status_code == 200 html = response.text @@ -85,7 +85,7 @@ def test_file_detail_with_gpt_metadata(client: TestClient, db_session, sample_pd db_session.refresh(file_record) # Get detail page - response = client.get(f"/files/{file_record.id}/detail") + response = client.get(f"/files/{file_record.id}/process") assert response.status_code == 200 html = response.text @@ -190,7 +190,7 @@ def test_file_detail_shows_file_status_indicators(client: TestClient, db_session db_session.commit() db_session.refresh(file_record) - response = client.get(f"/files/{file_record.id}/detail") + response = client.get(f"/files/{file_record.id}/process") assert response.status_code == 200 html = response.text diff --git a/tests/test_files_view_extended.py b/tests/test_files_view_extended.py index d968b641..bd66c447 100644 --- a/tests/test_files_view_extended.py +++ b/tests/test_files_view_extended.py @@ -142,7 +142,7 @@ class TestFileDetailPage: db_session.commit() # Test file detail page - response = client.get(f"/files/{file_record.id}/detail") + response = client.get(f"/files/{file_record.id}/process") assert response.status_code == 200 content = response.text assert "test.pdf" in content @@ -150,7 +150,7 @@ class TestFileDetailPage: def test_file_detail_page_with_missing_file(self, client: TestClient, db_session): """Test file detail page with non-existent file""" # Try to access non-existent file - response = client.get("/files/99999/detail") + response = client.get("/files/99999/process") assert response.status_code == 200 content = response.text assert "not found" in content.lower() @@ -193,7 +193,7 @@ class TestFileDetailPage: db_session.commit() # Test file detail page - response = client.get(f"/files/{file_record.id}/detail") + response = client.get(f"/files/{file_record.id}/process") assert response.status_code == 200 content = response.text assert "create_file_record" in content @@ -232,7 +232,7 @@ class TestFileDetailPage: db_session.commit() # Test file detail page - response = client.get(f"/files/{file_record.id}/detail") + response = client.get(f"/files/{file_record.id}/process") assert response.status_code == 200 content = response.text # Should show metadata diff --git a/tests/test_views_files_comprehensive.py b/tests/test_views_files_comprehensive.py index d522a637..c36c62ee 100644 --- a/tests/test_views_files_comprehensive.py +++ b/tests/test_views_files_comprehensive.py @@ -206,12 +206,12 @@ class TestFileDetailPage: db_session.add(file) db_session.commit() - response = client.get(f"/files/{file.id}/detail") + response = client.get(f"/files/{file.id}/process") assert response.status_code == 200 def test_file_detail_page_not_found(self, client: TestClient, db_session): """Test file detail page for non-existent file.""" - response = client.get("/files/99999/detail") + response = client.get("/files/99999/process") assert response.status_code == 200 # Still renders template with error def test_file_detail_page_with_processing_logs(self, client: TestClient, db_session, tmp_path): @@ -244,7 +244,7 @@ class TestFileDetailPage: db_session.add(log2) db_session.commit() - response = client.get(f"/files/{file.id}/detail") + response = client.get(f"/files/{file.id}/process") assert response.status_code == 200 def test_file_detail_page_with_metadata_json(self, client: TestClient, db_session, tmp_path): @@ -272,7 +272,7 @@ class TestFileDetailPage: db_session.add(file) db_session.commit() - response = client.get(f"/files/{file.id}/detail") + response = client.get(f"/files/{file.id}/process") assert response.status_code == 200 def test_file_detail_checks_original_file_exists(self, client: TestClient, db_session, tmp_path): @@ -289,7 +289,7 @@ class TestFileDetailPage: db_session.add(file) db_session.commit() - response = client.get(f"/files/{file.id}/detail") + response = client.get(f"/files/{file.id}/process") assert response.status_code == 200 def test_file_detail_error_handling(self, client: TestClient, db_session): @@ -1113,7 +1113,7 @@ class TestFileDetailPageAdditional: db_session.add(file) db_session.commit() - response = client.get(f"/files/{file.id}/detail") + response = client.get(f"/files/{file.id}/process") assert response.status_code == 200 def test_file_detail_step_summary_fallback(self, client: TestClient, db_session, tmp_path): @@ -1144,7 +1144,7 @@ class TestFileDetailPageAdditional: db_session.commit() with patch("app.utils.step_manager.get_step_summary", side_effect=Exception("Table not found")): - response = client.get(f"/files/{file.id}/detail") + response = client.get(f"/files/{file.id}/process") assert response.status_code == 200 def test_file_detail_error_handling(self, client: TestClient, db_session): @@ -1155,7 +1155,7 @@ class TestFileDetailPageAdditional: Mock(status_code=200), ] try: - response = client.get("/files/1/detail") + response = client.get("/files/1/process") assert response.status_code in (200, 500) except Exception: pass @@ -1638,7 +1638,7 @@ class TestFileDetailNoJsonSidecar: db_session.add(file) db_session.commit() - response = client.get(f"/files/{file.id}/detail") + response = client.get(f"/files/{file.id}/process") assert response.status_code == 200 @@ -1936,7 +1936,7 @@ class TestPipelineInfoInViews: pipeline = self._make_system_pipeline(db_session) file_rec = self._make_file(db_session, pipeline_id=None) - response = client.get(f"/files/{file_rec.id}/detail") + response = client.get(f"/files/{file_rec.id}/process") assert response.status_code == 200 assert b"Standard Processing Pipeline" in response.content @@ -1946,7 +1946,7 @@ class TestPipelineInfoInViews: self._make_system_pipeline(db_session) file_rec = self._make_file(db_session, pipeline_id=None) - response = client.get(f"/files/{file_rec.id}/detail") + response = client.get(f"/files/{file_rec.id}/process") assert response.status_code == 200 assert b"System Default" in response.content @@ -1956,28 +1956,28 @@ class TestPipelineInfoInViews: pipeline = self._make_custom_pipeline(db_session) file_rec = self._make_file(db_session, pipeline_id=pipeline.id) - response = client.get(f"/files/{file_rec.id}/detail") + response = client.get(f"/files/{file_rec.id}/process") assert response.status_code == 200 assert b"My Custom Pipeline" in response.content assert b"Custom" in response.content def test_file_view_page_includes_pipeline_name(self, client, db_session): - """GET /files/{id} response body contains the pipeline name in the sidebar.""" + """GET /files/{id}/detail response body contains the pipeline name in the sidebar.""" pipeline = self._make_system_pipeline(db_session) file_rec = self._make_file(db_session, pipeline_id=None) - response = client.get(f"/files/{file_rec.id}") + response = client.get(f"/files/{file_rec.id}/detail") assert response.status_code == 200 assert b"Standard Processing Pipeline" in response.content def test_file_view_page_no_pipeline_shows_standard(self, client, db_session): - """When no pipeline exists, file view shows 'Standard' fallback text.""" + """When no pipeline exists, file detail view shows 'Standard' fallback text.""" # No pipeline in DB file_rec = self._make_file(db_session, pipeline_id=None) - response = client.get(f"/files/{file_rec.id}") + response = client.get(f"/files/{file_rec.id}/detail") assert response.status_code == 200 assert b"Standard" in response.content diff --git a/vendor/embed-pdf-viewer b/vendor/embed-pdf-viewer new file mode 160000 index 00000000..aa45d6ef --- /dev/null +++ b/vendor/embed-pdf-viewer @@ -0,0 +1 @@ +Subproject commit aa45d6ef07aa2c6a77e15387d74661e835239408 From d71add1484d01e4598a61f2e59c106afa8971d6a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Mar 2026 11:57:12 +0000 Subject: [PATCH 4/4] fix: make back-link text consistent with aria-labels across file views Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com> Agent-Logs-Url: https://github.com/christianlouis/DocuElevate/sessions/12276514-bd3d-4e3e-84d9-5977d1f82b19 --- frontend/templates/file_annotations.html | 2 +- frontend/templates/file_detail.html | 2 +- frontend/templates/file_view.html | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/templates/file_annotations.html b/frontend/templates/file_annotations.html index 299ba942..c070d550 100644 --- a/frontend/templates/file_annotations.html +++ b/frontend/templates/file_annotations.html @@ -558,7 +558,7 @@ diff --git a/frontend/templates/file_detail.html b/frontend/templates/file_detail.html index 7c00f2b0..aacda356 100644 --- a/frontend/templates/file_detail.html +++ b/frontend/templates/file_detail.html @@ -1099,7 +1099,7 @@