88 Group , HoverCard , Popover , SegmentedControl , Select , Stack , Text ,
99 Tooltip ,
1010} from '@mantine/core' ;
11- import { useSearchParams } from 'react-router' ;
11+ import { useLocation , useNavigate , useSearchParams } from 'react-router' ;
1212import {
1313 useCallback , useEffect , useMemo , useState ,
1414} from 'react' ;
@@ -25,8 +25,8 @@ import {
2525import { AudioProvenanceVis } from '../../../components/audioAnalysis/AudioProvenanceVis' ;
2626import { TranscriptSegmentsVis } from './TranscriptSegmentsVis' ;
2727import { TagSelector } from './tags/TagSelector' ;
28- import { getSequenceFlatMap } from '../../../utils/getSequenceFlatMap' ;
2928import { encryptIndex } from '../../../utils/encryptDecryptIndex' ;
29+ import { parseTrialOrder } from '../../../utils/parseTrialOrder' ;
3030import { PREFIX } from '../../../utils/Prefix' ;
3131import { handleTaskAudio , handleTaskScreenRecording } from '../../../utils/handleDownloadFiles' ;
3232import { ParticipantRejectModal } from '../ParticipantRejectModal' ;
@@ -77,6 +77,8 @@ export function ThinkAloudFooter({
7777 const auth = useAuth ( ) ;
7878
7979 const [ searchParams , setSearchParams ] = useSearchParams ( ) ;
80+ const navigate = useNavigate ( ) ;
81+ const location = useLocation ( ) ;
8082
8183 const participantId = useMemo ( ( ) => searchParams . get ( 'participantId' ) || '' , [ searchParams ] ) ;
8284
@@ -160,13 +162,9 @@ export function ThinkAloudFooter({
160162 if ( currentTrial . includes ( '__dynamicLoading' ) ) {
161163 return '' ;
162164 }
163- // if we find ourselves with a wrong current trial, erase it
164- if ( participant && ! participant . answers [ currentTrial ] ) {
165- setSearchParams ( { participantId, currentTrial : Object . entries ( participant . answers ) . find ( ( [ _ , ans ] ) => + ans . trialOrder . split ( '_' ) [ 0 ] === 0 ) ?. [ 0 ] || '' } ) ;
166- }
167165
168166 return participant ?. answers [ currentTrial ] ?. componentName ?? '' ;
169- } , [ currentTrial , participant , participantId , setSearchParams ] ) ;
167+ } , [ currentTrial , participant ] ) ;
170168
171169 const xScale = useMemo ( ( ) => {
172170 if ( ! participant || ! participant . answers [ currentTrial ] ) {
@@ -220,34 +218,90 @@ export function ThinkAloudFooter({
220218 value : visibleParticipants [ index ] ,
221219 } ) ;
222220
223- setSearchParams ( { participantId : visibleParticipants [ index ] || '' } ) ;
221+ setSearchParams ( ( params ) => {
222+ params . set ( 'participantId' , visibleParticipants [ index ] || '' ) ;
223+ params . delete ( 'currentTrial' ) ;
224+ return params ;
225+ } ) ;
224226 } , [ participantId , setSearchParams , visibleParticipants ] ) ;
225227
226- const nextTaskCallback = useCallback ( ( indexChange : number ) => {
227- if ( ! participant || ! currentTrial ) {
228+ const orderedAnswers = useMemo ( ( ) => {
229+ if ( ! participant ) {
230+ return [ ] ;
231+ }
232+
233+ const answers = Object . values ( participant . answers ) ;
234+ answers . sort ( ( answerA , answerB ) => {
235+ const a = parseTrialOrder ( answerA . trialOrder ) ;
236+ const b = parseTrialOrder ( answerB . trialOrder ) ;
237+
238+ if ( a . step !== b . step ) {
239+ return ( a . step ?? Number . MAX_SAFE_INTEGER ) - ( b . step ?? Number . MAX_SAFE_INTEGER ) ;
240+ }
241+
242+ if ( a . funcIndex !== b . funcIndex ) {
243+ return ( a . funcIndex ?? - 1 ) - ( b . funcIndex ?? - 1 ) ;
244+ }
245+
246+ return answerA . identifier . localeCompare ( answerB . identifier ) ;
247+ } ) ;
248+
249+ return answers ;
250+ } , [ participant ] ) ;
251+
252+ const navigateToTask = useCallback ( ( answerIdentifier : string ) => {
253+ if ( ! participant ) {
228254 return ;
229255 }
230256
231- // This doesnt work for dynamic
232- let index = + participant . answers [ currentTrial ] . trialOrder . split ( '_' ) [ 0 ] + indexChange ;
257+ const answer = participant . answers [ answerIdentifier ] ;
258+ if ( ! answer ) {
259+ return ;
260+ }
233261
234- if ( index >= Object . values ( participant . answers ) . length ) {
235- index = 0 ;
236- } else if ( index < 0 ) {
237- index = Object . values ( participant . answers ) . length - 1 ;
262+ const { step, funcIndex } = parseTrialOrder ( answer . trialOrder ) ;
263+ if ( step === null ) {
264+ return ;
238265 }
239266
240- const newTrial = Object . values ( participant . answers ) . find ( ( ans ) => + ans . trialOrder . split ( '_' ) [ 0 ] === index ) ;
241- const newTrialName = newTrial ? `${ newTrial . componentName } _${ newTrial . trialOrder . split ( '_' ) [ 0 ] } ` : '' ;
267+ const pathname = isReplay ? ( funcIndex === null
268+ ? `/${ studyId } /${ encryptIndex ( step ) } `
269+ : `/${ studyId } /${ encryptIndex ( step ) } /${ encryptIndex ( funcIndex ) } ` ) : `/analysis/stats/${ studyId } /tagging/${ encodeURIComponent ( answerIdentifier ) } ` ;
242270
243- syncChannel . postMessage ( {
244- key : 'currentTrial' ,
245- value : newTrialName ,
271+ navigate ( {
272+ pathname ,
273+ search : location . search ,
246274 } ) ;
247275
248- // This isnt actually doing anything without the other analysis tab open
249- setSearchParams ( { participantId, currentTrial : newTrialName } ) ;
250- } , [ currentTrial , participant , participantId , setSearchParams ] ) ;
276+ if ( answer . trialOrder ) {
277+ syncChannel . postMessage ( {
278+ key : 'trialOrder' ,
279+ value : answer . trialOrder ,
280+ } ) ;
281+ }
282+ } , [ isReplay , location . search , navigate , participant , studyId ] ) ;
283+
284+ const nextTaskCallback = useCallback ( ( indexChange : number ) => {
285+ if ( ! currentTrial || orderedAnswers . length === 0 ) {
286+ return ;
287+ }
288+
289+ const currentIndex = orderedAnswers . findIndex ( ( answer ) => answer . identifier === currentTrial ) ;
290+ if ( currentIndex === - 1 ) {
291+ const fallbackIndex = indexChange >= 0 ? 0 : orderedAnswers . length - 1 ;
292+ navigateToTask ( orderedAnswers [ fallbackIndex ] . identifier ) ;
293+ return ;
294+ }
295+
296+ let nextIndex = currentIndex + indexChange ;
297+ if ( nextIndex >= orderedAnswers . length ) {
298+ nextIndex = 0 ;
299+ } else if ( nextIndex < 0 ) {
300+ nextIndex = orderedAnswers . length - 1 ;
301+ }
302+
303+ navigateToTask ( orderedAnswers [ nextIndex ] . identifier ) ;
304+ } , [ currentTrial , navigateToTask , orderedAnswers ] ) ;
251305
252306 const setTags = useCallback ( ( _tags : Tag [ ] , type : 'task' | 'participant' ) => {
253307 if ( storageEngine ) {
@@ -299,6 +353,21 @@ export function ThinkAloudFooter({
299353 return buildProvenanceLegendEntries ( Object . values ( answer . provenanceGraph ) ) ;
300354 } , [ participant , currentTrial ] ) ;
301355
356+ const tasksList = useMemo ( ( ) => orderedAnswers . map ( ( answer ) => ( {
357+ label : answer . componentName ,
358+ value : answer . identifier ,
359+ } ) ) , [ orderedAnswers ] ) ;
360+
361+ const transcriptHref = useMemo ( ( ) => `${ PREFIX } analysis/stats/${ studyId } /tagging${ currentTrial ? `/${ encodeURIComponent ( currentTrial ) } ` : '' } ?participantId=${ participantId } &revisitPageId=${ revisitPageId } ` , [ currentTrial , participantId , studyId ] ) ;
362+
363+ const replayHref = useMemo ( ( ) => {
364+ const { step, funcIndex } = parseTrialOrder ( participant ?. answers [ currentTrial ] ?. trialOrder ) ;
365+ const currentStep = step ?? 0 ;
366+ const funcPath = funcIndex === null ? '' : `/${ encryptIndex ( funcIndex ) } ` ;
367+
368+ return `${ PREFIX } ${ studyId } /${ encryptIndex ( currentStep ) } ${ funcPath } ?participantId=${ participantId } &revisitPageId=${ revisitPageId } ` ;
369+ } , [ currentTrial , participant , participantId , studyId ] ) ;
370+
302371 return (
303372 < AppShell . Footer zIndex = { 101 } withBorder = { false } >
304373 { currentTrial && participant && currentTrialClean === '' && (
@@ -380,7 +449,11 @@ export function ThinkAloudFooter({
380449 style = { { width : '200px' } }
381450 value = { participantId }
382451 onChange = { ( e : string | null ) => {
383- setSearchParams ( { currentTrial, participantId : e || '' } ) ;
452+ setSearchParams ( ( params ) => {
453+ params . set ( 'participantId' , e || '' ) ;
454+ params . delete ( 'currentTrial' ) ;
455+ return params ;
456+ } ) ;
384457 syncChannel . postMessage ( {
385458 key : 'participantId' ,
386459 value : e || '' ,
@@ -439,20 +512,14 @@ export function ThinkAloudFooter({
439512 ) }
440513 label = "Task"
441514 style = { { width : '200px' } }
442- value = { currentTrialClean }
515+ value = { currentTrial }
443516 // this needs to be in a helper or two which we dont currently have
444517 onChange = { ( e : string | null ) => {
445- if ( participant && e ) {
446- const trial = Object . entries ( participant . answers ) . find ( ( [ _key , ans ] ) => + ans . trialOrder . split ( '_' ) [ 0 ] === getSequenceFlatMap ( participant ?. sequence ) . indexOf ( e ) ) ?. [ 0 ] || '' ;
447- syncChannel . postMessage ( {
448- key : 'currentTrial' ,
449- value : trial ,
450- } ) ;
451-
452- setSearchParams ( { participantId, currentTrial : trial } ) ;
518+ if ( e ) {
519+ navigateToTask ( e ) ;
453520 }
454521 } }
455- data = { participant ? getSequenceFlatMap ( participant ?. sequence ) : [ ] }
522+ data = { tasksList }
456523 searchable
457524 />
458525 < Stack gap = "4" >
@@ -492,7 +559,7 @@ export function ThinkAloudFooter({
492559 mt = "lg"
493560 variant = "light"
494561 component = "a"
495- href = { isReplay ? ` ${ PREFIX } analysis/stats/ ${ studyId } /tagging?participantId= ${ participantId } ¤tTrial= ${ currentTrial } &revisitPageId= ${ revisitPageId } ` : ` ${ PREFIX } ${ studyId } / ${ encryptIndex ( participant ? + ( participant . answers [ currentTrial ] ?. trialOrder . split ( '_' ) [ 0 ] || 0 ) : 0 ) } ?participantId= ${ participantId } ¤tTrial= ${ currentTrial } &revisitPageId= ${ revisitPageId } ` }
562+ href = { isReplay ? transcriptHref : replayHref }
496563 target = "_blank"
497564 >
498565 { isReplay ? 'Transcript' : 'Replay' }
0 commit comments