Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
4ab6f14
Add numerical input warning
jaykim1213 Oct 10, 2025
18487b4
Fix numerical response check
jaykim1213 Oct 10, 2025
8408b4b
Remove min and max from numeric input
jaykim1213 Oct 10, 2025
8aff7cc
Remove comment
jaykim1213 Oct 10, 2025
5dfd682
Fix numerical response warning
jaykim1213 Oct 15, 2025
9f18964
code for virtual chinrest plugin
Oct 15, 2025
e15d340
Merge branch 'zc/ml/virtual-chinrest-plugin' into virtual-chinrest-pl…
ZachCutler04 Oct 20, 2025
f9827fe
Merge pull request #915 from mika-long/virtual-chinrest-plugin
ZachCutler04 Oct 20, 2025
9a36d39
removing sidebar and container
ZachCutler04 Oct 20, 2025
faded3c
linting stuff
ZachCutler04 Oct 20, 2025
9205f3c
Co-authored-by: Jay Kim <yeonkim1213@users.noreply.github.com>
JackWilb Oct 21, 2025
4f569c8
Make non required error color orange, add matrix validation
JackWilb Oct 21, 2025
2c5e78b
Fix numerical response validation
JackWilb Oct 21, 2025
f2cc831
Merge pull request #908 from revisit-studies/jk/responseWarning
jaykim1213 Oct 21, 2025
9c91ea6
Fixing weird layout issue with sidebar
ZachCutler04 Oct 21, 2025
c1500ab
fixing weird layout issue with sidebar
ZachCutler04 Oct 21, 2025
251121b
fixing weird type in ResponseBlock
ZachCutler04 Oct 21, 2025
59219d9
Merge pull request #926 from revisit-studies/zc/ml/virtual-chinrest-p…
JackWilb Oct 21, 2025
1e5d107
fixing problem introduced by new shuffling
ZachCutler04 Oct 21, 2025
ec86490
Merge pull request #930 from revisit-studies/zc/sidebarFix
JackWilb Oct 21, 2025
f5582e5
Merge pull request #932 from revisit-studies/zc/sequencesBugFix
ZachCutler04 Oct 21, 2025
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
5 changes: 5 additions & 0 deletions public/global.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"library-screen-recording",
"library-smeq",
"library-sus",
"library-virtual-chinrest",
"library-vlat",
"test-audio",
"test-library",
Expand Down Expand Up @@ -211,6 +212,10 @@
"path": "test-step-logic/config.json",
"test": true
},
"library-virtual-chinrest": {
"path": "library-virtual-chinrest/config.json",
"test": true
},
"example-VLAT-adaptive": {
"path": "example-VLAT-adaptive/config.json",
"test": true
Expand Down
78 changes: 78 additions & 0 deletions public/libraries/virtual-chinrest/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
{
"$schema": "https://raw.githubusercontent.com/revisit-studies/study/v2.2.0/src/parser/LibraryConfigSchema.json",
"description": "A library for visual calibration tasks including virtual chinrest (card size) and viewing distance calibration using blindspot tracking.",
"reference": "Custom calibration library for visual perception studies",
"doi": "10.1038/s41598-019-57204-1",
"externalLink": "https://github.com/QishengLi/virtual_chinrest",
"components": {
"card-size": {
"type": "react-component",
"path": "libraries/virtual-chinrest/assets/VirtualChinrestCalibration.tsx",
"withSidebar": false,
"parameters": {
"taskid": "pixelsPerMM",
"itemWidthMM": 85.6,
"itemHeightMM": 53.98
},
"nextButtonLocation": "belowStimulus",
"response": [
{
"id": "pixelsPerMM",
"prompt": "Calibration results",
"required": true,
"type": "reactive",
"hidden": true
}
]
},
"blindspot-distance": {
"type": "react-component",
"path": "libraries/virtual-chinrest/assets/ViewingDistanceCalibration.tsx",
"nextButtonLocation": "belowStimulus",
"withSidebar": false,
"parameters": {
"blindspotAngle": 13.5
},
"response": [
{
"id": "dist-calibration-MM",
"prompt": "Distance Calibration results in MM",
"required": true,
"type": "reactive",
"hidden": true
},
{
"id": "dist-calibration-CM",
"prompt": "Distance Calibration results in CM",
"required": false,
"type": "reactive",
"hidden": true
},
{
"id": "ball-positions",
"prompt": "Position of balls in pixels",
"required": false,
"type": "reactive",
"hidden": true
},
{
"id": "square-position",
"prompt": "Position of the black square in pixels",
"required": false,
"type": "reactive",
"hidden": true
}
]
}
},
"sequences": {
"full": {
"id": "virtual-chinrest",
"order": "fixed",
"components": [
"card-size",
"blindspot-distance"
]
}
}
}
26 changes: 26 additions & 0 deletions public/library-virtual-chinrest/assets/introduction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Introduction

This is an example of the library `virtual-chinrest`.

The GitHub implementation is here: https://github.com/QishengLi/virtual_chinrest.

The `jsPsych` plugin documentation is here: https://www.jspsych.org/v7/plugins/virtual-chinrest/.

## References

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

```
@article{Li2020,
doi = {10.1038/s41598-019-57204-1},
url = {https://doi.org/10.1038/s41598-019-57204-1},
year = {2020},
month = jan,
publisher = {Springer Science and Business Media {LLC}},
volume = {10},
number = {1},
author = {Qisheng Li and Sung Jun Joo and Jason D. Yeatman and Katharina Reinecke},
title = {Controlling for Participants' Viewing Distance in Large-Scale, Psychophysical Online Experiments Using a Virtual Chinrest},
journal = {Scientific Reports}
}
```
40 changes: 40 additions & 0 deletions public/library-virtual-chinrest/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"$schema": "https://raw.githubusercontent.com/revisit-studies/study/v2.2.0/src/parser/StudyConfigSchema.json",
"studyMetadata": {
"title": "Virtual Chinrest Calibration",
"version": "1.0.0",
"authors": [
"Qisheng Li",
"Sheng Long"
],
"date": "2025-01-20",
"description": "Example study using the virtual chinrest calibration library.",
"organizations": [
""
]
},
"uiConfig": {
"contactEmail": "",
"logoPath": "revisitAssets/revisitLogoSquare.svg",
"withProgressBar": true,
"withSidebar": true
},
"importedLibraries": [
"virtual-chinrest"
],
"components": {
"introduction": {
"type": "markdown",
"path": "library-virtual-chinrest/assets/introduction.md",
"response": [],
"nextButtonLocation": "belowStimulus"
}
},
"sequence": {
"order": "fixed",
"components": [
"introduction",
"$virtual-chinrest.se.full"
]
}
}
14 changes: 7 additions & 7 deletions src/components/NextButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,24 @@ import { PreviousButton } from './PreviousButton';
type Props = {
label?: string;
disabled?: boolean;
configInUse?: IndividualComponent;
config?: IndividualComponent;
location?: ResponseBlockLocation;
checkAnswer: JSX.Element | null;
};

