From 18295e8e4fc0a80eb85e6847721ef05e1501a5ee Mon Sep 17 00:00:00 2001 From: curo1305 Date: Tue, 14 Apr 2026 16:12:45 +0200 Subject: [PATCH] Add tag editing and PDF preview to documents feature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each document's tags are now editable inline: click Edit to enter a tag editor (Enter/comma to add, × to remove, Save to persist). The View button opens the PDF in a new browser tab via blob URL. Both features work through the existing proxy — no proxy changes needed. Co-Authored-By: Claude Sonnet 4.6 --- features/doc-service/app/routers/documents.py | 24 +++- features/doc-service/app/schemas/document.py | 4 + frontend/src/api/client.ts | 12 ++ frontend/src/pages/DocumentsPage.tsx | 129 ++++++++++++++++-- 4 files changed, 157 insertions(+), 12 deletions(-) diff --git a/features/doc-service/app/routers/documents.py b/features/doc-service/app/routers/documents.py index 6b52e12..eda9f09 100644 --- a/features/doc-service/app/routers/documents.py +++ b/features/doc-service/app/routers/documents.py @@ -16,7 +16,7 @@ from app.deps import get_user_id from app.models.category import DocumentCategory from app.models.category_assignment import CategoryAssignment from app.models.document import Document -from app.schemas.document import DocumentOut, DocumentStatusOut, DocumentTypeUpdate +from app.schemas.document import DocumentOut, DocumentStatusOut, DocumentTypeUpdate, TagsUpdate from app.services.ai_client import AIServiceError, classify_document from app.services.config_reader import load_doc_config from app.services.storage import delete_file, get_upload_path, save_upload @@ -205,6 +205,28 @@ async def update_document_type( return _doc_with_categories(doc) +@router.patch("/{doc_id}/tags", response_model=DocumentOut) +async def update_document_tags( + doc_id: str, + body: TagsUpdate, + user_id: str = Depends(get_user_id), + db: AsyncSession = Depends(get_db), +) -> DocumentOut: + doc = await _get_user_doc(doc_id, user_id, db) + # Normalise: strip whitespace, drop empties, deduplicate while preserving order + seen: set[str] = set() + clean: list[str] = [] + for t in body.tags: + t = t.strip() + if t and t.lower() not in seen: + seen.add(t.lower()) + clean.append(t) + doc.tags = json.dumps(clean) + await db.commit() + doc = await _get_user_doc(doc_id, user_id, db) + return _doc_with_categories(doc) + + @router.delete("/{doc_id}", status_code=204) async def delete_document( doc_id: str, diff --git a/features/doc-service/app/schemas/document.py b/features/doc-service/app/schemas/document.py index b00c506..33888aa 100644 --- a/features/doc-service/app/schemas/document.py +++ b/features/doc-service/app/schemas/document.py @@ -37,3 +37,7 @@ class DocumentStatusOut(BaseModel): class DocumentTypeUpdate(BaseModel): document_type: str + + +class TagsUpdate(BaseModel): + tags: list[str] diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 90e2aa0..89349e8 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -136,6 +136,18 @@ export const downloadDocument = async (id: string, filename: string) => { URL.revokeObjectURL(url); }; +export const viewDocument = async (id: string): Promise => { + const response = await api.get(`/documents/${id}/file`, { responseType: "blob" }); + const url = URL.createObjectURL(response.data); + const win = window.open(url, "_blank"); + // Revoke after a generous delay — the new tab needs time to load the blob + setTimeout(() => URL.revokeObjectURL(url), 60_000); + if (!win) alert("Pop-up blocked. Please allow pop-ups for this site to preview PDFs."); +}; + +export const updateDocumentTags = (id: string, tags: string[]) => + api.patch(`/documents/${id}/tags`, { tags }).then((r) => r.data); + export const assignCategory = (docId: string, catId: string) => api.post(`/documents/${docId}/categories/${catId}`); diff --git a/frontend/src/pages/DocumentsPage.tsx b/frontend/src/pages/DocumentsPage.tsx index 19ea90c..b4bd5a1 100644 --- a/frontend/src/pages/DocumentsPage.tsx +++ b/frontend/src/pages/DocumentsPage.tsx @@ -6,11 +6,13 @@ import { uploadDocument, deleteDocument, downloadDocument, + viewDocument, getDocumentStatus, listCategories, createCategory, assignCategory, removeCategory, + updateDocumentTags, type DocumentOut, type CategoryOut, } from "../api/client"; @@ -87,6 +89,110 @@ function SuggestionChip({ name, existing, onAccept, onDismiss }: SuggestionChipP // ── Document row ──────────────────────────────────────────────────────────── +// ── Tag editor ────────────────────────────────────────────────────────────── + +function TagEditor({ + docId, + initialTags, + onSaved, +}: { + docId: string; + initialTags: string[]; + onSaved: () => void; +}) { + const [tags, setTags] = useState(initialTags); + const [input, setInput] = useState(""); + const [editing, setEditing] = useState(false); + + const saveMut = useMutation({ + mutationFn: (t: string[]) => updateDocumentTags(docId, t), + onSuccess: () => { onSaved(); setEditing(false); }, + }); + + const addTag = () => { + const val = input.trim(); + if (!val || tags.map((t) => t.toLowerCase()).includes(val.toLowerCase())) { + setInput(""); + return; + } + const next = [...tags, val]; + setTags(next); + setInput(""); + }; + + const removeTag = (idx: number) => { + setTags((prev) => prev.filter((_, i) => i !== idx)); + }; + + if (!editing) { + return ( +
+ Tags:{" "} + {tags.length === 0 && none} + {tags.map((t) => ( + + {t} + + ))} + +
+ ); + } + + return ( +
+ Tags: +
+ {tags.map((t, i) => ( + + {t} + + + ))} + setInput(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === ",") { e.preventDefault(); addTag(); } + if (e.key === "Escape") { setEditing(false); setTags(initialTags); setInput(""); } + }} + placeholder="Add tag…" + style={{ fontSize: 12, padding: "2px 6px", border: "1px solid #ccc", borderRadius: 3, width: 100 }} + autoFocus + /> +
+
+ + +
+

Enter or comma to add · Esc to cancel

+
+ ); +} + +// ── Document row ──────────────────────────────────────────────────────────── + function DocumentRow({ doc, categories, @@ -192,6 +298,12 @@ function DocumentRow({ {(doc.file_size / 1024).toFixed(0)} KB +