From 15bc5df59998d15439a2afb055b753102051960a Mon Sep 17 00:00:00 2001 From: Zach Watson Date: Mon, 6 Apr 2026 17:36:19 -0700 Subject: [PATCH 1/4] repeat something same practice on incorrect --- .../matrix-reasoning/trials/instructions.ts | 7 +-- .../trials/downexInstructions.ts | 8 ++-- .../same-different-selection/catTimeline.ts | 2 +- .../same-different-selection/timeline.ts | 2 +- .../trials/afcMatch.ts | 7 ++- .../trials/stimulus.ts | 44 +++++++++++++++++-- .../src/tasks/shared/trials/feedback.ts | 1 + 7 files changed, 53 insertions(+), 18 deletions(-) diff --git a/task-launcher/src/tasks/matrix-reasoning/trials/instructions.ts b/task-launcher/src/tasks/matrix-reasoning/trials/instructions.ts index e1140f9a..676e82cc 100644 --- a/task-launcher/src/tasks/matrix-reasoning/trials/instructions.ts +++ b/task-launcher/src/tasks/matrix-reasoning/trials/instructions.ts @@ -7,6 +7,7 @@ import { setupReplayAudio, camelize, addPracticeButtonListeners, + disableOkButton, } from '../../shared/helpers'; import { isTouchScreen, jsPsych } from '../../taskSetup'; import { taskStore } from '../../../taskStore'; @@ -210,11 +211,7 @@ export const downexInstructions1 = { target.style.zIndex = ''; } - // disable ok button - const okButton: HTMLButtonElement | null = document.querySelector('.primary'); - if (okButton) { - okButton.disabled = true; - } + disableOkButton(); // set up animations let itemsToAnimate = [target, buttons, stimImage]; diff --git a/task-launcher/src/tasks/mental-rotation/trials/downexInstructions.ts b/task-launcher/src/tasks/mental-rotation/trials/downexInstructions.ts index 384aee86..1a84f45b 100644 --- a/task-launcher/src/tasks/mental-rotation/trials/downexInstructions.ts +++ b/task-launcher/src/tasks/mental-rotation/trials/downexInstructions.ts @@ -1,7 +1,7 @@ import jsPsychHtmlMultiResponse from '@jspsych-contrib/plugin-html-multi-response'; import { mediaAssets } from '../../..'; import { taskStore } from '../../../taskStore'; -import { camelize, PageAudioHandler, replayButtonSvg } from '../../shared/helpers'; +import { camelize, disableOkButton, PageAudioHandler, replayButtonSvg } from '../../shared/helpers'; import { animate } from '../helpers/animate'; import { jsPsych } from '../../taskSetup'; import { pulseOkButton } from '../../shared/helpers/pulseOkButton'; @@ -123,10 +123,8 @@ export const downexInstructions = downexData.map((data: any) => { if (replayButton) { (replayButton as HTMLButtonElement).disabled = true; } - const okButton: HTMLButtonElement | null = document.querySelector('.primary'); - if (okButton) { - okButton.disabled = true; - } + + disableOkButton(); // Preserve stim-container height before animation const stimContainer = document.getElementById('stim-container'); diff --git a/task-launcher/src/tasks/same-different-selection/catTimeline.ts b/task-launcher/src/tasks/same-different-selection/catTimeline.ts index a4495fd7..930561ab 100644 --- a/task-launcher/src/tasks/same-different-selection/catTimeline.ts +++ b/task-launcher/src/tasks/same-different-selection/catTimeline.ts @@ -100,7 +100,7 @@ export default function buildSameDifferentTimelineCat(config: Record, }; const updateSomethingSame = () => { - timeline.push({ ...setupStimulus, stimulus: '' }); + timeline.push({ ...setupStimulus, stimulus: '', trial_duration: 0 }); timeline.push(stimulusBlock); timeline.push(buttonNoise); timeline.push(dataQualityBlock); diff --git a/task-launcher/src/tasks/same-different-selection/trials/afcMatch.ts b/task-launcher/src/tasks/same-different-selection/trials/afcMatch.ts index 148d3058..0bd73a1c 100644 --- a/task-launcher/src/tasks/same-different-selection/trials/afcMatch.ts +++ b/task-launcher/src/tasks/same-different-selection/trials/afcMatch.ts @@ -203,6 +203,7 @@ export const afcMatch = (trial?: StimulusType) => { okButton.disabled = true; okButton.addEventListener('click', () => { if (!isPractice || compareSelections(selectedCards, previousSelections, getIgnoreDims(stim))) { + numberOfErrors = 0; jsPsych.finishTrial(); } else { const prompt = document.getElementById('afc-match-prompt') as HTMLParagraphElement; @@ -210,12 +211,15 @@ export const afcMatch = (trial?: StimulusType) => { taskStore().translations[camelize(audioFile)] }`; + numberOfErrors++; + const numberOfErrorsThisCall = numberOfErrors; + const audioConfig: AudioConfigType = { restrictRepetition: { enabled: false, maxRepetitions: 2, }, - onEnded: (numberOfErrorsThisCall: number) => { + onEnded: () => { if (numberOfErrorsThisCall === numberOfErrors) { // don't overlap audio PageAudioHandler.playAudio(mediaAssets.audio[camelize(audioFile)]); @@ -229,7 +233,6 @@ export const afcMatch = (trial?: StimulusType) => { responseBtns.forEach((btn) => btn.classList.remove(SELECT_CLASS_NAME)); selectedCards = []; disableOkButton(); - numberOfErrors++; if (numberOfErrors >= 2) { let animationStarted = false; diff --git a/task-launcher/src/tasks/same-different-selection/trials/stimulus.ts b/task-launcher/src/tasks/same-different-selection/trials/stimulus.ts index 27843264..c12eb714 100644 --- a/task-launcher/src/tasks/same-different-selection/trials/stimulus.ts +++ b/task-launcher/src/tasks/same-different-selection/trials/stimulus.ts @@ -229,7 +229,7 @@ export const stimulus = (trial?: StimulusType) => { response_ends_trial: () => { const stim = trial || taskStore().nextStimulus; - return stim.trialType !== 'test-dimensions'; + return !(stim.trialType === 'test-dimensions' || (stim.trialType === 'something-same-2' && stim.assessmentStage === 'practice_response')); }, on_load: () => { startTime = performance.now(); @@ -296,8 +296,7 @@ export const stimulus = (trial?: StimulusType) => { leftPrompt.style.height = `${contentBoxHeight}px`; } - const okButton = document.querySelector('.primary') as HTMLButtonElement; - okButton.disabled = true; + disableOkButton(); const responseBtns = Array.from( document.getElementById('img-button-container')?.children as any, @@ -335,6 +334,43 @@ export const stimulus = (trial?: StimulusType) => { setTimeout(() => enableBtns(responseBtns), 500); }); }); + + let numberOfErrors = 0; + + if (stimulus.assessmentStage === 'practice_response') { + const okButton = document.querySelector('.primary') as HTMLButtonElement; + okButton.addEventListener('click', (e) => { + if (selectionIdx !== taskStore().correctResponseIdx) { + const rightPrompt = document.getElementById('right-prompt') as HTMLParagraphElement; + const leftPrompt = document.getElementById('left-prompt') as HTMLParagraphElement; + rightPrompt.innerHTML = `

