From 42d558b385ce399612d59f2dd0b3a61faa3213c4 Mon Sep 17 00:00:00 2001 From: Hilson Shrestha Date: Sat, 1 Nov 2025 22:13:21 +0100 Subject: [PATCH 1/2] Fix screen recording replay and sync with provenance timeline --- .../thinkAloud/ThinkAloudFooter.tsx | 16 ++++++++------ .../audioAnalysis/AudioProvenanceVis.tsx | 7 ++++--- src/components/interface/AnalysisFooter.tsx | 21 +++++++++++++++---- 3 files changed, 31 insertions(+), 13 deletions(-) diff --git a/src/analysis/individualStudy/thinkAloud/ThinkAloudFooter.tsx b/src/analysis/individualStudy/thinkAloud/ThinkAloudFooter.tsx index fc18ba3682..7a3aec7b98 100644 --- a/src/analysis/individualStudy/thinkAloud/ThinkAloudFooter.tsx +++ b/src/analysis/individualStudy/thinkAloud/ThinkAloudFooter.tsx @@ -62,9 +62,9 @@ async function getTags(storageEngine: StorageEngine | undefined, type: 'particip } export function ThinkAloudFooter({ - visibleParticipants, rawTranscript, currentShownTranscription, width, onTimeUpdate, isReplay, editedTranscript, currentTrial, saveProvenance, jumpedToLine = 0, studyId, setHasAudio, storageEngine, + visibleParticipants, rawTranscript, currentShownTranscription, width, onTimeUpdate, isReplay, editedTranscript, currentTrial, saveProvenance, jumpedToLine = 0, studyId, setHasAudio, storageEngine, forceMute, onAnalysisIsPlayingChange, onProvenanceTimelineChange, } : { - visibleParticipants: string[], rawTranscript: TranscribedAudio | null, currentShownTranscription: number | null, width: number, onTimeUpdate: (n: number) => void, isReplay: boolean, editedTranscript?: EditedText[], currentTrial: string, saveProvenance: (prov: unknown) => void, jumpedToLine?: number, studyId: string, setHasAudio: (b: boolean) => void, storageEngine: StorageEngine | undefined, + visibleParticipants: string[], rawTranscript: TranscribedAudio | null, currentShownTranscription: number | null, width: number, onTimeUpdate: (n: number) => void, isReplay: boolean, editedTranscript?: EditedText[], currentTrial: string, saveProvenance: (prov: unknown) => void, jumpedToLine?: number, studyId: string, setHasAudio: (b: boolean) => void, storageEngine: StorageEngine | undefined, forceMute?: boolean, onAnalysisIsPlayingChange?: (b: boolean) => void, onProvenanceTimelineChange?: (n: number)=> void, }) { const auth = useAuth(); @@ -80,7 +80,9 @@ export function ThinkAloudFooter({ const { value: allParticipantTags, execute: pullAllParticipantTags } = useAsync(getTags, [storageEngine, 'participant']); const [analysisIsPlaying, _setAnalysisIsPlaying] = useState(false); - const [isMuted, setIsMuted] = useState(true); + + const [_isMuted, setIsMuted] = useState(true); + const isMuted = forceMute ? true : _isMuted; const [audioUrl, setAudioUrl] = useState(null); const [screenRecordingUrl, setScreenRecordingUrl] = useState(null); @@ -142,7 +144,9 @@ export function ThinkAloudFooter({ setIsMuted(true); } _setAnalysisIsPlaying(playing); - }, []); + + onAnalysisIsPlayingChange && onAnalysisIsPlayingChange(playing); + }, [onAnalysisIsPlayingChange]); const [transcriptLines, setTranscriptLines] = useState(null); @@ -278,7 +282,7 @@ export function ThinkAloudFooter({ - setTimeString(_t)} answers={participant ? participant.answers : {}} taskName={currentTrial} context={isReplay ? 'provenanceVis' : 'audioAnalysis'} /> + setTimeString(_t)} answers={participant ? participant.answers : {}} taskName={currentTrial} context={isReplay ? 'provenanceVis' : 'audioAnalysis'} onProvenanceTimelineChange={onProvenanceTimelineChange} /> {xScale && transcriptLines ? : null } @@ -286,7 +290,7 @@ export function ThinkAloudFooter({ {timeString} - { setAnalysisIsPlaying(!analysisIsPlaying); setIsMuted(!isMuted); }}> + { setAnalysisIsPlaying(!analysisIsPlaying); setIsMuted(!_isMuted); }}> {analysisIsPlaying ? : } diff --git a/src/components/audioAnalysis/AudioProvenanceVis.tsx b/src/components/audioAnalysis/AudioProvenanceVis.tsx index 784b075439..9c67e0d6be 100644 --- a/src/components/audioAnalysis/AudioProvenanceVis.tsx +++ b/src/components/audioAnalysis/AudioProvenanceVis.tsx @@ -31,8 +31,8 @@ function safe(p: Promise): Promise { } export function AudioProvenanceVis({ - setTimeString, answers, setTime, taskName, context, saveProvenance, analysisIsPlaying, setAnalysisIsPlaying, speed, jumpedToAudioTime, setHasAudio, setSpeed, isMuted, -}: { setTimeString: (time: string) => void; answers: Record, setTime: (time: number) => void, taskName: string, context: 'audioAnalysis' | 'provenanceVis', saveProvenance: ((state: unknown) => void), analysisIsPlaying: boolean, setAnalysisIsPlaying: (b: boolean) => void, speed: number, jumpedToAudioTime: number, setHasAudio: (b: boolean) => void, setSpeed: (n: number) => void, isMuted: boolean }) { + setTimeString, answers, setTime, taskName, context, saveProvenance, analysisIsPlaying, setAnalysisIsPlaying, speed, jumpedToAudioTime, setHasAudio, setSpeed, isMuted, onProvenanceTimelineChange, +}: { setTimeString: (time: string) => void; answers: Record, setTime: (time: number) => void, taskName: string, context: 'audioAnalysis' | 'provenanceVis', saveProvenance: ((state: unknown) => void), analysisIsPlaying: boolean, setAnalysisIsPlaying: (b: boolean) => void, speed: number, jumpedToAudioTime: number, setHasAudio: (b: boolean) => void, setSpeed: (n: number) => void, isMuted: boolean, onProvenanceTimelineChange?: (n: number) => void }) { const [searchParams, setSearchParams] = useSearchParams(); const participantId = useMemo(() => searchParams.get('participantId') || '', [searchParams]); @@ -139,6 +139,7 @@ export function AudioProvenanceVis({ } setPlayTime(n); + onProvenanceTimelineChange && onProvenanceTimelineChange(n - startTime); if (wavesurfer.current && percent !== undefined && !Number.isNaN(percent)) { setTimeout(() => { @@ -149,7 +150,7 @@ export function AudioProvenanceVis({ if (setTime) { setTime(n); } - }, [setAnalysisIsPlaying, setTime, startTime, totalAudioLength]); + }, [setAnalysisIsPlaying, setTime, startTime, totalAudioLength, onProvenanceTimelineChange]); useEffect(() => { _setPlayTime(startTime + jumpedToAudioTime * 1000 + 1, undefined); diff --git a/src/components/interface/AnalysisFooter.tsx b/src/components/interface/AnalysisFooter.tsx index 02226c4fa9..bb6eec878e 100644 --- a/src/components/interface/AnalysisFooter.tsx +++ b/src/components/interface/AnalysisFooter.tsx @@ -1,13 +1,14 @@ -import { useDispatch } from 'react-redux'; import { useParams } from 'react-router'; +import { useCallback } from 'react'; import { useAsync } from '../../store/hooks/useAsync'; import { useStorageEngine } from '../../storage/storageEngineHooks'; import { StorageEngine } from '../../storage/engines/types'; import { ThinkAloudFooter } from '../../analysis/individualStudy/thinkAloud/ThinkAloudFooter'; import { useCurrentIdentifier } from '../../routes/utils'; -import { useStoreActions } from '../../store/store'; +import { useStoreActions, useStoreDispatch } from '../../store/store'; +import { useRecordingConfig } from '../../store/hooks/useRecordingConfig'; function getAllParticipantsNames(storageEngine: StorageEngine | undefined) { if (storageEngine) { @@ -23,7 +24,9 @@ export function AnalysisFooter({ setHasAudio }: {setHasAudio: (b: boolean) => vo const identifier = useCurrentIdentifier(); - const dispatch = useDispatch(); + const storeDispatch = useStoreDispatch(); + + const { currentComponentHasScreenRecording } = useRecordingConfig(); const { studyId } = useParams(); @@ -31,8 +34,18 @@ export function AnalysisFooter({ setHasAudio }: {setHasAudio: (b: boolean) => vo saveAnalysisState, } = useStoreActions(); + const { setAnalysisIsPlaying, setProvenanceJumpTime } = useStoreActions(); + + const handleProvenanceTimelineChange = useCallback((n: number) => { + storeDispatch(setProvenanceJumpTime(n)); + }, [storeDispatch, setProvenanceJumpTime]); + + const handleAnalysisIsPlayingChange = useCallback((isPlaying: boolean) => { + storeDispatch(setAnalysisIsPlaying(isPlaying)); + }, [storeDispatch, setAnalysisIsPlaying]); + return ( // eslint-disable-next-line @typescript-eslint/no-explicit-any - {}} saveProvenance={(prov: any) => dispatch(saveAnalysisState(prov))} /> + {}} saveProvenance={(prov: any) => storeDispatch(saveAnalysisState(prov))} onProvenanceTimelineChange={handleProvenanceTimelineChange} onAnalysisIsPlayingChange={handleAnalysisIsPlayingChange} forceMute={currentComponentHasScreenRecording} /> ); } From 51ab59a8fd26345e369d9e4e572d27d480b48f5d Mon Sep 17 00:00:00 2001 From: Hilson Shrestha Date: Sun, 2 Nov 2025 12:25:48 +0100 Subject: [PATCH 2/2] use callback for save provenance --- src/components/interface/AnalysisFooter.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/interface/AnalysisFooter.tsx b/src/components/interface/AnalysisFooter.tsx index bb6eec878e..621135d519 100644 --- a/src/components/interface/AnalysisFooter.tsx +++ b/src/components/interface/AnalysisFooter.tsx @@ -44,8 +44,10 @@ export function AnalysisFooter({ setHasAudio }: {setHasAudio: (b: boolean) => vo storeDispatch(setAnalysisIsPlaying(isPlaying)); }, [storeDispatch, setAnalysisIsPlaying]); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const saveProvenance = useCallback((prov: any) => storeDispatch(saveAnalysisState(prov)), [storeDispatch, saveAnalysisState]); + return ( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - {}} saveProvenance={(prov: any) => storeDispatch(saveAnalysisState(prov))} onProvenanceTimelineChange={handleProvenanceTimelineChange} onAnalysisIsPlayingChange={handleAnalysisIsPlayingChange} forceMute={currentComponentHasScreenRecording} /> + {}} saveProvenance={saveProvenance} onProvenanceTimelineChange={handleProvenanceTimelineChange} onAnalysisIsPlayingChange={handleAnalysisIsPlayingChange} forceMute={currentComponentHasScreenRecording} /> ); }