Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions localization/TranscriberAdmin-en-1.2.xliff
Original file line number Diff line number Diff line change
Expand Up @@ -11928,6 +11928,34 @@
<context context-type="sourcefile">passageDetail.tsx</context>
</context-group>
</trans-unit>
<trans-unit id="workflowSteps.incompleteStepComplete">
<source>Complete</source>
<target/>
<context-group>
<context context-type="sourcefile">StepNavigationConfirmDialog.tsx</context>
</context-group>
</trans-unit>
<trans-unit id="workflowSteps.incompleteStepContinue">
<source>Continue</source>
<target/>
<context-group>
<context context-type="sourcefile">StepNavigationConfirmDialog.tsx</context>
</context-group>
</trans-unit>
<trans-unit id="workflowSteps.incompleteStepNavigate">
<source>The current step ({0}) is not complete. Are you sure?</source>
<target/>
<context-group>
<context context-type="sourcefile">StepNavigationConfirmDialog.tsx</context>
</context-group>
</trans-unit>
<trans-unit id="workflowSteps.incompleteStepNavigateTitle">
<source>Step not complete</source>
<target/>
<context-group>
<context context-type="sourcefile">StepNavigationConfirmDialog.tsx</context>
</context-group>
</trans-unit>
<trans-unit id="workflowSteps.internalize">
<source>Internalize</source>
<target/>
Expand Down
24 changes: 24 additions & 0 deletions localization/TranscriberAdmin-en.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -10222,6 +10222,30 @@
<target/>
</segment>
</unit>
<unit id="workflowSteps.incompleteStepComplete">
<segment>
<source>Complete</source>
<target/>
</segment>
</unit>
<unit id="workflowSteps.incompleteStepContinue">
<segment>
<source>Continue</source>
<target/>
</segment>
</unit>
<unit id="workflowSteps.incompleteStepNavigate">
<segment>
<source>The current step ({0}) is not complete. Are you sure?</source>
<target/>
</segment>
</unit>
<unit id="workflowSteps.incompleteStepNavigateTitle">
<segment>
<source>Step not complete</source>
<target/>
</segment>
</unit>
<unit id="workflowSteps.internalize">
<segment>
<source>Internalize</source>
Expand Down
1 change: 1 addition & 0 deletions src/renderer/public/localization/strings05e142f9.json

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion src/renderer/public/localization/strings7c4590f5.json

This file was deleted.

3 changes: 3 additions & 0 deletions src/renderer/src/components/MediaRecord.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ interface IProps {
onDockedRecordButton?: (node: React.ReactNode | null) => void;
/** When true, show the docked record button even if allowRecord is false (button may be disabled). */
showDockedRecordButton?: boolean;
onRecordingCleared?: () => void;
}
export const DEFAULT_COMPRESSED_MIME = 'audio/ogg;codecs=opus';

