From a10b6d783785ce5a71434670a272f70f1c5a8563 Mon Sep 17 00:00:00 2001 From: Zach Watson Date: Mon, 15 Dec 2025 10:18:19 -0800 Subject: [PATCH 1/5] SDS redesign in progress --- task-launcher/serve/serve.js | 2 - .../src/styles/layout/_containers.scss | 19 ++ .../helpers/setTrialBlock.ts | 78 ++--- .../same-different-selection/timeline.ts | 106 ++----- .../trials/afcMatch.ts | 51 ++-- .../trials/stimulus.ts | 269 +++++++++++------- .../src/tasks/shared/helpers/config.ts | 2 - .../tasks/shared/helpers/disableOkButton.ts | 6 + .../src/tasks/shared/helpers/getCorpus.ts | 73 +---- .../src/tasks/shared/helpers/index.ts | 1 + 10 files changed, 292 insertions(+), 315 deletions(-) create mode 100644 task-launcher/src/tasks/shared/helpers/disableOkButton.ts diff --git a/task-launcher/serve/serve.js b/task-launcher/serve/serve.js index 86869809c..9edf8635c 100644 --- a/task-launcher/serve/serve.js +++ b/task-launcher/serve/serve.js @@ -35,7 +35,6 @@ const urlParams = new URLSearchParams(queryString); const taskName = urlParams.get('task') ?? 'egma-math'; const corpus = urlParams.get('corpus'); const buttonLayout = urlParams.get('buttonLayout'); -const numOfPracticeTrials = urlParams.get('practiceTrials'); const numberOfTrials = urlParams.get('trials') === null ? null : parseInt(urlParams.get('trials'), 10); const maxIncorrect = urlParams.get('maxIncorrect') === null ? null : parseInt(urlParams.get('maxIncorrect'), 10); const stimulusBlocks = urlParams.get('blocks') === null ? null : parseInt(urlParams.get('blocks'), 10); @@ -80,7 +79,6 @@ async function startWebApp() { sequentialStimulus, corpus, buttonLayout, - numOfPracticeTrials, numberOfTrials, maxIncorrect, stimulusBlocks, diff --git a/task-launcher/src/styles/layout/_containers.scss b/task-launcher/src/styles/layout/_containers.scss index b6bd18dfc..b86724849 100644 --- a/task-launcher/src/styles/layout/_containers.scss +++ b/task-launcher/src/styles/layout/_containers.scss @@ -95,6 +95,11 @@ } } + &.instruction-half-screen { + @extend .lev-row-container, .instruction; + max-width: 40vw !important; + } + &.instruction-small { display: flex; padding: min($lev-spacing-s, 3.5vh); @@ -163,6 +168,11 @@ } } +.lev-stimulus-container-wide { + @extend .lev-stimulus-container; + max-width: 90vw; +} + .lev-stim-content { display: flex; width: 100%; @@ -283,3 +293,12 @@ color: black; font-size: 4rem; } + +.horizontal-wrapper { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + width: 100%; + gap: 16px; +} diff --git a/task-launcher/src/tasks/same-different-selection/helpers/setTrialBlock.ts b/task-launcher/src/tasks/same-different-selection/helpers/setTrialBlock.ts index 5a44942c3..c34c1fa94 100644 --- a/task-launcher/src/tasks/same-different-selection/helpers/setTrialBlock.ts +++ b/task-launcher/src/tasks/same-different-selection/helpers/setTrialBlock.ts @@ -1,46 +1,50 @@ import { taskStore } from '../../../taskStore'; +function compareTrialTypes(trialType1: string, trialType2: string) { + return ( + trialType1 === trialType2 || + (trialType1.includes('match') && trialType2.includes('match')) || + (trialType1.includes('something-same') && trialType2.includes('something-same')) + ); +} + +function getBlockOperations(trialType: string) { + if (trialType.includes('match')) { + return 'updateMatching'; + } else if (trialType.includes('something-same')) { + return 'updateSomethingSame'; + } else { + return 'updateTestDimensions'; + } +} + export function setTrialBlock(cat: boolean) { // create list of numbers of trials per block const blockCountList: number[] = []; - let testDimPhase2: boolean = false; + const blockOperations: string[] = []; + let currentTrialType = "test-dimensions"; + let currentBlockCount = 0; + + if (cat) { + taskStore().corpora.stimulus.forEach((block: StimulusType[]) => { + blockCountList.push(block.length); + }); + } else { + taskStore().corpora.stimulus.forEach((trial: StimulusType) => { + if (!compareTrialTypes(trial.trialType, currentTrialType) && trial.trialType !== "instructions") { + blockCountList.push(currentBlockCount); + blockOperations.push(getBlockOperations(currentTrialType)); + + currentTrialType = trial.trialType; + currentBlockCount = 0; + } - cat - ? taskStore().corpora.stimulus.forEach((block: StimulusType[]) => { - blockCountList.push(block.length); - }) - : taskStore().corpora.stimulus.forEach((trial: StimulusType) => { - // if not running a CAT, trials are blocked by their type - let trialBlock: number; - switch (trial.trialType) { - case 'test-dimensions': - trialBlock = testDimPhase2 ? 3 : 0; - break; - case 'something-same-1': - testDimPhase2 = true; - trialBlock = 1; - break; - case 'something-same-2': - trialBlock = 1; - break; - case '2-match': - trialBlock = 2; - break; - case '3-match': - trialBlock = 4; - break; - case '4-match': - trialBlock = 5; - break; - default: - trialBlock = NaN; - break; - } + currentBlockCount++; + }); - if (!Number.isNaN(trialBlock)) { - blockCountList[trialBlock] = (blockCountList[trialBlock] || 0) + 1; - } - }); + blockCountList.push(currentBlockCount); + blockOperations.push(getBlockOperations(currentTrialType)); + } - return blockCountList; + return {blockCountList, blockOperations}; } diff --git a/task-launcher/src/tasks/same-different-selection/timeline.ts b/task-launcher/src/tasks/same-different-selection/timeline.ts index a9564f9a5..6efd4ccd4 100644 --- a/task-launcher/src/tasks/same-different-selection/timeline.ts +++ b/task-launcher/src/tasks/same-different-selection/timeline.ts @@ -2,7 +2,7 @@ import 'regenerator-runtime/runtime'; import { jsPsych } from '../taskSetup'; import { initTrialSaving, initTimeline, createPreloadTrials, filterMedia, batchMediaAssets } from '../shared/helpers'; -import { prepareCorpus, prepareMultiBlockCat } from '../shared/helpers/prepareCat'; +import { prepareMultiBlockCat } from '../shared/helpers/prepareCat'; import { initializeCat } from '../taskSetup'; // trials import { dataQualityScreen } from '../shared/trials/dataQuality'; @@ -19,27 +19,20 @@ import { import { afcMatch } from './trials/afcMatch'; import { stimulus } from './trials/stimulus'; import { taskStore } from '../../taskStore'; -import { - somethingSameDemo1, - somethingSameDemo2, - somethingSameDemo3, - matchDemo1, - matchDemo2, - heavyPractice, -} from './trials/heavyInstructions'; import { setTrialBlock } from './helpers/setTrialBlock'; -import { getLeftoverAssets } from '../shared/helpers/batchPreloading'; +import { batchTrials, getLeftoverAssets } from '../shared/helpers/batchPreloading'; export default function buildSameDifferentTimeline(config: Record, mediaAssets: MediaAssetsType) { const heavy: boolean = taskStore().heavyInstructions; - const corpus: StimulusType[] = taskStore().corpora.stimulus; - const preparedCorpus = prepareCorpus(corpus); - - // create list of trials in each block - const blockList = prepareMultiBlockCat(corpus); + let corpus: StimulusType[] = taskStore().corpora.stimulus; - const batchedMediaAssets = batchMediaAssets(mediaAssets, blockList, ['image', 'answer', 'distractors']); + console.log(corpus.filter((trial) => trial.assessmentStage === 'practice_response')); + // organize corpus into batches for preloading + const batchSize = 25; + const batchedCorpus = batchTrials(corpus, batchSize); + console.log(batchedCorpus); + const batchedMediaAssets = batchMediaAssets(mediaAssets, batchedCorpus, ['image', 'answer', 'distractors']); const initialMediaAssets = getLeftoverAssets(batchedMediaAssets, mediaAssets); initialMediaAssets.images = {}; // all sds images used in the task are specifed in corpus @@ -68,13 +61,6 @@ export default function buildSameDifferentTimeline(config: Record, }, }; - // used for instruction and practice trials - const ipBlock = (trial: StimulusType) => { - return { - timeline: [{ ...fixationOnly, stimulus: '' }, stimulus(trial)], - }; - }; - const stimulusBlock = { timeline: [stimulus()], }; @@ -90,16 +76,8 @@ export default function buildSameDifferentTimeline(config: Record, }, }; - // all instructions + practice trials - const instructionPractice: StimulusType[] = heavy ? preparedCorpus.ipHeavy : preparedCorpus.ipLight; - - // returns practice + instruction trials for a given block - function getPracticeInstructions(blockNum: number): StimulusType[] { - return instructionPractice.filter((trial) => trial.blockIndex == blockNum); - } - // create list of numbers of trials per block - const blockCountList = setTrialBlock(false); + const {blockCountList, blockOperations} = setTrialBlock(false); const totalRealTrials = blockCountList.reduce((acc, total) => acc + total, 0); taskStore('totalTestTrials', totalRealTrials); @@ -109,83 +87,51 @@ export default function buildSameDifferentTimeline(config: Record, // function to preload assets in batches at the beginning of each task block function preloadBlock() { + console.log(createPreloadTrials(batchedMediaAssets[currPreloadBatch]).default); timeline.push(createPreloadTrials(batchedMediaAssets[currPreloadBatch]).default); currPreloadBatch++; } // functions to add trials to blocks of each type - function updateTestDimensions() { + const updateTestDimensions = () => { timeline.push({ ...setupStimulus, stimulus: '' }); timeline.push(stimulusBlock); } - function updateSomethingSame() { + const updateSomethingSame = () => { timeline.push({ ...setupStimulus, stimulus: '' }); timeline.push(stimulusBlock); timeline.push(buttonNoise); timeline.push(dataQualityBlock); } - function updateMatching() { + const updateMatching = () => { timeline.push({ ...setupStimulus, stimulus: '' }); timeline.push(afcBlock); timeline.push(buttonNoise); timeline.push(dataQualityBlock); } - // add to this list with any additional blocks - const blockOperations = [ + // map of block operation functions + const blockFunctions = { updateTestDimensions, updateSomethingSame, updateMatching, - updateTestDimensions, - updateMatching, - updateMatching, - ]; + }; - // preload next batch of assets at these blocks - const preloadBlockIndexes = [0, 1, 2]; + let trialCount = 0; // add trials to timeline according to block structure defined in blockOperations blockCountList.forEach((count, index) => { - if (preloadBlockIndexes.includes(index)) { - preloadBlock(); - } - - const currentBlockInstructionPractice = getPracticeInstructions(index); - - // push in instruction + practice trials - if (index === 1 && heavy) { - // something's the same block has demo trials in between instructions - const firstInstruction = currentBlockInstructionPractice.shift(); - if (firstInstruction != undefined) { - timeline.push(ipBlock(firstInstruction)); - } - - timeline.push(somethingSameDemo1); - timeline.push(somethingSameDemo2); - timeline.push(somethingSameDemo3); - currentBlockInstructionPractice.forEach((trial) => { - timeline.push(ipBlock(trial)); - }); - heavyPractice.forEach((trial) => { - timeline.push(trial); - }); - timeline.push({ ...practiceTransition(), conditional_function: () => true }); - } else { - currentBlockInstructionPractice.forEach((trial) => { - timeline.push(ipBlock(trial)); - }); - } - - if (index === 2 && heavy) { - // 2-match has demo trials after instructions - timeline.push(matchDemo1); - timeline.push(matchDemo2); - } - + // push in trials for (let i = 0; i < count; i += 1) { - blockOperations[index](); + // preload assets + if (trialCount % batchSize === 0) { + preloadBlock(); + } + + blockFunctions[blockOperations[index] as keyof typeof blockFunctions](); + trialCount++; } }); 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 79f6dc546..1641f225c 100644 --- a/task-launcher/src/tasks/same-different-selection/trials/afcMatch.ts +++ b/task-launcher/src/tasks/same-different-selection/trials/afcMatch.ts @@ -8,6 +8,8 @@ import { PageStateHandler, PageAudioHandler, camelize, + enableOkButton, + disableOkButton, } from '../../shared/helpers'; import { finishExperiment } from '../../shared/trials'; import { taskStore } from '../../../taskStore'; @@ -49,14 +51,7 @@ export const afcMatch = { }, prompt: () => { const stimulus = taskStore().nextStimulus; - let prompt = camelize(stimulus.audioFile); - if ( - taskStore().heavyInstructions && - stimulus.assessmentStage !== 'practice_response' && - stimulus.trialType !== 'instructions' - ) { - prompt += 'Heavy'; - } + const prompt = camelize(stimulus.audioFile); const t = taskStore().translations; return `
@@ -94,15 +89,7 @@ export const afcMatch = { // can select multiple cards and deselect them startTime = performance.now(); const stim = taskStore().nextStimulus; - - let audioFile = stim.audioFile; - if ( - taskStore().heavyInstructions && - stim.assessmentStage !== 'practice_response' && - stim.trialType !== 'instructions' - ) { - audioFile += '-heavy'; - } + const audioFile = stim.audioFile; const audioConfig: AudioConfigType = { restrictRepetition: { @@ -114,7 +101,20 @@ export const afcMatch = { const pageStateHandler = new PageStateHandler(audioFile, true); setupReplayAudio(pageStateHandler); + const buttonContainer = document.getElementById('jspsych-audio-multi-response-btngroup') as HTMLDivElement; + + // Add primary OK button under the other buttons + const okButton = document.createElement('button'); + okButton.className = 'primary'; + okButton.textContent = 'OK'; + okButton.style.marginTop = '16px'; + okButton.disabled = true; + okButton.addEventListener('click', () => { + jsPsych.finishTrial(); + }); + buttonContainer.parentNode?.insertBefore(okButton, buttonContainer.nextSibling); + const responseBtns = Array.from(buttonContainer.children) .map((btnDiv) => btnDiv.firstChild as HTMLButtonElement) .filter((btn) => !!btn); @@ -127,10 +127,12 @@ export const afcMatch = { } responseBtns.forEach((card, i) => card.addEventListener('click', async (e) => { - const answer = (card?.firstChild as HTMLImageElement)?.alt; + const answer = ((card as HTMLButtonElement)?.firstChild as HTMLImageElement)?.alt; + if (!card) { return; } + if (card.classList.contains(SELECT_CLASS_NAME)) { card.classList.remove(SELECT_CLASS_NAME); selectedCards.splice(selectedCards.indexOf(answer), 1); @@ -139,13 +141,14 @@ export const afcMatch = { card.classList.add(SELECT_CLASS_NAME); selectedCards.push(answer); selectedCardIdxs.push(i); - // afcMatch trial types look like n-match / n-unique - const requiredSelections = stim.requiredSelections; - - if (selectedCards.length === requiredSelections) { - setTimeout(() => jsPsych.finishTrial(), 500); - } } + + if (selectedCards.length === stim.requiredSelections) { + enableOkButton(); + } else { + disableOkButton(); + } + setTimeout(() => enableBtns(responseBtns), 500); }), ); 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 e8b107d59..802191230 100644 --- a/task-launcher/src/tasks/same-different-selection/trials/stimulus.ts +++ b/task-launcher/src/tasks/same-different-selection/trials/stimulus.ts @@ -7,17 +7,22 @@ import { setupReplayAudio, PageAudioHandler, camelize, + enableOkButton, + disableOkButton, } from '../../shared/helpers'; import { finishExperiment } from '../../shared/trials'; import { isTouchScreen, jsPsych } from '../../taskSetup'; import { taskStore } from '../../../taskStore'; -import { handleStaggeredButtons } from '../../shared/helpers/staggerButtons'; import { updateTheta } from '../../shared/helpers'; import { setNextCatTrial } from '../helpers/setNextCatTrial'; const replayButtonHtmlId = 'replay-btn-revisited'; let incorrectPracticeResponses: string[] = []; let startTime: number; +let selection: string | null = null; +let selectionIdx: number | null = null; + +const SELECT_CLASS_NAME = 'info-shadow'; export const generateImageChoices = (choices: string[]) => { return choices.map((choice) => { @@ -30,6 +35,96 @@ function enableBtns(btnElements: HTMLButtonElement[]) { btnElements.forEach((btn) => btn.removeAttribute('disabled')); } +function getTestDimensionsHtml(stim: StimulusType) { + const prompt = typeof stim.audioFile === 'string' ? + camelize(stim.audioFile) : + stim.audioFile.map((file) => camelize(file)).join(' '); + + const t = taskStore().translations; + + return ` +
+ +
+

${t[prompt]}

+
+
`; +} + +function getSomethingSameHtml(stim: StimulusType) { + const t = taskStore().translations; + + const leftImageSrc: string = stim.trialType == "something-same-1" ? stim.image[0] : stim.image as string; + + const leftPromptHtml = stim.trialType === "something-same-2" ? `

${t[camelize(stim.audioFile[0])]}

` : ''; + const rightPromptHtml = stim.trialType === "something-same-2" ? `

${t[camelize(stim.audioFile[1])]}

` : `

${t[camelize(stim.audioFile as string)]}

`; + + const leftImageHtml = ` + ${stim.trialType == "something-same-1" ? `
` : `
`} + +
+ `; + + // randomize choices if there is an answer + const randomize = !!stim.answer ? 'yes' : 'no'; + const { choices } = prepareChoices(stim.answer as string, stim.distractors as string[], randomize); + const images: string[] = stim.trialType == "something-same-1" ? + (stim.image as string[]).map((image) => { + return `${image}`; + }) : + generateImageChoices(choices); + + const rightImageHtml = ` + ${(images) + .map((image) => { + return ``; + }) + .join('')} + `; + + return ` +
+ +
+ ${stim.trialType === "something-same-2" ? + `
` : + ` +
+

${rightPromptHtml}

+
+
+
+
+ ${leftImageHtml} +
+
+ ${rightImageHtml} +
+
+
+
+
+
+
+ `; +} + export function handleButtonFeedback( btn: HTMLButtonElement, cards: HTMLButtonElement[], @@ -98,120 +193,98 @@ export const stimulus = (trial?: StimulusType) => { }, stimulus: () => { const stim = trial || taskStore().nextStimulus; - let prompt = camelize(stim.audioFile); - if ( - taskStore().heavyInstructions && - stim.assessmentStage !== 'practice_response' && - stim.trialType !== 'instructions' - ) { - prompt += 'Heavy'; - } - const t = taskStore().translations; - return `
- -
-

${t[prompt]}

-
- - ${ - stim.image && !Array.isArray(stim.image) - ? `
` - : '' - } - - ${ - stim.image && Array.isArray(stim.image) - ? `
- ${ - stim.trialType === 'something-same-1' - ? ` -
- -
- ` - : '' - } -
- ${(stim.image as string[]) - .map((shape) => { - return ``; - }) - .join('')} -
-
` - : '' - } -
`; + return stim.trialType.includes("something-same") ? getSomethingSameHtml(stim) : getTestDimensionsHtml(stim); }, prompt_above_buttons: true, button_choices: () => { const stim = trial || taskStore().nextStimulus; - if (stim.trialType === 'instructions' || stim.trialType == 'something-same-1') { - return ['OK']; - } else { + if (stim.trialType === 'test-dimensions') { const randomize = !!stim.answer ? 'yes' : 'no'; // Randomize choices if there is an answer const { choices } = prepareChoices(stim.answer, stim.distractors, randomize); return generateImageChoices(choices); + } else { + return ['OK']; } }, button_html: () => { const stim = trial || taskStore().nextStimulus; - const buttonClass = - stim.trialType === 'instructions' || stim.trialType === 'something-same-1' ? 'primary' : 'image-medium'; - return ``; + const buttonClass = stim.trialType === 'test-dimensions' ? 'image-medium' : 'primary'; + const buttonStyle = stim.trialType !== 'test-dimensions' ? 'margin: 16px' : ''; + + return ``; }, response_ends_trial: () => { const stim = trial || taskStore().nextStimulus; - return !( - stim.trialType === 'test-dimensions' || - (stim.assessmentStage === 'practice_response' && stim.trialType !== 'something-same-1') - ); + + return stim.trialType !== 'test-dimensions'; }, on_load: () => { startTime = performance.now(); const stimulus = trial || taskStore().nextStimulus; - let audioFile = stimulus.audioFile; - if ( - taskStore().heavyInstructions && - stimulus.assessmentStage !== 'practice_response' && - stimulus.trialType !== 'instructions' - ) { - audioFile += '-heavy'; - } + const audioFile = stimulus.audioFile; + const trialType = stimulus.trialType; PageAudioHandler.playAudio(mediaAssets.audio[camelize(audioFile)]); const pageStateHandler = new PageStateHandler(audioFile, true); setupReplayAudio(pageStateHandler); - const buttonContainer = document.getElementById('jspsych-html-multi-response-btngroup') as HTMLDivElement; - buttonContainer.classList.add('lev-response-row'); - buttonContainer.classList.add('multi-4'); + const jspsychButtonContainer = document.getElementById('jspsych-html-multi-response-btngroup') as HTMLDivElement; + jspsychButtonContainer.classList.add('lev-response-row'); + jspsychButtonContainer.classList.add('multi-4'); - const trialType = stimulus.trialType; - const assessmentStage = stimulus.assessmentStage; + if (trialType.includes("something-same")) { + // widen the jspsych container so that the buttons are not squished + const jsPsychHtmlMultiResponseContainer = document.getElementById('jspsych-html-multi-response-stimulus') as HTMLDivElement; + jsPsychHtmlMultiResponseContainer.style.width = '100%'; + jsPsychHtmlMultiResponseContainer.style.display = 'flex'; + jsPsychHtmlMultiResponseContainer.style.justifyContent = 'center'; + + const okButtonContainer = document.getElementById('ok-button-container') as HTMLDivElement; + okButtonContainer.appendChild(jspsychButtonContainer); + } + + if (trialType === 'something-same-2') { + const okButton = document.querySelector('.primary') as HTMLButtonElement; + okButton.disabled = true; + + const responseBtns = Array.from(document.getElementById('img-button-container')?.children as any) as HTMLButtonElement[]; + responseBtns.forEach((card, i) => { + card.addEventListener('click', () => { + const answer = ((card as HTMLButtonElement).children[0] as HTMLImageElement)?.alt; + + if (!card) { + return; + } + + if (card.classList.contains(SELECT_CLASS_NAME)) { + card.classList.remove(SELECT_CLASS_NAME); + selection = null; + selectionIdx = null; + } else { + card.classList.add(SELECT_CLASS_NAME); + selection = answer; + selectionIdx = i; + + responseBtns.forEach((card, j) => { + if (j !== i) { + card.classList.remove(SELECT_CLASS_NAME); + } + }); + } + + if (selection !== null) { + enableOkButton(); + } else { + disableOkButton(); + } + + setTimeout(() => enableBtns(responseBtns), 500); + console.log(selection, selectionIdx); + }); + }); + } // if the task is running in a cypress test, the correct answer should be indicated with 'correct' class if (window.Cypress && trialType !== 'something-same-1') { @@ -224,19 +297,9 @@ export const stimulus = (trial?: StimulusType) => { }); } - if (stimulus.trialType === 'something-same-2' && taskStore().heavyInstructions) { - handleStaggeredButtons(pageStateHandler, buttonContainer, [ - 'same-different-selection-highlight-1', - 'same-different-selection-highlight-2', - ]); - } - - if ( - trialType === 'test-dimensions' || - (assessmentStage === 'practice_response' && trialType !== 'something-same-1') - ) { + if (trialType === 'test-dimensions') { // cards should give feedback during test dimensions block - const practiceBtns = Array.from(buttonContainer.children) + const practiceBtns = Array.from(jspsychButtonContainer.children) .map((btnDiv) => btnDiv.firstChild) .filter((btn) => !!btn) as HTMLButtonElement[]; @@ -284,8 +347,8 @@ export const stimulus = (trial?: StimulusType) => { correct: isCorrect, distractors: stim.distractors, corpusTrialType: stim.trialType, - response: choices[data.button_response], - responseLocation: data.button_response, + response: selection, + responseLocation: selectionIdx, itemUid: stim.itemUid, audioFile: stim.audioFile, corpus: taskStore().corpus, diff --git a/task-launcher/src/tasks/shared/helpers/config.ts b/task-launcher/src/tasks/shared/helpers/config.ts index a5cc5f2e4..e0d66c1e4 100644 --- a/task-launcher/src/tasks/shared/helpers/config.ts +++ b/task-launcher/src/tasks/shared/helpers/config.ts @@ -81,7 +81,6 @@ export const setSharedConfig = async ( numberOfTrials, taskName, stimulusBlocks, - numOfPracticeTrials, maxIncorrect, keyHelpers, age, @@ -108,7 +107,6 @@ export const setSharedConfig = async ( numberOfTrials: numberOfTrials ?? 300, task: taskName ?? 'egma-math', stimulusBlocks: Number(stimulusBlocks) || 3, - numOfPracticeTrials: Number(numOfPracticeTrials) || 2, maxIncorrect: Number(maxIncorrect) || 3, keyHelpers: !!keyHelpers, language: language ?? i18next.language, diff --git a/task-launcher/src/tasks/shared/helpers/disableOkButton.ts b/task-launcher/src/tasks/shared/helpers/disableOkButton.ts new file mode 100644 index 000000000..09521ed50 --- /dev/null +++ b/task-launcher/src/tasks/shared/helpers/disableOkButton.ts @@ -0,0 +1,6 @@ +export function disableOkButton() { + const okButton: HTMLButtonElement | null = document.querySelector('.primary'); + if (okButton != null) { + okButton.disabled = true; + } +} \ No newline at end of file diff --git a/task-launcher/src/tasks/shared/helpers/getCorpus.ts b/task-launcher/src/tasks/shared/helpers/getCorpus.ts index dea16c4b7..5751ff8ba 100644 --- a/task-launcher/src/tasks/shared/helpers/getCorpus.ts +++ b/task-launcher/src/tasks/shared/helpers/getCorpus.ts @@ -50,15 +50,6 @@ type ParsedRowType = { downex?: string; }; -export const sdsPhaseCount = { - block1: 0, - block2: 0, - block3: 0, - block4: 0, - block5: 0, - block6: 0, -}; - let totalTrials = 0; let totalDownexTrials = 0; @@ -80,22 +71,12 @@ function containsLettersOrSlash(str: string) { const transformCSV = ( csvInput: ParsedRowType[], - numOfPracticeTrials: number, sequentialStimulus: boolean, task: string, ) => { let currTrialTypeBlock = ''; let currPracticeAmount = 0; - // Phase 1 is test-dimensions and something-same - // Phase 2 is match and unique - subphases are either matching or test-dimensions - let sdsBlock1Count = 0; - let sdsBlock2Count = 0; - let sdsBlock3Count = 0; - let sdsBlock4Count = 0; - let sdsBlock5Count = 0; - let sdsBlock6Count = 0; - csvInput.forEach((row) => { // Leaving this here for quick testing of a certain type of trial // if (!row.trial_type.includes('Number Line')) return; @@ -147,28 +128,6 @@ const transformCSV = ( if (row.task === 'same-different-selection') { newRow.requiredSelections = parseInt(row.required_selections); - newRow.blockIndex = parseInt(row.block_index || ''); - // all instructions are part of phase 1 - if (newRow.blockIndex == 1) { - sdsBlock1Count += 1; - } else if (newRow.blockIndex == 2) { - sdsBlock2Count += 1; - } else if (newRow.blockIndex == 3) { - sdsBlock3Count += 1; - } else if (newRow.blockIndex == 4) { - sdsBlock4Count += 1; - } else if (newRow.blockIndex == 5) { - sdsBlock5Count += 1; - } else { - sdsBlock6Count += 1; - } - - sdsPhaseCount.block1 = sdsBlock1Count; - sdsPhaseCount.block2 = sdsBlock2Count; - sdsPhaseCount.block3 = sdsBlock3Count; - sdsPhaseCount.block4 = sdsBlock4Count; - sdsPhaseCount.block5 = sdsBlock5Count; - sdsPhaseCount.block6 = sdsBlock6Count; } let currentTrialType = newRow.trialType; @@ -179,32 +138,12 @@ const transformCSV = ( if (newRow.downex) { // Add to downex corpus - if (newRow.assessmentStage === 'practice_response') { - if (currPracticeAmount < numOfPracticeTrials) { - // Only push in the specified amount of practice trials - currPracticeAmount += 1; - downexData.push(newRow); - totalDownexTrials += 1; - } // else skip extra practice - } else { - // instruction and stimulus - downexData.push(newRow); - totalDownexTrials += 1; - } + downexData.push(newRow); + totalDownexTrials += 1; } else { // Add to stimulus corpus - if (newRow.assessmentStage === 'practice_response') { - if (currPracticeAmount < numOfPracticeTrials) { - // Only push in the specified amount of practice trials - currPracticeAmount += 1; - stimulusData.push(newRow); - totalTrials += 1; - } // else skip extra practice - } else { - // instruction and stimulus - stimulusData.push(newRow); - totalTrials += 1; - } + stimulusData.push(newRow); + totalTrials += 1; } }); @@ -225,7 +164,7 @@ const transformCSV = ( }; export const getCorpus = async (config: Record, isDev: boolean) => { - const { corpus, task, sequentialStimulus, numOfPracticeTrials } = config; + const { corpus, task, sequentialStimulus } = config; const bucketName = getBucketName(task, isDev, 'corpus'); @@ -238,7 +177,7 @@ export const getCorpus = async (config: Record, isDev: boolean) => header: true, skipEmptyLines: true, complete: function (results) { - transformCSV(results.data, numOfPracticeTrials, sequentialStimulus, task); + transformCSV(results.data, sequentialStimulus, task); resolve(results.data); }, error: function (error) { diff --git a/task-launcher/src/tasks/shared/helpers/index.ts b/task-launcher/src/tasks/shared/helpers/index.ts index c1cf8622d..bd171b187 100644 --- a/task-launcher/src/tasks/shared/helpers/index.ts +++ b/task-launcher/src/tasks/shared/helpers/index.ts @@ -43,4 +43,5 @@ export { getAssetsPerTask } from './getAssetsPerTask'; export { getChildSurveyResponses } from './childSurveyResponses'; export { equalizeButtonSizes } from './equalizeButtonSizes'; export { enableOkButton } from './enableOkButton'; +export { disableOkButton } from './disableOkButton'; export { popAnimation, triggerAnimation, matrixDragAnimation } from './animateImages'; From 53267d28ae423f4a8ff7ed8339e62f80ade4b907 Mon Sep 17 00:00:00 2001 From: Zach Watson Date: Mon, 26 Jan 2026 10:58:38 -0800 Subject: [PATCH 2/5] finish redesign of default and CAT SDS --- task-launcher/serve/serve.js | 2 + .../src/styles/layout/_containers.scss | 2 +- .../same-different-selection/catTimeline.ts | 23 +++++- .../same-different-selection/timeline.ts | 3 - .../trials/afcMatch.ts | 44 +++++----- .../trials/stimulus.ts | 82 ++++++++++++++----- .../src/tasks/shared/helpers/config.ts | 2 + 7 files changed, 112 insertions(+), 46 deletions(-) diff --git a/task-launcher/serve/serve.js b/task-launcher/serve/serve.js index 9edf8635c..86869809c 100644 --- a/task-launcher/serve/serve.js +++ b/task-launcher/serve/serve.js @@ -35,6 +35,7 @@ const urlParams = new URLSearchParams(queryString); const taskName = urlParams.get('task') ?? 'egma-math'; const corpus = urlParams.get('corpus'); const buttonLayout = urlParams.get('buttonLayout'); +const numOfPracticeTrials = urlParams.get('practiceTrials'); const numberOfTrials = urlParams.get('trials') === null ? null : parseInt(urlParams.get('trials'), 10); const maxIncorrect = urlParams.get('maxIncorrect') === null ? null : parseInt(urlParams.get('maxIncorrect'), 10); const stimulusBlocks = urlParams.get('blocks') === null ? null : parseInt(urlParams.get('blocks'), 10); @@ -79,6 +80,7 @@ async function startWebApp() { sequentialStimulus, corpus, buttonLayout, + numOfPracticeTrials, numberOfTrials, maxIncorrect, stimulusBlocks, diff --git a/task-launcher/src/styles/layout/_containers.scss b/task-launcher/src/styles/layout/_containers.scss index b86724849..dcc48a10a 100644 --- a/task-launcher/src/styles/layout/_containers.scss +++ b/task-launcher/src/styles/layout/_containers.scss @@ -300,5 +300,5 @@ justify-content: center; align-items: center; width: 100%; - gap: 16px; + gap: 10vw; } diff --git a/task-launcher/src/tasks/same-different-selection/catTimeline.ts b/task-launcher/src/tasks/same-different-selection/catTimeline.ts index 0f33bac50..ec30100bf 100644 --- a/task-launcher/src/tasks/same-different-selection/catTimeline.ts +++ b/task-launcher/src/tasks/same-different-selection/catTimeline.ts @@ -12,6 +12,7 @@ import { afcMatch } from './trials/afcMatch'; import { enterFullscreen, exitFullscreen, + feedback, finishExperiment, fixationOnly, getAudioResponse, @@ -67,11 +68,21 @@ export default function buildSameDifferentTimelineCat(config: Record { + const trialGenerator = trial.trialType.includes('match') ? afcMatch : stimulus; + const practice = trial.assessmentStage === 'practice_response'; + const timeline = practice && !trial.trialType.includes('something-same-1') ? + [{ ...fixationOnly, stimulus: '' }, trialGenerator(trial), feedbackBlock] : + [{ ...fixationOnly, stimulus: '' }, trialGenerator(trial)]; + return { - timeline: [{ ...fixationOnly, stimulus: '' }, stimulus(trial)], + timeline: timeline, }; }; + const feedbackBlock = { + timeline: [feedback(true, 'feedbackCorrect', 'feedbackNotQuiteRight')] + }; + // returns timeline object containing the appropriate trials - only runs if they match what is in taskStore function runCatTrials(trialNum: number, trialType: 'stimulus' | 'afc') { const timeline = []; @@ -80,7 +91,7 @@ export default function buildSameDifferentTimelineCat(config: Record trial.blockIndex == blockNum); + return instructionPractice.filter((trial) => { + if (trial.block_index == undefined) return; + + return parseInt(trial.block_index) == blockNum; + }); } // create list of numbers of trials per block - const blockCountList = setTrialBlock(true); + const blockCountList = setTrialBlock(true).blockCountList; const totalRealTrials = blockCountList.reduce((acc, total) => acc + total, 0); taskStore('totalTestTrials', totalRealTrials); diff --git a/task-launcher/src/tasks/same-different-selection/timeline.ts b/task-launcher/src/tasks/same-different-selection/timeline.ts index 6efd4ccd4..cd4006e35 100644 --- a/task-launcher/src/tasks/same-different-selection/timeline.ts +++ b/task-launcher/src/tasks/same-different-selection/timeline.ts @@ -27,11 +27,9 @@ export default function buildSameDifferentTimeline(config: Record, let corpus: StimulusType[] = taskStore().corpora.stimulus; - console.log(corpus.filter((trial) => trial.assessmentStage === 'practice_response')); // organize corpus into batches for preloading const batchSize = 25; const batchedCorpus = batchTrials(corpus, batchSize); - console.log(batchedCorpus); const batchedMediaAssets = batchMediaAssets(mediaAssets, batchedCorpus, ['image', 'answer', 'distractors']); const initialMediaAssets = getLeftoverAssets(batchedMediaAssets, mediaAssets); @@ -87,7 +85,6 @@ export default function buildSameDifferentTimeline(config: Record, // function to preload assets in batches at the beginning of each task block function preloadBlock() { - console.log(createPreloadTrials(batchedMediaAssets[currPreloadBatch]).default); timeline.push(createPreloadTrials(batchedMediaAssets[currPreloadBatch]).default); currPreloadBatch++; } 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 1641f225c..9a5cb7e27 100644 --- a/task-launcher/src/tasks/same-different-selection/trials/afcMatch.ts +++ b/task-launcher/src/tasks/same-different-selection/trials/afcMatch.ts @@ -34,10 +34,11 @@ function enableBtns(btnElements: HTMLButtonElement[]) { btnElements.forEach((btn) => btn.removeAttribute('disabled')); } -export const afcMatch = { +export const afcMatch = (trial?: StimulusType) => { + return { type: jsPsychAudioMultiResponse, data: () => { - const stim = taskStore().nextStimulus; + const stim = trial || taskStore().nextStimulus; let isPracticeTrial = stim.assessmentStage === 'practice_response'; return { save_trial: stim.trialType !== 'instructions', @@ -50,7 +51,7 @@ export const afcMatch = { return mediaAssets.audio.nullAudio; }, prompt: () => { - const stimulus = taskStore().nextStimulus; + const stimulus = trial || taskStore().nextStimulus; const prompt = camelize(stimulus.audioFile); const t = taskStore().translations; @@ -68,7 +69,7 @@ export const afcMatch = { }, prompt_above_buttons: true, button_choices: () => { - const stim = taskStore().nextStimulus; + const stim = trial || taskStore().nextStimulus; if (stim.assessmentStage === 'instructions') { return ['OK']; } else { @@ -79,7 +80,7 @@ export const afcMatch = { } }, button_html: () => { - const stim = taskStore().nextStimulus; + const stim = trial || taskStore().nextStimulus; const buttonClass = stim.assessmentStage === 'instructions' ? 'primary' : 'image-medium'; return ``; }, @@ -88,7 +89,7 @@ export const afcMatch = { // on click they will be selected // can select multiple cards and deselect them startTime = performance.now(); - const stim = taskStore().nextStimulus; + const stim = trial || taskStore().nextStimulus; const audioFile = stim.audioFile; const audioConfig: AudioConfigType = { @@ -105,17 +106,18 @@ export const afcMatch = { const buttonContainer = document.getElementById('jspsych-audio-multi-response-btngroup') as HTMLDivElement; // Add primary OK button under the other buttons - const okButton = document.createElement('button'); - okButton.className = 'primary'; - okButton.textContent = 'OK'; - okButton.style.marginTop = '16px'; - okButton.disabled = true; - okButton.addEventListener('click', () => { - jsPsych.finishTrial(); - }); - buttonContainer.parentNode?.insertBefore(okButton, buttonContainer.nextSibling); + if (stim.trialType !== 'instructions') { + const okButton = document.createElement('button'); + okButton.className = 'primary'; + okButton.textContent = 'OK'; + okButton.style.marginTop = '16px'; + okButton.disabled = true; + okButton.addEventListener('click', () => { + jsPsych.finishTrial(); + }); + buttonContainer.parentNode?.insertBefore(okButton, buttonContainer.nextSibling); - const responseBtns = Array.from(buttonContainer.children) + const responseBtns = Array.from(buttonContainer.children) .map((btnDiv) => btnDiv.firstChild as HTMLButtonElement) .filter((btn) => !!btn); if (responseBtns.length === 5) { @@ -152,10 +154,13 @@ export const afcMatch = { setTimeout(() => enableBtns(responseBtns), 500); }), ); + } + }, + response_ends_trial: () => { + return (trial || taskStore().nextStimulus).trialType === 'instructions'; }, - response_ends_trial: false, on_finish: () => { - const stim = taskStore().nextStimulus; + const stim = trial || taskStore().nextStimulus; const cat = taskStore().runCat; const endTime = performance.now(); @@ -323,5 +328,6 @@ export const afcMatch = { taskStore('sequentialTrials', newSequentialTrials); } } - }, + }, + }; }; 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 802191230..efa643a94 100644 --- a/task-launcher/src/tasks/same-different-selection/trials/stimulus.ts +++ b/task-launcher/src/tasks/same-different-selection/trials/stimulus.ts @@ -21,6 +21,7 @@ let incorrectPracticeResponses: string[] = []; let startTime: number; let selection: string | null = null; let selectionIdx: number | null = null; +let currentTrialId: string = ''; // used to prevent audio from overlapping between trials const SELECT_CLASS_NAME = 'info-shadow'; @@ -65,11 +66,9 @@ function getSomethingSameHtml(stim: StimulusType) { const rightPromptHtml = stim.trialType === "something-same-2" ? `

${t[camelize(stim.audioFile[1])]}

` : `

${t[camelize(stim.audioFile as string)]}

`; const leftImageHtml = ` - ${stim.trialType == "something-same-1" ? `
` : `
`} - -
+ `; // randomize choices if there is an answer @@ -101,11 +100,11 @@ function getSomethingSameHtml(stim: StimulusType) {
${stim.trialType === "something-same-2" ? - `
` : + `
` : ` -
+

${rightPromptHtml}

@@ -226,7 +225,32 @@ export const stimulus = (trial?: StimulusType) => { const audioFile = stimulus.audioFile; const trialType = stimulus.trialType; - PageAudioHandler.playAudio(mediaAssets.audio[camelize(audioFile)]); + currentTrialId = stimulus.itemId; + + if (trialType === 'something-same-2') { + // something-same-2 trials have multiple audio files + const audioFiles = audioFile as string[]; + + const audioConfig: AudioConfigType = { + restrictRepetition: { + enabled: false, + maxRepetitions: 2, + }, + onEnded: () => { + if (currentTrialId !== stimulus.itemId) { + return; + } + + if (audioFiles.length) { + PageAudioHandler.playAudio(mediaAssets.audio[camelize(audioFiles.shift() as string)], audioConfig); + } + }, + } + + PageAudioHandler.playAudio(mediaAssets.audio[camelize(audioFiles.shift() as string)], audioConfig); + } else { + PageAudioHandler.playAudio(mediaAssets.audio[camelize(audioFile)]); + } const pageStateHandler = new PageStateHandler(audioFile, true); setupReplayAudio(pageStateHandler); @@ -246,6 +270,18 @@ export const stimulus = (trial?: StimulusType) => { } if (trialType === 'something-same-2') { + const leftPrompt = document.getElementById('left-prompt') as HTMLParagraphElement; + const rightPrompt = document.getElementById('right-prompt') as HTMLParagraphElement; + + // equalize prompt box heights + if (leftPrompt && rightPrompt) { + const styles = getComputedStyle(rightPrompt); + const paddingY = parseFloat(styles.paddingTop) + parseFloat(styles.paddingBottom); + + const contentBoxHeight = rightPrompt.clientHeight - paddingY; + leftPrompt.style.height = `${contentBoxHeight}px`; + } + const okButton = document.querySelector('.primary') as HTMLButtonElement; okButton.disabled = true; @@ -281,7 +317,6 @@ export const stimulus = (trial?: StimulusType) => { } setTimeout(() => enableBtns(responseBtns), 500); - console.log(selection, selectionIdx); }); }); } @@ -313,12 +348,15 @@ export const stimulus = (trial?: StimulusType) => { } }, on_finish: (data: any) => { + PageAudioHandler.stopAndDisconnectNode(); + currentTrialId = ''; + const stim = trial || taskStore().nextStimulus; const choices = taskStore().choices; const endTime = performance.now(); const cat = taskStore().runCat; - PageAudioHandler.stopAndDisconnectNode(); + jsPsych.data.addDataToLastTrial({ audioButtonPresses: PageAudioHandler.replayPresses, }); @@ -326,20 +364,26 @@ export const stimulus = (trial?: StimulusType) => { // TODO: Discuss with ROAR team to remove this check if (stim.assessmentStage !== 'instructions') { let isCorrect; - if (stim.trialType === 'test-dimensions' || stim.assessmentStage === 'practice_response') { + if (stim.trialType === 'test-dimensions') { // if no incorrect answers were clicked, that trial is correct isCorrect = incorrectPracticeResponses.length === 0; } else { - isCorrect = data.button_response === taskStore().correctResponseIdx; + isCorrect = selectionIdx === taskStore().correctResponseIdx; } + incorrectPracticeResponses = []; - // update task store - taskStore('isCorrect', isCorrect); - if (isCorrect === false) { - taskStore.transact('numIncorrect', (oldVal: number) => oldVal + 1); - } else { - taskStore('numIncorrect', 0); + + // don't update task store for something-same-1 trials + if (stim.trialType !== 'something-same-1') { + // update task store + taskStore('isCorrect', isCorrect); + if (isCorrect === false) { + taskStore.transact('numIncorrect', (oldVal: number) => oldVal + 1); + } else { + taskStore('numIncorrect', 0); + } } + jsPsych.data.addDataToLastTrial({ // specific to this trial item: stim.item, @@ -371,7 +415,7 @@ export const stimulus = (trial?: StimulusType) => { taskStore.transact('testTrialCount', (oldVal: number) => oldVal + 1); } // if heavy instructions is true, show data quality screen before ending - if (taskStore().numIncorrect >= taskStore().maxIncorrect && !taskStore().heavyInstructions) { + if (taskStore().numIncorrect >= taskStore().maxIncorrect && !taskStore().heavyInstructions && !cat) { finishExperiment(); } diff --git a/task-launcher/src/tasks/shared/helpers/config.ts b/task-launcher/src/tasks/shared/helpers/config.ts index e0d66c1e4..a5cc5f2e4 100644 --- a/task-launcher/src/tasks/shared/helpers/config.ts +++ b/task-launcher/src/tasks/shared/helpers/config.ts @@ -81,6 +81,7 @@ export const setSharedConfig = async ( numberOfTrials, taskName, stimulusBlocks, + numOfPracticeTrials, maxIncorrect, keyHelpers, age, @@ -107,6 +108,7 @@ export const setSharedConfig = async ( numberOfTrials: numberOfTrials ?? 300, task: taskName ?? 'egma-math', stimulusBlocks: Number(stimulusBlocks) || 3, + numOfPracticeTrials: Number(numOfPracticeTrials) || 2, maxIncorrect: Number(maxIncorrect) || 3, keyHelpers: !!keyHelpers, language: language ?? i18next.language, From b68f37374627fa6e5e6756f92ab4fb6f4932a1f6 Mon Sep 17 00:00:00 2001 From: Zach Watson Date: Wed, 28 Jan 2026 09:01:16 -0800 Subject: [PATCH 3/5] update age-dependent logic --- .../same-different-selection/catTimeline.ts | 20 ++----------- .../same-different-selection/timeline.ts | 30 +++++++++++++++---- 2 files changed, 28 insertions(+), 22 deletions(-) diff --git a/task-launcher/src/tasks/same-different-selection/catTimeline.ts b/task-launcher/src/tasks/same-different-selection/catTimeline.ts index ec30100bf..b74c0a31f 100644 --- a/task-launcher/src/tasks/same-different-selection/catTimeline.ts +++ b/task-launcher/src/tasks/same-different-selection/catTimeline.ts @@ -140,27 +140,13 @@ export default function buildSameDifferentTimelineCat(config: Record { const currentBlockInstructionPractice = getPracticeInstructions(index); - // push in instruction + practice trials - if (index === 1 && heavy) { - // something's the same block has demo trials in between instructions - const firstInstruction = currentBlockInstructionPractice.shift(); - if (firstInstruction != undefined) { - timeline.push(ipBlock(firstInstruction)); - } - - timeline.push(somethingSameDemo1); - timeline.push(somethingSameDemo2); - timeline.push(somethingSameDemo3); - } - currentBlockInstructionPractice.forEach((trial) => { timeline.push(ipBlock(trial)); }); - if (index === 2 && heavy) { - // 2-match has demo trials after instructions - timeline.push(matchDemo1); - timeline.push(matchDemo2); + // only younger kids get something-same blocks + if (!heavy && index === 1) { + return; } const numOfTrials = index === 0 ? count : count / 2; // change this based on simulation results? diff --git a/task-launcher/src/tasks/same-different-selection/timeline.ts b/task-launcher/src/tasks/same-different-selection/timeline.ts index cd4006e35..60fa30174 100644 --- a/task-launcher/src/tasks/same-different-selection/timeline.ts +++ b/task-launcher/src/tasks/same-different-selection/timeline.ts @@ -1,20 +1,19 @@ // setup import 'regenerator-runtime/runtime'; import { jsPsych } from '../taskSetup'; -import { initTrialSaving, initTimeline, createPreloadTrials, filterMedia, batchMediaAssets } from '../shared/helpers'; -import { prepareMultiBlockCat } from '../shared/helpers/prepareCat'; +import { initTrialSaving, initTimeline, createPreloadTrials, batchMediaAssets } from '../shared/helpers'; import { initializeCat } from '../taskSetup'; // trials import { dataQualityScreen } from '../shared/trials/dataQuality'; import { setupStimulus, - fixationOnly, exitFullscreen, taskFinished, getAudioResponse, enterFullscreen, finishExperiment, practiceTransition, + feedback, } from '../shared/trials'; import { afcMatch } from './trials/afcMatch'; import { stimulus } from './trials/stimulus'; @@ -27,6 +26,17 @@ export default function buildSameDifferentTimeline(config: Record, let corpus: StimulusType[] = taskStore().corpora.stimulus; + if (!heavy) { + corpus = corpus.filter((trial) => { + return !(trial.trialType.includes('something-same') && !(trial.assessmentStage === 'practice_response')); + }); + } + + taskStore('corpora', { + practice: taskStore().corpora.practice, + stimulus: corpus, + }); + // organize corpus into batches for preloading const batchSize = 25; const batchedCorpus = batchTrials(corpus, batchSize); @@ -59,12 +69,22 @@ export default function buildSameDifferentTimeline(config: Record, }, }; + const feedbackBlock = { + timeline: [feedback(true, 'feedbackCorrect', 'feedbackNotQuiteRight')], + conditional_function: () => { + return ( + taskStore().nextStimulus.assessmentStage === 'practice_response' && + !taskStore().nextStimulus.trialType.includes('something-same-1') + ); + }, + }; + const stimulusBlock = { - timeline: [stimulus()], + timeline: [stimulus(), feedbackBlock], }; const afcBlock = { - timeline: [afcMatch], + timeline: [afcMatch(), feedbackBlock], }; const dataQualityBlock = { From 36f7b5f84cb2bce7d14e0bd0fef311f652e59bf9 Mon Sep 17 00:00:00 2001 From: Zach Watson Date: Wed, 18 Feb 2026 18:21:54 -0800 Subject: [PATCH 4/5] add feature flag for new sds as url param --- task-launcher/serve/serve.js | 2 + task-launcher/src/taskStore/index.ts | 3 + .../same-different-selection/catTimeline.ts | 29 +- .../same-different-selection/timeline.ts | 21 +- .../trials/afcMatch.ts | 37 +- .../trials/legacyStimulus.ts | 337 ++++++++++++++++++ .../src/tasks/shared/helpers/config.ts | 2 + .../src/tasks/shared/helpers/getCorpus.ts | 11 +- .../src/tasks/shared/helpers/prepareCat.ts | 14 +- 9 files changed, 414 insertions(+), 42 deletions(-) create mode 100644 task-launcher/src/tasks/same-different-selection/trials/legacyStimulus.ts diff --git a/task-launcher/serve/serve.js b/task-launcher/serve/serve.js index ac879bf27..486ea70e5 100644 --- a/task-launcher/serve/serve.js +++ b/task-launcher/serve/serve.js @@ -56,6 +56,7 @@ const sequentialStimulus = stringToBoolean(urlParams.get('sequentialStimulus'), const storeItemId = stringToBoolean(urlParams.get('storeItemId'), false); const cat = stringToBoolean(urlParams.get('cat'), false); const heavyInstructions = stringToBoolean(urlParams.get('heavyInstructions'), false); +const newSds = stringToBoolean(urlParams.get('newSds'), false); const emulatorConfig = EMULATORS ? firebaseJSON.emulators : undefined; // if running in demo mode, no data will be saved to Firestore @@ -97,6 +98,7 @@ async function startWebApp() { startingTheta, heavyInstructions, demoMode, + newSds, }; const taskInfo = { diff --git a/task-launcher/src/taskStore/index.ts b/task-launcher/src/taskStore/index.ts index 871a450f3..e0b0aefa7 100644 --- a/task-launcher/src/taskStore/index.ts +++ b/task-launcher/src/taskStore/index.ts @@ -56,6 +56,7 @@ import store from 'store2'; * @property {Array} previousChoices - Array containing previously randomized order of choices for the current block. * ------- SDS only ------- * @property {StimulusType[]} sequentialTrials - Should be run sequentially in blocks by trial number in an SDS CAT. + * @property {boolean} newSds - Temporary parameter to use the new version SDS, default is false. */ export type TaskStoreDataType = { @@ -81,6 +82,7 @@ export type TaskStoreDataType = { language?: string; maxTime?: number; demoMode: boolean; + newSds: boolean; }; /** @@ -128,6 +130,7 @@ export const setTaskStore = (config: TaskStoreDataType) => { testPhase: false, maxTime: config.maxTime, demoMode: config.demoMode, + newSds: config.newSds, }); }; diff --git a/task-launcher/src/tasks/same-different-selection/catTimeline.ts b/task-launcher/src/tasks/same-different-selection/catTimeline.ts index b74c0a31f..2872c62c9 100644 --- a/task-launcher/src/tasks/same-different-selection/catTimeline.ts +++ b/task-launcher/src/tasks/same-different-selection/catTimeline.ts @@ -20,22 +20,16 @@ import { taskFinished, } from '../shared/trials'; import { setTrialBlock } from './helpers/setTrialBlock'; -import { - matchDemo1, - matchDemo2, - somethingSameDemo1, - somethingSameDemo2, - somethingSameDemo3, -} from './trials/heavyInstructions'; import { dataQualityScreen } from '../shared/trials/dataQuality'; import { initializeCat, jsPsych } from '../taskSetup'; +import { legacyStimulus } from './trials/legacyStimulus'; export default function buildSameDifferentTimelineCat(config: Record, mediaAssets: MediaAssetsType) { const preloadTrials = createPreloadTrials(mediaAssets).default; const heavy: boolean = taskStore().heavyInstructions; const corpus: StimulusType[] = taskStore().corpora.stimulus; - const preparedCorpus = prepareCorpus(corpus); + const preparedCorpus = prepareCorpus(corpus, true); const catCorpus = setupSds(taskStore().corpora.stimulus); const allBlocks = prepareMultiBlockCat(catCorpus); @@ -68,7 +62,15 @@ export default function buildSameDifferentTimelineCat(config: Record { - const trialGenerator = trial.trialType.includes('match') ? afcMatch : stimulus; + let trialGenerator; + if (trial.trialType.includes('match')) { + trialGenerator = afcMatch; + } else if (taskStore().newSds) { + trialGenerator = stimulus; + } else { + trialGenerator = legacyStimulus; + } + const practice = trial.assessmentStage === 'practice_response'; const timeline = practice && !trial.trialType.includes('something-same-1') ? [{ ...fixationOnly, stimulus: '' }, trialGenerator(trial), feedbackBlock] : @@ -80,7 +82,10 @@ export default function buildSameDifferentTimelineCat(config: Record { + return taskStore().newSds; + }, }; // returns timeline object containing the appropriate trials - only runs if they match what is in taskStore @@ -88,7 +93,7 @@ export default function buildSameDifferentTimelineCat(config: Record, let corpus: StimulusType[] = taskStore().corpora.stimulus; - if (!heavy) { + if (!heavy && taskStore().newSds) { corpus = corpus.filter((trial) => { return !(trial.trialType.includes('something-same') && !(trial.assessmentStage === 'practice_response')); }); - } - taskStore('corpora', { - practice: taskStore().corpora.practice, - stimulus: corpus, - }); + taskStore('corpora', { + practice: taskStore().corpora.practice, + stimulus: corpus, + }); + } // organize corpus into batches for preloading const batchSize = 25; @@ -74,13 +75,14 @@ export default function buildSameDifferentTimeline(config: Record, conditional_function: () => { return ( taskStore().nextStimulus.assessmentStage === 'practice_response' && - !taskStore().nextStimulus.trialType.includes('something-same-1') + !taskStore().nextStimulus.trialType.includes('something-same-1') && + taskStore().newSds ); }, }; const stimulusBlock = { - timeline: [stimulus(), feedbackBlock], + timeline: [(taskStore().newSds ? stimulus() : legacyStimulus()), feedbackBlock], }; const afcBlock = { @@ -94,8 +96,9 @@ export default function buildSameDifferentTimeline(config: Record, }, }; + // create list of numbers of trials per block - const {blockCountList, blockOperations} = setTrialBlock(false); + const {blockCountList, blockOperations} = setTrialBlock(false); const totalRealTrials = blockCountList.reduce((acc, total) => acc + total, 0); taskStore('totalTestTrials', totalRealTrials); 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 9a5cb7e27..63e9b8246 100644 --- a/task-launcher/src/tasks/same-different-selection/trials/afcMatch.ts +++ b/task-launcher/src/tasks/same-different-selection/trials/afcMatch.ts @@ -107,15 +107,17 @@ export const afcMatch = (trial?: StimulusType) => { // Add primary OK button under the other buttons if (stim.trialType !== 'instructions') { - const okButton = document.createElement('button'); - okButton.className = 'primary'; - okButton.textContent = 'OK'; - okButton.style.marginTop = '16px'; - okButton.disabled = true; - okButton.addEventListener('click', () => { - jsPsych.finishTrial(); - }); - buttonContainer.parentNode?.insertBefore(okButton, buttonContainer.nextSibling); + if (taskStore().newSds) { + const okButton = document.createElement('button'); + okButton.className = 'primary'; + okButton.textContent = 'OK'; + okButton.style.marginTop = '16px'; + okButton.disabled = true; + okButton.addEventListener('click', () => { + jsPsych.finishTrial(); + }); + buttonContainer.parentNode?.insertBefore(okButton, buttonContainer.nextSibling); + } const responseBtns = Array.from(buttonContainer.children) .map((btnDiv) => btnDiv.firstChild as HTMLButtonElement) @@ -145,11 +147,20 @@ export const afcMatch = (trial?: StimulusType) => { selectedCardIdxs.push(i); } - if (selectedCards.length === stim.requiredSelections) { - enableOkButton(); + if (taskStore().newSds) { + if (selectedCards.length === stim.requiredSelections) { + enableOkButton(); + } else { + disableOkButton(); + } } else { - disableOkButton(); + const requiredSelections = stim.requiredSelections; + + if (selectedCards.length === requiredSelections) { + setTimeout(() => jsPsych.finishTrial(), 500); + } } + setTimeout(() => enableBtns(responseBtns), 500); }), @@ -157,7 +168,7 @@ export const afcMatch = (trial?: StimulusType) => { } }, response_ends_trial: () => { - return (trial || taskStore().nextStimulus).trialType === 'instructions'; + return (trial || taskStore().nextStimulus).trialType === 'instructions' && taskStore().newSds; }, on_finish: () => { const stim = trial || taskStore().nextStimulus; diff --git a/task-launcher/src/tasks/same-different-selection/trials/legacyStimulus.ts b/task-launcher/src/tasks/same-different-selection/trials/legacyStimulus.ts new file mode 100644 index 000000000..c9121f2d1 --- /dev/null +++ b/task-launcher/src/tasks/same-different-selection/trials/legacyStimulus.ts @@ -0,0 +1,337 @@ +import jsPsychHtmlMultiResponse from '@jspsych-contrib/plugin-html-multi-response'; +import { mediaAssets } from '../../..'; +import { + PageStateHandler, + prepareChoices, + replayButtonSvg, + setupReplayAudio, + PageAudioHandler, + camelize, +} from '../../shared/helpers'; +import { finishExperiment } from '../../shared/trials'; +import { isTouchScreen, jsPsych } from '../../taskSetup'; +import { taskStore } from '../../../taskStore'; +import { handleStaggeredButtons } from '../../shared/helpers/staggerButtons'; +import { updateTheta } from '../../shared/helpers'; +import { setNextCatTrial } from '../helpers/setNextCatTrial'; + +const replayButtonHtmlId = 'replay-btn-revisited'; +let incorrectPracticeResponses: string[] = []; +let startTime: number; + +export const generateImageChoices = (choices: string[]) => { + return choices.map((choice) => { + const imageUrl = mediaAssets.images[camelize(choice)]; + return `${choice}`; + }); +}; + +function enableBtns(btnElements: HTMLButtonElement[]) { + btnElements.forEach((btn) => btn.removeAttribute('disabled')); +} + +export function handleButtonFeedback( + btn: HTMLButtonElement, + cards: HTMLButtonElement[], + isKeyBoardResponse: boolean, + responsevalue: number, + correctAudio: string, +) { + const choice = btn?.parentElement?.id || ''; + const answer = taskStore().correctResponseIdx.toString(); + + const isCorrectChoice = choice.includes(answer); + let feedbackAudio; + if (isCorrectChoice) { + btn.classList.add('success-shadow'); + feedbackAudio = mediaAssets.audio[correctAudio]; + } else { + btn.classList.add('error-shadow'); + feedbackAudio = mediaAssets.audio.feedbackTryAgain; + // renable buttons + setTimeout(() => enableBtns(cards), 500); + incorrectPracticeResponses.push(choice); + } + + function finishTrial() { + jsPsych.finishTrial({ + response: choice, + incorrectPracticeResponses, + button_response: !isKeyBoardResponse ? responsevalue : null, + keyboard_response: isKeyBoardResponse ? responsevalue : null, + }); + } + + const correctAudioConfig: AudioConfigType = { + restrictRepetition: { + enabled: true, + maxRepetitions: 2, + }, + onEnded: finishTrial, + }; + + const incorrectAudioConfig: AudioConfigType = { + restrictRepetition: { + enabled: false, + maxRepetitions: 2, + }, + }; + + PageAudioHandler.stopAndDisconnectNode(); // disconnect first to avoid overlap + isCorrectChoice + ? PageAudioHandler.playAudio(feedbackAudio, correctAudioConfig) + : PageAudioHandler.playAudio(feedbackAudio, incorrectAudioConfig); +} + +export const legacyStimulus = (trial?: StimulusType) => { + return { + type: jsPsychHtmlMultiResponse, + data: () => { + const stim = trial || taskStore().nextStimulus; + let isPracticeTrial = stim.assessmentStage === 'practice_response'; + return { + save_trial: stim.assessmentStage !== 'instructions', + assessment_stage: stim.assessmentStage, + // not for firekit + isPracticeTrial: isPracticeTrial, + }; + }, + stimulus: () => { + const stim = trial || taskStore().nextStimulus; + let prompt = camelize(stim.audioFile); + if ( + taskStore().heavyInstructions && + stim.assessmentStage !== 'practice_response' && + stim.trialType !== 'instructions' + ) { + prompt += 'Heavy'; + } + + const t = taskStore().translations; + return `
+ +
+

${t[prompt]}

+
+ + ${ + stim.image && !Array.isArray(stim.image) + ? `
` + : '' + } + + ${ + stim.image && Array.isArray(stim.image) + ? `
+ ${ + stim.trialType === 'something-same-1' + ? ` +
+ +
+ ` + : '' + } +
+ ${(stim.image as string[]) + .map((shape) => { + return ``; + }) + .join('')} +
+
` + : '' + } +
`; + }, + prompt_above_buttons: true, + button_choices: () => { + const stim = trial || taskStore().nextStimulus; + if (stim.trialType === 'instructions' || stim.trialType == 'something-same-1') { + return ['OK']; + } else { + const randomize = !!stim.answer ? 'yes' : 'no'; + // Randomize choices if there is an answer + const { choices } = prepareChoices(stim.answer, stim.distractors, randomize); + return generateImageChoices(choices); + } + }, + button_html: () => { + const stim = trial || taskStore().nextStimulus; + const buttonClass = + stim.trialType === 'instructions' || stim.trialType === 'something-same-1' ? 'primary' : 'image-medium'; + return ``; + }, + response_ends_trial: () => { + const stim = trial || taskStore().nextStimulus; + return !( + stim.trialType === 'test-dimensions' || + (stim.assessmentStage === 'practice_response' && stim.trialType !== 'something-same-1') + ); + }, + on_load: () => { + startTime = performance.now(); + const stimulus = trial || taskStore().nextStimulus; + let audioFile = stimulus.audioFile; + if ( + taskStore().heavyInstructions && + stimulus.assessmentStage !== 'practice_response' && + stimulus.trialType !== 'instructions' + ) { + audioFile += '-heavy'; + } + + PageAudioHandler.playAudio(mediaAssets.audio[camelize(audioFile)]); + + const pageStateHandler = new PageStateHandler(audioFile, true); + setupReplayAudio(pageStateHandler); + const buttonContainer = document.getElementById('jspsych-html-multi-response-btngroup') as HTMLDivElement; + buttonContainer.classList.add('lev-response-row'); + buttonContainer.classList.add('multi-4'); + + const trialType = stimulus.trialType; + const assessmentStage = stimulus.assessmentStage; + + // if the task is running in a cypress test, the correct answer should be indicated with 'correct' class + if (window.Cypress && trialType !== 'something-same-1') { + const responseBtns = document.querySelectorAll('.image-medium'); + responseBtns.forEach((button) => { + const imgAlt = button.querySelector('img')?.getAttribute('alt'); + if (imgAlt === taskStore().nextStimulus.answer) { + button.classList.add('correct'); + } + }); + } + + if (stimulus.trialType === 'something-same-2' && taskStore().heavyInstructions) { + handleStaggeredButtons(pageStateHandler, buttonContainer, [ + 'same-different-selection-highlight-1', + 'same-different-selection-highlight-2', + ]); + } + + if ( + trialType === 'test-dimensions' || + (assessmentStage === 'practice_response' && trialType !== 'something-same-1') + ) { + // cards should give feedback during test dimensions block + const practiceBtns = Array.from(buttonContainer.children) + .map((btnDiv) => btnDiv.firstChild) + .filter((btn) => !!btn) as HTMLButtonElement[]; + + practiceBtns.forEach((card, i) => { + const eventType = isTouchScreen ? 'touchend' : 'click'; + + card.addEventListener(eventType, (e) => { + handleButtonFeedback(card, practiceBtns, false, i, 'feedbackGoodJob'); + }); + }); + } + }, + on_finish: (data: any) => { + const stim = trial || taskStore().nextStimulus; + const choices = taskStore().choices; + const endTime = performance.now(); + const cat = taskStore().runCat; + + PageAudioHandler.stopAndDisconnectNode(); + jsPsych.data.addDataToLastTrial({ + audioButtonPresses: PageAudioHandler.replayPresses, + }); + // Always need to write correct key because of firekit. + // TODO: Discuss with ROAR team to remove this check + if (stim.assessmentStage !== 'instructions') { + let isCorrect; + if (stim.trialType === 'test-dimensions' || stim.assessmentStage === 'practice_response') { + // if no incorrect answers were clicked, that trial is correct + isCorrect = incorrectPracticeResponses.length === 0; + } else { + isCorrect = data.button_response === taskStore().correctResponseIdx; + } + incorrectPracticeResponses = []; + // update task store + taskStore('isCorrect', isCorrect); + if (isCorrect === false) { + taskStore.transact('numIncorrect', (oldVal: number) => oldVal + 1); + } else { + taskStore('numIncorrect', 0); + } + jsPsych.data.addDataToLastTrial({ + // specific to this trial + item: stim.item, + answer: stim.answer, + correct: isCorrect, + distractors: stim.distractors, + corpusTrialType: stim.trialType, + response: choices[data.button_response], + responseLocation: data.button_response, + itemUid: stim.itemUid, + audioFile: stim.audioFile, + corpus: taskStore().corpus, + }); + + if (taskStore().storeItemId) { + jsPsych.data.addDataToLastTrial({ + itemId: stim.itemId, + }); + } + + if (stim.trialType === 'test-dimensions' || stim.assessmentStage === 'practice_response') { + const calculatedRt = Math.round(endTime - startTime); + jsPsych.data.addDataToLastTrial({ + rt: calculatedRt, + }); + } + + if (stim.assessmentStage === 'test_response') { + taskStore.transact('testTrialCount', (oldVal: number) => oldVal + 1); + } + // if heavy instructions is true, show data quality screen before ending + if (taskStore().numIncorrect >= taskStore().maxIncorrect && !taskStore().heavyInstructions) { + finishExperiment(); + } + + if (stim.trialType !== 'something-same-1' && stim.trialType !== 'instructions') { + updateTheta(stim, isCorrect); + } + + if (cat && !(stim.assessmentStage === 'practice_response')) { + setNextCatTrial(stim); + } + } + + if (stim.trialType === 'test-dimensions' || stim.assessmentStage === 'practice_response') { + const calculatedRt = Math.round(endTime - startTime); + + jsPsych.data.addDataToLastTrial({ + rt: calculatedRt, + }); + } + + if (stim.assessmentStage === 'test_response') { + taskStore.transact('testTrialCount', (oldVal: number) => oldVal + 1); + } + }, + }; +}; diff --git a/task-launcher/src/tasks/shared/helpers/config.ts b/task-launcher/src/tasks/shared/helpers/config.ts index 7d0d342c0..fb19e2e34 100644 --- a/task-launcher/src/tasks/shared/helpers/config.ts +++ b/task-launcher/src/tasks/shared/helpers/config.ts @@ -93,6 +93,7 @@ export const setSharedConfig = async ( semThreshold, startingTheta, demoMode, + newSds, } = cleanParams; const config = { @@ -123,6 +124,7 @@ export const setSharedConfig = async ( semThreshold: Number(semThreshold), startingTheta: Number(startingTheta), demoMode: !!demoMode, + newSds: !!newSds, }; // default corpus if nothing is passed in diff --git a/task-launcher/src/tasks/shared/helpers/getCorpus.ts b/task-launcher/src/tasks/shared/helpers/getCorpus.ts index 5751ff8ba..8a5d91f14 100644 --- a/task-launcher/src/tasks/shared/helpers/getCorpus.ts +++ b/task-launcher/src/tasks/shared/helpers/getCorpus.ts @@ -90,7 +90,7 @@ const transformCSV = ( item: writeItem(row), origItemNum: row.orig_item_num, trialType: row.trial_type, - image: row?.image?.includes(',') ? (row.image as string).split(',') : row?.image, + image: row?.image?.includes(',') ? (row.image as string).replace(" ", "").split(',') : row?.image, timeLimit: row.time_limit, answer: _toNumber(row.answer) || row.answer, assessmentStage: row.assessment_stage, @@ -110,11 +110,8 @@ const transformCSV = ( } })(), audioFile: row.audio_file?.includes(',') ? (row.audio_file as string).split(',') : row.audio_file as string, - // difficulty must be undefined for non-instruction/practice trials to avoid running cat - difficulty: - taskStore().runCat || row.trial_type === 'instructions' || row.assessment_stage === 'practice_response' - ? parseFloat(row.d || row.difficulty) - : NaN, + // difficulty must be undefined to avoid running cat + difficulty: taskStore().runCat ? parseFloat(row.d || row.difficulty) : NaN, randomize: row.randomize as 'yes' | 'no' | 'at_block_level', trialNumber: row.trial_num, downex: row.downex?.toUpperCase() === 'TRUE', @@ -168,7 +165,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`; + const corpusUrl = `https://storage.googleapis.com/${bucketName}/${corpus}.csv?alt=media?v=2`; function downloadCSV(url: string) { return new Promise((resolve, reject) => { diff --git a/task-launcher/src/tasks/shared/helpers/prepareCat.ts b/task-launcher/src/tasks/shared/helpers/prepareCat.ts index 9e155d489..7b653fff1 100644 --- a/task-launcher/src/tasks/shared/helpers/prepareCat.ts +++ b/task-launcher/src/tasks/shared/helpers/prepareCat.ts @@ -4,7 +4,7 @@ import { cat } from '../../taskSetup'; import { jsPsych } from '../../taskSetup'; // separates trials from corpus into blocks depending on for heavy/light instructions and CAT -export function prepareCorpus(corpus: StimulusType[]) { +export function prepareCorpus(corpus: StimulusType[], fillInSdsDifficulty: boolean = false) { const excludedTrialTypes = '3D'; // limit random starting items so that their difficulty is less than 0 const maxTrialDifficulty = 0; @@ -29,6 +29,18 @@ export function prepareCorpus(corpus: StimulusType[]) { test: corpus.filter((trial) => !instructionPracticeTrials.includes(trial)), }; + // something same 1 trials inherit difficulty from the corresponding something same 2 trial + if (fillInSdsDifficulty) { + corpusParts.test.forEach((trial, index) => { + if (trial.trialType === 'something-same-1') { + const nextTrial = corpusParts.test[index + 1]; + if (nextTrial.trialType === 'something-same-2') { + trial.difficulty = nextTrial.difficulty; + } + } + }); + } + // separate out normed/unnormed trials const unnormedTrials: StimulusType[] = corpusParts.test.filter( (trial) => trial.difficulty == null || isNaN(Number(trial.difficulty)), From eb19b45eec10aac598bbea2c752f099ef0302794 Mon Sep 17 00:00:00 2001 From: Zach Watson Date: Wed, 25 Feb 2026 14:59:44 -0800 Subject: [PATCH 5/5] replace newSds with taskVersion param for reusability --- task-launcher/serve/serve.js | 5 +++-- task-launcher/src/taskStore/index.ts | 6 +++--- .../src/tasks/same-different-selection/catTimeline.ts | 8 ++++---- .../src/tasks/same-different-selection/timeline.ts | 6 +++--- .../src/tasks/same-different-selection/trials/afcMatch.ts | 6 +++--- task-launcher/src/tasks/shared/helpers/config.ts | 4 ++-- 6 files changed, 18 insertions(+), 17 deletions(-) diff --git a/task-launcher/serve/serve.js b/task-launcher/serve/serve.js index 486ea70e5..588ce66d1 100644 --- a/task-launcher/serve/serve.js +++ b/task-launcher/serve/serve.js @@ -47,6 +47,7 @@ const inferenceNumStories = urlParams.get('inferenceNumStories') === null ? null : parseInt(urlParams.get('inferenceNumStories'), 10); const semThreshold = Number(urlParams.get('semThreshold') || '0'); const startingTheta = Number(urlParams.get('theta') || '0'); +const taskVersion = urlParams.get('taskVersion') === null ? null : parseInt(urlParams.get('taskVersion'), 10); // Boolean parameters const keyHelpers = stringToBoolean(urlParams.get('keyHelpers')); @@ -56,7 +57,7 @@ const sequentialStimulus = stringToBoolean(urlParams.get('sequentialStimulus'), const storeItemId = stringToBoolean(urlParams.get('storeItemId'), false); const cat = stringToBoolean(urlParams.get('cat'), false); const heavyInstructions = stringToBoolean(urlParams.get('heavyInstructions'), false); -const newSds = stringToBoolean(urlParams.get('newSds'), false); + const emulatorConfig = EMULATORS ? firebaseJSON.emulators : undefined; // if running in demo mode, no data will be saved to Firestore @@ -98,7 +99,7 @@ async function startWebApp() { startingTheta, heavyInstructions, demoMode, - newSds, + taskVersion, }; const taskInfo = { diff --git a/task-launcher/src/taskStore/index.ts b/task-launcher/src/taskStore/index.ts index e0b0aefa7..12d637520 100644 --- a/task-launcher/src/taskStore/index.ts +++ b/task-launcher/src/taskStore/index.ts @@ -56,7 +56,7 @@ import store from 'store2'; * @property {Array} previousChoices - Array containing previously randomized order of choices for the current block. * ------- SDS only ------- * @property {StimulusType[]} sequentialTrials - Should be run sequentially in blocks by trial number in an SDS CAT. - * @property {boolean} newSds - Temporary parameter to use the new version SDS, default is false. + * @property {boolean} taskVersion - A version number for the task, default is 1. Can be used as a feature flag. */ export type TaskStoreDataType = { @@ -82,7 +82,7 @@ export type TaskStoreDataType = { language?: string; maxTime?: number; demoMode: boolean; - newSds: boolean; + taskVersion: number; }; /** @@ -130,7 +130,7 @@ export const setTaskStore = (config: TaskStoreDataType) => { testPhase: false, maxTime: config.maxTime, demoMode: config.demoMode, - newSds: config.newSds, + taskVersion: config.taskVersion || 1, }); }; diff --git a/task-launcher/src/tasks/same-different-selection/catTimeline.ts b/task-launcher/src/tasks/same-different-selection/catTimeline.ts index 2872c62c9..24939ec8a 100644 --- a/task-launcher/src/tasks/same-different-selection/catTimeline.ts +++ b/task-launcher/src/tasks/same-different-selection/catTimeline.ts @@ -65,7 +65,7 @@ export default function buildSameDifferentTimelineCat(config: Record { - return taskStore().newSds; + return taskStore().taskVersion === 2; }, }; @@ -93,7 +93,7 @@ export default function buildSameDifferentTimelineCat(config: Record, let corpus: StimulusType[] = taskStore().corpora.stimulus; - if (!heavy && taskStore().newSds) { + if (!heavy && taskStore().taskVersion === 2) { corpus = corpus.filter((trial) => { return !(trial.trialType.includes('something-same') && !(trial.assessmentStage === 'practice_response')); }); @@ -76,13 +76,13 @@ export default function buildSameDifferentTimeline(config: Record, return ( taskStore().nextStimulus.assessmentStage === 'practice_response' && !taskStore().nextStimulus.trialType.includes('something-same-1') && - taskStore().newSds + taskStore().taskVersion === 2 ); }, }; const stimulusBlock = { - timeline: [(taskStore().newSds ? stimulus() : legacyStimulus()), feedbackBlock], + timeline: [(taskStore().taskVersion === 2 ? stimulus() : legacyStimulus()), feedbackBlock], }; const afcBlock = { 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 63e9b8246..57188d806 100644 --- a/task-launcher/src/tasks/same-different-selection/trials/afcMatch.ts +++ b/task-launcher/src/tasks/same-different-selection/trials/afcMatch.ts @@ -107,7 +107,7 @@ export const afcMatch = (trial?: StimulusType) => { // Add primary OK button under the other buttons if (stim.trialType !== 'instructions') { - if (taskStore().newSds) { + if (taskStore().taskVersion === 2) { const okButton = document.createElement('button'); okButton.className = 'primary'; okButton.textContent = 'OK'; @@ -147,7 +147,7 @@ export const afcMatch = (trial?: StimulusType) => { selectedCardIdxs.push(i); } - if (taskStore().newSds) { + if (taskStore().taskVersion === 2) { if (selectedCards.length === stim.requiredSelections) { enableOkButton(); } else { @@ -168,7 +168,7 @@ export const afcMatch = (trial?: StimulusType) => { } }, response_ends_trial: () => { - return (trial || taskStore().nextStimulus).trialType === 'instructions' && taskStore().newSds; + return (trial || taskStore().nextStimulus).trialType === 'instructions' && taskStore().taskVersion === 2; }, on_finish: () => { const stim = trial || taskStore().nextStimulus; diff --git a/task-launcher/src/tasks/shared/helpers/config.ts b/task-launcher/src/tasks/shared/helpers/config.ts index fb19e2e34..1a810c73e 100644 --- a/task-launcher/src/tasks/shared/helpers/config.ts +++ b/task-launcher/src/tasks/shared/helpers/config.ts @@ -93,7 +93,7 @@ export const setSharedConfig = async ( semThreshold, startingTheta, demoMode, - newSds, + taskVersion, } = cleanParams; const config = { @@ -124,7 +124,7 @@ export const setSharedConfig = async ( semThreshold: Number(semThreshold), startingTheta: Number(startingTheta), demoMode: !!demoMode, - newSds: !!newSds, + taskVersion: Number(taskVersion || 1), }; // default corpus if nothing is passed in