From 8c26876345113a36cf43004d8669d474b6186ab0 Mon Sep 17 00:00:00 2001 From: Marcel Date: Sun, 5 Apr 2026 21:05:39 +0200 Subject: [PATCH] feat(transcription): add block-level comment threads with quote support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TranscriptionBlock.svelte: - "Kommentieren" button opens expandable comment thread per block - Text selection in textarea captured as quoted text (> "...") prefix - Quote hint "Text markieren für Zitat" shown when block is active/focused - Comment thread uses existing CommentThread with blockId prop CommentThread.svelte: - Add blockId prop for block-level comments URL routing - Add quotedText prop — pre-fills comment input with markdown blockquote - commentsBase now supports 3 URL patterns: document, annotation, block TranscriptionEditView.svelte: - Pass canComment + currentUserId through to block components 3 new frontend tests: - Kommentieren button present - Quote hint shown when active - Quote hint hidden when inactive Co-Authored-By: Claude Sonnet 4.6 --- .../src/lib/components/CommentThread.svelte | 19 ++++- .../lib/components/TranscriptionBlock.svelte | 76 ++++++++++++++++++- .../TranscriptionBlock.svelte.spec.ts | 20 +++++ .../components/TranscriptionEditView.svelte | 15 +++- .../src/routes/documents/[id]/+page.svelte | 2 + 5 files changed, 126 insertions(+), 6 deletions(-) diff --git a/frontend/src/lib/components/CommentThread.svelte b/frontend/src/lib/components/CommentThread.svelte index b741c632..65eff1c4 100644 --- a/frontend/src/lib/components/CommentThread.svelte +++ b/frontend/src/lib/components/CommentThread.svelte @@ -9,24 +9,28 @@ import type { MentionDTO } from '$lib/types'; type Props = { documentId: string; annotationId?: string | null; + blockId?: string | null; initialComments?: Comment[]; loadOnMount?: boolean; canComment: boolean; currentUserId: string | null; canAdmin: boolean; targetCommentId?: string | null; + quotedText?: string | null; onCountChange?: (count: number) => void; }; let { documentId, annotationId = null, + blockId = null, initialComments = [], loadOnMount = false, canComment, currentUserId, canAdmin, targetCommentId = null, + quotedText = null, onCountChange }: Props = $props(); @@ -43,11 +47,20 @@ let replyMentionCandidates: MentionDTO[] = $state([]); let editMentionCandidates: MentionDTO[] = $state([]); const commentsBase = $derived( - annotationId - ? `/api/documents/${documentId}/annotations/${annotationId}/comments` - : `/api/documents/${documentId}/comments` + blockId + ? `/api/documents/${documentId}/transcription-blocks/${blockId}/comments` + : annotationId + ? `/api/documents/${documentId}/annotations/${annotationId}/comments` + : `/api/documents/${documentId}/comments` ); +// Pre-fill comment box with quoted text when selection changes +$effect(() => { + if (quotedText && quotedText.trim()) { + newText = `> "${quotedText}"\n\n`; + } +}); + function timeAgo(iso: string): string { const diff = Date.now() - new Date(iso).getTime(); const minutes = Math.floor(diff / 60000); diff --git a/frontend/src/lib/components/TranscriptionBlock.svelte b/frontend/src/lib/components/TranscriptionBlock.svelte index 0b07068f..0b370561 100644 --- a/frontend/src/lib/components/TranscriptionBlock.svelte +++ b/frontend/src/lib/components/TranscriptionBlock.svelte @@ -1,15 +1,19 @@
-
+
+
+ + {#if active} + + {m.transcription_block_quote_hint()} + + {/if} +
+
{#if saveState === 'saving'} @@ -143,5 +185,35 @@ function handleDelete() {
+ + + {#if commentOpen} +
+
+ + {m.comment_section_title()} + + +
+ +
+ {/if}
diff --git a/frontend/src/lib/components/TranscriptionBlock.svelte.spec.ts b/frontend/src/lib/components/TranscriptionBlock.svelte.spec.ts index e3158023..7489ed52 100644 --- a/frontend/src/lib/components/TranscriptionBlock.svelte.spec.ts +++ b/frontend/src/lib/components/TranscriptionBlock.svelte.spec.ts @@ -8,11 +8,14 @@ afterEach(cleanup); function renderBlock(overrides: Record = {}) { return render(TranscriptionBlock, { blockId: 'block-1', + documentId: 'doc-1', blockNumber: 3, text: 'Liebe Mutter,', label: null, active: false, saveState: 'idle' as const, + canComment: true, + currentUserId: 'user-1', onTextChange: vi.fn(), onFocus: vi.fn(), onDeleteClick: vi.fn(), @@ -111,4 +114,21 @@ describe('TranscriptionBlock — interactions', () => { await textarea.click(); expect(onFocus).toHaveBeenCalled(); }); + + it('shows Kommentieren button that opens comment thread', async () => { + renderBlock(); + const btn = page.getByText('Kommentieren'); + await expect.element(btn).toBeInTheDocument(); + }); + + it('shows quote hint when block is active', async () => { + renderBlock({ active: true }); + await expect.element(page.getByText('Text markieren für Zitat')).toBeInTheDocument(); + }); + + it('hides quote hint when block is not active', async () => { + renderBlock({ active: false }); + const hint = page.getByText('Text markieren für Zitat'); + await expect.element(hint).not.toBeInTheDocument(); + }); }); diff --git a/frontend/src/lib/components/TranscriptionEditView.svelte b/frontend/src/lib/components/TranscriptionEditView.svelte index e30d10f5..65fba8f0 100644 --- a/frontend/src/lib/components/TranscriptionEditView.svelte +++ b/frontend/src/lib/components/TranscriptionEditView.svelte @@ -9,12 +9,22 @@ type SaveState = 'idle' | 'saving' | 'saved' | 'fading' | 'error'; type Props = { documentId: string; blocks: TranscriptionBlockData[]; + canComment: boolean; + currentUserId: string | null; onBlockFocus: (blockId: string) => void; onSaveBlock: (blockId: string, text: string) => Promise; onDeleteBlock: (blockId: string) => Promise; }; -let { documentId, blocks, onBlockFocus, onSaveBlock, onDeleteBlock }: Props = $props(); +let { + documentId, + blocks, + canComment, + currentUserId, + onBlockFocus, + onSaveBlock, + onDeleteBlock +}: Props = $props(); let activeBlockId: string | null = $state(null); let saveStates = new SvelteMap(); @@ -149,11 +159,14 @@ $effect(() => {
handleTextChange(block.id, text)} onFocus={() => handleFocus(block.id)} onDeleteClick={() => handleDelete(block.id)} diff --git a/frontend/src/routes/documents/[id]/+page.svelte b/frontend/src/routes/documents/[id]/+page.svelte index ff05d7f4..2b8d4aaf 100644 --- a/frontend/src/routes/documents/[id]/+page.svelte +++ b/frontend/src/routes/documents/[id]/+page.svelte @@ -234,6 +234,8 @@ onMount(() => {