From abfa8151ddbe8ee2620bc690471770268920ca40 Mon Sep 17 00:00:00 2001 From: cchang-vassar <79338042+cchang-vassar@users.noreply.github.com> Date: Tue, 20 Jun 2023 02:26:20 -0400 Subject: [PATCH 1/2] add spatial cueing package --- .changeset/strange-penguins-stare.md | 5 + .changeset/tender-deers-chew.md | 5 + .gitignore | 2 + packages/spatial-cueing-task/README.md | 51 +++ .../spatial-cueing-task/examples/index.html | 17 + packages/spatial-cueing-task/jest.config.js | 1 + packages/spatial-cueing-task/package.json | 34 ++ packages/spatial-cueing-task/src/index.ts | 377 ++++++++++++++++++ packages/spatial-cueing-task/src/styles.css | 35 ++ packages/spatial-cueing-task/tsconfig.json | 10 + 10 files changed, 537 insertions(+) create mode 100644 .changeset/strange-penguins-stare.md create mode 100644 .changeset/tender-deers-chew.md create mode 100644 packages/spatial-cueing-task/README.md create mode 100644 packages/spatial-cueing-task/examples/index.html create mode 100644 packages/spatial-cueing-task/jest.config.js create mode 100644 packages/spatial-cueing-task/package.json create mode 100644 packages/spatial-cueing-task/src/index.ts create mode 100644 packages/spatial-cueing-task/src/styles.css create mode 100644 packages/spatial-cueing-task/tsconfig.json diff --git a/.changeset/strange-penguins-stare.md b/.changeset/strange-penguins-stare.md new file mode 100644 index 0000000..ef1c700 --- /dev/null +++ b/.changeset/strange-penguins-stare.md @@ -0,0 +1,5 @@ +--- +"@jspsych-timelines/spatial-cueing-task": minor +--- + +add spatial cueing task package diff --git a/.changeset/tender-deers-chew.md b/.changeset/tender-deers-chew.md new file mode 100644 index 0000000..eeb7fe1 --- /dev/null +++ b/.changeset/tender-deers-chew.md @@ -0,0 +1,5 @@ +--- +"@jspsych-timelines/cli": minor +--- + +Added the false memory task diff --git a/.gitignore b/.gitignore index c6bba59..0947dac 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +.DS_Store + # Logs logs *.log diff --git a/packages/spatial-cueing-task/README.md b/packages/spatial-cueing-task/README.md new file mode 100644 index 0000000..e553438 --- /dev/null +++ b/packages/spatial-cueing-task/README.md @@ -0,0 +1,51 @@ +# spatial-cueing-task + +## Overview + +A shareable timeline of the Posner spatial cueing task. + +## Loading + +### In browser + +```html + + + + + + + + + + diff --git a/packages/spatial-cueing-task/jest.config.js b/packages/spatial-cueing-task/jest.config.js new file mode 100644 index 0000000..bd1aace --- /dev/null +++ b/packages/spatial-cueing-task/jest.config.js @@ -0,0 +1 @@ +module.exports = require("@jspsych/config/jest").makePackageConfig(__dirname); \ No newline at end of file diff --git a/packages/spatial-cueing-task/package.json b/packages/spatial-cueing-task/package.json new file mode 100644 index 0000000..ac49cd3 --- /dev/null +++ b/packages/spatial-cueing-task/package.json @@ -0,0 +1,34 @@ +{ + "name": "@jspsych-timelines/spatial-cueing-task", + "version": "0.0.1", + "description": "A shareable timeline of the Posner spatial cueing task.", + "type": "module", + "main": "dist/index.mjs", + "types": "dist/index.d.ts", + "unpkg": "dist/index.global.js", + "scripts": { + "build": "tsup src/index.ts --format esm,iife --sourcemap --dts --treeshake --clean --global-name jsPsychTimelineSpatialCueingTask" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/jspsych/jspsych-timelines.git" + }, + "keywords": [ + "jsPsych" + ], + "author": "Cherrie", + "license": "MIT", + "bugs": { + "url": "https://github.com/jspsych/jspsych-timelines/issues" + }, + "homepage": "https://github.com/jspsych/jspsych-timelines/packages/spatial-cueing-task#readme", + "peerDependencies": { + "jspsych": "^7.3.3" + }, + "dependencies": { + }, + "devDependencies": { + "tsup": "^6.7.0", + "typescript": "^5.0.2" + } + } \ No newline at end of file diff --git a/packages/spatial-cueing-task/src/index.ts b/packages/spatial-cueing-task/src/index.ts new file mode 100644 index 0000000..ba67713 --- /dev/null +++ b/packages/spatial-cueing-task/src/index.ts @@ -0,0 +1,377 @@ +import { JsPsych } from "jspsych"; +import jsPsychHtmlKeyboardResponse from '@jspsych/plugin-html-keyboard-response' + +/* internal constants */ +const KEYPRESS_TO_NEXT_SLIDE: String = 'Press any key to move to the next slide.'; + +enum StimulusBox { + Default = `

