feat(transcription): add frontend transcription editing UI (#176)
Some checks failed
CI / Unit & Component Tests (push) Failing after 1m27s
CI / Backend Unit Tests (push) Failing after 2m40s
CI / E2E Tests (push) Failing after 4m44s
CI / Unit & Component Tests (pull_request) Failing after 1m21s
CI / Backend Unit Tests (pull_request) Failing after 2m27s
CI / E2E Tests (pull_request) Failing after 4m47s

TranscriptionBlock.svelte: editable block card with auto-resize textarea,
  per-block save indicator, turquoise focus border, delete with confirmation
TranscriptionEditView.svelte: right panel with sorted block list,
  debounced auto-save (1.5s), beforeunload flush, empty state CTA
DocumentTopBar: add Transcribe/Done toggle with turquoise styling,
  mode exclusivity (transcribe and annotate mutually exclusive)
Document detail page: split view in transcribe mode (PDF left, blocks right),
  load/save/delete blocks via fetch, block focus syncs to annotation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-04-05 11:34:01 +02:00
parent 5211e0b9f7
commit 1efd3d8e23
4 changed files with 541 additions and 33 deletions

View File

@@ -31,9 +31,17 @@ type Props = {
canAnnotate: boolean;
fileUrl: string;
annotateMode: boolean;
transcribeMode: boolean;
};
let { doc, canWrite, canAnnotate, fileUrl, annotateMode = $bindable() }: Props = $props();
let {
doc,
canWrite,
canAnnotate,
fileUrl,
annotateMode = $bindable(),
transcribeMode = $bindable()
}: Props = $props();
let detailsOpen = $state(false);
@@ -92,6 +100,65 @@ let mobileMenuOpen = $state(false);
</button>
{/snippet}
{#snippet transcribeBtn(mobile: boolean)}
<button
onclick={() => {
transcribeMode = true;
annotateMode = false;
if (mobile) mobileMenuOpen = false;
}}
aria-label={m.transcription_mode_label()}
aria-pressed={false}
class={mobile
? 'flex w-full items-center gap-2 rounded px-3 py-2 text-left text-[16px] text-ink transition hover:bg-muted focus-visible:ring-2 focus-visible:ring-primary'
: 'hidden items-center gap-1.5 rounded border border-[#00C7B1] px-3 py-1.5 font-sans text-[16px] font-medium text-ink transition hover:bg-[#00C7B1] hover:text-white focus-visible:ring-2 focus-visible:ring-primary md:flex'}
>
<svg
class="h-5 w-5 shrink-0"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="1.5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"
/>
</svg>
{m.transcription_mode_label()}
</button>
{/snippet}
{#snippet transcribeStopBtn(mobile: boolean)}
<button
onclick={() => {
transcribeMode = false;
if (mobile) mobileMenuOpen = false;
}}
aria-label={m.transcription_mode_stop()}
aria-pressed={true}
class={mobile
? 'flex w-full items-center gap-2 rounded bg-[#00C7B1] px-3 py-2 text-left text-[16px] text-white transition focus-visible:ring-2 focus-visible:ring-primary'
: 'flex items-center gap-1.5 rounded bg-[#00C7B1] px-3 py-1.5 font-sans text-[16px] font-medium text-white transition focus-visible:ring-2 focus-visible:ring-primary'}
>
<svg
class="h-5 w-5 shrink-0"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="1.5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"
/>
</svg>
{m.transcription_mode_stop()}
</button>
{/snippet}
{#snippet downloadLink(mobile: boolean)}
<a
href={fileUrl}
@@ -189,7 +256,15 @@ let mobileMenuOpen = $state(false);
<!-- Action buttons -->
<div class="flex shrink-0 items-center gap-1.5 pr-3 font-sans">
{#if canAnnotate && isPdf && !annotateMode}
{#if canWrite && isPdf && !transcribeMode && !annotateMode}
{@render transcribeBtn(false)}
{/if}
{#if transcribeMode}
{@render transcribeStopBtn(false)}
{/if}
{#if canAnnotate && isPdf && !annotateMode && !transcribeMode}
{@render annotateBtn(false)}
{/if}
@@ -197,7 +272,7 @@ let mobileMenuOpen = $state(false);
{@render annotateStopBtn(false)}
{/if}
{#if canWrite && !annotateMode}
{#if canWrite && !annotateMode && !transcribeMode}
<a
href="/documents/{doc.id}/edit"
aria-label={m.btn_edit()}
@@ -246,7 +321,11 @@ let mobileMenuOpen = $state(false);
role="menu"
class="absolute top-full right-0 z-50 mt-1 min-w-[200px] rounded-md border border-line bg-surface p-2 shadow-lg"
>
{#if canAnnotate && isPdf && !annotateMode}
{#if canWrite && isPdf && !transcribeMode && !annotateMode}
{@render transcribeBtn(true)}
{/if}
{#if canAnnotate && isPdf && !annotateMode && !transcribeMode}
{@render annotateBtn(true)}
{/if}