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/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..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"); + + 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[] { @@ -156,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: '' }); + 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); 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..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'; @@ -203,6 +225,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 +233,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 +255,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/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/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 = (