Skip to content

Commit 5aecc62

Browse files
Merge pull request #1098 from revisit-studies/fix-crash-with-same-sequence-name
Fix crash with same sequence name
2 parents af0c91e + 5803e2f commit 5aecc62

6 files changed

Lines changed: 228 additions & 78 deletions

File tree

src/analysis/individualStudy/thinkAloud/ThinkAloudAnalysis.tsx

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ import {
22
Center,
33
Group, Stack, Text,
44
} from '@mantine/core';
5-
import { useParams, useSearchParams } from 'react-router';
5+
import {
6+
useLocation, useNavigate, useParams, useSearchParams,
7+
} from 'react-router';
68
import {
79
useCallback, useEffect, useMemo, useState,
810
} from 'react';
@@ -20,6 +22,7 @@ import { ThinkAloudFooter } from './ThinkAloudFooter';
2022
import { useEvent } from '../../../store/hooks/useEvent';
2123
import { FirebaseStorageEngine } from '../../../storage/engines/FirebaseStorageEngine';
2224
import { ReplayContext, useReplay } from '../../../store/hooks/useReplay';
25+
import { parseTrialOrder } from '../../../utils/parseTrialOrder';
2326

2427
async function getTranscript(storageEngine: FirebaseStorageEngine, partId: string | undefined, trialName: string | undefined, authEmail: string | null | undefined) {
2528
if (storageEngine && partId && trialName && authEmail) {
@@ -57,20 +60,44 @@ function getRawTranscript(storageEngine: FirebaseStorageEngine, currentTrial: st
5760
return null;
5861
}
5962

63+
function getFirstTrialIdentifier(participant: ParticipantData | null | undefined): string {
64+
if (!participant) {
65+
return '';
66+
}
67+
68+
const orderedAnswers = Object.values(participant.answers).sort((answerA, answerB) => {
69+
const a = parseTrialOrder(answerA.trialOrder);
70+
const b = parseTrialOrder(answerB.trialOrder);
71+
72+
if (a.step !== b.step) {
73+
return (a.step ?? Number.MAX_SAFE_INTEGER) - (b.step ?? Number.MAX_SAFE_INTEGER);
74+
}
75+
76+
if (a.funcIndex !== b.funcIndex) {
77+
return (a.funcIndex ?? -1) - (b.funcIndex ?? -1);
78+
}
79+
80+
return answerA.identifier.localeCompare(answerB.identifier);
81+
});
82+
83+
return orderedAnswers[0]?.identifier || '';
84+
}
85+
6086
export function ThinkAloudAnalysis({ visibleParticipants, storageEngine } : { visibleParticipants: ParticipantData[], storageEngine: FirebaseStorageEngine }) {
6187
const auth = useAuth();
6288

6389
const [searchParams, setSearchParams] = useSearchParams();
90+
const location = useLocation();
91+
const navigate = useNavigate();
6492
const [jumpedToLine, setJumpedToLine] = useState<number>(0);
6593

66-
const currentTrial = useMemo(() => searchParams.get('currentTrial') || '', [searchParams]);
67-
6894
const participantId = useMemo(() => searchParams.get('participantId') || '', [searchParams]);
95+
const { studyId, trialId } = useParams();
96+
const currentTrial = useMemo(() => trialId || '', [trialId]);
6997

7098
const [currentShownTranscription, setCurrentShownTranscription] = useState(0);
7199

72100
const { value: participant } = useAsync(getParticipantData, [participantId, storageEngine]);
73-
const { studyId } = useParams();
74101

75102
const replay = useReplay();
76103

@@ -91,13 +118,34 @@ export function ThinkAloudAnalysis({ visibleParticipants, storageEngine } : { vi
91118
}, [currentTrial, auth.user.user?.email, storageEngine, participantId]);
92119

93120
useEffect(() => {
94-
if (!currentTrial && !participantId && visibleParticipants.length > 0) {
95-
setSearchParams({ participantId: visibleParticipants[0].participantId, currentTrial: Object.entries(visibleParticipants[0].answers).find(([_, ans]) => +ans.trialOrder.split('_')[0] === 0)?.[0] || '' });
121+
if (!participantId && visibleParticipants.length > 0) {
122+
setSearchParams((params) => {
123+
params.set('participantId', visibleParticipants[0].participantId);
124+
params.delete('currentTrial');
125+
return params;
126+
});
96127
}
97128
// I really only want to do this on mount, so leaving this empty
98129
// eslint-disable-next-line react-hooks/exhaustive-deps
99130
}, []);
100131

132+
useEffect(() => {
133+
if (!studyId || !participant) {
134+
return;
135+
}
136+
137+
if (currentTrial && participant.answers[currentTrial]) {
138+
return;
139+
}
140+
141+
const firstTrialIdentifier = getFirstTrialIdentifier(participant);
142+
if (!firstTrialIdentifier) {
143+
return;
144+
}
145+
146+
navigate(`/analysis/stats/${studyId}/tagging/${encodeURIComponent(firstTrialIdentifier)}${location.search}`, { replace: true });
147+
}, [currentTrial, location.search, navigate, participant, studyId]);
148+
101149
const setEditedTranscript = useCallback((editedText: EditedText[]) => {
102150
_setEditedTranscript(editedText);
103151
debouncedSave(editedText);

src/analysis/individualStudy/thinkAloud/ThinkAloudFooter.tsx

Lines changed: 103 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
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';
1212
import {
1313
useCallback, useEffect, useMemo, useState,
1414
} from 'react';
@@ -25,8 +25,8 @@ import {
2525
import { AudioProvenanceVis } from '../../../components/audioAnalysis/AudioProvenanceVis';
2626
import { TranscriptSegmentsVis } from './TranscriptSegmentsVis';
2727
import { TagSelector } from './tags/TagSelector';
28-
import { getSequenceFlatMap } from '../../../utils/getSequenceFlatMap';
2928
import { encryptIndex } from '../../../utils/encryptDecryptIndex';
29+
import { parseTrialOrder } from '../../../utils/parseTrialOrder';
3030
import { PREFIX } from '../../../utils/Prefix';
3131
import { handleTaskAudio, handleTaskScreenRecording } from '../../../utils/handleDownloadFiles';
3232
import { 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}&currentTrial=${currentTrial}&revisitPageId=${revisitPageId}` : `${PREFIX}${studyId}/${encryptIndex(participant ? +(participant.answers[currentTrial]?.trialOrder.split('_')[0] || 0) : 0)}?participantId=${participantId}&currentTrial=${currentTrial}&revisitPageId=${revisitPageId}`}
562+
href={isReplay ? transcriptHref : replayHref}
496563
target="_blank"
497564
>
498565
{isReplay ? 'Transcript' : 'Replay'}

0 commit comments

Comments
 (0)