diff --git a/.gitignore b/.gitignore index 12940b5..9df9658 100644 --- a/.gitignore +++ b/.gitignore @@ -16,9 +16,12 @@ .DS_Store .env* .env - +*.vs npm-debug.log* yarn-debug.log* yarn-error.log* psiturkit/*_turk +.vs/VSWorkspaceState.json +.vs/slnx.sqlite +.vs/Effort-Task-3/v16/.suo diff --git a/.vs/Effort-Task-3/v16/.suo b/.vs/Effort-Task-3/v16/.suo new file mode 100644 index 0000000..7d2f772 Binary files /dev/null and b/.vs/Effort-Task-3/v16/.suo differ diff --git a/.vs/ProjectSettings.json b/.vs/ProjectSettings.json new file mode 100644 index 0000000..f8b4888 --- /dev/null +++ b/.vs/ProjectSettings.json @@ -0,0 +1,3 @@ +{ + "CurrentProjectSetting": null +} \ No newline at end of file diff --git a/.vs/VSWorkspaceState.json b/.vs/VSWorkspaceState.json new file mode 100644 index 0000000..da01583 --- /dev/null +++ b/.vs/VSWorkspaceState.json @@ -0,0 +1,9 @@ +{ + "ExpandedNodes": [ + "", + "\\src", + "\\src\\trials" + ], + "SelectedNode": "\\README.md", + "PreviewInSolutionExplorer": false +} \ No newline at end of file diff --git a/.vs/slnx.sqlite b/.vs/slnx.sqlite new file mode 100644 index 0000000..4f0dbaf Binary files /dev/null and b/.vs/slnx.sqlite differ diff --git a/README.md b/README.md index 80ecb74..c181bf3 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # Effort Task +

@@ -10,81 +11,94 @@ This repo contains the Effort task. It is a [jspsych](https://www.jspsych.org/) task built with React and Electron. This task uses the [Neuro Task Starter](https://www.github.com/brown-ccv/neuro-task-starter). ## TO RUN THE TASK - BEHAVIORAL ONLY + Go to the releases tab (https://github.com/lnccbrown/task-effort/releases) and download the recent installer for your machine. ## TO RUN THE TASK - EEG + Follow the instructions above for behavioral version (i.e., download relevant installer). In addition, follow instructions on the repo Wiki to set the port name: https://github.com/lnccbrown/task-effort/wiki/Task-Effort-Running-Notes-(EEG) - ## TO QUIT THE TASK + If you want to quit in the middle of the task, you can use these keyboard shortcuts: + ``` Ctrl+W (for PC/Windows) ``` + ``` Cmd+Q (for Mac) ``` + Partial data will be saved. ## TO REUSE A SUBJECT ID NUMBER + If you want to overwrite a subject's data file: -1) Go to your desktop to where the Effort-Data folder is. Delete the .json data file corresponding to the SubjectID you want to reuse. +1. Go to your desktop to where the Effort-Data folder is. Delete the .json data file corresponding to the SubjectID you want to reuse. + +2. Go to the corresponding locations (dependent on operating system): -2) Go to the corresponding locations (dependent on operating system): ``` Mac OS: ~/Library/Application Support/task-effort Windows: C:\Users\\AppData\Roaming\task-effort Linux: ~/.config/task-effort ``` -In order to find this directory on a PC, you may need to enable viewing hidden folders and files. Alternatively, you can right-click on the app shortcut on the desktop, and select 'Open File Location.' +In order to find this directory on a PC, you may need to enable viewing hidden folders and files. Alternatively, you can right-click on the app shortcut on the desktop, and select 'Open File Location.' Once you have succesfully navigated to the corresponding location, delete the .json data file corresponding to the SubjectID you want to overwrite or reuse. Once you have deleted both of these .json files, you should be able to use a SubjectID you have used in the past. - - - - ## THE FOLLOWING INSTRUCTIONS ARE FOR MAKING CONTRIBUTIONS TO TASK CODE ## Getting Started 1. Clone this repo onto your computer using git. + ``` git clone https://github.com/lnccbrown/task-effort.git ``` + 2. Navigate to the task-effort directory and then install the dependencies. You may first need to install Node.js (https://nodejs.org/en/download/) before being able to use npm commands in the terminal. + ``` npm install ``` + 3. Build the package. + ``` npm run build ``` + 4. Run the task in development (dev) mode - this should launch an electron window with the task with the inspector open to the console and will hot-reload when changes are made to the app. For Mac and Linux: + ``` npm run dev ``` For Windows: You will need to open 2 terminals. In the first (and make sure you are in the task-effort repo directory), run the command: + ``` npm start ``` + In the second terminal (and make you are in the task-effort repo directory), run: + ``` npm run electron-dev ``` -5. Check out the data - the data is saved throughout the task to the users's app directory. This is logged at the beginning of the task wherever you ran `npm run dev`. It is also stored in a folder that is generated by the app named 'Effort-Data', which should be found on the desktop. +5. Check out the data - the data is saved throughout the task to the users's app directory. This is logged at the beginning of the task wherever you ran `npm run dev`. It is also stored in a folder that is generated by the app named 'Effort-Data', which should be found on the desktop. ## Contributing @@ -104,7 +118,7 @@ This project directory is organized to be very modular and composable. In genera ### `package.json` -The `package.json` file contains metadata about your project and scripts to run tasks related to your task. The `name` should be updated to your task's name and `scripts` can be added as desired, but otherwise this file should not be edited manually. To remove or add a dependency use `npm install` or `npm uninstall` with the `-D` flag if installing a dev dependency. +The `package.json` file contains metadata about your project and scripts to run tasks related to your task. The `name` should be updated to your task's name and `scripts` can be added as desired, but otherwise this file should not be edited manually. To remove or add a dependency use `npm install` or `npm uninstall` with the `-D` flag if installing a dev dependency. The `package-lock.json` contains metadata about the package installation. It should never be manually updated. @@ -122,7 +136,7 @@ This file contains all of the code relating to the electron app. This includes t #### `config/` -The `config` directory contains the config files needed for the electron app. This includes the event-marker details and event codes. +The `config` directory contains the config files needed for the electron app. This includes the event-marker details and event codes. Note: the productId can be overwritten by the environment variable EVENT_MARKER_PRODUCT_ID @@ -148,7 +162,7 @@ This folder contains any static files that are used by the app, such as images. #### `config/` -In the `config/` directory, there are `.js` files which contain settings for the different parts of the task. Every task should have a `main` config and a `trigger` config (assuming use of the event marker). The `main` config has all global settings for the task (such as whether it is in mturk mode or not), load the appropriate language file, and set up a default (or only) configuration object for the task. The `trigger` config has settings specific to the event marker and uses a slightly different style of javascript as it is imported both in the React app as well as the electron process. +In the `config/` directory, there are `.js` files which contain settings for the different parts of the task. Every task should have a `main` config and a `trigger` config (assuming use of the event marker). The `main` config has all global settings for the task (such as whether it is in mturk mode or not), load the appropriate language file, and set up a default (or only) configuration object for the task. The `trigger` config has settings specific to the event marker and uses a slightly different style of javascript as it is imported both in the React app as well as the electron process. Other config files can be used to add settings for specific blocks or sub-sections of the experiment. @@ -158,7 +172,7 @@ Any language that is displayed in the experiment should be stored in this folder #### `lib/` -The `lib/` directory contains utility functions and markup that is used in the tasks. This allows for functions and html to be re-used wherever needed. The `lib/utils.js` file contains functions that are generally useful across many tasks, whereas `lib/taskUtils.js` contains functions specific to this task. +The `lib/` directory contains utility functions and markup that is used in the tasks. This allows for functions and html to be re-used wherever needed. The `lib/utils.js` file contains functions that are generally useful across many tasks, whereas `lib/taskUtils.js` contains functions specific to this task. #### `lib/markup` @@ -166,27 +180,28 @@ The `lib/` directory contains utility functions and markup that is used in the t #### `timelines` -`jspsych` uses `timelines` to control what `trials` are displayed in what order. `timelines` can contain other `timelines`, which is why there may be several files in this directory. The `main.js` file should have the timeline that is called by `App.js`. +`jspsych` uses `timelines` to control what `trials` are displayed in what order. `timelines` can contain other `timelines`, which is why there may be several files in this directory. The `main.js` file should have the timeline that is called by `App.js`. #### `trials` `jspsych` uses `trials` as its base unit of an experiment. These trials do things such as display some stimulus or request a response. + ## Environment Variables The following are environment variables used by the app: -* `ELECTRON_START_URL` [string]: URL (e.g. `http://localhost:3000`) where the front end of the app is being hosted - also used in `electron.js` to indicate the app is running in dev mode -* `EVENT_MARKER_PRODUCT_ID` [string]: The product ID of the event marker (e.g. `0487`). If not set, it will use the `productID` set in `public/config/trigger.js`. -* `REACT_APP_AT_HOME` [boolean]: whether the app is being used in home mode (true) or clinic mode (false) -* `REACT_APP_PATIENT_ID` [string]: The default patient id to show when requesting a patient ID in `userID`. If not set, no default is shown (blank input box). +- `ELECTRON_START_URL` [string]: URL (e.g. `http://localhost:3000`) where the front end of the app is being hosted - also used in `electron.js` to indicate the app is running in dev mode +- `EVENT_MARKER_PRODUCT_ID` [string]: The product ID of the event marker (e.g. `0487`). If not set, it will use the `productID` set in `public/config/trigger.js`. +- `REACT_APP_AT_HOME` [boolean]: whether the app is being used in home mode (true) or clinic mode (false) +- `REACT_APP_PATIENT_ID` [string]: The default patient id to show when requesting a patient ID in `userID`. If not set, no default is shown (blank input box). ## Usage with PsiTurk -While this set up is optimized for Electron, we added functionality that will make use with PsiTurk easy. The application will detect if it's being used in a Turk environment and will: +While this set up is optimized for Electron, we added functionality that will make use with PsiTurk easy. The application will detect if it's being used in a Turk environment and will: -- Save the data to the default PsiTurk SQLite database. -- Switch the language to Turk specific, if `src/language/.mturk.json` exists. -- Use the Turk specific timeline if different than the primary timeline. +- Save the data to the default PsiTurk SQLite database. +- Switch the language to Turk specific, if `src/language/.mturk.json` exists. +- Use the Turk specific timeline if different than the primary timeline. **Prebuilt version** When GitHub Actions is run, a psiturk build will be created automatically, and can be downloaded from its artifacts (skip next step if using). @@ -196,16 +211,18 @@ To set up your PsiTurk project, we provide a script that does the conversion. PsiTurk is a Python package used to manage HITs in Mechanical Turk. Before using the provided script, install [PsiTurk](https://psiturk.org/). You'll need to follow these steps (the path to the PsiTurk project should be a directory you wish to be created): -- Build the application: `npm run build` + +- Build the application: `npm run build` - Move to the `psiturkit` directory: `cd psiturkit` - If it's the first time you're running the script: - `./psiturk-it -p ` + `./psiturk-it -p ` - To update an existing PsiTurk project (the path to the PsiTurk project should already exist from the previous steps): `./psiturk-it -u -p ` **Running psiturk** After that, just navigate to your newly created PsiTurk project directory. + ```shell shell> psiturk #start psiturk psiturk> server on #start server @@ -220,21 +237,21 @@ psiturk> debug #debug mode ### Use git flow (ish) -Your `master` branch should be where official releases are made (whenever code is used in real life tasks) and `develop` should be the working copy. Use branches for any new features or fixes and then use pull requests to merge those into `develop`. Merge `develop` into `master` when using the task and make sure to tag a release. This will ensure you can always go back to exactly the code that was working with a specific subject/session. +Your `master` branch should be where official releases are made (whenever code is used in real life tasks) and `develop` should be the working copy. Use branches for any new features or fixes and then use pull requests to merge those into `develop`. Merge `develop` into `master` when using the task and make sure to tag a release. This will ensure you can always go back to exactly the code that was working with a specific subject/session. ### Keep your code style consistent -* `let` instead of `var` -* fat arrow functions (`const myFunc = (var) => doSomething(var)`) instead of es5/6 functions (`function myFunc(var) { doSomething(var) }`) -* camel case for variable, and function names (`doSomething`) instead of snake case (`do_something`) -* but snake case inside json is fine -* a `tab` === two spaces -* file exports at the bottom of the file in one chunk instead of exporting the function declaration -* when in doubt, leave future you a comment (you'll never regret it) +- `let` instead of `var` +- fat arrow functions (`const myFunc = (var) => doSomething(var)`) instead of es5/6 functions (`function myFunc(var) { doSomething(var) }`) +- camel case for variable, and function names (`doSomething`) instead of snake case (`do_something`) +- but snake case inside json is fine +- a `tab` === two spaces +- file exports at the bottom of the file in one chunk instead of exporting the function declaration +- when in doubt, leave future you a comment (you'll never regret it) ## Troubleshooting -When developing electron apps there are two processes: `main`, and `renderer`. In this case `main` corresponds to `electron-starter.js` and its console is wherever you called `npm run dev` or `electron .` from. `renderer` corresponds to the React App - this is everything else. The react app's console is in the electron/browser window and can be seen by using dev tools to inspect the window. When running `npm run dev`, it should open by default. +When developing electron apps there are two processes: `main`, and `renderer`. In this case `main` corresponds to `electron-starter.js` and its console is wherever you called `npm run dev` or `electron .` from. `renderer` corresponds to the React App - this is everything else. The react app's console is in the electron/browser window and can be seen by using dev tools to inspect the window. When running `npm run dev`, it should open by default. ### Potential Issues @@ -242,14 +259,13 @@ When developing electron apps there are two processes: `main`, and `renderer`. Try deleting your `node_modules` folder and the `package-lock.json` then running `npm install` then `npm run rebuild`. - ## Available Scripts In the project directory, you can run: ### `npm run dev` -Runs `npm start` and `npm run electron-dev` concurrently. This may not play nicely with windows. If it doesn't, run `npm start` and `npm run electron-dev` from different terminal windows. +Runs `npm start` and `npm run electron-dev` concurrently. This may not play nicely with windows. If it doesn't, run `npm start` and `npm run electron-dev` from different terminal windows. ### `npm start` @@ -266,14 +282,13 @@ See the section about [running tests](https://facebook.github.io/create-react-ap ### `npm build` -Creates a production build of the app (renderer). This must be done before running `package:platform` or the psiturk build instructions. +Creates a production build of the app (renderer). This must be done before running `package:platform` or the psiturk build instructions. ### `npm run package:platform` -It correctly bundles creates electron packages for the given platform. It then creates an installer for that platform. The output can be found in `/dist` +It correctly bundles creates electron packages for the given platform. It then creates an installer for that platform. The output can be found in `/dist` platforms: windows, mac, linux. - #### Prerequisites If not running this command on a windows machine, must have `mono` and `wine` installed. diff --git a/package-lock.json b/package-lock.json index 862368a..db37d2f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { - "name": "task-effort", - "version": "1.3.2", + "name": "effort-v1.3.3", + "version": "1.3.3", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/src/config/experiment.js b/src/config/experiment.js index b1005f6..f29311b 100644 --- a/src/config/experiment.js +++ b/src/config/experiment.js @@ -16,11 +16,11 @@ exptBlock1.get_reward = [true, true, true, false]; exptBlock1.num_breaks = 3; // debug settings where there are only 2 trials in main task block -// exptBlock1.probs = ["100%"]; -// exptBlock1.counterbalance = true; -// exptBlock1.value = [3]; -// exptBlock1.effort = [5]; -// exptBlock1.keys = ["q", "p", "m"]; -// exptBlock1.get_reward = [true]; +//exptBlock1.probs = ["100%"]; +//exptBlock1.counterbalance = true; +//exptBlock1.value = [3]; +//exptBlock1.effort = [5]; +//exptBlock1.keys = ["q", "p", "m"]; +//exptBlock1.get_reward = [true]; export { exptBlock1 }; diff --git a/src/config/main.js b/src/config/main.js index df832a6..9240972 100644 --- a/src/config/main.js +++ b/src/config/main.js @@ -69,6 +69,7 @@ try { // then assume it's online in the browser // with MTurk or Prolific ONLINE = AT_HOME && !IS_ELECTRON ? true : false; +//ONLINE = true; console.log("ONLINE:", ONLINE); // note: it _is_ possible to do both firebase & mturk if desired diff --git a/src/config/trigger.js b/src/config/trigger.js index 0fc57ec..f5226c8 100644 --- a/src/config/trigger.js +++ b/src/config/trigger.js @@ -7,7 +7,7 @@ const eventCodes = { // rewardProbabilityEnd: 11, // reward probability (50% of 100%) offset frameSpikeStart: 20, // frame and spike onset // frameSpikeEnd: 21, // frame and spike offset - costBenefitsStart: 30, // points and effort values for both options onset + costBenefitsStart: 30, // points and effort values for both options onset // costBenefitsEnd: 31, // points and effort values for both options offset choiceStart: 40, // frame + spike + balloons, when participant can choose blue or green // choiceEnd: 41, // frame + spike + balloons offset, once participant has made a choice diff --git a/src/language/en_us.json b/src/language/en_us.json index ec33acd..ed384b8 100644 --- a/src/language/en_us.json +++ b/src/language/en_us.json @@ -35,29 +35,29 @@ }, "instructions": { "welcome": "Welcome!", - "choose_btwn_two_balloons": "In this experiment, you will be making a choice between inflating one of 2 balloons by pressing corresponding keys on the keyboard.", - "your_job": "Your job is to pop the balloons by pumping them up to a spike in order to win points!", + "choose_btwn_two_balloons": "In this experiment, you will be making a choice between inflating one of 2 balloons by pressing corresponding keys on the keyboard: Q and P", + "your_job": "Your job is to pop the balloons by pumping them up to a spike in order to win points!", "pop_balloon_earn_money": "The points earned from popping a balloon will be shown with a '+' sign in front of it (i.e. +1).", "points_to_bonus_conversion": "For every 20 points that you win in the game, you will get $1 at the end of the experiment.", - "cumulative_rewards": "You will be shown the total number of points earned at the end of each trial.", + "cumulative_rewards": "You will be shown the total number of points earned at the end of each trial. *Insert picture here*", "two_balloons": "There are 2 kinds of balloons:
GREEN and BLUE balloons.", - "blue_balloon_points": "BLUE balloons will pop after you pump 20 times and you may win 1 point.", + "blue_balloon_points": "BLUE balloons will always pop after you pump 20 times and you may win 1 point.", "green_balloon_points": "GREEN balloons may win you more points, but will require more pumps.", "green_balloon_pop_time": "GREEN balloons will always pop after 25 seconds.", "green_balloon_pump_bonus": "If you continue to pump the GREEN balloon after it hits the spike within the 25 seconds time limit, the bigger the balloon will get and the more bonus points you will earn.

If you pump the GREEN balloon and it does not reach the spike within the 25 seconds time limit, you will earn some of the points for pumping the balloon.

(Example: If the total possible points you can earn for a balloon is 2 points and you pump the balloon halfway to the spike, you will earn 1 point.)

", "green_balloon_variable_points_pumps": "For the GREEN balloon, the number of pumps needed and the number of points you could win can change on each round. The number of pumps and points are listed below the GREEN balloon.", "blue_balloon_constant_points_pumps": "For the BLUE balloon, the potential win will always stay the same (1 point),
and it will always require 20 pumps.", - "reward_prob_variable": "For some rounds, the chances that you will win points for either balloon are 50%
(even after popping the balloon).", - "reward_prob_certain": "In other rounds the chances of winning will be 100%.", - "prob_display_location": "The probability at which you will receive points for a trial will appear at the bottom of the screen.", + "reward_prob_variable": "For some rounds, the chances that you will win points for either balloon are 50%
(even after popping the balloon). ", + "reward_prob_certain": "In other rounds the chances of winning will be 100%.", + "prob_display_location": "The probability at which you will receive points for a trial will appear at the bottom of the screen.", "reward_prob_display_event": "The chances of winning points on that round, and the number of points possible to win, will be shown to you while you choose which balloon to inflate.", "pump_keys": "Press the Q key to pump the balloon on the left.
Press the P key to pump the balloon on the right.", "choice_locked_in": "Once you have chosen a balloon, you can only pump that balloon.", "ready": "The experiment will start now.
Press next when ready!", - "blue_practice_pump": "In the next trial, try popping the BLUE balloon:
To pump it, press the Q key with your left index finger to first choose the blue balloon and then to pump it.", - "green_practice_pump": "In the next trial, try popping the GREEN balloon:
To pump it, press the P key with your right index finger to first choose the green balloon and then to pump it.", + "blue_practice_pump": "In the next trial, try popping the BLUE balloon:
To pump it, press the Q key with your left index finger to first choose the blue balloon and then to pump it. Wait for the balloon to appear on screen before beginning to pump.", + "green_practice_pump": "In the next trial, try popping the GREEN balloon:
To pump it, press the P key with your right index finger to first choose the green balloon and then to pump it. Wait for the balloon to appear on screen before beginning to pump.", "wait_pump": "Wait for the balloon to appear on screen before beginning to pump.", - "missed_choice": "You will have 6 seconds to make your decision. If you do not respond in time, you will lose the opportunity to win points on that round and will move on to a new round." + "missed_choice": "You will have 6 seconds to make your decision. If you do not respond in time, you will lose the opportunity to win points on that round and will move on to a new round." }, "quiz": { "confirm_understanding": "To make sure you understand the task instructions, we will ask you some questions about how to perform the task.", @@ -103,7 +103,7 @@ "total": "Total:
" }, "break": { - "prompt": "You may now take a break.", + "prompt": "You may now take a break. You have completed Block ", "done": "Press any key to end your break and to resume the task." }, "questionnaires": { diff --git a/src/timelines/breakTrial.js b/src/timelines/breakTrial.js index cc2c34a..41e07d3 100644 --- a/src/timelines/breakTrial.js +++ b/src/timelines/breakTrial.js @@ -3,17 +3,17 @@ import { lang } from '../config/main' import breakScreen from '../trials/breakScreen' import buildCountdown from '../trials/countdown' -const breakTrial = () => { +const breakTrial = (iBreak) => { - let timeline = [ - breakScreen(), - buildCountdown(lang.countdown.post_break_resume, 3) - ] + let timeline = [ + breakScreen(iBreak), + buildCountdown(lang.countdown.post_break_resume, 3) + ] return { - type: 'html_keyboard_response', - timeline: timeline - } + type: 'html_keyboard_response', + timeline: timeline + } } export default breakTrial diff --git a/src/timelines/main.js b/src/timelines/main.js index 9ab6a5c..09d85a9 100644 --- a/src/timelines/main.js +++ b/src/timelines/main.js @@ -73,7 +73,7 @@ const onlineTimeline = MTURK : // PROLIFIC VERSION OF THE TASK BELOW: [ // commented out for now/quick debugging: - // experimentStart(), + experimentStart(), userId(), preamble, bluePracticeInstructions(), diff --git a/src/timelines/taskBlock.js b/src/timelines/taskBlock.js index f4fb97b..4688566 100644 --- a/src/timelines/taskBlock.js +++ b/src/timelines/taskBlock.js @@ -3,26 +3,26 @@ import breakTrial from './breakTrial' import { generateStartingOpts } from '../lib/taskUtils' const taskBlock = (blockSettings) => { - // initialize block + // initialize block const startingOpts = generateStartingOpts(blockSettings) - const blockDetails = { - block_earnings: 0.0, + const blockDetails = { + block_earnings: 0.0, optimal_earnings: 0.0, continue_block: true } // timeline = loop through trials - let timeline = startingOpts.map( (opt) => taskTrial(blockSettings, blockDetails, opt)) + let timeline = startingOpts.map((opt) => taskTrial(blockSettings, blockDetails, opt)) if (blockSettings.num_breaks > 0) { let breakInterval = Math.floor(timeline.length / (blockSettings.num_breaks + 1)) for (let iBreak = 1; iBreak < blockSettings.num_breaks + 1; iBreak++) { - timeline.splice(iBreak*breakInterval, 0, breakTrial()) - } + timeline.splice(iBreak * breakInterval, 0, breakTrial(iBreak)) + } } - return { + return { type: 'html_keyboard_response', timeline: timeline } diff --git a/src/timelines/taskTrial.js b/src/timelines/taskTrial.js index 98c06b7..24f3e81 100644 --- a/src/timelines/taskTrial.js +++ b/src/timelines/taskTrial.js @@ -1,9 +1,12 @@ // import trials + import fixation from "../trials/fixation"; import rewardProbability from "../trials/rewardProbability"; import frameSpike from "../trials/frameSpike"; import choice from "../trials/choice"; import costBenefits from "../trials/costBenefits"; +import { ONLINE } from "../config/main"; +import { addData } from "../lib/taskUtils"; import pressBalloon from "../trials/pressBalloon"; import rewardFeedback from "../trials/rewardFeedback"; import cumulativeReward from "../trials/cumulativeReward"; @@ -26,9 +29,9 @@ const taskTrial = (blockSettings, blockDetails, opts) => { // show condition fixation(300), rewardProbability(1000, blockSettings, opts, trialDetails), - frameSpike(700, blockSettings, opts, trialDetails), - costBenefits(1500, blockSettings, opts, trialDetails), - choice(5000, blockSettings, opts), + //frameSpike(700, blockSettings, opts, trialDetails), + //costBenefits(1500, blockSettings, opts, trialDetails), + choice(6000, blockSettings, opts, trialDetails), pressBalloon(25000, blockSettings, opts), fixation(500), rewardFeedback(1000, blockSettings, opts, trialDetails), @@ -36,11 +39,27 @@ const taskTrial = (blockSettings, blockDetails, opts) => { cumulativeReward(1000, blockSettings, blockDetails, opts, trialDetails), // end the trial trialEnd(500), - ]; + ]; + + let timeline_inlab = [ + // show condition + fixation(300), + rewardProbability(1000, blockSettings, opts, trialDetails), + frameSpike(700, blockSettings, opts, trialDetails), + costBenefits(1500, blockSettings, opts, trialDetails), + choice(5000, blockSettings, opts, trialDetails), + pressBalloon(25000, blockSettings, opts), + fixation(500), + rewardFeedback(1000, blockSettings, opts, trialDetails), + fixation(500), + cumulativeReward(1000, blockSettings, blockDetails, opts, trialDetails), + // end the trial + trialEnd(500), + ]; return { type: "html_keyboard_response", - timeline: timeline, + timeline: ONLINE ? timeline : timeline_inlab, }; }; -export default taskTrial; +export default taskTrial; \ No newline at end of file diff --git a/src/trials/breakScreen.js b/src/trials/breakScreen.js index 4448ff6..e7723b1 100644 --- a/src/trials/breakScreen.js +++ b/src/trials/breakScreen.js @@ -1,22 +1,22 @@ import { lang } from "../config/main"; import { baseStimulus } from "../lib/markup/stimuli"; -const breakScreen = () => { - let stimulus = baseStimulus( - ` +const breakScreen = (iBreak) => { + let stimulus = baseStimulus( + `
-

${lang.break.prompt} +

${lang.break.prompt}${iBreak} out of 3

`, - true - ); + true + ); - return { - type: "html_keyboard_response", - stimulus: stimulus, - prompt: lang.break.done, - response_ends_trial: true, - }; + return { + type: "html_keyboard_response", + stimulus: stimulus, + prompt: lang.break.done, + response_ends_trial: true, + }; }; export default breakScreen; diff --git a/src/trials/choice.js b/src/trials/choice.js index 5640441..8851022 100644 --- a/src/trials/choice.js +++ b/src/trials/choice.js @@ -10,7 +10,7 @@ const canvasHTML = ` `; -const choice = (duration, blockSettings, opts) => { +const choice = (duration, blockSettings, opts, trialDetails) => { let stimulus = `
` + canvasHTML + @@ -133,7 +133,7 @@ const choice = (duration, blockSettings, opts) => { canvasSettings.balloonYpos, canvasSettings.balloonRadius ); - }; + }; canvasDraw(); var timer = setInterval(function () { @@ -159,7 +159,10 @@ const choice = (duration, blockSettings, opts) => { jsPsych.pluginAPI.cancelKeyboardResponse(keyboardListener); if (info.key === keys["Q"]) { // 1 key + var timeWhenPressed = new Date().getTime(); + var rt = timeWhenPressed - timeWhenStarted; var returnObj = { + rt:rt, key: info.key, effort: effort[0], value: value[0], @@ -170,16 +173,25 @@ const choice = (duration, blockSettings, opts) => { done(returnObj); } else if (info.key === keys["P"]) { // 0 key + timeWhenPressed = new Date().getTime(); + rt = timeWhenPressed - timeWhenStarted; returnObj = { + rt:rt, key: info.key, effort: effort[1], value: value[1], high_effort: high_effort[1], get_reward: get_reward[1], + subtrial_type: "choice", }; done(returnObj); } } + trialDetails.probability = probability; + trialDetails.effort = effort; + trialDetails.high_effort = high_effort; + trialDetails.value = value; + trialDetails.subtrial_type = "choice"; var keyboardListener = jsPsych.pluginAPI.getKeyboardResponse({ callback_function: after_response, diff --git a/src/trials/costBenefits.js b/src/trials/costBenefits.js index 4aef027..d6dff62 100644 --- a/src/trials/costBenefits.js +++ b/src/trials/costBenefits.js @@ -1,5 +1,6 @@ // imports -import { eventCodes, canvasSize, canvasSettings } from "../config/main"; +import { jsPsych } from "jspsych-react"; +import { eventCodes, keys, canvasSize, canvasSettings } from "../config/main"; import { photodiodeGhostBox, pdSpotEncode } from "../lib/markup/photodiode"; import { removeCursor } from "../lib/utils"; import { addData } from "../lib/taskUtils"; @@ -12,14 +13,14 @@ const canvasHTML = `
`; const costBenefits = (duration, blockSettings, opts, trialDetails) => { - let stimulus = - `
` + - canvasHTML + - fixationHTML + - photodiodeGhostBox() + - `
`; + let stimulus = + `
` + + canvasHTML + + fixationHTML + + photodiodeGhostBox() + + `
`; - const startCode = eventCodes.costBenefitsStart; + const startCode = eventCodes.costBenefitsStart; let probability = blockSettings.is_practice ? opts : opts.prob; let value = blockSettings.is_practice ? blockSettings.value : opts.value; @@ -27,11 +28,20 @@ const costBenefits = (duration, blockSettings, opts, trialDetails) => { let high_effort = blockSettings.is_practice ? blockSettings.high_effort : opts.high_effort; - + let valid_keys = blockSettings.keys; + let get_reward = blockSettings.is_practice + ? blockSettings.get_reward + : opts.get_reward; return { type: "call_function", async: true, func: (done) => { + trialDetails.probability = probability; + trialDetails.effort = effort; + trialDetails.high_effort = high_effort; + trialDetails.value = value; + trialDetails.subtrial_type = "costBenefits"; + addData(trialDetails, blockSettings, opts); // add stimulus to the DOM document.getElementById("jspsych-content").innerHTML = stimulus; // $('#jspsych-content').addClass('task-container') @@ -39,92 +49,143 @@ const costBenefits = (duration, blockSettings, opts, trialDetails) => { // set up canvas let canvas = document.querySelector("#jspsych-canvas"); let ctx = canvas.getContext("2d"); + let timeWhenStarted = new Date().getTime(); - const canvasDraw = () => { - // transparent background - ctx.clearRect(0, 0, canvas.width, canvas.height); - var inflateBy; - var spikeHeight = [0, 0]; - for (let i = 0; i < 2; i++) { - if (high_effort[i]) { - inflateBy = canvasSettings.inflateByHE; - } else { - inflateBy = canvasSettings.inflateByNHE; - } + const canvasDraw = () => { + // transparent background + ctx.clearRect(0, 0, canvas.width, canvas.height); + var inflateBy; + var spikeHeight = [0, 0]; + for (let i = 0; i < 2; i++) { + if (high_effort[i]) { + inflateBy = canvasSettings.inflateByHE; + } else { + inflateBy = canvasSettings.inflateByNHE; + } - // how far should the spike be - var targetDist = 2 * inflateBy * (effort[i] - 1); - var balloonBaseHeight = - canvasSettings.balloonBaseHeight + 2 * canvasSettings.balloonRadius; - // distance of the spike from the top - spikeHeight[i] = effort[i] - ? canvasSettings.frameDimensions[1] - - balloonBaseHeight - - targetDist - - canvasSettings.spiketopHeight - : 0; - } + // how far should the spike be + var targetDist = 2 * inflateBy * (effort[i] - 1); + var balloonBaseHeight = + canvasSettings.balloonBaseHeight + 2 * canvasSettings.balloonRadius; + // distance of the spike from the top + spikeHeight[i] = effort[i] + ? canvasSettings.frameDimensions[1] - + balloonBaseHeight - + targetDist - + canvasSettings.spiketopHeight + : 0; + } - drawText( - ctx, - `${probability}`, - canvasSettings.rewProbXpos, - canvasSettings.rewProbYpos, - "undefined" - ); + drawText( + ctx, + `${probability}`, + canvasSettings.rewProbXpos, + canvasSettings.rewProbYpos, + "undefined" + ); - // drawFrame(ctx, canvasSettings.frameDimensions[0], canvasSettings.frameDimensions[1], canvasSettings.frameXpos[0], canvasSettings.frameYpos, canvasSettings.frameLinecolor, false) - drawEffort( - ctx, - value[0], - effort[0], - canvasSettings.textXpos[0], - canvasSettings.textYpos, - high_effort[0] - ); - drawSpike( - ctx, - canvasSettings.spikeWidth, - spikeHeight[0], - canvasSettings.spikeXpos[0], - canvasSettings.spikeYpos, - canvasSettings.frameLinecolor, - canvasSettings.frameLinecolor, - false - ); + // drawFrame(ctx, canvasSettings.frameDimensions[0], canvasSettings.frameDimensions[1], canvasSettings.frameXpos[0], canvasSettings.frameYpos, canvasSettings.frameLinecolor, false) + drawEffort( + ctx, + value[0], + effort[0], + canvasSettings.textXpos[0], + canvasSettings.textYpos, + high_effort[0] + ); + drawSpike( + ctx, + canvasSettings.spikeWidth, + spikeHeight[0], + canvasSettings.spikeXpos[0], + canvasSettings.spikeYpos, + canvasSettings.frameLinecolor, + canvasSettings.frameLinecolor, + false + ); - // drawFrame(ctx, canvasSettings.frameDimensions[0], canvasSettings.frameDimensions[1], canvasSettings.frameXpos[1], canvasSettings.frameYpos, canvasSettings.frameLinecolor, false) - drawEffort( - ctx, - value[1], - effort[1], - canvasSettings.textXpos[1], - canvasSettings.textYpos, - high_effort[1] - ); - drawSpike( - ctx, - canvasSettings.spikeWidth, - spikeHeight[1], - canvasSettings.spikeXpos[1], - canvasSettings.spikeYpos, - canvasSettings.frameLinecolor, - canvasSettings.frameLinecolor, - false - ); - }; + // drawFrame(ctx, canvasSettings.frameDimensions[0], canvasSettings.frameDimensions[1], canvasSettings.frameXpos[1], canvasSettings.frameYpos, canvasSettings.frameLinecolor, false) + drawEffort( + ctx, + value[1], + effort[1], + canvasSettings.textXpos[1], + canvasSettings.textYpos, + high_effort[1] + ); + drawSpike( + ctx, + canvasSettings.spikeWidth, + spikeHeight[1], + canvasSettings.spikeXpos[1], + canvasSettings.spikeYpos, + canvasSettings.frameLinecolor, + canvasSettings.frameLinecolor, + false + ); + }; + canvasDraw(); + var timer = setInterval(function () { + var now = new Date().getTime(); + var percTimePassed = (now - timeWhenStarted) / 1000 / (duration / 1000); - trialDetails.probability = probability; - trialDetails.effort = effort; - trialDetails.high_effort = high_effort; - trialDetails.value = value; - trialDetails.subtrial_type = "cost_benefits"; + if (percTimePassed >= 1) { + jsPsych.pluginAPI.cancelKeyboardResponse(keyboardListener); + clearInterval(timer); + var returnObj = { + key: 0, + effort: 0, + value: 0, + high_effort: 0, + get_reward: 0, + subtrial_type: "costBenefits", + }; + done(returnObj); + } + }, 50); + function after_response(info) { + clearInterval(timer); + jsPsych.pluginAPI.cancelKeyboardResponse(keyboardListener); + if (info.key === keys["Q"]) { + // 1 key + var timeWhenPressed = new Date().getTime(); + var rt = timeWhenPressed - timeWhenStarted; + var returnObj = { + rt:rt, + key: info.key, + effort: effort[0], + value: value[0], + high_effort: high_effort[0], + get_reward: get_reward[0], + subtrial_type: "costBenefits", + }; + done(returnObj); + } else if (info.key === keys["P"]) { + // 0 key + timeWhenPressed = new Date().getTime(); + rt = timeWhenPressed - timeWhenStarted; + returnObj = { + rt: rt, + key: info.key, + effort: effort[1], + value: value[1], + high_effort: high_effort[1], + get_reward: get_reward[1], + subtrial_type: "costBenefits" + }; + done(returnObj); + } + } - canvasDraw(); - setTimeout(() => { - done(addData(trialDetails, blockSettings, opts)); - }, duration); - }, + var keyboardListener = jsPsych.pluginAPI.getKeyboardResponse({ + callback_function: after_response, + valid_responses: valid_keys, + rt_method: "date", + persist: true, + allow_held_key: false, + }); + + }, on_load: () => { removeCursor("experiment"); pdSpotEncode(startCode); diff --git a/src/trials/rewardFeedback.js b/src/trials/rewardFeedback.js index c9cb428..6260e73 100644 --- a/src/trials/rewardFeedback.js +++ b/src/trials/rewardFeedback.js @@ -15,7 +15,9 @@ const rewardFeedback = (duration, blockSettings, opts, trialDetails) => { let rewards = jsPsych.data.get().select("value").values; let last = rewards[rewards.length - 1]; let stimulus; - if (last) { + console.log("last:", last); + console.log("rewards:", rewards); + if (last) { stimulus = `

+${last.reward.toFixed(2)}

` + photodiodeGhostBox() +