feat(transcription): enable drawing turquoise rectangles on PDF to create blocks
Some checks failed
CI / Backend Unit Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Unit & Component Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Failing after 1m29s
CI / Backend Unit Tests (pull_request) Failing after 2m40s
CI / E2E Tests (pull_request) Failing after 1h22m53s
Some checks failed
CI / Backend Unit Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Unit & Component Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Failing after 1m29s
CI / Backend Unit Tests (pull_request) Failing after 2m40s
CI / E2E Tests (pull_request) Failing after 1h22m53s
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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<string, number>;
|
||||
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<DrawRect | null>(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;' : ''}
|
||||
"
|
||||
>
|
||||
|
||||
@@ -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();
|
||||
</script>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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<string | null>(null),
|
||||
activeAnnotationPage = $bindable<number | null>(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<string, number>();
|
||||
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}
|
||||
<AnnotationLayer
|
||||
annotations={visibleAnnotations.filter((a) => 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}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user