`, + Highlighted = `

`, + WithStimulus = `

X

`, + WithStimulusHighlighted = `

X

` +}; + +enum FixationBox { + NoCue = `

 

+

 

`, + LeftCue = `

+

 

`, + RightCue = `

+

 

`, + BiCue = `

↔︎

+

 

`, +}; + +enum Direction { + Left = -1, + Right = 1, + Bi = 2, + None = 0 +}; + +enum Validity { + Valid = 1, + Invalid = -1, + Neutral = 0, + None = -2, +}; + +const ALL_TEST_COMBOS = [ + { + validity: Validity.Valid, + stimulus_direction: Direction.Left, + }, + { + validity: Validity.Valid, + stimulus_direction: Direction.Right, + }, + { + validity: Validity.Invalid, + stimulus_direction: Direction.Left, + }, + { + validity: Validity.Invalid, + stimulus_direction: Direction.Right, + }, + { + validity: Validity.Neutral, + stimulus_direction: Direction.Left, + }, + { + validity: Validity.Neutral, + stimulus_direction: Direction.Right, + } +]; + +// function that generate start instructions +function showStartInstruction(endogenous_cue: boolean, blank_period: number, cue_period: number) { + const start_instruction = { + type: jsPsychHtmlKeyboardResponse, + stimulus: () => { + return ( + `

This is a demo of the Posner spatial cueing task

` + + `

In each trial, you will see a fixation cross in the center and two empty boxes on the left and right of the cross.\n` + + (endogenous_cue + ? `After ` + + blank_period + + `seconds, you will see an arrow pointing left, right or both directions appear on top of the fixation cross.\n` + : `After ` + + blank_period + + `seconds, you will see either the left, right or both boxes darken slightly.\n`) + + `Then after ` + + cue_period + + `seconds, you will see a black 'X' appear in either one of the boxes.\n` + + (endogenous_cue + ? `Most of the time, the location of the 'X' matches the direction the arrow is pointing at.\n` + + `However, sometimes they do not match. For example, sometimes the arrow points to both directions,` + + `but the 'X' can only be in one box. Another example would be if the arrow points left, but the 'X' is in the right box.\n` + : `Most of the time, the 'X' appears in the darkened box.\n` + + `However, sometimes it may appear in the other, non-darkened box.`) + + `Your task is to respond as quickly as possible when you see the 'X'. If it appears in the left box, press 'f' on your keyboard. If it appears in the right box, press 'j'.

` + + KEYPRESS_TO_NEXT_SLIDE + `
` + ); + }, + }; + + return start_instruction; +}; + +// function that generate end instruction +function showEndInstruction() { + const end_instruction = { + type: jsPsychHtmlKeyboardResponse, + stimulus: `
+

That is the end of the demo.

+

Thank you for participating!

