From 99e2e6e5c1653233eafc7fc27170cf93dbab9a71 Mon Sep 17 00:00:00 2001 From: Marcel Date: Sun, 5 Apr 2026 20:44:45 +0200 Subject: [PATCH] feat(transcription): enable drawing turquoise rectangles on PDF to create blocks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AnnotationLayer: add dimColor prop — annotations matching dim color render at 30% opacity with pointer-events disabled (300ms transition) - PdfViewer: add transcribeMode prop, derived drawingEnabled/drawColor; in transcribe mode draws with turquoise (#00C7B1), routes draw events to onTranscriptionDraw callback instead of annotation endpoint - DocumentViewer: pass through transcribeMode + onTranscriptionDraw - Document detail page: createBlockFromDraw() POSTs to transcription blocks API on draw completion, adds created block to list - Mode-based dimming: yellow annotations dim in transcribe mode, turquoise annotations dim in annotate mode Co-Authored-By: Claude Sonnet 4.6 --- .../src/lib/components/AnnotationLayer.svelte | 12 +++++-- .../src/lib/components/DocumentViewer.svelte | 10 +++++- frontend/src/lib/components/PdfViewer.svelte | 25 +++++++++++--- .../src/routes/documents/[id]/+page.svelte | 33 +++++++++++++++++++ 4 files changed, 73 insertions(+), 7 deletions(-) diff --git a/frontend/src/lib/components/AnnotationLayer.svelte b/frontend/src/lib/components/AnnotationLayer.svelte index 3d79b61b..5d103b11 100644 --- a/frontend/src/lib/components/AnnotationLayer.svelte +++ b/frontend/src/lib/components/AnnotationLayer.svelte @@ -12,6 +12,7 @@ let { annotations = [], canAnnotate, color, + dimColor, onDraw, onDelete, commentCounts, @@ -20,12 +21,18 @@ let { annotations: Annotation[]; canAnnotate: boolean; color: string; + dimColor?: string; onDraw: (rect: { x: number; y: number; width: number; height: number }) => void; onDelete: (id: string) => void; commentCounts?: Record; onAnnotationClick?: (id: string) => void; } = $props(); +function isDimmed(annotation: Annotation): boolean { + if (!dimColor) return false; + return annotation.color.toLowerCase() === dimColor.toLowerCase(); +} + let drawStart = $state<{ x: number; y: number } | null>(null); let drawRect = $state(null); @@ -123,8 +130,9 @@ const containerStyle = $derived( height: {annotation.height * 100}%; background-color: {hexToRgba(annotation.color, hoveredId === annotation.id ? 0.5 : 0.3)}; box-shadow: {hoveredId === annotation.id ? `inset 0 0 0 2px ${hexToRgba(annotation.color, 0.8)}` : 'none'}; - pointer-events: auto; - transition: background-color 0.15s ease, box-shadow 0.15s ease; + opacity: {isDimmed(annotation) ? 0.3 : 1}; + pointer-events: {isDimmed(annotation) ? 'none' : 'auto'}; + transition: background-color 0.15s ease, box-shadow 0.15s ease, opacity 0.3s ease; {onAnnotationClick && !canAnnotate ? 'cursor: pointer;' : ''} " > diff --git a/frontend/src/lib/components/DocumentViewer.svelte b/frontend/src/lib/components/DocumentViewer.svelte index 0806d784..5f70772c 100644 --- a/frontend/src/lib/components/DocumentViewer.svelte +++ b/frontend/src/lib/components/DocumentViewer.svelte @@ -9,15 +9,19 @@ type Doc = { fileHash?: string | null; }; +type DrawRect = { x: number; y: number; width: number; height: number; pageNumber: number }; + type Props = { doc: Doc; fileUrl: string; isLoading: boolean; error: string; annotateMode: boolean; + transcribeMode?: boolean; activeAnnotationId: string | null; activeAnnotationPage: number | null; onAnnotationClick: (id: string) => void; + onTranscriptionDraw?: (rect: DrawRect) => void; }; let { @@ -26,9 +30,11 @@ let { isLoading, error, annotateMode = $bindable(), + transcribeMode = false, activeAnnotationId = $bindable(), activeAnnotationPage = $bindable(), - onAnnotationClick + onAnnotationClick, + onTranscriptionDraw }: Props = $props(); @@ -81,9 +87,11 @@ let { url={fileUrl} documentId={doc.id} bind:annotateMode={annotateMode} + transcribeMode={transcribeMode} bind:activeAnnotationId={activeAnnotationId} bind:activeAnnotationPage={activeAnnotationPage} onAnnotationClick={onAnnotationClick} + onTranscriptionDraw={onTranscriptionDraw} documentFileHash={doc.fileHash ?? null} /> {:else if fileUrl} diff --git a/frontend/src/lib/components/PdfViewer.svelte b/frontend/src/lib/components/PdfViewer.svelte index 704a8a07..fa07b050 100644 --- a/frontend/src/lib/components/PdfViewer.svelte +++ b/frontend/src/lib/components/PdfViewer.svelte @@ -6,21 +6,27 @@ import AnnotationLayer from './AnnotationLayer.svelte'; import type { Annotation } from '$lib/types'; import { m } from '$lib/paraglide/messages.js'; +type DrawRect = { x: number; y: number; width: number; height: number; pageNumber: number }; + let { url, documentId = '', annotateMode = $bindable(false), + transcribeMode = false, activeAnnotationId = $bindable(null), activeAnnotationPage = $bindable(null), onAnnotationClick, + onTranscriptionDraw, documentFileHash }: { url: string; documentId?: string; annotateMode?: boolean; + transcribeMode?: boolean; activeAnnotationId?: string | null; activeAnnotationPage?: number | null; onAnnotationClick?: (id: string) => void; + onTranscriptionDraw?: (rect: DrawRect) => void; documentFileHash?: string | null; } = $props(); @@ -49,6 +55,10 @@ let annotateColor = $state('#ffff00'); let commentCounts = new SvelteMap(); let showAnnotations = $state(true); +const TRANSCRIPTION_COLOR = '#00C7B1'; +const drawingEnabled = $derived(annotateMode || transcribeMode); +const drawColor = $derived(transcribeMode ? TRANSCRIPTION_COLOR : annotateColor); + const visibleAnnotations = $derived( annotations.filter((a) => !a.fileHash || !documentFileHash || a.fileHash === documentFileHash) ); @@ -194,8 +204,14 @@ async function loadAnnotations(docId: string) { } } -async function handleAnnotationDraw(rect: { x: number; y: number; width: number; height: number }) { +async function handleDraw(rect: { x: number; y: number; width: number; height: number }) { if (!documentId) return; + + if (transcribeMode) { + onTranscriptionDraw?.({ ...rect, pageNumber: currentPage }); + return; + } + try { const res = await fetch(`/api/documents/${documentId}/annotations`, { method: 'POST', @@ -486,9 +502,10 @@ function zoomOut() { {#if showAnnotations} a.pageNumber === currentPage)} - canAnnotate={annotateMode} - color={annotateColor} - onDraw={handleAnnotationDraw} + canAnnotate={drawingEnabled} + color={drawColor} + dimColor={transcribeMode ? '#ffff00' : annotateMode ? TRANSCRIPTION_COLOR : undefined} + onDraw={handleDraw} onDelete={handleAnnotationDelete} commentCounts={Object.fromEntries(commentCounts)} onAnnotationClick={handleAnnotationClick} diff --git a/frontend/src/routes/documents/[id]/+page.svelte b/frontend/src/routes/documents/[id]/+page.svelte index fac7d987..ff05d7f4 100644 --- a/frontend/src/routes/documents/[id]/+page.svelte +++ b/frontend/src/routes/documents/[id]/+page.svelte @@ -99,6 +99,37 @@ async function deleteBlock(blockId: string) { transcriptionBlocks = transcriptionBlocks.filter((b) => b.id !== blockId); } +async function createBlockFromDraw(rect: { + x: number; + y: number; + width: number; + height: number; + pageNumber: number; +}) { + try { + const res = await fetch(`/api/documents/${doc.id}/transcription-blocks`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + pageNumber: rect.pageNumber, + x: rect.x, + y: rect.y, + width: rect.width, + height: rect.height, + text: '', + label: null + }) + }); + if (res.ok) { + const created = (await res.json()) as TranscriptionBlockData; + transcriptionBlocks = [...transcriptionBlocks, created]; + activeAnnotationId = created.annotationId; + } + } catch (e) { + console.error('Failed to create transcription block:', e); + } +} + function handleBlockFocus(blockId: string) { const block = transcriptionBlocks.find((b) => b.id === blockId); if (block) { @@ -172,11 +203,13 @@ onMount(() => { isLoading={isLoading} error={fileError} bind:annotateMode={annotateMode} + transcribeMode={transcribeMode} bind:activeAnnotationId={activeAnnotationId} bind:activeAnnotationPage={activeAnnotationPage} onAnnotationClick={(id) => { activeAnnotationId = id; }} + onTranscriptionDraw={createBlockFromDraw} />