${taskStore().translations.feedbackNotQuiteRight}

`; + leftPrompt.style.visibility = 'hidden'; + + numberOfErrors++; + + const audioConfig: AudioConfigType = { + restrictRepetition: { + enabled: false, + maxRepetitions: 2, + } + }; + + PageAudioHandler.stopAndDisconnectNode(); + PageAudioHandler.playAudio(mediaAssets.audio.feedbackNotQuiteRight, audioConfig); + + responseBtns.forEach((btn) => btn.classList.remove(SELECT_CLASS_NAME)); + selection = null; + selectionIdx = null; + + if (numberOfErrors >= 2) { + responseBtns[taskStore().correctResponseIdx].style.animation = 'pulse 2s infinite'; + } + } else { + e.stopPropagation(); // prevents jspsych from disabling the buttons in the next trial + jsPsych.finishTrial(); + } + }); + } } // if the task is running in a cypress test, the correct answer should be indicated with 'correct' class @@ -392,7 +428,7 @@ export const stimulus = (trial?: StimulusType) => { if (stim.trialType !== 'something-same-1') { // update task store taskStore('isCorrect', isCorrect); - if (isCorrect === false) { + if (isCorrect === false && stim.assessmentStage !== 'practice_response') { taskStore.transact('numIncorrect', (oldVal: number) => oldVal + 1); } else { taskStore('numIncorrect', 0); diff --git a/task-launcher/src/tasks/shared/trials/feedback.ts b/task-launcher/src/tasks/shared/trials/feedback.ts index 305b5e4d..601f6776 100644 --- a/task-launcher/src/tasks/shared/trials/feedback.ts +++ b/task-launcher/src/tasks/shared/trials/feedback.ts @@ -2,6 +2,7 @@ import jsPsychHtmlMultiResponse from '@jspsych-contrib/plugin-html-multi-respons import { mediaAssets } from '../../..'; import { PageAudioHandler } from '../helpers'; import { taskStore } from '../../../taskStore'; +import { enableOkButton } from '../helpers/enableButtons'; // isPractice parameter is for tasks that don't have a corpus (e.g. memory game) export const feedback = ( From 5443b2b9618f95483c88fb0c160ed0e722406b35 Mon Sep 17 00:00:00 2001 From: Zach Watson Date: Mon, 6 Apr 2026 18:20:06 -0700 Subject: [PATCH 2/4] add 5-card intro screen --- .../tasks/same-different-selection/catTimeline.ts | 14 +++++++++++++- .../src/tasks/shared/helpers/getCorpus.ts | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/task-launcher/src/tasks/same-different-selection/catTimeline.ts b/task-launcher/src/tasks/same-different-selection/catTimeline.ts index 930561ab..9d7ae8c3 100644 --- a/task-launcher/src/tasks/same-different-selection/catTimeline.ts +++ b/task-launcher/src/tasks/same-different-selection/catTimeline.ts @@ -125,7 +125,18 @@ export default function buildSameDifferentTimelineCat(config: Record trial.itemId === "sds-instruct5") as StimulusType; + instructionPractice = instructionPractice.filter((trial) => trial.itemId !== "sds-instruct5"); + + const fiveBlockIntro = { + timeline: [ipBlock(fiveBlockIntroTrial)], + conditional_function: () => { + return taskStore().nextStimulus.trialType === "4-match"; + } + }; // returns practice + instruction trials for a given block function getPracticeInstructions(blockNum: number): StimulusType[] { @@ -165,6 +176,7 @@ export default function buildSameDifferentTimelineCat(config: Record, isDev: boolean) => const bucketName = getBucketName(task, isDev, 'corpus'); - const corpusUrl = `https://storage.googleapis.com/${bucketName}/${corpus}.csv?alt=media`; + const corpusUrl = `https://storage.googleapis.com/${bucketName}/${corpus}.csv?alt=media&v=2`; function downloadCSV(url: string) { return new Promise((resolve, reject) => { From 0d875d0a578fcfe809b866d0ee4ed7c8870531ff Mon Sep 17 00:00:00 2001 From: Zach Watson Date: Mon, 13 Apr 2026 08:55:07 -0700 Subject: [PATCH 3/4] fix sds timelines --- .../same-different-selection/catTimeline.ts | 34 +++++++++++-------- .../same-different-selection/timeline.ts | 6 ++-- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/task-launcher/src/tasks/same-different-selection/catTimeline.ts b/task-launcher/src/tasks/same-different-selection/catTimeline.ts index 9d7ae8c3..a8f72f6c 100644 --- a/task-launcher/src/tasks/same-different-selection/catTimeline.ts +++ b/task-launcher/src/tasks/same-different-selection/catTimeline.ts @@ -99,10 +99,6 @@ export default function buildSameDifferentTimelineCat(config: Record trial.itemId === "sds-instruct5") as StimulusType; - instructionPractice = instructionPractice.filter((trial) => trial.itemId !== "sds-instruct5"); + let fiveBlockIntroTrial: StimulusType; + let fiveBlockIntro: any; + if (taskStore().taskVersion === 2) { + // separate this out so that it is inserted at the right place in the timeline + fiveBlockIntroTrial = instructionPractice.find((trial) => trial.itemId === "sds-instruct5") as StimulusType; + instructionPractice = instructionPractice.filter((trial) => trial.itemId !== "sds-instruct5"); - const fiveBlockIntro = { - timeline: [ipBlock(fiveBlockIntroTrial)], - conditional_function: () => { - return taskStore().nextStimulus.trialType === "4-match"; - } - }; + fiveBlockIntro = { + timeline: [ipBlock(fiveBlockIntroTrial)], + conditional_function: () => { + return taskStore().nextStimulus.trialType === "4-match"; + } + }; + } + // returns practice + instruction trials for a given block function getPracticeInstructions(blockNum: number): StimulusType[] { @@ -167,6 +168,7 @@ export default function buildSameDifferentTimelineCat(config: Record, timeline.push(stimulusBlock); }; + const setupTrialDuration = taskStore().taskVersion === 2 ? 0 : 350; + const updateSomethingSame = () => { - timeline.push({ ...setupStimulus, stimulus: '', trial_duration: 0 }); + timeline.push({ ...setupStimulus, stimulus: '', trial_duration: setupTrialDuration}); timeline.push(stimulusBlock); timeline.push(buttonNoise); timeline.push(dataQualityBlock); }; const updateMatching = () => { - timeline.push({ ...setupStimulus, stimulus: '' }); + timeline.push({ ...setupStimulus, stimulus: '', trial_duration: setupTrialDuration}); timeline.push(afcBlock); timeline.push(buttonNoise); timeline.push(dataQualityBlock); From 8dfd93bc556194577aaa87a14b576bd320a20834 Mon Sep 17 00:00:00 2001 From: Zach Watson Date: Mon, 13 Apr 2026 16:18:33 -0700 Subject: [PATCH 4/4] add progress indicator to matching trials --- .../src/styles/layout/_containers.scss | 8 +++++++ .../trials/afcMatch.ts | 24 ++++++++++++++++++- .../src/tasks/shared/helpers/components.ts | 12 ++++++++++ .../src/tasks/shared/helpers/getCorpus.ts | 2 +- 4 files changed, 44 insertions(+), 2 deletions(-) diff --git a/task-launcher/src/styles/layout/_containers.scss b/task-launcher/src/styles/layout/_containers.scss index dcc48a10..6e805006 100644 --- a/task-launcher/src/styles/layout/_containers.scss +++ b/task-launcher/src/styles/layout/_containers.scss @@ -302,3 +302,11 @@ width: 100%; gap: 10vw; } + +.sds-progress-container { + max-width: 50vw; + display: flex; + flex-direction: row; + align-items: center; + gap: 30px; +} diff --git a/task-launcher/src/tasks/same-different-selection/trials/afcMatch.ts b/task-launcher/src/tasks/same-different-selection/trials/afcMatch.ts index 0bd73a1c..e9842e99 100644 --- a/task-launcher/src/tasks/same-different-selection/trials/afcMatch.ts +++ b/task-launcher/src/tasks/same-different-selection/trials/afcMatch.ts @@ -15,6 +15,7 @@ import { import { finishExperiment } from '../../shared/trials'; import { taskStore } from '../../../taskStore'; import { updateTheta } from '../../shared/helpers'; +import { sdsProgressComponentFilled, sdsProgressComponentEmpty } from '../../shared/helpers/components'; let selectedCards: string[] = []; let selectedCardIdxs: number[] = []; @@ -193,9 +194,30 @@ export const afcMatch = (trial?: StimulusType) => { let numberOfErrors = 0; - // Add primary OK button under the other buttons if (stim.trialType !== 'instructions') { if (taskStore().taskVersion === 2) { + // insert progress indicator + const numbers = { + 'first_response': 1, + 'second_response': 2, + 'third_response': 3, + 'fourth_response': 4, + } + const currentResponse = numbers[stim.assessmentStage as keyof typeof numbers]; + const maxResponses = Number(stim.trialType[0]); + + if (currentResponse !== undefined) { + const progressContainer = document.createElement('div'); + progressContainer.className = 'sds-progress-container'; + progressContainer.innerHTML = ` + ${sdsProgressComponentFilled.repeat(currentResponse)} ${sdsProgressComponentEmpty.repeat(maxResponses - currentResponse)} + `; + progressContainer.style.marginTop = '32px'; + + buttonContainer.parentNode?.insertBefore(progressContainer, buttonContainer.nextSibling); + } + + // Add primary OK button under the other buttons const okButton = document.createElement('button'); okButton.className = 'primary'; okButton.textContent = 'OK'; diff --git a/task-launcher/src/tasks/shared/helpers/components.ts b/task-launcher/src/tasks/shared/helpers/components.ts index e53db004..4a1b5dec 100644 --- a/task-launcher/src/tasks/shared/helpers/components.ts +++ b/task-launcher/src/tasks/shared/helpers/components.ts @@ -19,3 +19,15 @@ export const arrowKeyEmojis = [ `, ], ]; + +export const sdsProgressComponentFilled = ` + +`; + +export const sdsProgressComponentEmpty = ` + +`; diff --git a/task-launcher/src/tasks/shared/helpers/getCorpus.ts b/task-launcher/src/tasks/shared/helpers/getCorpus.ts index 8a70fce1..3d945497 100644 --- a/task-launcher/src/tasks/shared/helpers/getCorpus.ts +++ b/task-launcher/src/tasks/shared/helpers/getCorpus.ts @@ -176,7 +176,7 @@ export const getCorpus = async (config: Record, isDev: boolean) => const bucketName = getBucketName(task, isDev, 'corpus'); - const corpusUrl = `https://storage.googleapis.com/${bucketName}/${corpus}.csv?alt=media&v=2`; + const corpusUrl = `https://storage.googleapis.com/${bucketName}/${corpus}.csv?alt=media`; function downloadCSV(url: string) { return new Promise((resolve, reject) => {