Skip to content

Commit 419c985

Browse files
authored
Merge pull request #931 from revisit-studies/dev
v2.3.1
2 parents d858368 + f5582e5 commit 419c985

23 files changed

Lines changed: 670 additions & 83 deletions

public/global.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"library-screen-recording",
4141
"library-smeq",
4242
"library-sus",
43+
"library-virtual-chinrest",
4344
"library-vlat",
4445
"test-audio",
4546
"test-library",
@@ -211,6 +212,10 @@
211212
"path": "test-step-logic/config.json",
212213
"test": true
213214
},
215+
"library-virtual-chinrest": {
216+
"path": "library-virtual-chinrest/config.json",
217+
"test": true
218+
},
214219
"example-VLAT-adaptive": {
215220
"path": "example-VLAT-adaptive/config.json",
216221
"test": true
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
{
2+
"$schema": "https://raw.githubusercontent.com/revisit-studies/study/v2.2.0/src/parser/LibraryConfigSchema.json",
3+
"description": "A library for visual calibration tasks including virtual chinrest (card size) and viewing distance calibration using blindspot tracking.",
4+
"reference": "Custom calibration library for visual perception studies",
5+
"doi": "10.1038/s41598-019-57204-1",
6+
"externalLink": "https://github.com/QishengLi/virtual_chinrest",
7+
"components": {
8+
"card-size": {
9+
"type": "react-component",
10+
"path": "libraries/virtual-chinrest/assets/VirtualChinrestCalibration.tsx",
11+
"withSidebar": false,
12+
"parameters": {
13+
"taskid": "pixelsPerMM",
14+
"itemWidthMM": 85.6,
15+
"itemHeightMM": 53.98
16+
},
17+
"nextButtonLocation": "belowStimulus",
18+
"response": [
19+
{
20+
"id": "pixelsPerMM",
21+
"prompt": "Calibration results",
22+
"required": true,
23+
"type": "reactive",
24+
"hidden": true
25+
}
26+
]
27+
},
28+
"blindspot-distance": {
29+
"type": "react-component",
30+
"path": "libraries/virtual-chinrest/assets/ViewingDistanceCalibration.tsx",
31+
"nextButtonLocation": "belowStimulus",
32+
"withSidebar": false,
33+
"parameters": {
34+
"blindspotAngle": 13.5
35+
},
36+
"response": [
37+
{
38+
"id": "dist-calibration-MM",
39+
"prompt": "Distance Calibration results in MM",
40+
"required": true,
41+
"type": "reactive",
42+
"hidden": true
43+
},
44+
{
45+
"id": "dist-calibration-CM",
46+
"prompt": "Distance Calibration results in CM",
47+
"required": false,
48+
"type": "reactive",
49+
"hidden": true
50+
},
51+
{
52+
"id": "ball-positions",
53+
"prompt": "Position of balls in pixels",
54+
"required": false,
55+
"type": "reactive",
56+
"hidden": true
57+
},
58+
{
59+
"id": "square-position",
60+
"prompt": "Position of the black square in pixels",
61+
"required": false,
62+
"type": "reactive",
63+
"hidden": true
64+
}
65+
]
66+
}
67+
},
68+
"sequences": {
69+
"full": {
70+
"id": "virtual-chinrest",
71+
"order": "fixed",
72+
"components": [
73+
"card-size",
74+
"blindspot-distance"
75+
]
76+
}
77+
}
78+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Introduction
2+
3+
This is an example of the library `virtual-chinrest`.
4+
5+
The GitHub implementation is here: https://github.com/QishengLi/virtual_chinrest.
6+
7+
The `jsPsych` plugin documentation is here: https://www.jspsych.org/v7/plugins/virtual-chinrest/.
8+
9+
## References
10+
11+
Li, Q., Joo, S. J., Yeatman, J. D., & Reinecke, K. (2020). Controlling for Participants’ Viewing Distance in Large-Scale, Psychophysical Online Experiments Using a Virtual Chinrest. Scientific Reports, 10(1), 1-11. doi: 10.1038/s41598-019-57204-1
12+
13+
```
14+
@article{Li2020,
15+
doi = {10.1038/s41598-019-57204-1},
16+
url = {https://doi.org/10.1038/s41598-019-57204-1},
17+
year = {2020},
18+
month = jan,
19+
publisher = {Springer Science and Business Media {LLC}},
20+
volume = {10},
21+
number = {1},
22+
author = {Qisheng Li and Sung Jun Joo and Jason D. Yeatman and Katharina Reinecke},
23+
title = {Controlling for Participants' Viewing Distance in Large-Scale, Psychophysical Online Experiments Using a Virtual Chinrest},
24+
journal = {Scientific Reports}
25+
}
26+
```
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"$schema": "https://raw.githubusercontent.com/revisit-studies/study/v2.2.0/src/parser/StudyConfigSchema.json",
3+
"studyMetadata": {
4+
"title": "Virtual Chinrest Calibration",
5+
"version": "1.0.0",
6+
"authors": [
7+
"Qisheng Li",
8+
"Sheng Long"
9+
],
10+
"date": "2025-01-20",
11+
"description": "Example study using the virtual chinrest calibration library.",
12+
"organizations": [
13+
""
14+
]
15+
},
16+
"uiConfig": {
17+
"contactEmail": "",
18+
"logoPath": "revisitAssets/revisitLogoSquare.svg",
19+
"withProgressBar": true,
20+
"withSidebar": true
21+
},
22+
"importedLibraries": [
23+
"virtual-chinrest"
24+
],
25+
"components": {
26+
"introduction": {
27+
"type": "markdown",
28+
"path": "library-virtual-chinrest/assets/introduction.md",
29+
"response": [],
30+
"nextButtonLocation": "belowStimulus"
31+
}
32+
},
33+
"sequence": {
34+
"order": "fixed",
35+
"components": [
36+
"introduction",
37+
"$virtual-chinrest.se.full"
38+
]
39+
}
40+
}

src/components/NextButton.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,24 +12,24 @@ import { PreviousButton } from './PreviousButton';
1212
type Props = {
1313
label?: string;
1414
disabled?: boolean;
15-
configInUse?: IndividualComponent;
15+
config?: IndividualComponent;
1616
location?: ResponseBlockLocation;
1717
checkAnswer: JSX.Element | null;
1818
};
1919

2020
export function NextButton({
2121
label = 'Next',
2222
disabled = false,
23-
configInUse,
23+
config,
2424
location,
2525
checkAnswer,
2626
}: Props) {
2727
const { isNextDisabled, goToNextStep } = useNextStep();
2828
const studyConfig = useStudyConfig();
2929
const navigate = useNavigate();
3030

31-
const nextButtonDisableTime = useMemo(() => configInUse?.nextButtonDisableTime ?? studyConfig.uiConfig.nextButtonDisableTime, [configInUse, studyConfig]);
32-
const nextButtonEnableTime = useMemo(() => configInUse?.nextButtonEnableTime ?? studyConfig.uiConfig.nextButtonEnableTime ?? 0, [configInUse, studyConfig]);
31+
const nextButtonDisableTime = useMemo(() => config?.nextButtonDisableTime ?? studyConfig.uiConfig.nextButtonDisableTime, [config, studyConfig]);
32+
const nextButtonEnableTime = useMemo(() => config?.nextButtonEnableTime ?? studyConfig.uiConfig.nextButtonEnableTime ?? 0, [config, studyConfig]);
3333

3434
const [timer, setTimer] = useState<number | undefined>(undefined);
3535
// Start a timer on first render, update timer every 100ms
@@ -59,7 +59,7 @@ export function NextButton({
5959
[nextButtonDisableTime, nextButtonEnableTime, timer],
6060
);
6161

62-
const nextOnEnter = useMemo(() => configInUse?.nextOnEnter ?? studyConfig.uiConfig.nextOnEnter, [configInUse, studyConfig]);
62+
const nextOnEnter = useMemo(() => config?.nextOnEnter ?? studyConfig.uiConfig.nextOnEnter, [config, studyConfig]);
6363

6464
useEffect(() => {
6565
const handleKeyDown = (event: KeyboardEvent) => {
@@ -78,12 +78,12 @@ export function NextButton({
7878
}, [disabled, isNextDisabled, buttonTimerSatisfied, goToNextStep, nextOnEnter]);
7979

8080
const nextButtonDisabled = useMemo(() => disabled || isNextDisabled || !buttonTimerSatisfied, [disabled, isNextDisabled, buttonTimerSatisfied]);
81-
const previousButtonText = useMemo(() => configInUse?.previousButtonText ?? studyConfig.uiConfig.previousButtonText ?? 'Previous', [configInUse, studyConfig]);
81+
const previousButtonText = useMemo(() => config?.previousButtonText ?? studyConfig.uiConfig.previousButtonText ?? 'Previous', [config, studyConfig]);
8282

8383
return (
8484
<>
8585
<Group justify="right" gap="xs" mt="sm">
86-
{configInUse?.previousButton && (
86+
{config?.previousButton && (
8787
<PreviousButton
8888
label={previousButtonText}
8989
px={location === 'sidebar' && checkAnswer ? 8 : undefined}
Lines changed: 11 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { AppShell, Box, Text } from '@mantine/core';
1+
import { Box, Text } from '@mantine/core';
22
import { useMemo } from 'react';
33
import { ReactMarkdownWrapper } from '../ReactMarkdownWrapper';
44
import { useStudyConfig } from '../../store/hooks/useStudyConfig';
@@ -22,14 +22,12 @@ export function AppNavBar({ width, top, sidebarOpen }: { width: number, top: num
2222
}, [stepConfig, studyConfig]);
2323

2424
const status = useStoredAnswer();
25-
const trialHasSideBar = currentConfig?.withSidebar ?? studyConfig.uiConfig.withSidebar;
26-
const trialHasSideBarResponses = true;
2725

2826
const instruction = currentConfig?.instruction || '';
2927
const instructionLocation = useMemo(() => currentConfig?.instructionLocation ?? studyConfig.uiConfig.instructionLocation ?? 'sidebar', [currentConfig, studyConfig]);
3028
const instructionInSideBar = instructionLocation === 'sidebar';
3129

32-
return trialHasSideBar && currentConfig ? (
30+
return currentConfig ? (
3331
<Box className="sidebar" bg="gray.1" display={sidebarOpen ? 'block' : 'none'} style={{ zIndex: 0, marginTop: top, position: 'relative' }} w={width} miw={width}>
3432
{instructionInSideBar && instruction !== '' && (
3533
<Box
@@ -43,26 +41,14 @@ export function AppNavBar({ width, top, sidebarOpen }: { width: number, top: num
4341
</Box>
4442
)}
4543

46-
{trialHasSideBarResponses && (
47-
<Box p="md">
48-
<ResponseBlock
49-
key={`${currentComponent}-sidebar-response-block`}
50-
status={status}
51-
config={currentConfig}
52-
location="sidebar"
53-
/>
54-
</Box>
55-
)}
44+
<Box p="md">
45+
<ResponseBlock
46+
key={`${currentComponent}-sidebar-response-block`}
47+
status={status}
48+
config={currentConfig}
49+
location="sidebar"
50+
/>
51+
</Box>
5652
</Box>
57-
) : (
58-
<AppShell.Navbar bg="gray.1" display="block" style={{ zIndex: 0 }}>
59-
<ResponseBlock
60-
key={`${currentComponent}-sidebar-response-block`}
61-
status={status}
62-
config={currentConfig}
63-
location="sidebar"
64-
style={{ display: 'hidden' }}
65-
/>
66-
</AppShell.Navbar>
67-
);
53+
) : null;
6854
}

src/components/response/ButtonsInput.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ export function ButtonsInput({
4444
description={secondaryText}
4545
key={response.id}
4646
{...answer}
47-
// This overrides the answers error. Which..is bad?
4847
error={error}
48+
errorProps={{ c: required ? 'red' : 'orange' }}
4949
style={{ '--input-description-size': 'calc(var(--mantine-font-size-md) - calc(0.125rem * var(--mantine-scale)))' }}
5050
>
5151
<Flex justify="space-between" align="center" gap="xl" mt="xs">

src/components/response/CheckBoxInput.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export function CheckBoxInput({
5050
description={secondaryText}
5151
{...answer}
5252
error={error}
53+
errorProps={{ c: required ? 'red' : 'orange' }}
5354
style={{ '--input-description-size': 'calc(var(--mantine-font-size-md) - calc(0.125rem * var(--mantine-scale)))' }}
5455
>
5556
<Box mt="xs">

src/components/response/DropdownInput.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ export function DropdownInput({
4242
{...answer}
4343
value={answer.value === '' ? [] : Array.isArray(answer.value) ? answer.value : [answer.value]}
4444
error={generateErrorMessage(response, answer, optionsAsStringOptions)}
45+
withErrorStyles={required}
46+
errorProps={{ c: required ? 'red' : 'orange' }}
4547
classNames={{ input: classes.fixDisabled }}
4648
maxDropdownHeight={200}
4749
clearable
@@ -59,6 +61,8 @@ export function DropdownInput({
5961
{...answer}
6062
value={answer.value === '' ? null : answer.value}
6163
error={generateErrorMessage(response, answer, optionsAsStringOptions)}
64+
withErrorStyles={required}
65+
errorProps={{ c: required ? 'red' : 'orange' }}
6266
classNames={{ input: classes.fixDisabled }}
6367
maxDropdownHeight={200}
6468
/>

src/components/response/MatrixInput.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import checkboxClasses from './css/Checkbox.module.css';
1010
import radioClasses from './css/Radio.module.css';
1111
import { useStoredAnswer } from '../../store/hooks/useStoredAnswer';
1212
import { InputLabel } from './InputLabel';
13+
import { generateErrorMessage } from './utils';
1314

1415
function CheckboxComponent({
1516
_choices,
@@ -161,6 +162,8 @@ export function MatrixInput({
161162
storeDispatch(setMatrixAnswersCheckbox(payload));
162163
};
163164

165+
const error = generateErrorMessage(response, answer);
166+
164167
const _n = _choices.length;
165168
const _m = orderedQuestions.length;
166169
return (
@@ -287,6 +290,11 @@ export function MatrixInput({
287290
))}
288291
</div>
289292
</Box>
293+
{error && (
294+
<Text c={required ? 'red' : 'orange'} size="sm" mt="xs">
295+
{error}
296+
</Text>
297+
)}
290298
</>
291299
);
292300
}

0 commit comments

Comments
 (0)