+ `, + }; + + return end_instruction; +} + +// function that generate blank template +function makeDefaultTemplate(blank_period: number) { + const stimulus = `
` + StimulusBox.Default + FixationBox.NoCue + StimulusBox.Default + '
'; + const default_template = { + type: jsPsychHtmlKeyboardResponse, + stimulus: stimulus, + trial_duration: blank_period, + choices: "NO_KEYS" + }; + + return default_template; +}; + +// function that generate endogenous cue +function makeEndogenousCue(jsPsych: JsPsych, cue_period: number) { + const endogenous_cue = { + type: jsPsychHtmlKeyboardResponse, + stimulus: () => { + const validity = jsPsych.timelineVariable("validity"); + const stimulus_direction = jsPsych.timelineVariable("stimulus_direction"); + const cue_direction = cueDirectionMapper(validity, stimulus_direction); + + var stimulus = `
` + StimulusBox.Default; + switch (cue_direction) { + case Direction.Left: + stimulus += FixationBox.LeftCue; + break; + case Direction.Right: + stimulus += FixationBox.RightCue; + break; + case Direction.Bi: + stimulus += FixationBox.BiCue; + break; + default: + stimulus += FixationBox.NoCue; + }; + stimulus += StimulusBox.Default + `
`; + + return stimulus; + }, + trial_duration: cue_period, + choices: "NO_KEYS" + }; + + return endogenous_cue; +}; + +// function that generate exogenous cue +function makeExogenousCue(jsPsych: JsPsych, cue_period: number) { + const exogenous_cue = { + type: jsPsychHtmlKeyboardResponse, + stimulus: () => { + const validity = jsPsych.timelineVariable("validity"); + const stimulus_direction = jsPsych.timelineVariable("stimulus_direction"); + const cue_direction = cueDirectionMapper(validity, stimulus_direction); + + var stimulus = `
` + + (cue_direction == Direction.Left || cue_direction == Direction.Bi ? StimulusBox.Highlighted : StimulusBox.Default) + + FixationBox.NoCue + + (cue_direction == Direction.Right || cue_direction == Direction.Bi ? StimulusBox.Highlighted : StimulusBox.Default) + + `
`; + + return stimulus; + }, + trial_duration: cue_period, + choices: "NO_KEYS" + }; + + return exogenous_cue; +}; + +// function that call the cue maker based on endogenous vs exogenous +function makeCue(jsPsych: JsPsych, endogenous_cue: boolean, cue_period: number) { + return endogenous_cue ? makeEndogenousCue(jsPsych, cue_period) : makeExogenousCue(jsPsych, cue_period); +}; + +// function that generate exogenous stimulus +function makeExogenousStimulus(jsPsych: JsPsych) { + const exogenous_stimulus = { + type: jsPsychHtmlKeyboardResponse, + stimulus: () => { + const validity = jsPsych.timelineVariable("validity"); + const stimulus_direction = jsPsych.timelineVariable("stimulus_direction"); + const cue_direction = cueDirectionMapper(validity, stimulus_direction); + + var stimulus = `
`; + if (cue_direction == Direction.Left || cue_direction == Direction.Bi) { + stimulus += jsPsych.timelineVariable("stimulus_direction") == Direction.Left ? StimulusBox.WithStimulusHighlighted : StimulusBox.Highlighted; + } + else if (jsPsych.timelineVariable("stimulus_direction") == Direction.Left) { + stimulus += StimulusBox.WithStimulus; + } + else { + stimulus += StimulusBox.Default; + } + + stimulus += FixationBox.NoCue; + + if (cue_direction == Direction.Right || cue_direction == Direction.Bi) { + stimulus += jsPsych.timelineVariable("stimulus_direction") == Direction.Right ? StimulusBox.WithStimulusHighlighted : StimulusBox.Highlighted; + } + else if (jsPsych.timelineVariable("stimulus_direction") == Direction.Right) { + stimulus += StimulusBox.WithStimulus; + } + else { + stimulus += StimulusBox.Default; + } + + stimulus += `
`; + + return stimulus; + }, + choices: ['f', 'j'], + data: { + task: "stimulus", + cue_type: "exogenous", + correct_response: () => { + return jsPsych.timelineVariable("stimulus_direction") == Direction.Left ? 'f' : 'j'; + } + }, + on_finish: function (data: any) { + data.correct = jsPsych.pluginAPI.compareKeys( + data.response, + data.correct_response + ); + } + }; + + return exogenous_stimulus; +}; + +// function that generate endogenous stimulus +function makeEndogenousStimulus(jsPsych: JsPsych) { + const endogenous_stimulus = { + type: jsPsychHtmlKeyboardResponse, + stimulus: () => { + const validity = jsPsych.timelineVariable("validity"); + const stimulus_direction = jsPsych.timelineVariable("stimulus_direction"); + const cue_direction = cueDirectionMapper(validity, stimulus_direction); + + var stimulus = `
` + (jsPsych.timelineVariable("stimulus_direction") == Direction.Left ? StimulusBox.WithStimulus : StimulusBox.Default); + switch (cue_direction) { + case Direction.Left: + stimulus += FixationBox.LeftCue; + break; + case Direction.Right: + stimulus += FixationBox.RightCue; + break; + case Direction.Bi: + stimulus += FixationBox.BiCue; + break; + default: + stimulus += FixationBox.NoCue; + break; + }; + + stimulus += (jsPsych.timelineVariable("stimulus_direction") == Direction.Right ? StimulusBox.WithStimulus : StimulusBox.Default) + `
`; + + return stimulus; + }, + choices: ['f', 'j'], + data: { + task: "stimulus", + cue_type: "endogenous", + correct_response: () => { + return jsPsych.timelineVariable("stimulus_direction") == Direction.Left ? 'f' : 'j'; + } + }, + on_finish: function (data: any) { + data.correct = jsPsych.pluginAPI.compareKeys( + data.response, + data.correct_response + ); + } + }; + + return endogenous_stimulus; +}; + +// function to map stimulus direction to cue direction based on cue validity +function cueDirectionMapper(validity: Validity, stimulus_direction: Direction) { + switch (validity) { + case Validity.Valid: + return stimulus_direction; + case Validity.Invalid: + return stimulus_direction * -1; + case Validity.Neutral: + return Direction.Bi; + case Validity.None: + return Direction.None; + default: + return Direction.None; + }; +}; + +// function that call the stimulus maker based on endogenous vs exogenous +function makeStimulus(jsPsych: JsPsych, endogenous_cue: boolean) { + return endogenous_cue ? makeEndogenousStimulus(jsPsych) : makeExogenousStimulus(jsPsych); +}; + + +// make a trial consisting of blank, cue and stimulus +function createTrialTimeline(jsPsych: JsPsych, endogenous_cue: boolean, blank_period: number, cue_period: number) { + const single_trial_timeline = { + timeline: [ + makeDefaultTemplate(blank_period), + makeCue(jsPsych, endogenous_cue, cue_period), + makeStimulus(jsPsych, endogenous_cue) + ] + }; + + return single_trial_timeline; +}; + +// generate a Posner spatial cueing task timeline +export function createTimeline( + jsPsych: JsPsych, { + endogenous_cue = false, + blank_period = 2000, + cue_period = 2000, + num_trials = 60, // must be divisible by 6 + valid_proportion = 0.8 + }: { + endogenous_cue?: boolean, + blank_period?: number, + cue_period?: number, + num_trials?: number, + valid_proportion?: number + } = {}) + { + jsPsych = jsPsych; + const num_each_valid_trial_type = num_trials * valid_proportion / 2; + const num_each_invalid_trial_type = (num_trials - (num_each_valid_trial_type * 2)) / 4; + var weights: number[] = [ + num_each_valid_trial_type, num_each_valid_trial_type, + num_each_invalid_trial_type, num_each_invalid_trial_type, + num_each_invalid_trial_type, num_each_invalid_trial_type + ]; + + const spatial_cueing_timeline = { + timeline: [ + createTrialTimeline(jsPsych, endogenous_cue, blank_period, cue_period), + ], + timeline_variables: ALL_TEST_COMBOS, + sample: { + type: "with-replacement", + size: num_trials, + weights: weights + } + }; + + return spatial_cueing_timeline; +}; + +export const timelineUnits = { + createTrialTimeline +}; + +export const utils = { + showStartInstruction, + showEndInstruction, + makeDefaultTemplate, + cueDirectionMapper, + makeEndogenousCue, + makeExogenousCue, + makeCue, + makeExogenousStimulus, + makeEndogenousStimulus, + makeStimulus +} \ No newline at end of file diff --git a/packages/spatial-cueing-task/src/styles.css b/packages/spatial-cueing-task/src/styles.css new file mode 100644 index 0000000..9704040 --- /dev/null +++ b/packages/spatial-cueing-task/src/styles.css @@ -0,0 +1,35 @@ +.jspsych-spatial-cueing-instruction { + max-width: 50vw; +} + +.jspsych-spatial-cueing-container { + width: 30vw; + height: 10vh; + display: flex; + flex-direction: row; + justify-content: space-evenly; + align-items: center; +} + +.jspsych-spatial-cueing-target-container, .jspsych-spatial-cueing-target-container-bold, .jspsych-spatial-cueing-target-container-stimulus { + width: 48px; + height: 48px; + border: 1px solid black; + justify-content: center; + align-items: center; + line-height: 0px; + font-size: 24px; +} + +.jspsych-spatial-cueing-target-container-bold { + background-color: #BEBEBE; +} + +.jspsych-spatial-cueing-fixation-container { + width: 48px; + height: fit-content; + justify-content: center; + align-items: center; + line-height: 0px; + font-size: 24px; +} \ No newline at end of file diff --git a/packages/spatial-cueing-task/tsconfig.json b/packages/spatial-cueing-task/tsconfig.json new file mode 100644 index 0000000..33655f7 --- /dev/null +++ b/packages/spatial-cueing-task/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "@jspsych/config/tsconfig.contrib.json", + "compilerOptions": { + "baseUrl": "." + }, + "include": [ + "src", + "../cli/src/build.js" + ] +} \ No newline at end of file From 6078789c22340f15b39fad2f8f9004ee761cd671 Mon Sep 17 00:00:00 2001 From: Josh de Leeuw Date: Wed, 21 Jun 2023 09:41:25 -0400 Subject: [PATCH 2/2] Delete tender-deers-chew.md --- .changeset/tender-deers-chew.md | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 .changeset/tender-deers-chew.md diff --git a/.changeset/tender-deers-chew.md b/.changeset/tender-deers-chew.md deleted file mode 100644 index eeb7fe1..0000000 --- a/.changeset/tender-deers-chew.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@jspsych-timelines/cli": minor ---- - -Added the false memory task