Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add next button wait and timeout #489

Merged
merged 7 commits into from
Nov 5, 2024
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
6 changes: 4 additions & 2 deletions public/demo-image/config.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"$schema": "https://raw.githubusercontent.com/revisit-studies/study/v1.0.5/src/parser/StudyConfigSchema.json",
"$schema": "https://raw.githubusercontent.com/revisit-studies/study/time-to-next/src/parser/StudyConfigSchema.json",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confused what the change is for? Just for testing the schema validation?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This lets the tests pass here, not just locally. It uses the updated schema

"studyMetadata": {
"title": "Simple Images as Stimuli: Decision-Making with Uncertainty Visualizations",
"version": "pilot",
Expand Down Expand Up @@ -54,7 +54,9 @@
"No"
]
}
]
],
"nextButtonEnableTime": 3000,
"nextButtonDisableTime": 15000
},
"dotplot-medium": {
"meta": {
Expand Down
25 changes: 14 additions & 11 deletions src/analysis/individualStudy/table/TableView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,19 @@ function AnswerCell({ cellData }: { cellData: StoredAnswer }) {
return Number.isFinite(cellData.endTime) && Number.isFinite(cellData.startTime) ? (
<Table.Td>
<Stack miw={100}>
{Object.entries(cellData.answer).map(([key, storedAnswer]) => (
<Box key={`cell-${key}`}>
<Text fw={700} span>
{' '}
{`${key}: `}
</Text>
<Text span>
{`${storedAnswer}`}
</Text>
</Box>
))}
{cellData.timedOut
? <Text>Timed out</Text>
: Object.entries(cellData.answer).map(([key, storedAnswer]) => (
<Box key={`cell-${key}`}>
<Text fw={700} span>
{' '}
{`${key}: `}
</Text>
<Text span>
{`${storedAnswer}`}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
{`${storedAnswer}`}
{storedAnswer}

Can it just be this? Note sure if there is a "string/number" issue that you're trying to avoid here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it's a type issue:

Type '{ id: string; value: unknown; }' is not assignable to type 'ReactNode'.

</Text>
</Box>
))}
</Stack>
</Table.Td>
) : (
Expand Down Expand Up @@ -237,6 +239,7 @@ export function TableView({
endTime: Math.max(...Object.values(record.answers).filter((a) => a.endTime !== -1 && a.endTime !== undefined).map((a) => a.endTime)),
answer: {},
windowEvents: Object.values(record.answers).flatMap((a) => a.windowEvents),
timedOut: false, // not used
}}
key={`cell-${record.participantId}-total-duration`}
/>
Expand Down
9 changes: 9 additions & 0 deletions src/components/TimedOut.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Text } from '@mantine/core';

export function TimedOut() {
return (
<Text>
Thank you for participating. Unfortunately, you have not answered the questions within the given time limit. Because of this, you are no longer eligible to participate in the study. You may close this window now.
</Text>
);
}
73 changes: 71 additions & 2 deletions src/components/response/ResponseBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {

import React, { useEffect, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { IconAlertTriangle, IconInfoCircle } from '@tabler/icons-react';
import {
IndividualComponent,
ResponseBlockLocation,
Expand All @@ -20,6 +21,8 @@ import { useAnswerField } from './utils';
import ResponseSwitcher from './ResponseSwitcher';
import { StoredAnswer, TrrackedProvenance } from '../../store/types';
import { useStorageEngine } from '../../storage/storageEngineHooks';
import { useStudyConfig } from '../../store/hooks/useStudyConfig';
import { useNextStep } from '../../store/hooks/useNextStep';

type Props = {
status?: StoredAnswer;
Expand All @@ -42,6 +45,7 @@ export default function ResponseBlock({
const storedAnswer = status?.answer;

const studyId = useStudyId();
const studyConfig = useStudyConfig();

const navigate = useNavigate();

Expand All @@ -62,6 +66,26 @@ export default function ResponseBlock({

const showNextBtn = location === (configInUse?.nextButtonLocation || 'belowStimulus');

const nextButtonDisableTime = configInUse?.nextButtonDisableTime;
const nextButtonEnableTime = configInUse?.nextButtonEnableTime || 0;
const [timer, setTimer] = useState<number | undefined>(undefined);
// Start a timer on first render, update timer every 100ms
useEffect(() => {
let time = 0;
const interval = setInterval(() => {
time += 100;
setTimer(time);
}, 500);
return () => {
clearInterval(interval);
};
}, []);
useEffect(() => {
if (timer && nextButtonDisableTime && timer >= nextButtonDisableTime && studyConfig.uiConfig.timeoutReject) {
navigate('./__timeout');
}
}, [nextButtonDisableTime, timer, navigate, studyConfig.uiConfig.timeoutReject]);

useEffect(() => {
const iframeResponse = responses.find((r) => r.type === 'iframe');
if (iframeAnswers && iframeResponse) {
Expand Down Expand Up @@ -156,10 +180,28 @@ export default function ResponseBlock({
}
});

setEnableNextButton((allowFailedTraining && newAttemptsUsed >= trainingAttempts) || (Object.values(correctAnswers).every((isCorrect) => isCorrect) && newAttemptsUsed <= trainingAttempts));
setEnableNextButton(
(
allowFailedTraining && newAttemptsUsed >= trainingAttempts
) || (
Object.values(correctAnswers).every((isCorrect) => isCorrect)
&& newAttemptsUsed <= trainingAttempts
),
);
}
};

const buttonTimerSatisfied = useMemo(
() => {
const nextButtonDisableSatisfied = nextButtonDisableTime && timer ? timer <= nextButtonDisableTime : true;
const nextButtonEnableSatisfied = timer ? timer >= nextButtonEnableTime : true;
return nextButtonDisableSatisfied && nextButtonEnableSatisfied;
},
[nextButtonDisableTime, nextButtonEnableTime, timer],
);

const { goToNextStep } = useNextStep();

return (
<div style={style}>
{responses.map((response, index) => {
Expand Down Expand Up @@ -216,11 +258,38 @@ export default function ResponseBlock({
)}
{showNextBtn && (
<NextButton
disabled={(hasCorrectAnswerFeedback && !enableNextButton) || !answerValidator.isValid()}
disabled={(hasCorrectAnswerFeedback && !enableNextButton) || !answerValidator.isValid() || !buttonTimerSatisfied}
label={configInUse.nextButtonText || 'Next'}
/>
)}
</Group>
{showNextBtn && nextButtonEnableTime > 0 && timer && timer < nextButtonEnableTime && (
<Alert mt="md" title="Please wait" color="blue" icon={<IconInfoCircle />}>
The next button will be enabled in
{' '}
{Math.ceil((nextButtonEnableTime - timer) / 1000)}
{' '}
seconds.
</Alert>
)}
{showNextBtn && nextButtonDisableTime && timer && (nextButtonDisableTime - timer) < 10000 && (
(nextButtonDisableTime - timer) > 0
? (
<Alert mt="md" title="Next button disables soon" color="yellow" icon={<IconAlertTriangle />}>
The next button disables in
{' '}
{Math.ceil((nextButtonDisableTime - timer) / 1000)}
{' '}
seconds.
</Alert>
) : !studyConfig.uiConfig.timeoutReject && (
<Alert mt="md" title="Next button disabled" color="red" icon={<IconAlertTriangle />}>
The next button has timed out and is now disabled.
<Group justify="right" mt="sm">
<Button onClick={() => goToNextStep(false)} variant="link" color="red">Proceed</Button>
</Group>
</Alert>
))}
</div>
);
}
6 changes: 6 additions & 0 deletions src/controllers/ComponentController.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { useStoreActions, useStoreDispatch } from '../store/store';
import { StudyEnd } from '../components/StudyEnd';
import { TrainingFailed } from '../components/TrainingFailed';
import ResourceNotFound from '../ResourceNotFound';
import { TimedOut } from '../components/TimedOut';

// current active stimuli presented to the user
export default function ComponentController() {
Expand Down Expand Up @@ -56,6 +57,11 @@ export default function ComponentController() {
return <TrainingFailed />;
}

// Handle timed out participants
if (currentComponent === '__timedOut') {
return <TimedOut />;
}

if (currentComponent === 'Notfound') {
return <ResourceNotFound email={studyConfig.uiConfig.contactEmail} />;
}
Expand Down
56 changes: 56 additions & 0 deletions src/parser/LibraryConfigSchema.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,14 @@
"description": "The meta data for the component. This is used to identify and provide additional information for the component in the admin panel.",
"type": "object"
},
"nextButtonDisableTime": {
"description": "A timeout (in ms) after which the next button will be disabled.",
"type": "number"
},
"nextButtonEnableTime": {
"description": "A timer (in ms) after which the next button will be enabled.",
"type": "number"
},
"nextButtonLocation": {
"$ref": "#/definitions/ResponseBlockLocation",
"description": "The location of the next button."
Expand Down Expand Up @@ -486,6 +494,14 @@
"description": "The meta data for the component. This is used to identify and provide additional information for the component in the admin panel.",
"type": "object"
},
"nextButtonDisableTime": {
"description": "A timeout (in ms) after which the next button will be disabled.",
"type": "number"
},
"nextButtonEnableTime": {
"description": "A timer (in ms) after which the next button will be enabled.",
"type": "number"
},
"nextButtonLocation": {
"$ref": "#/definitions/ResponseBlockLocation",
"description": "The location of the next button."
Expand Down Expand Up @@ -658,6 +674,14 @@
"description": "The meta data for the component. This is used to identify and provide additional information for the component in the admin panel.",
"type": "object"
},
"nextButtonDisableTime": {
"description": "A timeout (in ms) after which the next button will be disabled.",
"type": "number"
},
"nextButtonEnableTime": {
"description": "A timer (in ms) after which the next button will be enabled.",
"type": "number"
},
"nextButtonLocation": {
"$ref": "#/definitions/ResponseBlockLocation",
"description": "The location of the next button."
Expand Down Expand Up @@ -949,6 +973,14 @@
"description": "The meta data for the component. This is used to identify and provide additional information for the component in the admin panel.",
"type": "object"
},
"nextButtonDisableTime": {
"description": "A timeout (in ms) after which the next button will be disabled.",
"type": "number"
},
"nextButtonEnableTime": {
"description": "A timer (in ms) after which the next button will be enabled.",
"type": "number"
},
"nextButtonLocation": {
"$ref": "#/definitions/ResponseBlockLocation",
"description": "The location of the next button."
Expand Down Expand Up @@ -1103,6 +1135,14 @@
"description": "The meta data for the component. This is used to identify and provide additional information for the component in the admin panel.",
"type": "object"
},
"nextButtonDisableTime": {
"description": "A timeout (in ms) after which the next button will be disabled.",
"type": "number"
},
"nextButtonEnableTime": {
"description": "A timer (in ms) after which the next button will be enabled.",
"type": "number"
},
"nextButtonLocation": {
"$ref": "#/definitions/ResponseBlockLocation",
"description": "The location of the next button."
Expand Down Expand Up @@ -1272,6 +1312,14 @@
"description": "The meta data for the component. This is used to identify and provide additional information for the component in the admin panel.",
"type": "object"
},
"nextButtonDisableTime": {
"description": "A timeout (in ms) after which the next button will be disabled.",
"type": "number"
},
"nextButtonEnableTime": {
"description": "A timer (in ms) after which the next button will be enabled.",
"type": "number"
},
"nextButtonLocation": {
"$ref": "#/definitions/ResponseBlockLocation",
"description": "The location of the next button."
Expand Down Expand Up @@ -1581,6 +1629,14 @@
"description": "The meta data for the component. This is used to identify and provide additional information for the component in the admin panel.",
"type": "object"
},
"nextButtonDisableTime": {
"description": "A timeout (in ms) after which the next button will be disabled.",
"type": "number"
},
"nextButtonEnableTime": {
"description": "A timer (in ms) after which the next button will be enabled.",
"type": "number"
},
"nextButtonLocation": {
"$ref": "#/definitions/ResponseBlockLocation",
"description": "The location of the next button."
Expand Down
Loading
Loading