feat(transcribe): wire keyboard shortcuts into the document panel (#327)
Attaches the transcribeShortcuts action to the document page and wires every command to existing context setters: j/k walk the sortOrder-sorted regions and set activeAnnotationId, e toggles read/edit, n arms a draw cue (edit only), Delete routes to the existing confirm path, ? opens the cheatsheet, and Esc is now owned solely by the action — the inline onMount Esc listener is removed (decision B1). Renders ShortcutCheatsheet and a draw-armed hint. "t" toggles the document-level KURRENT_RECOGNITION training enrollment (the only training surface that exists; there is no per-region flag yet — see #321) and no-ops unless a region is active. Also reconciles annotation Delete: the shape no longer self-handles the key, with onfocus syncing the active region so the action deletes exactly once. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,8 @@ import DocumentViewer from '$lib/document/DocumentViewer.svelte';
|
||||
import TranscriptionEditView from '$lib/document/transcription/TranscriptionEditView.svelte';
|
||||
import TranscriptionReadView from '$lib/document/transcription/TranscriptionReadView.svelte';
|
||||
import TranscriptionPanelHeader from '$lib/document/transcription/TranscriptionPanelHeader.svelte';
|
||||
import ShortcutCheatsheet from '$lib/document/transcription/ShortcutCheatsheet.svelte';
|
||||
import { transcribeShortcuts } from '$lib/shared/actions/transcribeShortcuts';
|
||||
import { createOcrJob } from '$lib/ocr/useOcrJob.svelte';
|
||||
import { createTranscriptionBlocks } from '$lib/document/transcription/useTranscriptionBlocks.svelte';
|
||||
import { createFileLoader } from '$lib/document/viewer/useFileLoader.svelte';
|
||||
@@ -42,6 +44,8 @@ let activeAnnotationId = $state<string | null>(null);
|
||||
let highlightBlockId = $state<string | null>(null);
|
||||
let flashAnnotationId = $state<string | null>(null);
|
||||
let pdfStripExpanded = $state(false);
|
||||
let cheatsheetOpen = $state(false);
|
||||
let drawArmed = $state(false);
|
||||
// Flag set by the deep-link helper so the transcribe-mode $effect does not
|
||||
// overwrite the panelMode it picked (e.g. forcing 'edit' on notification
|
||||
// click-through). One-shot: consumed after the effect's loadBlocks resolves.
|
||||
@@ -86,12 +90,69 @@ async function createBlockFromDraw(rect: {
|
||||
height: number;
|
||||
pageNumber: number;
|
||||
}) {
|
||||
drawArmed = false;
|
||||
const created = await transcription.createFromDraw(rect);
|
||||
if (created) {
|
||||
activeAnnotationId = created.annotationId;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Keyboard shortcuts (issue #327) ──────────────────────────────────────────
|
||||
|
||||
const sortedBlocks = $derived([...transcription.blocks].sort((a, b) => a.sortOrder - b.sortOrder));
|
||||
|
||||
function goToRegion(delta: 1 | -1) {
|
||||
if (sortedBlocks.length === 0) return;
|
||||
const current = sortedBlocks.findIndex((b) => b.annotationId === activeAnnotationId);
|
||||
const next =
|
||||
current === -1
|
||||
? delta > 0
|
||||
? 0
|
||||
: sortedBlocks.length - 1
|
||||
: (current + delta + sortedBlocks.length) % sortedBlocks.length;
|
||||
activeAnnotationId = sortedBlocks[next].annotationId;
|
||||
}
|
||||
|
||||
function toggleMode() {
|
||||
if (canWrite) panelMode = panelMode === 'read' ? 'edit' : 'read';
|
||||
}
|
||||
|
||||
// Training enrollment is document-level — two fixed script-type chips
|
||||
// (KURRENT_RECOGNITION / KURRENT_SEGMENTATION); there is no per-region training
|
||||
// flag (that would arrive with #321). "t" toggles the primary recognition
|
||||
// enrollment and stays a no-op unless a region is active, so it reads as an
|
||||
// action on the region the transcriber is working on.
|
||||
const RECOGNITION_TRAINING_LABEL = 'KURRENT_RECOGNITION';
|
||||
|
||||
function toggleTrainingMark() {
|
||||
if (!activeAnnotationId) return;
|
||||
const enrolled = !(doc.trainingLabels ?? []).includes(RECOGNITION_TRAINING_LABEL);
|
||||
transcription.toggleTrainingLabel(RECOGNITION_TRAINING_LABEL, enrolled);
|
||||
}
|
||||
|
||||
function deleteCurrentRegion() {
|
||||
if (activeAnnotationId) handleAnnotationDeleteRequest(activeAnnotationId);
|
||||
}
|
||||
|
||||
// Disarm the draw cue whenever we leave edit mode.
|
||||
$effect(() => {
|
||||
if (panelMode !== 'edit') drawArmed = false;
|
||||
});
|
||||
|
||||
const shortcutOptions = {
|
||||
isPanelOpen: () => transcribeMode,
|
||||
isCheatsheetOpen: () => cheatsheetOpen,
|
||||
panelMode: () => panelMode,
|
||||
goToNextRegion: () => goToRegion(1),
|
||||
goToPrevRegion: () => goToRegion(-1),
|
||||
toggleMode,
|
||||
closePanel: () => (transcribeMode = false),
|
||||
startDrawMode: () => (drawArmed = true),
|
||||
toggleTrainingMark,
|
||||
deleteCurrentRegion,
|
||||
openCheatsheet: () => (cheatsheetOpen = true)
|
||||
};
|
||||
|
||||
function handleBlockFocus(blockId: string) {
|
||||
const block = transcription.blocks.find((b) => b.id === blockId);
|
||||
if (block) {
|
||||
@@ -208,14 +269,9 @@ onMount(() => {
|
||||
onStripUrl: () => replaceState(page.url.pathname, page.state ?? {})
|
||||
}).catch((e) => console.error('deep-link scroll failed', e));
|
||||
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
if (e.key === 'Escape' && transcribeMode) {
|
||||
transcribeMode = false;
|
||||
}
|
||||
}
|
||||
document.addEventListener('keydown', onKeyDown);
|
||||
// Esc is owned solely by the transcribeShortcuts action (issue #327, decision
|
||||
// B1) — no competing inline keydown listener here.
|
||||
return () => {
|
||||
document.removeEventListener('keydown', onKeyDown);
|
||||
ocrJob.destroy();
|
||||
};
|
||||
});
|
||||
@@ -229,6 +285,7 @@ onMount(() => {
|
||||
class="fixed right-0 bottom-0 left-0 z-40 flex flex-col overflow-hidden bg-surface"
|
||||
style="top: {navHeight}px"
|
||||
data-hydrated
|
||||
use:transcribeShortcuts={shortcutOptions}
|
||||
>
|
||||
<DocumentTopBar
|
||||
doc={doc}
|
||||
@@ -260,7 +317,7 @@ onMount(() => {
|
||||
bind:activeAnnotationId={activeAnnotationId}
|
||||
onAnnotationClick={handleAnnotationClick}
|
||||
onTranscriptionDraw={createBlockFromDraw}
|
||||
onDeleteAnnotationRequest={handleAnnotationDeleteRequest}
|
||||
onAnnotationFocus={(id) => (activeAnnotationId = id)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -369,4 +426,15 @@ onMount(() => {
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if drawArmed && panelMode === 'edit'}
|
||||
<div
|
||||
class="pointer-events-none absolute bottom-4 left-1/2 z-50 -translate-x-1/2 rounded-full bg-ink px-4 py-2 font-sans text-xs text-white shadow-lg"
|
||||
role="status"
|
||||
>
|
||||
{m.shortcut_draw_hint()}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<ShortcutCheatsheet open={cheatsheetOpen} onClose={() => (cheatsheetOpen = false)} />
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user