Expand Down Expand Up @@ -155,6 +156,7 @@ function MediaRecord(props: IProps) {
dockRecordButton,
onDockedRecordButton,
showDockedRecordButton,
onRecordingCleared,
} = props;
const context = usePassageDetailContext();
const simplified = Boolean(context?.isBoldWorkflow);
Expand Down Expand Up @@ -646,6 +648,7 @@ function MediaRecord(props: IProps) {
dockRecordButton={dockRecordButton}
onDockedRecordButton={onDockedRecordButton}
showDockedRecordButton={showDockedRecordButton}
onRecordingCleared={onRecordingCleared}
/>
{warning && !effectiveMobileView && (
<Typography sx={{ m: 2, color: 'warning.dark' }} id="warning">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,11 @@ const seekFor = (i: number) =>
let mockCompleted = new Set<number>();
let controlsProps: Record<string, unknown> | undefined;
let playerProps: Record<string, unknown> | undefined;
let confirmShown = false;
let confirmDismiss: (() => void) | undefined;
let mockStepComplete = false;
let mockRecordingRow:
| { mediafile: { id: string; attributes?: { sourceSegments?: string } } }
| undefined;

const mockSetStepComplete = jest.fn().mockResolvedValue(undefined);
const mockGotoNextStep = jest.fn();
const mockStepCompleteFn = jest.fn(() => mockStepComplete);
const mockWaitForSave = jest.fn().mockResolvedValue(undefined);
const mockMemoryUpdate = jest.fn().mockResolvedValue(undefined);

Expand Down Expand Up @@ -77,8 +72,7 @@ const ctx: {
carefulSpeechSegParams: {},
setCarefulSpeechSegParams: jest.fn(),
setStepComplete: mockSetStepComplete,
gotoNextStep: mockGotoNextStep,
stepComplete: mockStepCompleteFn,
stepComplete: jest.fn(() => mockStepComplete),
// updating the engine segment keeps getCurrentSegment consistent with the
// index the component believes it is on.
setCurrentSegment: jest.fn((region: IRegion) => {
Expand Down Expand Up @@ -179,20 +173,7 @@ jest.mock('./carefulSpeech/CarefulSpeechControls', () => ({
},
}));

jest.mock('../AlertDialog', () => ({
__esModule: true,
default: ({
text,
yesResponse,
}: {
text: string;
yesResponse: () => void;
}) => {
confirmShown = true;
confirmDismiss = yesResponse;
return <div data-testid="confirm">{text}</div>;
},
}));
let mockStepComplete = false;

// imported after the mocks so the component picks them up
import { PassageDetailCarefulSpeech } from './PassageDetailCarefulSpeech';
Expand Down Expand Up @@ -232,8 +213,6 @@ beforeEach(() => {
mockCompleted = new Set<number>();
controlsProps = undefined;
playerProps = undefined;
confirmShown = false;
confirmDismiss = undefined;
mockStepComplete = false;
mockRecordingRow = undefined;
ctx._seg = regions[0];
Expand Down Expand Up @@ -271,45 +250,28 @@ describe('PassageDetailCarefulSpeech — entry positioning', () => {
expect(stubControls.gotoTime).toHaveBeenCalledWith(seekFor(2), regions[2]);
});

it('all clauses recorded: enters recording (review) mode and shows the dialog', async () => {
it('all clauses recorded: enters review mode and marks step complete without advancing', async () => {
mockCompleted = new Set([0, 1, 2, 3, 4, 5, 6, 7]);
await mountAndSettle();

expect(controlsProps?.recordingPassStarted).toBe(true);
expect(controlsProps?.allClausesComplete).toBe(true);
await waitFor(() => expect(confirmShown).toBe(true));
});

it('dismissing the all-complete dialog marks the step complete and advances', async () => {
mockCompleted = new Set([0, 1, 2, 3, 4, 5, 6, 7]);
await mountAndSettle();
await waitFor(() => expect(confirmDismiss).toBeDefined());

await act(async () => {
confirmDismiss?.();
});

await waitFor(() =>
expect(mockWaitForSave).toHaveBeenCalledWith(undefined, 200)
);
await waitFor(() =>
expect(mockSetStepComplete).toHaveBeenCalledWith('step1', true)
);
expect(mockGotoNextStep).toHaveBeenCalled();
});

it('dismissing the all-complete dialog on a completed step does not advance', async () => {
mockCompleted = new Set([0, 1, 2, 3, 4, 5, 6, 7]);
it('marks step incomplete when any clause recording is missing', async () => {
mockCompleted = new Set([0, 1, 2]);
mockStepComplete = true;
await mountAndSettle();
await waitFor(() => expect(confirmDismiss).toBeDefined());

await act(async () => {
confirmDismiss?.();
});

expect(mockSetStepComplete).not.toHaveBeenCalled();
expect(mockGotoNextStep).not.toHaveBeenCalled();
await waitFor(() =>
expect(mockSetStepComplete).toHaveBeenCalledWith('step1', false)
);
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import { ICarefulSpeechStrings, ISharedStrings, MediaFileD } from '../../model';
import { passageDefaultFilename } from '../../utils/passageDefaultFilename';
import { related } from '../../crud/related';
import { RecordKeyMap } from '@orbit/records';
import Confirm from '../AlertDialog';
import CarefulSpeechControls, {
CarefulSpeechPhase,
} from './carefulSpeech/CarefulSpeechControls';
Expand Down Expand Up @@ -106,7 +105,6 @@ export function PassageDetailCarefulSpeech({ width }: IProps) {
carefulSpeechSegParams,
setCarefulSpeechSegParams,
setStepComplete,
gotoNextStep,
stepComplete,
} = usePassageDetailContext();
const { settings } = useStepTool(currentstep);
Expand Down Expand Up @@ -141,7 +139,6 @@ export function PassageDetailCarefulSpeech({ width }: IProps) {
const [resetMedia, setResetMedia] = useState(false);
const [statusText, setStatusText] = useState('');
const [canSave, setCanSave] = useState(false);
const [confirmAllComplete, setConfirmAllComplete] = useState(false);
const [recordingPassStarted, setRecordingPassStarted] = useState(false);
// Mirror of recordingPassStarted set synchronously at the call sites below.
// region-out can fire before React commits the state-update render, leaving
Expand All @@ -154,9 +151,6 @@ export function PassageDetailCarefulSpeech({ width }: IProps) {
// clause; this lets the recording effect swallow that single +1 advance while
// still treating any non-adjacent jump as a genuine user tap (TT-7360).
const pendingOvershootSwallowRef = useRef(false);
// Guards the all-complete dialog so it shows once per completion. Re-armed
// when a recording is cleared (allClausesComplete drops back to false).
const allCompleteNotifiedRef = useRef(false);
const [heardIndices, setHeardIndices] = useState<number[]>([]);
const [currentClausePlayed, setCurrentClausePlayed] = useState(false);
const [combineUndo, setCombineUndo] = useState<string | null>(null);
Expand Down Expand Up @@ -222,20 +216,30 @@ export function PassageDetailCarefulSpeech({ width }: IProps) {
[completedIndices, clauseRegions]
);

// Raise the all-complete dialog once when every clause has a recording —
// both on entry to an already-complete step and the moment the user records
// the last clause. Re-arms if a recording is later cleared.
// Mirror clause recording coverage into step completion (no auto-advance).
useEffect(() => {
if (!recordingPassStarted) return;
if (allClausesComplete) {
if (!allCompleteNotifiedRef.current) {
allCompleteNotifiedRef.current = true;
setConfirmAllComplete(true);
if (!bootstrapped || clauseRegions.length === 0) return;

const syncStepComplete = async () => {
const isComplete = stepComplete(currentstep);
if (allClausesComplete) {
if (!isComplete) {
try {
await waitForSave(undefined, 200);
} catch {
return;
}
await setStepComplete(currentstep, true);
}
} else if (isComplete) {
await setStepComplete(currentstep, false);
}
} else {
allCompleteNotifiedRef.current = false;
}
}, [allClausesComplete, recordingPassStarted]);
};

void syncStepComplete();
// stepComplete reads psgCompleted internally; waitForSave identity is unstable.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [allClausesComplete, bootstrapped, clauseRegions.length, currentstep]);

const currentRegion = clauseRegions[currentIndex];

Expand Down Expand Up @@ -477,7 +481,6 @@ export function PassageDetailCarefulSpeech({ width }: IProps) {
setRecordingPassStarted(false);
recordingPassStartedRef.current = false;
pendingOvershootSwallowRef.current = false;
allCompleteNotifiedRef.current = false;
setHeardIndices([]);
setCurrentClausePlayed(false);
setCombineUndo(null);
Expand Down Expand Up @@ -547,8 +550,7 @@ export function PassageDetailCarefulSpeech({ width }: IProps) {
if (firstIdx >= clauseRegions.length) {
// All clauses are recorded. Enter recording (review) mode positioned on
// the first clause so the user can replay both the original and the
// careful-speech take per clause. The all-complete dialog is raised by
// the effect that watches allClausesComplete (TT-7360).
// careful-speech take per clause. Step completion syncs via effect.
setRecordingPassStarted(true);
recordingPassStartedRef.current = true;
setShowRecorder(true);
Expand Down Expand Up @@ -932,7 +934,12 @@ export function PassageDetailCarefulSpeech({ width }: IProps) {
setCombineUndo(null);
const next = firstIncompleteClauseIndex(clauseRegions, completedIndices);
if (next >= clauseRegions.length) {
setConfirmAllComplete(true);
setCurrentIndex(0);
setCurrentSegment(clauseRegions[0], 0);
setCurrentClausePlayed(true);
setPhase('recorded');
void snapToClauseStart(0);
applyColors();
return;
}
setCurrentIndex(next);
Expand All @@ -942,14 +949,23 @@ export function PassageDetailCarefulSpeech({ width }: IProps) {
setPhase('readyToRecord');
setHighlightPlayButton(false);
void playCurrentClause(next);
}, [clauseRegions, completedIndices, setCurrentSegment, playCurrentClause]);
}, [
clauseRegions,
completedIndices,
setCurrentSegment,
playCurrentClause,
snapToClauseStart,
applyColors,
]);

const handleNextClause = useCallback(async () => {
const effectiveCompleted = new Set(completedIndices);
effectiveCompleted.add(currentIndex);
const next = firstIncompleteClauseIndex(clauseRegions, effectiveCompleted);
if (next >= clauseRegions.length) {
setConfirmAllComplete(true);
setCurrentClausePlayed(true);
setPhase('recorded');
applyColors();
return;
}
setCurrentIndex(next);
Expand All @@ -971,6 +987,7 @@ export function PassageDetailCarefulSpeech({ width }: IProps) {
currentIndex,
setCurrentSegment,
playCurrentClause,
applyColors,
]);

const afterUploadCb = useCallback(
Expand All @@ -984,15 +1001,6 @@ export function PassageDetailCarefulSpeech({ width }: IProps) {
[forceRefresh, applyColors]
);

const handleAllCompleteDismiss = useCallback(() => {
setConfirmAllComplete(false);
if (stepComplete(currentstep)) return;
waitForSave(undefined, 200).finally(async () => {
await setStepComplete(currentstep, true);
gotoNextStep();
});
}, [currentstep, stepComplete, setStepComplete, gotoNextStep, waitForSave]);

const handleClearRecording = useCallback(async () => {
if (!recordingRow?.mediafile?.id) return;
await memory.update((t) =>
Expand Down Expand Up @@ -1140,13 +1148,6 @@ export function PassageDetailCarefulSpeech({ width }: IProps) {
{statusText}
</Typography>
)}
{confirmAllComplete && (
<Confirm
text={t.allComplete}
yesResponse={handleAllCompleteDismiss}
noResponse={handleAllCompleteDismiss}
/>
)}
</Box>
);
}
Expand Down
Loading
Loading