export function NextButton({
label = 'Next',
disabled = false,
configInUse,
config,
location,
checkAnswer,
}: Props) {
const { isNextDisabled, goToNextStep } = useNextStep();
const studyConfig = useStudyConfig();
const navigate = useNavigate();

const nextButtonDisableTime = useMemo(() => configInUse?.nextButtonDisableTime ?? studyConfig.uiConfig.nextButtonDisableTime, [configInUse, studyConfig]);
const nextButtonEnableTime = useMemo(() => configInUse?.nextButtonEnableTime ?? studyConfig.uiConfig.nextButtonEnableTime ?? 0, [configInUse, studyConfig]);
const nextButtonDisableTime = useMemo(() => config?.nextButtonDisableTime ?? studyConfig.uiConfig.nextButtonDisableTime, [config, studyConfig]);
const nextButtonEnableTime = useMemo(() => config?.nextButtonEnableTime ?? studyConfig.uiConfig.nextButtonEnableTime ?? 0, [config, studyConfig]);

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

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

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

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

return (
<>
<Group justify="right" gap="xs" mt="sm">
{configInUse?.previousButton && (
{config?.previousButton && (
<PreviousButton
label={previousButtonText}
px={location === 'sidebar' && checkAnswer ? 8 : undefined}
Expand Down
36 changes: 11 additions & 25 deletions src/components/interface/AppNavBar.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AppShell, Box, Text } from '@mantine/core';
import { Box, Text } from '@mantine/core';
import { useMemo } from 'react';
import { ReactMarkdownWrapper } from '../ReactMarkdownWrapper';
import { useStudyConfig } from '../../store/hooks/useStudyConfig';
Expand All @@ -22,14 +22,12 @@ export function AppNavBar({ width, top, sidebarOpen }: { width: number, top: num
}, [stepConfig, studyConfig]);

const status = useStoredAnswer();
const trialHasSideBar = currentConfig?.withSidebar ?? studyConfig.uiConfig.withSidebar;
const trialHasSideBarResponses = true;

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

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

{trialHasSideBarResponses && (
<Box p="md">
<ResponseBlock
key={`${currentComponent}-sidebar-response-block`}
status={status}
config={currentConfig}
location="sidebar"
/>
</Box>
)}
<Box p="md">
<ResponseBlock
key={`${currentComponent}-sidebar-response-block`}
status={status}
config={currentConfig}
location="sidebar"
/>
</Box>
</Box>
) : (
<AppShell.Navbar bg="gray.1" display="block" style={{ zIndex: 0 }}>
<ResponseBlock
key={`${currentComponent}-sidebar-response-block`}
status={status}
config={currentConfig}
location="sidebar"
style={{ display: 'hidden' }}
/>
</AppShell.Navbar>
);
) : null;
}
2 changes: 1 addition & 1 deletion src/components/response/ButtonsInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ export function ButtonsInput({
description={secondaryText}
key={response.id}
{...answer}
// This overrides the answers error. Which..is bad?
error={error}
errorProps={{ c: required ? 'red' : 'orange' }}
style={{ '--input-description-size': 'calc(var(--mantine-font-size-md) - calc(0.125rem * var(--mantine-scale)))' }}
>
<Flex justify="space-between" align="center" gap="xl" mt="xs">
Expand Down
1 change: 1 addition & 0 deletions src/components/response/CheckBoxInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export function CheckBoxInput({
description={secondaryText}
{...answer}
error={error}
errorProps={{ c: required ? 'red' : 'orange' }}
style={{ '--input-description-size': 'calc(var(--mantine-font-size-md) - calc(0.125rem * var(--mantine-scale)))' }}
>
<Box mt="xs">
Expand Down
4 changes: 4 additions & 0 deletions src/components/response/DropdownInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ export function DropdownInput({
{...answer}
value={answer.value === '' ? [] : Array.isArray(answer.value) ? answer.value : [answer.value]}
error={generateErrorMessage(response, answer, optionsAsStringOptions)}
withErrorStyles={required}
errorProps={{ c: required ? 'red' : 'orange' }}
classNames={{ input: classes.fixDisabled }}
maxDropdownHeight={200}
clearable
Expand All @@ -59,6 +61,8 @@ export function DropdownInput({
{...answer}
value={answer.value === '' ? null : answer.value}
error={generateErrorMessage(response, answer, optionsAsStringOptions)}
withErrorStyles={required}
errorProps={{ c: required ? 'red' : 'orange' }}
classNames={{ input: classes.fixDisabled }}
maxDropdownHeight={200}
/>
Expand Down
8 changes: 8 additions & 0 deletions src/components/response/MatrixInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import checkboxClasses from './css/Checkbox.module.css';
import radioClasses from './css/Radio.module.css';
import { useStoredAnswer } from '../../store/hooks/useStoredAnswer';
import { InputLabel } from './InputLabel';
import { generateErrorMessage } from './utils';

function CheckboxComponent({
_choices,
Expand Down Expand Up @@ -161,6 +162,8 @@ export function MatrixInput({
storeDispatch(setMatrixAnswersCheckbox(payload));
};

const error = generateErrorMessage(response, answer);

const _n = _choices.length;
const _m = orderedQuestions.length;
return (
Expand Down Expand Up @@ -287,6 +290,11 @@ export function MatrixInput({
))}
</div>
</Box>
{error && (
<Text c={required ? 'red' : 'orange'} size="sm" mt="xs">
{error}
</Text>
)}
</>
);
}
6 changes: 2 additions & 4 deletions src/components/response/NumericInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ export function NumericInput({
const {
prompt,
required,
min,
max,
placeholder,
secondaryText,
infoText,
Expand All @@ -35,10 +33,10 @@ export function NumericInput({
description={secondaryText}
radius="md"
size="md"
min={min}
max={max}
{...answer}
error={generateErrorMessage(response, answer)}
withErrorStyles={required}
errorProps={{ c: required ? 'red' : 'orange' }}
classNames={{ input: classes.fixDisabled }}
/>
);
Expand Down
2 changes: 1 addition & 1 deletion src/components/response/RadioInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ export function RadioInput({
description={secondaryText}
key={response.id}
{...answer}
// This overrides the answers error. Which..is bad?
error={error}
errorProps={{ c: required ? 'red' : 'orange' }}
style={{ '--input-description-size': 'calc(var(--mantine-font-size-md) - calc(0.125rem * var(--mantine-scale)))' }}
>
<Group gap="lg" align="flex-end" mt={horizontal ? 0 : 'sm'}>
Expand Down
2 changes: 1 addition & 1 deletion src/components/response/RankingInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -570,7 +570,7 @@ export function RankingInput({
)}
{secondaryText && <Text c="dimmed" size="sm" mt={0}>{secondaryText}</Text>}
{error && (
<Text c="red" size="sm" mt="xs">
<Text c={required ? 'red' : 'orange'} size="sm" mt="xs">
{error}
</Text>
)}
Expand Down
Loading
Loading