diff --git a/frontend/src/lib/document/transcription/drawCue.spec.ts b/frontend/src/lib/document/transcription/drawCue.spec.ts new file mode 100644 index 00000000..f259bb38 --- /dev/null +++ b/frontend/src/lib/document/transcription/drawCue.spec.ts @@ -0,0 +1,14 @@ +import { describe, it, expect } from 'vitest'; +import { canArmDraw, shouldDisarmDraw } from './drawCue'; + +describe('draw cue policy', () => { + it('arms only in edit mode', () => { + expect(canArmDraw('edit')).toBe(true); + expect(canArmDraw('read')).toBe(false); + }); + + it('disarms in every mode except edit', () => { + expect(shouldDisarmDraw('read')).toBe(true); + expect(shouldDisarmDraw('edit')).toBe(false); + }); +}); diff --git a/frontend/src/lib/document/transcription/drawCue.ts b/frontend/src/lib/document/transcription/drawCue.ts new file mode 100644 index 00000000..3d034838 --- /dev/null +++ b/frontend/src/lib/document/transcription/drawCue.ts @@ -0,0 +1,20 @@ +/** + * Policy for the "draw a new region" keyboard cue (the `n` shortcut) in the + * transcribe panel — issue #327. + * + * The cue is only valid while editing: `n` arms it in edit mode, and it must + * clear when a region is drawn or when the panel leaves edit mode. Pure so both + * rules are testable without mounting the page. + */ + +type PanelMode = 'read' | 'edit'; + +/** The draw cue may only be armed while in edit mode. */ +export function canArmDraw(panelMode: PanelMode): boolean { + return panelMode === 'edit'; +} + +/** Leaving edit mode must disarm the draw cue. */ +export function shouldDisarmDraw(panelMode: PanelMode): boolean { + return !canArmDraw(panelMode); +} diff --git a/frontend/src/lib/document/transcription/trainingMark.spec.ts b/frontend/src/lib/document/transcription/trainingMark.spec.ts new file mode 100644 index 00000000..b8f2212b --- /dev/null +++ b/frontend/src/lib/document/transcription/trainingMark.spec.ts @@ -0,0 +1,30 @@ +import { describe, it, expect } from 'vitest'; +import { resolveTrainingMark, RECOGNITION_TRAINING_LABEL } from './trainingMark'; + +describe('resolveTrainingMark', () => { + it('is a no-op (null) when no region is active', () => { + expect(resolveTrainingMark(null, [])).toBe(null); + expect(resolveTrainingMark(null, [RECOGNITION_TRAINING_LABEL])).toBe(null); + }); + + it('enrols recognition training when a region is active and not yet enrolled', () => { + expect(resolveTrainingMark('ann-1', [])).toEqual({ + label: RECOGNITION_TRAINING_LABEL, + enrolled: true + }); + }); + + it('un-enrols when recognition training is already enrolled', () => { + expect(resolveTrainingMark('ann-1', [RECOGNITION_TRAINING_LABEL])).toEqual({ + label: RECOGNITION_TRAINING_LABEL, + enrolled: false + }); + }); + + it('ignores unrelated document training labels', () => { + expect(resolveTrainingMark('ann-1', ['KURRENT_SEGMENTATION'])).toEqual({ + label: RECOGNITION_TRAINING_LABEL, + enrolled: true + }); + }); +}); diff --git a/frontend/src/lib/document/transcription/trainingMark.ts b/frontend/src/lib/document/transcription/trainingMark.ts new file mode 100644 index 00000000..0f930c83 --- /dev/null +++ b/frontend/src/lib/document/transcription/trainingMark.ts @@ -0,0 +1,31 @@ +/** + * "Mark for training" (the `t` shortcut) decision logic — issue #327. + * + * Training enrollment is document-level — two fixed script-type chips + * (KURRENT_RECOGNITION / KURRENT_SEGMENTATION); there is no per-region training + * flag yet (that arrives with #321). `t` toggles the primary recognition + * enrollment and is a silent no-op unless a region is active, so it reads as an + * action on the region the transcriber is working on. + * + * Pure so the no-op-when-no-region guard and the enrolled flip are testable + * without mounting the page. + */ +export const RECOGNITION_TRAINING_LABEL = 'KURRENT_RECOGNITION'; + +export type TrainingMarkToggle = { label: string; enrolled: boolean }; + +/** + * Decide the recognition-training toggle for the active region, or null when no + * region is active (the `t` shortcut is then a silent no-op). + * + * @param activeAnnotationId the currently active region, or null + * @param currentLabels the document's currently enrolled training labels + */ +export function resolveTrainingMark( + activeAnnotationId: string | null, + currentLabels: readonly string[] +): TrainingMarkToggle | null { + if (!activeAnnotationId) return null; + const enrolled = !currentLabels.includes(RECOGNITION_TRAINING_LABEL); + return { label: RECOGNITION_TRAINING_LABEL, enrolled }; +} diff --git a/frontend/src/routes/documents/[id]/+page.svelte b/frontend/src/routes/documents/[id]/+page.svelte index 9d91378e..ff3ac527 100644 --- a/frontend/src/routes/documents/[id]/+page.svelte +++ b/frontend/src/routes/documents/[id]/+page.svelte @@ -13,6 +13,8 @@ import { transcribeShortcuts } from '$lib/shared/actions/transcribeShortcuts'; import { createOcrJob } from '$lib/ocr/useOcrJob.svelte'; import { createTranscriptionBlocks } from '$lib/document/transcription/useTranscriptionBlocks.svelte'; import { stepRegion } from '$lib/document/transcription/regionNavigation'; +import { resolveTrainingMark } from '$lib/document/transcription/trainingMark'; +import { canArmDraw, shouldDisarmDraw } from '$lib/document/transcription/drawCue'; import { createFileLoader } from '$lib/document/viewer/useFileLoader.svelte'; import { scrollToCommentFromQuery } from '$lib/shared/utils/deepLinkScroll'; import { getConfirmService } from '$lib/shared/services/confirm.svelte'; @@ -112,17 +114,9 @@ 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); + const toggle = resolveTrainingMark(activeAnnotationId, doc.trainingLabels ?? []); + if (toggle) transcription.toggleTrainingLabel(toggle.label, toggle.enrolled); } function deleteCurrentRegion() { @@ -131,7 +125,7 @@ function deleteCurrentRegion() { // Disarm the draw cue whenever we leave edit mode. $effect(() => { - if (panelMode !== 'edit') drawArmed = false; + if (shouldDisarmDraw(panelMode)) drawArmed = false; }); const shortcutOptions = { @@ -142,7 +136,9 @@ const shortcutOptions = { goToPrevRegion: () => goToRegion(-1), toggleMode, closePanel: () => (transcribeMode = false), - startDrawMode: () => (drawArmed = true), + startDrawMode: () => { + if (canArmDraw(panelMode)) drawArmed = true; + }, toggleTrainingMark, deleteCurrentRegion, openCheatsheet: () => (cheatsheetOpen = true)