diff --git a/package-lock.json b/package-lock.json index 940e6ed0..637813d1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,21 @@ { - "name": "jspsych-timelines", + "name": "@jspsych/jspsych-timelines", "lockfileVersion": 3, "requires": true, "packages": { "": { + "name": "@jspsych/jspsych-timelines", "workspaces": [ "packages/*", "templates/cli" ], "dependencies": { + "@jspsych/plugin-html-button-response": "^2.1.0", + "@jspsych/test-utils": "^1.2.0", "chokidar": "^3.6.0", "gulp": "^5.0.0", "gulp-replace": "^1.1.4", - "jspsych": "^8.0.2", + "jspsych": "^8.2.1", "run-func": "^3.0.0" }, "devDependencies": { @@ -47,7 +50,6 @@ "version": "7.26.2", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", - "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", "js-tokens": "^4.0.0", @@ -295,7 +297,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", - "dev": true, "engines": { "node": ">=6.9.0" } @@ -2209,7 +2210,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", - "dev": true, "dependencies": { "jest-get-type": "^29.6.3" }, @@ -2312,7 +2312,6 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "dev": true, "dependencies": { "@sinclair/typebox": "^0.27.8" }, @@ -2410,7 +2409,6 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "dev": true, "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", @@ -2427,7 +2425,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -2487,20 +2484,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@jspsych-timelines/arrow-flanker": { - "resolved": "packages/arrow-flanker", - "link": true - }, "node_modules/@jspsych-timelines/cli": { "resolved": "templates/cli", "link": true }, - "node_modules/@jspsych-timelines/false-memory": { - "resolved": "packages/false-memory", - "link": true - }, - "node_modules/@jspsych-timelines/spatial-cueing": { - "resolved": "packages/spatial-cueing", + "node_modules/@jspsych-timelines/flanker-inhibitory": { + "resolved": "packages/flanker-inhibitory", "link": true }, "node_modules/@jspsych/config": { @@ -2572,22 +2561,34 @@ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true }, - "node_modules/@jspsych/plugin-html-keyboard-response": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@jspsych/plugin-html-keyboard-response/-/plugin-html-keyboard-response-2.0.0.tgz", - "integrity": "sha512-O4tfd91ve4RJr0ilHmnk6rdxWX7IwGnOKbFpi85p1t0POUNm21Kfq39pUnyoSZTWr2anibIS3OuHP2IvUtIoMA==", + "node_modules/@jspsych/plugin-html-button-response": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@jspsych/plugin-html-button-response/-/plugin-html-button-response-2.1.0.tgz", + "integrity": "sha512-AhJPePA8s/ount+QYtqfgMtEkz7g+j+bnhQ9XqxfsnK2f5MdnlakYtpjxW9YgFx9y0Gs7yb7OZk1+omcKlzaEQ==", + "license": "MIT", "peerDependencies": { "jspsych": ">=7.1.0" } }, - "node_modules/@jspsych/plugin-survey-text": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@jspsych/plugin-survey-text/-/plugin-survey-text-2.0.0.tgz", - "integrity": "sha512-e30br1zD5umZrUdbhZId/00OIRejkgyExP7CWqLBOvC9i8SfBMrjuxgNeJwFf0gpdglQ+HYwE+jkXBNXdSYszQ==", + "node_modules/@jspsych/plugin-instructions": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@jspsych/plugin-instructions/-/plugin-instructions-2.1.0.tgz", + "integrity": "sha512-jg3bepI9axwenGMNGunmcnnsXn4V2xMhVrzfHUGkA7AP/TQLdS8aeNqwhq3SVy6EXPX8X+r/sr2GKHrXeViRFA==", + "license": "MIT", "peerDependencies": { "jspsych": ">=7.1.0" } }, + "node_modules/@jspsych/test-utils": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@jspsych/test-utils/-/test-utils-1.2.0.tgz", + "integrity": "sha512-3j0n0k6/DLSIz91ngqV/SXfXONnpaBC/McCrXysq6kMS08utzTEX56xKNzEmfY37bQKCijJsBreF6oH9Zss60w==", + "license": "MIT", + "peerDependencies": { + "@types/jest": "*", + "jspsych": ">=7.0.0" + } + }, "node_modules/@manypkg/find-root": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@manypkg/find-root/-/find-root-1.1.0.tgz", @@ -3060,8 +3061,7 @@ "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==" }, "node_modules/@sinonjs/commons": { "version": "3.0.1", @@ -3187,14 +3187,12 @@ "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", - "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", - "dev": true + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==" }, "node_modules/@types/istanbul-lib-report": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", - "dev": true, "dependencies": { "@types/istanbul-lib-coverage": "*" } @@ -3203,7 +3201,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", - "dev": true, "dependencies": { "@types/istanbul-lib-report": "*" } @@ -3212,7 +3209,6 @@ "version": "29.5.12", "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.12.tgz", "integrity": "sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==", - "dev": true, "dependencies": { "expect": "^29.0.0", "pretty-format": "^29.0.0" @@ -3269,8 +3265,7 @@ "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", - "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", - "dev": true + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==" }, "node_modules/@types/streamx": { "version": "2.9.5", @@ -3333,7 +3328,6 @@ "version": "17.0.33", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", - "dev": true, "dependencies": { "@types/yargs-parser": "*" } @@ -3341,8 +3335,7 @@ "node_modules/@types/yargs-parser": { "version": "21.0.3", "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", - "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", - "dev": true + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==" }, "node_modules/abab": { "version": "2.0.6", @@ -4044,6 +4037,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bundle-require": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-4.2.1.tgz", + "integrity": "sha512-7Q/6vkyYAwOmQNRw75x+4yRtZCZJXUDmHHlFdkiV0wgv/reNjtJwpu1jPJ0w2kbEpIM0uoKI3S4/f39dU7AjSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "load-tsconfig": "^0.2.3" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "esbuild": ">=0.17" + } + }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -4193,7 +4202,6 @@ "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", - "dev": true, "funding": [ { "type": "github", @@ -4818,7 +4826,6 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } @@ -5144,7 +5151,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", - "dev": true, "dependencies": { "@jest/expect-utils": "^29.7.0", "jest-get-type": "^29.6.3", @@ -7245,7 +7251,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", - "dev": true, "dependencies": { "chalk": "^4.0.0", "diff-sequences": "^29.6.3", @@ -7260,7 +7265,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -7364,7 +7368,6 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", - "dev": true, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } @@ -7411,7 +7414,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", - "dev": true, "dependencies": { "chalk": "^4.0.0", "jest-diff": "^29.7.0", @@ -7426,7 +7428,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -7442,7 +7443,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", - "dev": true, "dependencies": { "@babel/code-frame": "^7.12.13", "@jest/types": "^29.6.3", @@ -7462,7 +7462,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -7726,7 +7725,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", - "dev": true, "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", @@ -7743,7 +7741,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -7877,8 +7874,7 @@ "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "node_modules/js-yaml": { "version": "3.14.1", @@ -8063,9 +8059,10 @@ } }, "node_modules/jspsych": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/jspsych/-/jspsych-8.0.2.tgz", - "integrity": "sha512-R7Ve/Gl28PCEGLN4u5y/FA9fgVquJuyrPMxsVMyUfVtGZK2LB/XoyDbln9pHvAkJFmNcOS34kKnq08QbSiOk3w==", + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/jspsych/-/jspsych-8.2.1.tgz", + "integrity": "sha512-VpAxmkzj72/Y2eTKXt5ksZGFL7VqvgUL+66pUqm7KPUSZsIOSM1rKCtMN42u3fT2N8EonR7LGFFcsAvnZAOykQ==", + "license": "MIT", "dependencies": { "auto-bind": "^4.0.0", "random-words": "^1.1.1", @@ -9120,8 +9117,7 @@ "node_modules/picocolors": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", - "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", - "dev": true + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==" }, "node_modules/picomatch": { "version": "2.3.1", @@ -9173,6 +9169,46 @@ "semver-compare": "^1.0.0" } }, + "node_modules/postcss-load-config": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", + "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lilconfig": "^2.0.5", + "yaml": "^1.10.2" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-load-config/node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/prettier": { "version": "2.8.8", "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", @@ -9207,7 +9243,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", @@ -9221,7 +9256,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, "engines": { "node": ">=10" }, @@ -9325,8 +9359,7 @@ "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" }, "node_modules/read-yaml-file": { "version": "1.1.0", @@ -9895,7 +9928,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, "engines": { "node": ">=8" } @@ -10028,7 +10060,6 @@ "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", - "dev": true, "dependencies": { "escape-string-regexp": "^2.0.0" }, @@ -10040,7 +10071,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "dev": true, "engines": { "node": ">=8" } @@ -11571,6 +11601,7 @@ "packages/arrow-flanker": { "name": "@jspsych-timelines/arrow-flanker", "version": "0.2.0", + "extraneous": true, "license": "MIT", "dependencies": { "@jspsych/plugin-html-keyboard-response": "^2.0.0" @@ -11586,6 +11617,7 @@ "packages/false-memory": { "name": "@jspsych-timelines/false-memory", "version": "0.2.0", + "extraneous": true, "license": "MIT", "dependencies": { "@jspsych/plugin-html-keyboard-response": "^2.0.0", @@ -11599,9 +11631,566 @@ "jspsych": "^8.0.1" } }, + "packages/flanker-inhibitory": { + "name": "@jspsych-timelines/flanker-inhibitory", + "version": "0.0.1", + "license": "MIT", + "dependencies": { + "@jspsych/plugin-html-button-response": "^2.0.0", + "@jspsych/plugin-instructions": "^2.0.0" + }, + "devDependencies": { + "@jspsych/config": "^3.2.2", + "jspsych": "^8.0.0", + "tsup": "^6.7.0", + "typescript": "^5.0.2" + } + }, + "packages/flanker-inhibitory/node_modules/@esbuild/android-arm": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz", + "integrity": "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "packages/flanker-inhibitory/node_modules/@esbuild/android-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz", + "integrity": "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "packages/flanker-inhibitory/node_modules/@esbuild/android-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz", + "integrity": "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "packages/flanker-inhibitory/node_modules/@esbuild/darwin-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz", + "integrity": "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "packages/flanker-inhibitory/node_modules/@esbuild/darwin-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz", + "integrity": "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "packages/flanker-inhibitory/node_modules/@esbuild/freebsd-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz", + "integrity": "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "packages/flanker-inhibitory/node_modules/@esbuild/freebsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz", + "integrity": "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "packages/flanker-inhibitory/node_modules/@esbuild/linux-arm": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz", + "integrity": "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "packages/flanker-inhibitory/node_modules/@esbuild/linux-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz", + "integrity": "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "packages/flanker-inhibitory/node_modules/@esbuild/linux-ia32": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz", + "integrity": "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "packages/flanker-inhibitory/node_modules/@esbuild/linux-loong64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz", + "integrity": "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "packages/flanker-inhibitory/node_modules/@esbuild/linux-mips64el": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz", + "integrity": "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "packages/flanker-inhibitory/node_modules/@esbuild/linux-ppc64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz", + "integrity": "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "packages/flanker-inhibitory/node_modules/@esbuild/linux-riscv64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz", + "integrity": "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "packages/flanker-inhibitory/node_modules/@esbuild/linux-s390x": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz", + "integrity": "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "packages/flanker-inhibitory/node_modules/@esbuild/linux-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz", + "integrity": "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "packages/flanker-inhibitory/node_modules/@esbuild/netbsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz", + "integrity": "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "packages/flanker-inhibitory/node_modules/@esbuild/openbsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz", + "integrity": "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "packages/flanker-inhibitory/node_modules/@esbuild/sunos-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz", + "integrity": "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "packages/flanker-inhibitory/node_modules/@esbuild/win32-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz", + "integrity": "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "packages/flanker-inhibitory/node_modules/@esbuild/win32-ia32": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz", + "integrity": "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "packages/flanker-inhibitory/node_modules/@esbuild/win32-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz", + "integrity": "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "packages/flanker-inhibitory/node_modules/esbuild": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz", + "integrity": "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.17.19", + "@esbuild/android-arm64": "0.17.19", + "@esbuild/android-x64": "0.17.19", + "@esbuild/darwin-arm64": "0.17.19", + "@esbuild/darwin-x64": "0.17.19", + "@esbuild/freebsd-arm64": "0.17.19", + "@esbuild/freebsd-x64": "0.17.19", + "@esbuild/linux-arm": "0.17.19", + "@esbuild/linux-arm64": "0.17.19", + "@esbuild/linux-ia32": "0.17.19", + "@esbuild/linux-loong64": "0.17.19", + "@esbuild/linux-mips64el": "0.17.19", + "@esbuild/linux-ppc64": "0.17.19", + "@esbuild/linux-riscv64": "0.17.19", + "@esbuild/linux-s390x": "0.17.19", + "@esbuild/linux-x64": "0.17.19", + "@esbuild/netbsd-x64": "0.17.19", + "@esbuild/openbsd-x64": "0.17.19", + "@esbuild/sunos-x64": "0.17.19", + "@esbuild/win32-arm64": "0.17.19", + "@esbuild/win32-ia32": "0.17.19", + "@esbuild/win32-x64": "0.17.19" + } + }, + "packages/flanker-inhibitory/node_modules/rollup": { + "version": "3.29.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz", + "integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==", + "dev": true, + "license": "MIT", + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "packages/flanker-inhibitory/node_modules/source-map": { + "version": "0.8.0-beta.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", + "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "whatwg-url": "^7.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "packages/flanker-inhibitory/node_modules/tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "packages/flanker-inhibitory/node_modules/tsup": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/tsup/-/tsup-6.7.0.tgz", + "integrity": "sha512-L3o8hGkaHnu5TdJns+mCqFsDBo83bJ44rlK7e6VdanIvpea4ArPcU3swWGsLVbXak1PqQx/V+SSmFPujBK+zEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-require": "^4.0.0", + "cac": "^6.7.12", + "chokidar": "^3.5.1", + "debug": "^4.3.1", + "esbuild": "^0.17.6", + "execa": "^5.0.0", + "globby": "^11.0.3", + "joycon": "^3.0.1", + "postcss-load-config": "^3.0.1", + "resolve-from": "^5.0.0", + "rollup": "^3.2.5", + "source-map": "0.8.0-beta.0", + "sucrase": "^3.20.3", + "tree-kill": "^1.2.2" + }, + "bin": { + "tsup": "dist/cli-default.js", + "tsup-node": "dist/cli-node.js" + }, + "engines": { + "node": ">=14.18" + }, + "peerDependencies": { + "@swc/core": "^1", + "postcss": "^8.4.12", + "typescript": ">=4.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "postcss": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "packages/flanker-inhibitory/node_modules/webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true, + "license": "BSD-2-Clause" + }, + "packages/flanker-inhibitory/node_modules/whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, + "packages/pattern-comparison-task": { + "name": "@jspsych-timelines/pattern-comparison-task", + "version": "0.0.1", + "extraneous": true, + "license": "MIT", + "devDependencies": { + "@jspsych/config": "^3.2.2", + "jspsych": "^8.0.0", + "tsup": "^6.7.0", + "typescript": "^5.0.2" + } + }, + "packages/pattern-comparison-test": { + "name": "@jspsych-timelines/pattern-comparison-test", + "version": "0.0.1", + "extraneous": true, + "license": "MIT", + "devDependencies": { + "@jspsych/config": "^3.2.2", + "jspsych": "^8.0.0", + "tsup": "^6.7.0", + "typescript": "^5.0.2" + } + }, "packages/spatial-cueing": { "name": "@jspsych-timelines/spatial-cueing", "version": "0.2.0", + "extraneous": true, "license": "MIT", "devDependencies": { "tsup": "^8.2.4", @@ -11611,6 +12200,42 @@ "jspsych": "^8.0.1" } }, + "packages/spatial-nback": { + "name": "@jspsych-timelines/spatial-nback", + "version": "0.0.2", + "extraneous": true, + "license": "MIT", + "dependencies": { + "@jspsych-contrib/plugin-spatial-nback": "https://github.com/farhat60/jspsych-contrib/tree/main/", + "@jspsych/plugin-instructions": "^2.1.0", + "jsPsych": "npm:jspsych@^8.2.1" + }, + "devDependencies": { + "@jspsych/config": "^3.2.2", + "jspsych": "^8.2.1", + "tsup": "^6.7.0", + "typescript": "^5.0.2" + } + }, + "packages/spatial-nback-task": { + "version": "0.0.1", + "extraneous": true, + "license": "MIT", + "dependencies": { + "@jspsych/plugin-html-button-response": "^2.1.0", + "@jspsych/plugin-html-keyboard-response": "^2.1.0", + "jsPsych": "npm:jspsych@^8.2.1" + }, + "devDependencies": { + "@jspsych/config": "^3.2.2", + "jspsych": "^8.2.1", + "tsup": "^6.7.0", + "typescript": "^5.0.2" + } + }, + "packages/spatial-nback/git@https:/github.com/farhat60/jspsych-contrib/packages/plugin-spatial-nback": { + "extraneous": true + }, "templates/cli": { "name": "@jspsych-timelines/cli", "version": "0.2.0", diff --git a/package.json b/package.json index 1429c510..24f6b70d 100644 --- a/package.json +++ b/package.json @@ -50,10 +50,12 @@ ] }, "dependencies": { + "@jspsych/plugin-html-button-response": "^2.1.0", + "@jspsych/test-utils": "^1.2.0", "chokidar": "^3.6.0", "gulp": "^5.0.0", "gulp-replace": "^1.1.4", - "jspsych": "^8.0.2", + "jspsych": "^8.2.1", "run-func": "^3.0.0" } } diff --git a/packages/arrow-flanker/CHANGELOG.md b/packages/arrow-flanker/CHANGELOG.md deleted file mode 100644 index f53204c9..00000000 --- a/packages/arrow-flanker/CHANGELOG.md +++ /dev/null @@ -1,15 +0,0 @@ -# @jspsych-timelines/arrow-flanker - -## 0.2.0 - -### Minor Changes - -- 22c0236: delete -task suffix - -## 0.1.0 - -### Minor Changes - -- d6a0e3b: upgrade jspsych depedencies to v8 -- 0de5056: update all jspsych dependencies to v8 -- 0b18198: upgrade to use jspsych v8 diff --git a/packages/arrow-flanker/README.md b/packages/arrow-flanker/README.md deleted file mode 100644 index ce7ee295..00000000 --- a/packages/arrow-flanker/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# arrow-flanker - -## Overview - -This timeline shows a sequence of arrow flanker trials. Participants are supposed to respond to the arrow in the middle of the screen and ignore the flankers. Half of the trials will be congruent (flankers match the target) and half incongruent. - -## Functions - -### `createTimeline` - -### `timelineUnits` - -### `utils` \ No newline at end of file diff --git a/packages/arrow-flanker/examples/index.html b/packages/arrow-flanker/examples/index.html deleted file mode 100644 index 84cb5f2e..00000000 --- a/packages/arrow-flanker/examples/index.html +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/arrow-flanker/examples/load-from-unpkg.html b/packages/arrow-flanker/examples/load-from-unpkg.html deleted file mode 100644 index 75f63496..00000000 --- a/packages/arrow-flanker/examples/load-from-unpkg.html +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/arrow-flanker/jest.config.cjs b/packages/arrow-flanker/jest.config.cjs deleted file mode 100644 index 6ac19d5c..00000000 --- a/packages/arrow-flanker/jest.config.cjs +++ /dev/null @@ -1 +0,0 @@ -module.exports = require("@jspsych/config/jest").makePackageConfig(__dirname); diff --git a/packages/arrow-flanker/package.json b/packages/arrow-flanker/package.json deleted file mode 100644 index a61bcad6..00000000 --- a/packages/arrow-flanker/package.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "name": "@jspsych-timelines/arrow-flanker", - "version": "0.2.0", - "description": "Arrow flanker task for jsPsych", - "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 jsPsychTimelineArrowFlankerTask" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/jspsych/jspsych-timelines.git" - }, - "keywords": [ - "jsPsych" - ], - "author": { - "name": "Josh de Leeuw", - "url": "https://github.com/jodeleeuw" - }, - "license": "MIT", - "bugs": { - "url": "https://github.com/jspsych/jspsych-timelines/issues" - }, - "homepage": "https://github.com/jspsych/jspsych-timelines/packages/arrow-flanker#readme", - "peerDependencies": { - "jspsych": "^8.0.1" - }, - "dependencies": { - "@jspsych/plugin-html-keyboard-response": "^2.0.0" - }, - "devDependencies": { - "tsup": "^8.0.1", - "typescript": "^5.5.4" - } -} \ No newline at end of file diff --git a/packages/arrow-flanker/src/index.spec.ts b/packages/arrow-flanker/src/index.spec.ts deleted file mode 100644 index 29debd29..00000000 --- a/packages/arrow-flanker/src/index.spec.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { JsPsych, initJsPsych } from "jspsych"; -import { createTimeline } from "."; - -describe("createTimeline", () => { - it("should return a timeline", () => { - const jsPsych = initJsPsych(); - - const timeline = createTimeline(jsPsych); - expect(timeline).toBeDefined(); - }); -}); \ No newline at end of file diff --git a/packages/arrow-flanker/src/index.ts b/packages/arrow-flanker/src/index.ts deleted file mode 100644 index 120bc103..00000000 --- a/packages/arrow-flanker/src/index.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { JsPsych } from "jspsych"; -import jsPsychHtmlKeyboardResponse from '@jspsych/plugin-html-keyboard-response' - -const left_arrow = ``; -const right_arrow = ``; - -function createFlankerStim(direction, congruent) { - let html = `
`; - if (congruent) { - if (direction === "left") { - html += ` ${left_arrow} ${left_arrow} ${left_arrow} ${left_arrow} ${left_arrow} `; - } else { - html += ` ${right_arrow} ${right_arrow} ${right_arrow} ${right_arrow} ${right_arrow} `; - } - } else { - if (direction === "left") { - html += ` ${right_arrow} ${right_arrow} ${left_arrow} ${right_arrow} ${right_arrow} `; - } else { - html += ` ${left_arrow} ${left_arrow} ${right_arrow} ${left_arrow} ${left_arrow} `; - } - } - html += `
`; - return html; -} - -export function createTimeline(jsPsych: JsPsych, { - fixation_duration = 500, - n = 12, -} : { - fixation_duration?: number, - n?: number, -} = {}){ - - const timeline_variables = [ - {direction: 'left', congruent: true}, - {direction: 'left', congruent: false}, - {direction: 'right', congruent: true}, - {direction: 'right', congruent: false}, - ] - - const trials = jsPsych.randomization.repeat(timeline_variables, Math.floor(n/4)).concat(jsPsych.randomization.sampleWithoutReplacement(timeline_variables, n%4)); - - const fixation = { - type: jsPsychHtmlKeyboardResponse, - stimulus: '+', - choices: "NONE", - trial_duration: fixation_duration, - } - - const flanker = { - type: jsPsychHtmlKeyboardResponse, - stimulus: ()=>{ - return createFlankerStim(jsPsych.timelineVariable('direction'), jsPsych.timelineVariable('congruent')) - }, - choices: ['ArrowLeft', 'ArrowRight'], - } - - const flanker_task = { - timeline: [fixation, flanker], - timeline_variables: trials, - randomize_order: true - } - - return flanker_task; -} \ No newline at end of file diff --git a/packages/arrow-flanker/tsconfig.json b/packages/arrow-flanker/tsconfig.json deleted file mode 100644 index 3eabd0c2..00000000 --- a/packages/arrow-flanker/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "@jspsych/config/tsconfig.contrib.json", - "compilerOptions": { - "baseUrl": "." - }, - "include": ["src"] -} diff --git a/packages/false-memory/CHANGELOG.md b/packages/false-memory/CHANGELOG.md deleted file mode 100644 index 53a50af8..00000000 --- a/packages/false-memory/CHANGELOG.md +++ /dev/null @@ -1,15 +0,0 @@ -# @jspsych-timelines/false-memory - -## 0.2.0 - -### Minor Changes - -- 22c0236: delete -task suffix - -## 0.1.0 - -### Minor Changes - -- d6a0e3b: upgrade jspsych depedencies to v8 -- 0de5056: update all jspsych dependencies to v8 -- 0b18198: upgrade to use jspsych v8 diff --git a/packages/false-memory/README.md b/packages/false-memory/README.md deleted file mode 100644 index d23edb34..00000000 --- a/packages/false-memory/README.md +++ /dev/null @@ -1,49 +0,0 @@ -# false-memory - -## Overview - -A timeline for the generic DRM false memory task - -## Loading - -### In browser - -```html - - - - - - - - - - - diff --git a/packages/false-memory/package.json b/packages/false-memory/package.json deleted file mode 100644 index 920bce7e..00000000 --- a/packages/false-memory/package.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "name": "@jspsych-timelines/false-memory", - "version": "0.2.0", - "description": "False memory task for jsPsych", - "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 jsPsychTimelineFalseMemoryTask" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/jspsych/jspsych-timelines.git" - }, - "keywords": [ - "jsPsych" - ], - "author": { - "name": "Cherrie Chang", - "url": "https://github.com/cchang-vassar" - }, - "license": "MIT", - "bugs": { - "url": "https://github.com/jspsych/jspsych-timelines/issues" - }, - "homepage": "https://github.com/jspsych/jspsych-timelines/packages/false-memory#readme", - "peerDependencies": { - "jspsych": "^8.0.1" - }, - "dependencies": { - "@jspsych/plugin-html-keyboard-response": "^2.0.0", - "@jspsych/plugin-survey-text": "^2.0.0" - }, - "devDependencies": { - "tsup": "^8.2.4", - "typescript": "^5.5.4" - } -} diff --git a/packages/false-memory/src/index.ts b/packages/false-memory/src/index.ts deleted file mode 100644 index 42d6a842..00000000 --- a/packages/false-memory/src/index.ts +++ /dev/null @@ -1,451 +0,0 @@ -import { JsPsych } from "jspsych"; -import jsPsychHtmlKeyboardResponse from '@jspsych/plugin-html-keyboard-response'; -import jsPsychSurveyText from '@jspsych/plugin-survey-text'; - -/* constants */ -const NUM_LIST_CEILING: number = 12; -const NUM_WORDS_IN_LIST_CEILING: number = 15; -const ALL_LIST_KENT_ROSANOFF_INDEX: number[] = [32, 45, 97, 89, 26, 53, 33, 42, 63, 44, 7, 85]; //randomly chosen -const ALL_LIST_INDEX_ARRAY: number[] = Array.from({length: 12}, (_, index) => index + 1); -const KEYPRESS_TO_NEXT_SLIDE: String = 'Press any key to move to the next slide.'; - -const ALL_LIST = [ - { - lure: "citizen", - word_list: ["USA", "man", "person", "American", "country", "alien", "people", "state", "votes", "patriot", "flag", "voter", "foreigner", "government", "America"] - }, - { - lure: "trouble", - word_list: ["bad", "shooter", "worry", "danger", "sorrow", "fear", "school", "problem", "police", "fight", "sad", "happy", "difficulty", "peace", "hard"] - }, - { - lure: "king", - word_list: ["queen", "England", "crown", "pin", "ruler", "Kong", "throne", "cards", "Tut", "rule", "subjects", "chess", "royal", "power", "county"] - }, - { - lure: "mutton", - word_list: ["lamb", "sheep", "meat", "chops", "beef", "veal", "collar", "stew", "coat", "steak", "fur", "pork", "dog", "wool", "animal"] - }, - { - lure: "whistle", - word_list: ["stop", "train", "noise", "sing", "blow", "tune", "sound", "shrill", "lips", "wolf", "loud", "call", "mouth", "cop", "horn"] - }, - { - lure: "street", - word_list: ["Avenue", "oars", "lights", "city", "alley", "walk", "sign", "houses", "corner", "sidewalk", "town", "number", "scene", "drive", "name"] - }, - { - lure: "foot", - word_list: ["shoes", "hand", "toes", "soldier", "feet", "ball", "ankle", "arm", "sore", "inches", "boot", "yard", "socks", "head", "hurt"] - }, - { - lure: "working", - word_list: ["loafing", "sleeping", "playing", "resting", "labor", "lazy", "job", "tired", "hours", "rest", "fast", "busy", "pay", "doing", "studying"] - }, - { - lure: "stomach", - word_list: ["ache", "intestine", "ulcer", "body", "organ", "pains", "hunger", "belly", "pump", "full", "digestion", "gut", "sick", "abdomen", "empty"] - }, - { - lure: "table", - word_list: ["chair", "desk", "top", "legs", "eat", "cloth", "dishes", "wood", "dinner", "tennis", "maple", "door", "cup", "room", "fork"] - }, - { - lure: "bath", - word_list: ["clean", "soap", "tub", "wash", "shower", "dirty", "dirt", "warm", "cleanliness", "hot", "towel", "cold", "bathtub", "bubble", "bathe"] - }, - { - lure: "lion", - word_list: ["tiger", "roar", "cub", "den", "zoo", "jungle", "cage", "tamer", "mouse", "bear", "hearted", "mane", "Africa", "Leo", "growl"] - } -]; - -/* internal vars */ -var list_studied = []; -var test_stimuli = []; -var weights: number[] = []; - -var list_num = 1; -var word_num = 0; - -/* internal functions */ -// helper function that gives error message if the studied and unstudied lists overlap -function checkListOverflowOverlap(list_studied_index_array: number[], list_unstudied_index_array: number[]) { - if ((list_studied_index_array.length + list_unstudied_index_array.length) > NUM_LIST_CEILING) { - //TODO: some kind of warning message - }; - let listIntersection = list_studied_index_array.filter(x => list_unstudied_index_array.includes(x)); - if (listIntersection.length > 0) { - //TODO: some kind of warning message - }; -}; - -// helper function to check if input number of words in lists exceed ceiling -function checkNumWordsOverflow(input_num_words: number) { - if (input_num_words > NUM_WORDS_IN_LIST_CEILING) { - //TODO: some kind of warning message - }; -}; - -function pushTestStimuliStudied(num_studied_lists: number, num_words_in_list: number, list_studied_index_array: number[], test_all_studied_lures: boolean){ - for (let i = 0; i < num_studied_lists; i++){ - var studied_list_to_add = ALL_LIST[list_studied_index_array[i] - 1]; - list_studied.push(studied_list_to_add); - test_stimuli.push({ - stimulus: studied_list_to_add.lure, - correct_response: 'j' - }); - weights.push(test_all_studied_lures ? 99999 : 1); - for (let j = 0; j < num_words_in_list; j++){ - test_stimuli.push({ - stimulus: studied_list_to_add.word_list[j], - correct_response: 'f' - }) - weights.push(1); - } - }; -}; - -function pushTestStimuliUnstudied(num_unstudied_lists: number, num_words_in_list: number, list_unstudied_index_array: number[], test_all_unstudied_lures: boolean){ - for (let i = 0; i < num_unstudied_lists; i++){ - var unstudied_list_to_add = ALL_LIST[list_unstudied_index_array[i] - 1]; - test_stimuli.push({ - stimulus: unstudied_list_to_add.lure, - correct_response: 'j' - }); - weights.push(test_all_unstudied_lures ? 99999 : 1); - for (let j = 0; j < num_words_in_list; j++){ - test_stimuli.push({ - stimulus: unstudied_list_to_add.word_list[j], - correct_response: 'f' - }) - weights.push(1); - }; - }; -}; - -/* timeline component generating functions */ -// generate start instructions -function showStartInstruction(num_studied_lists: number, num_words_in_list: number, recall_period: number) { - const stimulus = `

This is a demo of the DRM false memory experiment paradigm.

` - + `

In the following trials, you will learn` - + num_studied_lists - + `lists of words, with each list containing` - + num_words_in_list - + `words.
` - + `The words in each list will be presented one by one.

` - + `

After each list is presented, you will be given an empty text box and` - + (recall_period / 1000) - + `seconds to freely recall as many words you can remember from the list as possible.` - + `What you write in this text box will not be recorded, but please do not try to capture the words you recall in this box by taking a screenshot, writing them down on paper, etc.

` - + `

Try to remember as many words from all the lists as possible!

` - + `

` + KEYPRESS_TO_NEXT_SLIDE + `

`; - - const introduction = { - type: jsPsychHtmlKeyboardResponse, - stimulus: () => { - return stimulus; - } - }; - - return introduction; -}; - -// generate a study trial for a list -function createListStudyTimelineUnit( - jsPsych: JsPsych, - num_words_in_list: number, - word_show_period: number, - list_title_card_period: number, - list_end_card_period: number, - recall_period: number, - allow_recall: boolean) { - - // study through list of words in a single list - const study_trial = { - type: jsPsychHtmlKeyboardResponse, - stimulus: () => { - return "

" + jsPsych.timelineVariable("word_list")[word_num] + "

"; - }, - choices: "NO_KEYS", - trial_duration: word_show_period - }; - - const word_study_timeline = { - timeline: [study_trial], - timeline_variables: jsPsych.timelineVariable("word_list")[list_num], - loop_function: () => { - word_num++; - return word_num < num_words_in_list; - } - }; - - // wrap single list study with title cards and recall period - const list_start_card = { - type: jsPsychHtmlKeyboardResponse, - stimulus: () => { - return "

List " + list_num + "

"; - }, - choices: "NO_KEYS", - trial_duration: list_title_card_period - }; - - const list_end_card = { - type: jsPsychHtmlKeyboardResponse, - stimulus: () => { - return "

End of List " + list_num + "

"; - }, - choices: "NO_KEYS", - on_finish: () => { - list_num++; - word_num = 0; - }, - trial_duration: list_end_card_period - }; - - const recall = { - type: jsPsychSurveyText, - on_start: () => { - jsPsych.pluginAPI.setTimeout(() => { - let element : HTMLFormElement = document.querySelector('#jspsych-survey-text-form'); - element.requestSubmit(); - }, recall_period) - }, - questions: [ - { prompt: 'Use this space to recall as many words as you can from the previous list.
Please do not try to capture what you type.', rows: 5 } - ] - }; - - const single_list_study_timeline = { - timeline: [ - list_start_card, - word_study_timeline, - list_end_card, - (allow_recall? recall : null) - ] - }; - - return single_list_study_timeline; -}; - -// generate a series of study trials for a list of lists -function createListStudyTimeline( - jsPsych: JsPsych, - num_studied_lists: number, - num_words_in_list: number, - word_show_period: number, - list_title_card_period: number, - list_end_card_period: number, - recall_period: number, - allow_recall: boolean) { - const all_list_study_timeline = { - timeline: [createListStudyTimelineUnit( - jsPsych, - num_words_in_list, - word_show_period, - list_title_card_period, - list_end_card_period, - recall_period, - allow_recall - )], - timeline_variables: list_studied, - randomize_order: true, - sample: { - type: "without-replacement", - size: num_studied_lists - } - }; - - return all_list_study_timeline; -}; - -// generate instruction for break section -function showBreakTimelineInstruction(break_period: number) { - const break_instruction = { - type: jsPsychHtmlKeyboardResponse, - stimulus: () => { - var stimulus = `

That's all the lists!

` - + `

Now, take a short ` - + (break_period / 1000 / 60) - + ` minute break.
` - + `The experiment will automatically start after the timer reaches 0.` - + `

` - + KEYPRESS_TO_NEXT_SLIDE - + `

`; - - return stimulus; - } - }; -} - -// generate a break timer -function createBreakTimeline(break_period: number) { - const break_timer = { - type: jsPsychHtmlKeyboardResponse, - stimulus: () => { - var break_period_min = Math.floor(break_period / 60000); - var break_period_s = ((break_period / 1000) % 60); - return "

" + break_period_min + ":" + ((break_period_s < 10) ? "0" : "") + break_period_s + "

"; - }, - choices: "NO_KEYS", - trial_duration: 1000 - }; - - const break_timer_wrapper_timeline = { - timeline: [break_timer], - loop_function: () => { - break_period -= 1000; - return break_period >= 0; - } - }; - - const break_timeline = { - timeline: [ - break_timer_wrapper_timeline - ] - }; - - return break_timeline; -}; - -// function that generates the instructions for the test section -function showTestTimelineInstruction() { - const test_instruction = { - type: jsPsychHtmlKeyboardResponse, - stimulus: `
-

Welcome back!

-

Now, you will be shown a series of words.
- Some are words that were in the lists you just studied, and some are not.
- For each word, press 'f' if you remember seeing it in one of the lists;
- and press 'j' if you do not.

-

${KEYPRESS_TO_NEXT_SLIDE}

` - }; - - return test_instruction; -} - -// generate a series of test trials for all studied/unstudied lures + words -function createTestTimeline(jsPsych: JsPsych, test_with_replacement: boolean, num_tests: number) { - const test_trial_timeline = { - timeline: [ - { - type: jsPsychHtmlKeyboardResponse, - stimulus: () => { - return "

" + jsPsych.timelineVariable("stimulus") + "

"; - }, - choices: ['f', 'j'], - data: { - task: "test_response", - correct_response: jsPsych.timelineVariable("correct_response"), - }, - on_finish: function (data) { - data.correct = jsPsych.pluginAPI.compareKeys( - data.response, - data.correct_response - ); - } - } - ], - timeline_variables: test_stimuli, - sample: { - type: (test_with_replacement ? 'with-replacement' : 'without-replacement'), - size: num_tests, - weights: weights - } - }; - - return test_trial_timeline; -}; - -// generate the end instruction -function showEndInstruction() { - const end_experiment_instruction = { - type: jsPsychHtmlKeyboardResponse, - stimulus: `
-

That's the end of the experiment!

-

Thank you so much for participating in this demo of the DRM false memory experiment.
- Goodbye!

- ` - }; - - return end_experiment_instruction; -} - -// generate entire false memory task timeline -export function createTimeline( - jsPsych: JsPsych, { - list_studied_index_array = [1, 2, 3, 4, 5, 6], // how to give option of letting these take the default value? - list_unstudied_index_array = [7, 8, 9, 10, 11, 12], - num_words_in_list = NUM_WORDS_IN_LIST_CEILING, - num_tests = 90, - list_title_card_period = 3000, - list_end_card_period = 3000, - word_show_period = 2000, - recall_period = 30000, - break_period = 120000, - test_all_studied_lures = true, - test_all_unstudied_lures = true, - allow_recall = true, - test_with_replacement = false - }: { - list_studied_index_array?: number[], - list_unstudied_index_array?: number[], - num_words_in_list?: number, - num_tests?: number, - list_title_card_period?: number, - list_end_card_period?: number, - word_show_period?: number, - recall_period?: number, - break_period?: number, - test_all_studied_lures?: boolean, - test_all_unstudied_lures?: boolean, - allow_recall?: boolean, - test_with_replacement?: boolean - } = {}) -{ - var num_studied_lists: number = list_studied_index_array.length; - var num_unstudied_lists: number = list_unstudied_index_array.length; - var num_tests: number = (num_studied_lists + num_unstudied_lists) * (num_words_in_list + 1); - - checkListOverflowOverlap(list_studied_index_array, list_unstudied_index_array); - checkNumWordsOverflow(num_words_in_list); - - pushTestStimuliStudied(num_studied_lists, num_words_in_list, list_studied_index_array, test_all_studied_lures); - pushTestStimuliUnstudied(num_unstudied_lists, num_words_in_list, list_unstudied_index_array, test_all_unstudied_lures); - - const false_memory_timeline = { - timeline: [ - createListStudyTimeline( - jsPsych, - num_studied_lists, - num_words_in_list, - word_show_period, - list_title_card_period, - list_end_card_period, - recall_period, - allow_recall), - createBreakTimeline(break_period), - createTestTimeline(jsPsych, test_with_replacement, num_tests), - ] - }; - - return false_memory_timeline; -}; - -export const timelineUnits = { - createListStudyTimeline, - createListStudyTimelineUnit, - createBreakTimeline, - createTestTimeline -}; - -export const utils = { - checkListOverflowOverlap, - checkNumWordsOverflow, - pushTestStimuliStudied, - pushTestStimuliUnstudied, - showStartInstruction, - showBreakTimelineInstruction, - showTestTimelineInstruction, - showEndInstruction -} \ No newline at end of file diff --git a/packages/false-memory/src/styles.css b/packages/false-memory/src/styles.css deleted file mode 100644 index 69b2949d..00000000 --- a/packages/false-memory/src/styles.css +++ /dev/null @@ -1,3 +0,0 @@ -.jspsych-false-memory-instruction { - max-width: 50vw; -} \ No newline at end of file diff --git a/packages/flanker-inhibitory/README.md b/packages/flanker-inhibitory/README.md new file mode 100644 index 00000000..24d2b048 --- /dev/null +++ b/packages/flanker-inhibitory/README.md @@ -0,0 +1,107 @@ +# flanker-inhibitory + +## Overview + +The Flanker Inhibitory Control and Attention test is an assessment of inhibitory control and selective attention based on the NIH Toolbox Flanker task. Participants respond to the direction of a central stimulus while ignoring flanking stimuli that may be congruent or incongruent with the target. + +## Loading + +### In browser + +```html + +``` + +### Via NPM + +```bash +npm install @jspsych-timelines/flanker-inhibitory +``` + +```js +import { createTimeline, timelineUnits, utils } from "@jspsych-timelines/flanker-inhibitory" +``` + +## Compatibility + +`@jspsych-timelines/flanker-inhibitory` requires jsPsych v8.0.0 or later. + +## Documentation + +### createTimeline + +#### jsPsychTimelineFlankerInhibitory.createTimeline(jsPsych, { *options* }) ⇒ timeline + +Creates a complete flanker task timeline with instructions, practice trials, and main task trials. The task uses layered fish and arrow stimuli by default, with configurable parameters for customization. + +The following parameters can be specified in the **options** parameter. + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| stimuli_type | string | 'layered' | Type of stimuli to use: 'fish', 'arrow', 'layered', or 'custom' | +| svg | array | undefined | Override parameter - array of SVGs to layer (should be right-facing; left version auto-generated) | +| stimuli_amount | number | 5 | Number of stimuli in flanker display (must be odd ≥3) | +| fixation_duration | number | 500 | Duration of fixation cross in milliseconds | +| stimulus_duration | number | undefined | Maximum time for stimulus display before auto-advancing (undefined = no timeout) | +| show_instructions | boolean | true | Whether to show instruction screens | +| show_practice | boolean | true | Whether to include practice trials | +| num_practice | number | 8 | Number of practice trials | +| num_trials | number | 20 | Number of main task trials | + +#### Example Usage + +```js +const jsPsych = initJsPsych(); + +// Basic usage with defaults +const timeline = jsPsychTimelineFlankerInhibitory.createTimeline(jsPsych); + +// Custom configuration +const customTimeline = jsPsychTimelineFlankerInhibitory.createTimeline(jsPsych, { + stimuli_type: 'fish', + stimuli_amount: 7, + num_trials: 40, + fixation_duration: 750 +}); + +// Using custom SVG override (provide right-facing SVGs) +const svgTimeline = jsPsychTimelineFlankerInhibitory.createTimeline(jsPsych, { + svg: [ + '...right-facing base layer...', + '...right-facing overlay layer...' + ], + stimuli_amount: 9 + // Left-facing versions will be automatically generated by flipping +}); + +jsPsych.run([timeline]); +``` + +### timelineUnits + +Object containing identifiers for different phases of the task: +- `instructions`: Instruction screens +- `practice`: Practice trials +- `main`: Main task trials +- `completion`: Task completion screen + +### utils + +Utility functions for working with flanker stimuli and data: +- `calculatePerformance(data)`: Calculate accuracy and mean RT from trial data +- `createFlankerStim(direction, congruent, stimuli, stimuli_amount)`: Generate flanker stimulus HTML +- `createPracticeStim(direction, congruent, stimuli, stimuli_amount)`: Generate practice stimulus with highlighting + +## Features + +- **Adaptive stimuli**: Fish stimuli for younger children, arrows for older children, or layered combinations +- **Configurable difficulty**: Adjust number of flanker stimuli (3, 5, 7, 9, etc.) +- **Mobile-friendly**: Touch-optimized button interfaces +- **Practice mode**: Highlighted center stimulus for learning +- **Multiple input types**: Support for SVG arrays, custom stimuli objects, or simple overrides +- **Automatic flipping**: Single right-facing SVG automatically generates left-facing version +- **Layered SVGs**: Stack multiple SVG elements for complex stimuli + +## Author / Citation + +Hunter Farhat \ No newline at end of file diff --git a/packages/flanker-inhibitory/docs/flanker-inhibitory.md b/packages/flanker-inhibitory/docs/flanker-inhibitory.md new file mode 100644 index 00000000..7ecc39b6 --- /dev/null +++ b/packages/flanker-inhibitory/docs/flanker-inhibitory.md @@ -0,0 +1,182 @@ +# flanker-inhibitory + +The Flanker Inhibitory Control and Attention test is an assessment of inhibitory control and selective attention. Based on the NIH Toolbox Flanker task, participants must respond to the direction of a central stimulus while ignoring flanking stimuli that may be congruent (pointing the same direction) or incongruent (pointing the opposite direction). + +## Parameters + +### Configuration Parameters + +Configuration parameters can be passed when calling `createTimeline()`: + +```js +const timeline = jsPsychTimelineFlankerInhibitory.createTimeline(jsPsych, { + stimuli_type: 'fish', + stimuli_amount: 7, + num_trials: 40 +}); +``` + +Parameter | Type | Default Value | Description +----------|------|---------------|------------ +stimuli_type | string | 'layered' | Type of stimuli: 'fish', 'arrow', 'layered' (fish + arrow), or 'custom' +svg | array | undefined | Override parameter - array of right-facing SVG strings to layer (left version auto-generated) +stimuli_amount | number | 5 | Number of stimuli in flanker display (must be odd number ≥3) +fixation_duration | number | 500 | Duration of fixation cross display in milliseconds +stimulus_duration | number | undefined | Maximum time for stimulus display before auto-advancing (undefined = no timeout) +show_instructions | boolean | true | Whether to display instruction screens before the task +show_practice | boolean | true | Whether to include practice trials with feedback +num_practice | number | 8 | Number of practice trials to present +num_trials | number | 20 | Number of main task trials to present + +### Stimulus Configuration Examples + +```js +// Use fish stimuli only +const fishConfig = { stimuli_type: 'fish' }; + +// Use custom right-facing SVG with 7 total stimuli +const customConfig = { + svg: ['...right-facing custom svg...'], + stimuli_amount: 7 + // Left-facing version automatically generated +}; + +// Use layered stimuli with custom amounts +const layeredConfig = { + stimuli_type: 'layered', + stimuli_amount: 9 +}; + +// Use custom stimuli type with predefined custom_stimulus +const customTypeConfig = { + stimuli_type: 'custom', + stimuli_amount: 7 +}; + +``` + +### Timeline Structure + +The complete timeline includes: + +1. **Instructions** (if `show_instructions: true`) + - Multi-page instructions explaining the task + - Examples of congruent and incongruent trials + - Strategy tips for performance + +2. **Practice Trials** (if `show_practice: true`) + - Highlighted center stimulus to aid learning + - Immediate feedback after each response + - Performance summary at completion + +3. **Main Task** + - Equal numbers of congruent and incongruent trials + - Left and right direction trials + - Randomized trial order + +4. **Completion Screen** + - Final performance summary + - Task completion confirmation + +## Data Generated + +Each trial generates the following data: + +Name | Type | Value +-----|------|------ +task | string | 'flanker' for stimulus trials, 'fixation' for fixation periods +phase | string | 'practice' or 'main' depending on trial phase +direction | string | 'left' or 'right' - direction of center stimulus +congruent | boolean | true if flankers match center direction, false if opposite +response | number | 0 for left button, 1 for right button +rt | number | Response time in milliseconds +correct | boolean | true if response matches stimulus direction +trial_number | number | Sequential trial number within phase + +### Performance Calculation + +The `calculatePerformance()` utility function returns: + +```js +{ + accuracy: 85.5, // Percentage of correct responses + mean_rt: 650, // Mean response time for correct trials (ms) + total_trials: 20, // Total number of trials analyzed + correct_trials: 17 // Number of correct responses +} +``` + +## Functions + +### createTimeline(jsPsych, config) + +Creates the complete flanker task timeline with the specified configuration. + +**Parameters:** +- `jsPsych`: jsPsych instance +- `config`: Configuration object (see Parameters section) + +**Returns:** Timeline object ready for use with `jsPsych.run()` + +### calculatePerformance(data) + +Analyzes trial data to compute performance metrics. + +**Parameters:** +- `data`: Array of trial data objects + +**Returns:** Performance object with accuracy, mean RT, and trial counts + +### createFlankerStim(direction, congruent, stimuli, stimuli_amount) + +Generates HTML for flanker stimulus display. + +**Parameters:** +- `direction`: 'left' or 'right' +- `congruent`: boolean indicating trial type +- `stimuli`: Stimuli object or array +- `stimuli_amount`: Number of stimuli to display (default: 5) + +**Returns:** HTML string for stimulus display + +### createPracticeStim(direction, congruent, stimuli, stimuli_amount) + +Generates HTML for practice stimulus with center highlighting. + +**Parameters:** Same as `createFlankerStim()` + +**Returns:** HTML string with highlighted center stimulus + +## Stimulus System + +### Layered SVGs + +The package supports layering multiple SVG elements to create complex stimuli: + +```js +const layeredStimuli = [ + '...base fish shape...', + '...arrow overlay...', + '...additional elements...' +]; +``` + +### Automatic Flipping + +Single SVGs are automatically flipped to create left/right facing versions: + +```js +// Provide only right-facing SVG +const config = { + svg: ['...right-facing fish...'] +}; +// Left-facing version is automatically generated +``` + +### Mobile Optimization + +- Touch-friendly button interfaces +- Responsive sizing using vmin units +- Optimized for various screen sizes +- No keyboard input required + diff --git a/packages/flanker-inhibitory/examples/index.html b/packages/flanker-inhibitory/examples/index.html new file mode 100644 index 00000000..bf1d2572 --- /dev/null +++ b/packages/flanker-inhibitory/examples/index.html @@ -0,0 +1,101 @@ + + + + + Flanker Inhibitory Control Task Example + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/flanker-inhibitory/implementPlan.md b/packages/flanker-inhibitory/implementPlan.md new file mode 100644 index 00000000..0bd3957b --- /dev/null +++ b/packages/flanker-inhibitory/implementPlan.md @@ -0,0 +1,628 @@ +We are going to implement four things (speaktext function, createinstructions function and text.ts, styles.css) into this timeline project, some of which may already be implemented but we must edit them to match our desired standardized outcome and current project. Do not unnecessarily edit these implementations as they are standardized across multiple projects, only edit if necessary to add some project-specific functionality or style. For css ids and classes, comb the project's id's and classes to see if anything resembles what we have in the imported styles.css file (at the end of this file) and change it to match the imported file. We are trying to standardize styles, variables, and CSS here. If you make any notable or breaking changes make sure to alert me and describe them in detail. If there is a separate TTS object in text.ts or anything anywhere that you are unsure of what to do with just ask me, but with separate TTS objects we probably want them replaced and fully automated using our speakText function. + +First, createInstructions() function that makes an instructions trial using jsPsychInstructions and supports TTS. This function will need the timeline to have a few parameters added to the config: + + // ===== TEXT-TO-SPEECH CONFIGURATION ===== + enable_tts: true, // Enable text-to-speech functionality + tts_method: 'google', // Preferred TTS method: 'google', 'system' + tts_rate: 1.0, // Speech rate (0.1 to 10) + tts_pitch: 1.0, // Speech pitch (0 to 2) + tts_volume: 1.0, // Speech volume (0 to 1) + tts_lang: 'ar-SA', // Language code for TTS +}; + +in index.ts: + // TTS Configuration + enable_tts?: boolean; // Enable text-to-speech functionality + tts_method?: 'google' | 'system'; // Preferred TTS method (default: 'google') + tts_rate?: number; // Speech rate (0.1 to 10, default: 0.8) + tts_pitch?: number; // Speech pitch (0 to 2, default: 1.0) + tts_volume?: number; // Speech volume (0 to 1, default: 0.8) + tts_lang?: string; // Language code for TTS + + If this does not exist in the project, add it, and then go to index.html and add it there as well and make sure it is set by default to the above config in index.ts and index.html. Additionally, a text.ts file is required with specific variables: instruction_pages and trial_text. We have standardized these variable names so if there is similar objects or variables change them to match our format or just create new array instruction_pages and object trial_text. + +If there is an existing speakText() function or any other speaking function, just replace it with what we have and make sure to go through every trial object and rewrite the speakText call to be similar to the one in our createInstructions. + +Implementation for speakText and createInstructions(): + +// Global audio reference for stopping Google TTS +let currentGoogleAudio: HTMLAudioElement | null = null; + +/** + * Intelligent TTS with user preference support + * Tries user's preferred method first, then the other method as fallback + */ +async function speakText(text: string, options: { lang?: string, volume?: number, method?: 'google' | 'system' } = {}) { + // Stop any current speech first and wait for it to stop + stopAllSpeech(); + + const preferredMethod = options.method || 'google'; + + // Try preferred method first + try { + if (preferredMethod === 'google') { + await speakWithGoogleTTS(text, options.lang || 'en'); + return; + } else { + await speakWithSystemTTS(text, options); + return; + } + } catch (preferredSpeechError) { + // Preferred method failed, continue to try all methods + } + + // Try Google TTS regardless + stopAllSpeech(); + await new Promise(resolve => setTimeout(resolve, 100)); + + try { + await speakWithGoogleTTS(text, options.lang || 'en'); + return; + } catch (googleError) { + // Google failed, continue to system + } + + // Try system TTS as final fallback + stopAllSpeech(); + await new Promise(resolve => setTimeout(resolve, 100)); + + try { + await speakWithSystemTTS(text, options); + return; + } catch (systemError) { + console.warn('🔊 TTS unavailable'); + } +} + +/** + * Stop all speech including Google TTS audio - aggressively stops everything + */ +function stopAllSpeech() { + // Stop system TTS aggressively + if ('speechSynthesis' in window) { + speechSynthesis.cancel(); + speechSynthesis.pause(); + speechSynthesis.resume(); + speechSynthesis.cancel(); + } + + // Stop Google TTS audio aggressively + if (currentGoogleAudio) { + try { + currentGoogleAudio.pause(); + currentGoogleAudio.currentTime = 0; // Reset to beginning + currentGoogleAudio.src = ''; // Clear source to stop loading + } catch (e) { + // Ignore errors, just ensure we clear the reference + } + currentGoogleAudio = null; + } +} + +/** + * Simple system TTS function + * Browser will automatically select the best voice for the specified language + */ +function speakWithSystemTTS(text: string, options: { rate?: number, volume?: number, pitch?: number, lang?: string } = {}) { + return new Promise((resolve, reject) => { + if ('speechSynthesis' in window) { + // Create and speak the utterance + const utterance = new SpeechSynthesisUtterance(text); + + // Apply options with defaults + utterance.rate = options.rate ?? 0.8; + utterance.volume = options.volume ?? 0.8; + utterance.pitch = options.pitch ?? 1.0; + + // Set language if provided (browser will pick best voice) + if (options.lang) { + utterance.lang = options.lang; + } + + // Add event listeners + utterance.onstart = () => resolve(); + utterance.onend = () => resolve(); + utterance.onerror = (e) => { + if (e.error === 'not-allowed' || e.error === 'synthesis-failed') { + reject(new Error(e.error)); // Reject on critical errors + } else { + resolve(); // Don't fail on minor errors since this is a fallback + } + }; + + speechSynthesis.speak(utterance); + } else { + reject(new Error('speechSynthesis not supported')); + } + }); +} + +/** + * Defaultl TTS using Google Translate + * This works by creating an audio element that plays Google's TTS service + */ +function speakWithGoogleTTS(text: string, lang: string) { + return new Promise((resolve, reject) => { + try { + // Convert language code to simple 2-letter format for Google + const googleLang = lang ? lang.substring(0, 2).toLowerCase() : 'en'; + // Create Google Translate TTS URL + const encodedText = encodeURIComponent(text); + const googleTTSUrl = `https://translate.google.com/translate_tts?ie=UTF-8&tl=${googleLang}&client=tw-ob&q=${encodedText}`; + + // Create and play audio + const audio = new Audio(googleTTSUrl); + + // Store reference to current audio for stopping immediately + currentGoogleAudio = audio; + + audio.oncanplay = () => { + // Check if we were cancelled while loading + if (currentGoogleAudio !== audio) { + audio.pause(); + reject(new Error('Cancelled while loading')); + return; + } + audio.play().then(resolve).catch(reject); + }; + + audio.onended = () => { + // Only clear if this is still the current audio + if (currentGoogleAudio === audio) { + currentGoogleAudio = null; + } + resolve(); + }; + + audio.onerror = (e) => { + // Always try to pause and clear, even on error + audio.pause(); + if (currentGoogleAudio === audio) { + currentGoogleAudio = null; + } + reject(new Error('Google TTS failed')); + }; + + // Load the audio + audio.load(); + + } catch (error) { + reject(error); + } + }); +} + +/** + * Creates instruction pages with configurable text and TTS support + * Uses the jsPsych instructions plugin with simple HTML strings + */ +// Helper function to extract text from HTML for TTS +function extractTextFromHtml(htmlString: string): string { + // Use DOMParser for robust HTML to text extraction + const parser = new DOMParser(); + const doc = parser.parseFromString(htmlString, 'text/html'); + return doc.body.textContent?.replace(/\s+/g, ' ').trim() || ''; +} + +function createInstructions(instruction_pages_data = instruction_pages, enable_tts = false, ttsOptions = {}) { + // Closure variable to store the handler for cleanup + let handleButtonClick: ((event: Event) => void) | null = null; + + return { + type: jsPsychInstructions, + pages: instruction_pages_data.map(page => `

${page}

`), + show_clickable_nav: true, + allow_keys: true, + key_forward: 'ArrowRight', + key_backward: 'ArrowLeft', + button_label_previous: trial_text.back_button, + button_label_next: trial_text.next_button, + on_start: function() { + stopAllSpeech(); + }, + on_load: function() { + if (enable_tts) { + // Function to speak current page content + const speakCurrentPage = () => { + const instructionsContent = document.querySelector('.instructions-container'); + if (instructionsContent) { + const pageText = extractTextFromHtml(instructionsContent.innerHTML); + if (pageText.trim()) { + speakText(pageText, ttsOptions); + } + } + }; + + // Use closure variable for handler + handleButtonClick = (event: Event) => { + const target = event.target as HTMLElement; + if (target && (target.id === 'jspsych-instructions-next' || target.id === 'jspsych-instructions-back')) { + stopAllSpeech(); + // Wait longer to ensure speech has stopped before starting new speech + setTimeout(speakCurrentPage, 100); + } + }; + + // Add single event listener to document + document.addEventListener('click', handleButtonClick); + + // Speak initial page + setTimeout(speakCurrentPage, 100); + } + }, + on_finish: function(data: any) { + stopAllSpeech(); + // Clean up event listener using closure variable + if (handleButtonClick) { + document.removeEventListener('click', handleButtonClick); + handleButtonClick = null; + } + data.phase = 'instructions'; + } + }; +} + + +Implementation for text.ts file, you can add text placeholders and objects specific to the project we are working on. You should probably go through the index.ts file and make sure every piece of text is customizable through text.ts + +/* This file contains the configurable text used in the speeded matching timeline. + * Researchers can modify these texts to change the language or instructions. + */ + +export const trial_text = { + // Instruction pages buttons text, these will always have arrows < and > + // these do not work right now due to CSS fixed position, might fix later + next_button: "", + back_button: "", + + (MESSAGE TO CLAUDE: THE FOLLOWING TEXT ITEMS MAY OR MAY NOT EXIST IN THIS PROJECT. IF THEY DO NOT EXIST OR MATCH ANYTHING SIMILAR, REMOVE THEM. OTHERWISE KEEP THEM WITH THE SAME NAMING SCHEME.) + + // Task completion messages + task_complete_header: "Task Complete!", + task_complete_message: "Thank you for participating in the speeded matching task.", + + // Practice phase text + practice_header: "Practice Round", + practice_intro_message: "We'll now do a practice round to show you how the task works.", + practice_look_instruction: "Look at this picture", + practice_tap_instruction: "Tap the matching picture below", + practice_complete_header: "Are you ready?", + practice_complete_message: "Practice complete! Ready for the full test?", + + // Main task instructions + main_task_prompt: "Tap the matching picture below", + + // Fixation and inter-trial + fixation_cross: "+", + + // Feedback messages (optional) + correct_feedback: "Correct!", + incorrect_feedback: "Try again", + + // Timing messages + too_slow_message: "Please respond faster", +} + +/* + * This is an array of HTML strings for instruction pages displayed before the actual trials. + * Researchers can modify these instructions to change the task description, add new instruction + * pages, or translate to different languages. + * + * Each string should contain valid HTML that will be displayed as an instruction page. + * You can add more pages by adding more strings to the array, or modify existing pages + * by editing the HTML content. + */ +(MESSAGE TO CLAUDE: WHATEVER THE CONTENT OF EACH PAGE IS FOR THE CURRENT PROJECT, IF ITS TOO LONG OR INCLUDES UNNECESSARY INFO TO THE PARTICIPANT (SUCH AS THE EXPERIMENTS NAME OR ANYTHING OTHER THAN WHAT THE PARTICIPANT NEEDS TO KNOW TO DO THE EXPERIMENT) THEN SIMPLIFY. APPROACH TO REWRITE INSTRUCTIONS: MORE PAGES WITH SHORT TEXT RATHER THAN LONG PARAGRAPHS OR BULLET POINTS.) +export const instruction_pages = [ + "You will see a picture at the top.", + "Below it, you will see four pictures.", + "Click on the picture that matches the one at the top.", + "Work quickly but carefully.", + "Let's practice first." +]; + + + +Now, we will implement a standardized styles.css. This file may have extraneous styles not specific to our current timeline project (such as choice-options and whatnot). Remove these styles if they do not match anything in our current timeline project. If there are additional styles in the project that are not in this styles.css file, add them exactly as they are and do not change anything unneccesarily. Additionally, if there are any class names for instructions or trials that are here but not in the index.ts trials, you should probably add them to that trial's HTML. For example, for any html button response plugin usage, you should add button_html: (choice) => ``, + so that the styles will apply to the buttons. Do similar things for instructions container or anything that is in our styles.css but not in the index.ts if applicable. Be careful not to mess up things, if unsure just prompt me to decide on things even. Oh and if you change any HTML you should probably check the speakText functions again on what HTML and CSS containers they are querying and make sure you change those if necessary too. + +Here is the imported styles.css: + +/* GENERAL STYLES */ + +h1, h2, h3 { + line-height: 1.6; + margin: 3vmin; + word-wrap: break-word; + overflow-wrap: break-word; + hyphens: auto; + max-width: 100%; + box-sizing: border-box; +} + +p { + margin: 5vmin 1vmin; + font-size: clamp(15px, 8vmin, 40px); + line-height: 1.6; + /* Prevent horizontal overflow */ + word-wrap: break-word; + overflow-wrap: break-word; + hyphens: auto; + max-width: 100%; + box-sizing: border-box; +} + +/* Styles for any normal HTML button */ +.continue-button { + /* background-color: #3498db; + color: white; + border: none; */ + padding: clamp(15px, 6vmin, 35px) clamp(15px, 8vmin, 40px); + font-size: clamp(15px, 6vmin, 35px); + border-radius: 1vmin; + cursor: pointer; + margin: 2vmin; + margin-top: 10vh; + transition: background-color 0.3s; + min-height: 48px; + + /* Disable double-click zoom */ + touch-action: manipulation !important; + user-select: none !important; +} + +/* INSTRUCTIONS TRIAL STYLES */ + +.instructions-container { + max-width: 90vw; /* Limit width for better readability */ + margin: 0 auto; /* Center the container */ + padding: 2vmin; /* Add some padding */ + box-sizing: border-box; /* Include padding in width calculations */ + overflow-x: hidden; /* Prevent horizontal overflow */ +} + +/* Mobile-friendly instruction navigation buttons */ +.jspsych-instructions-nav { + display: flex; + gap: 25vw; + justify-content: center; + margin-top: auto; + padding: clamp(20px, 5vmin, 40px) !important; + min-height: clamp(20px, 50vmin, 250px); + align-items: flex-end; +} + +/* Instruction navigation button styles */ +.jspsych-instructions-nav .jspsych-btn { + background-color: #fff; + color: #000; + width: 20vmin; + height: 20vmin; + border: 3px solid #ccc; + border-radius: 50%; + font-size: clamp(15px, 7vmin, 35px); + cursor: pointer; + min-height: 100px; + min-width: 100px; + font-weight: 600; + + /* Disable double-click zoom */ + touch-action: manipulation !important; + user-select: none !important; +} + +.jspsych-instructions-nav .jspsych-btn:active { + background-color: #f0f0f0; + border-color: #999; +} + +.jspsych-instructions-nav .jspsych-btn:disabled { + visibility: hidden; /* Keeps the space but hides the button */ +} + +/* PRACTICE TRIAL STYLES */ + +/* pre-practice and post-practice round pages */ +.practice-instructions, .ready-screen { + max-width: 90vw; /* Limit width for better readability */ + margin: 0 auto; /* Center the container */ + padding: 2vmin; /* Add some padding */ + box-sizing: border-box; /* Include padding in width calculations */ + overflow-x: hidden; /* Prevent horizontal overflow */ + padding-bottom: 5vh; +} + +/* EXPERIMENT TRIAL STYLES */ + +/* Fixation cross (inter-trial), set to invisible by default */ +.fixation { + display: flex; + align-items: center; + justify-content: center; + font-size: 8vmin; + color: #ffffff; /* White color for invisibility */ + font-weight: bold; + min-height: 20vmin; +} +.trial-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; + font-family: Arial, sans-serif; + padding: 2vmin; +} + +/* Task instructions in trial container, text on top. Usually there is none. */ +.task-instructions { + text-align: center; + p { + font-size: clamp(15px, 6vmin, 35px); + margin: 2vmin; + } +} + +/* Target stimulus*/ +.target-stimulus { + width: 30vmin; + height: 30vmin; + border: 3.5px solid #000000bd; + border-radius: 2vmin; + background-color: white; + display: inline-flex; + align-items: center; + justify-content: center; + box-shadow: 0 2vmin 3vmin rgba(0, 0, 0, 0.15); + margin: 2vmin; + svg { + width: 100%; + height: 100%; + } +} + +.target-stimulus.flash { + animation: flash-border 0.75s ease-in-out 3; +} + +@keyframes flash-border { + 0%, 100% { + border-color: #2c3e50; + box-shadow: 0 2vmin 3vmin rgba(0, 0, 0, 0.15); + } + 50% { + border-color: #e74c3c; + box-shadow: 0 2vmin 4vmin rgba(231, 76, 60, 0.4); + } +} + +/* Button layout - flexible responsive grid */ +#jspsych-html-button-response-btngroup { + display: flex; + flex-wrap: wrap; + gap: 2vmin; + max-width: 95vw; + margin: 3vmin auto; + justify-content: center; + align-items: center; +} + + +/* Choice buttons - dynamic sizing based on available space */ +.choice-option { + border: 2px solid #bdc3c7; + border-radius: 2vmin; + background-color: white !important; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer !important; + transition: all 0.3s ease; + box-shadow: 0 1vmin 2vmin rgba(0, 0, 0, 0.1); + padding: 1.5vmin; + margin: 2px 5px; + /* Disable double-click zoom */ + touch-action: manipulation !important; + user-select: none !important; + /* Fix the flexbox sizing */ + flex: 0 0 auto; /* Don't grow, don't shrink, use basis */ + flex-basis: clamp(20vmin, calc(163vmin / var(--num-choices, 4)), 90vmin); + height: clamp(20vmin, calc(80vh / var(--num-choices, 4)), 33vmin); + + /* Container-linked SVG scaling */ + svg { + width: 100%; + height: 100%; + } +} + +/* Hover effect for choice options, only visible on desktop */ +.choice-option:hover { + border-color: #3498db; + transform: translateY(-0.5vmin); + box-shadow: 0 2vmin 3vmin rgba(0, 0, 0, 0.15); +} + +/* Flashing for practice round */ +.choice-option.flash-choices { + animation: flash-choices 1s ease-in-out infinite; +} + +@keyframes flash-choices { + 0%, 100% { + border-color: #bdc3c7; + box-shadow: 0 1vmin 2vmin rgba(0, 0, 0, 0.1); + } + 50% { + border-color: #f39c12; + box-shadow: 0 2vmin 3vmin rgba(243, 156, 18, 0.3); + } +} + +/* Styles for tablets */ +@media (max-width: 768px) { + .jspsych-instructions-nav { + position: fixed; + bottom: 30vmin; + left: 50%; + transform: translateX(-50%); + } + + /* Add padding to prevent content from going under fixed buttons */ + .instructions-container { + padding-bottom: 45vmin; /* 30vmin for button position + 5vmin safety */ + } + + /* Experiment-specific, adjusting sizes of images */ + .target-stimulus { + width: 35vmin; + height: 35vmin; + + } + .choice-option { + height: clamp(20vmin, calc(85vh / var(--num-choices, 4)), 30vh); + flex-basis: clamp(15vmin, calc(165vw / var(--num-choices, 4)), 43vw); + /* width: clamp(30vmin, calc(150vw / var(--num-choices, 4)), 43vmin); */ + /* aspect-ratio: 1;*/ + } + + +} + +/* Styles for phones to override tablet */ +@media (max-width: 480px) { + /* Smaller padding for phone to allow more text */ + .instructions-container { + padding-bottom: 40vmin; + } + .target-container { + margin-bottom: 4vmin; /* Adjusted for better spacing on small screens */ + } + /* Experiment-specific, adjusting sizes of images */ + .target-stimulus { + width: 45vmin; + height: 45vmin; + } +} + +/* Landscape mode adjustments */ +@media (orientation: landscape) { + .jspsych-instructions-nav { + bottom: 3vh; + gap: 30vw; + } + .target-container { + margin-bottom: 1vmin; + } + .target-stimulus { + width: 30vmin; + height: 30vmin; + border: 4px solid #000000bd; + } + .choice-option { + margin: 0; + flex-basis: clamp(15vmin, calc(85vw / var(--num-choices, 4)), 25vw) !important; + height: clamp(15vmin, calc(135vh / var(--num-choices, 4)), 35vmin) !important; + margin: -10px 2px; + } +} \ No newline at end of file diff --git a/packages/false-memory/jest.config.js b/packages/flanker-inhibitory/jest.config.cjs similarity index 100% rename from packages/false-memory/jest.config.js rename to packages/flanker-inhibitory/jest.config.cjs diff --git a/packages/flanker-inhibitory/package.json b/packages/flanker-inhibitory/package.json new file mode 100644 index 00000000..cb68b8c2 --- /dev/null +++ b/packages/flanker-inhibitory/package.json @@ -0,0 +1,44 @@ +{ + "name": "@jspsych-timelines/flanker-inhibitory", + "version": "0.0.1", + "description": "Flanker Inhibitory Control and Attention test timeline for jsPsych with configurable fish/arrow stimuli and layered SVG support", + "type": "module", + "main": "dist/index.js", + "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 jsPsychTimelineFlankerInhibitory" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/jspsych/jspsych-timelines.git", + "directory": "packages/flanker-inhibitory" + }, + "keywords": [ + "jsPsych", + "flanker", + "inhibitory control", + "attention", + "NIH Toolbox", + "cognitive assessment" + ], + "author": { + "name": "Hunter Farhat", + "url": "https://github.com/farhat60" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/jspsych/jspsych-timelines/issues" + }, + "homepage": "https://github.com/jspsych/jspsych-timelines/tree/main/packages/flanker-inhibitory#readme", + "dependencies": { + "@jspsych/plugin-html-button-response": "^2.0.0", + "@jspsych/plugin-instructions": "^2.0.0" + }, + "devDependencies": { + "jspsych": "^8.0.0", + "@jspsych/config": "^3.2.2", + "tsup": "^6.7.0", + "typescript": "^5.0.2" + } + } \ No newline at end of file diff --git a/packages/flanker-inhibitory/src/index.spec.ts b/packages/flanker-inhibitory/src/index.spec.ts new file mode 100644 index 00000000..0ac2266d --- /dev/null +++ b/packages/flanker-inhibitory/src/index.spec.ts @@ -0,0 +1,750 @@ +import { initJsPsych } from "jspsych"; +import { + createTimeline, + createFlankerStim, + createPracticeStim, + calculatePerformance, + default_stimuli, + fish_stimuli, + arrow_stimuli, + timelineUnits, + utils, + trial_text, + instruction_pages, + FlankerConfig +} from "./index"; +import { layered_stimuli } from "./stimuli"; + +describe("Flanker Inhibitory Control Task", () => { + let jsPsych: any; + + beforeEach(() => { + jsPsych = initJsPsych(); + }); + + // Helper function to count only top-level spans (flanker positions) + const countFlankerPositions = (html: string): number => { + // Extract the content inside the flanker-stim div + const flankerMatch = html.match(/
]*>([\s\S]*?)<\/div>/); + if (!flankerMatch) return 0; + + const content = flankerMatch[1]; + + // Count only direct child spans - these are the stimulus positions + // Look for spans that are not nested inside other spans + let count = 0; + let depth = 0; + let i = 0; + + while (i < content.length) { + if (content.substring(i).startsWith('', i) + 1; + } else if (content.substring(i).startsWith('')) { + depth--; + i = content.indexOf('>', i) + 1; + } else { + i++; + } + } + + return count; + }; + + describe("createTimeline", () => { + it("should return a timeline object with timeline array", () => { + const timeline = createTimeline(jsPsych); + expect(timeline).toBeDefined(); + expect(timeline.timeline).toBeDefined(); + expect(Array.isArray(timeline.timeline)).toBe(true); + }); + + it("should create timeline with default parameters", () => { + const timeline = createTimeline(jsPsych); + expect(timeline.timeline.length).toBeGreaterThan(0); + }); + + it("should create timeline without instructions when disabled", () => { + const timeline = createTimeline(jsPsych, { show_instructions: false }); + expect(timeline.timeline.length).toBeGreaterThan(0); + // Should have fewer trials than with instructions + const timelineWithInstructions = createTimeline(jsPsych, { show_instructions: true }); + expect(timeline.timeline.length).toBeLessThan(timelineWithInstructions.timeline.length); + }); + + it("should create timeline without practice when disabled", () => { + const timeline = createTimeline(jsPsych, { show_practice: false }); + expect(timeline.timeline.length).toBeGreaterThan(0); + }); + + it("should respect custom trial count", () => { + const customTrials = 15; + const timeline = createTimeline(jsPsych, { + num_trials: customTrials, + show_instructions: false, + show_practice: false + }); + + // Should have main trials plus completion screen + expect(timeline.timeline.length).toBeGreaterThanOrEqual(2); + }); + + it("should respect custom practice trial count", () => { + const customPractice = 6; + const timeline = createTimeline(jsPsych, { + num_practice: customPractice, + show_practice: true, + show_instructions: false + }); + + expect(timeline.timeline.length).toBeGreaterThan(0); + }); + + it("should handle different stimuli types", () => { + const fishTimeline = createTimeline(jsPsych, { stimuli_type: 'fish' }); + const arrowTimeline = createTimeline(jsPsych, { stimuli_type: 'arrow' }); + const layeredTimeline = createTimeline(jsPsych, { stimuli_type: 'layered' }); + const customTimeline = createTimeline(jsPsych, { stimuli_type: 'custom' }); + + expect(fishTimeline.timeline.length).toBeGreaterThan(0); + expect(arrowTimeline.timeline.length).toBeGreaterThan(0); + expect(layeredTimeline.timeline.length).toBeGreaterThan(0); + expect(customTimeline.timeline.length).toBeGreaterThan(0); + }); + + it("should handle SVG override parameter", () => { + const customSVGs = [ + '', + '' + ]; + + const timeline = createTimeline(jsPsych, { svg: customSVGs }); + expect(timeline.timeline.length).toBeGreaterThan(0); + }); + + + it("should set custom fixation duration", () => { + const customDuration = 1000; + const timeline = createTimeline(jsPsych, { fixation_duration: customDuration }); + expect(timeline.timeline.length).toBeGreaterThan(0); + }); + }); + + describe("createFlankerStim", () => { + it("should create HTML for congruent left stimulus", () => { + const html = createFlankerStim('left', true); + expect(html).toContain('flanker-stim'); + expect(html).toContain(''); + }); + + it("should create HTML for incongruent right stimulus", () => { + const html = createFlankerStim('right', false); + expect(html).toContain('flanker-stim'); + expect(html).toContain(''); + }); + + it("should create HTML for congruent right stimulus", () => { + const html = createFlankerStim('right', true); + expect(html).toContain('flanker-stim'); + expect(html).toContain(''); + }); + + it("should create HTML for incongruent left stimulus", () => { + const html = createFlankerStim('left', false); + expect(html).toContain('flanker-stim'); + expect(html).toContain(''); + }); + + it("should handle custom stimuli arrays", () => { + const customStimuli = ['test']; + const html = createFlankerStim('left', true, customStimuli); + expect(html).toContain('flanker-stim'); + }); + + it("should handle custom stimuli objects", () => { + const customStimuli = { + left: ['left test'], + right: ['right test'] + }; + const html = createFlankerStim('left', true, customStimuli); + expect(html).toContain('flanker-stim'); + }); + }); + + describe("createPracticeStim", () => { + it("should create HTML for practice stimulus with highlighting", () => { + const html = createPracticeStim('left', true); + expect(html).toContain('flanker-stim practice'); + expect(html).toContain('center highlighted'); + }); + + it("should create different HTML than regular stimulus", () => { + const practiceHtml = createPracticeStim('left', true); + const regularHtml = createFlankerStim('left', true); + + expect(practiceHtml).toContain('practice'); + expect(practiceHtml).toContain('highlighted'); + expect(regularHtml).not.toContain('practice'); + expect(regularHtml).not.toContain('highlighted'); + }); + + it("should handle all direction and congruency combinations", () => { + const combinations = [ + ['left', true], + ['left', false], + ['right', true], + ['right', false] + ]; + + combinations.forEach(([direction, congruent]) => { + const html = createPracticeStim(direction as string, congruent as boolean); + expect(html).toContain('flanker-stim practice'); + expect(html).toContain('center highlighted'); + }); + }); + }); + + describe("calculatePerformance", () => { + it("should return zero performance for empty data", () => { + const performance = calculatePerformance([]); + expect(performance.accuracy).toBe(0); + expect(performance.mean_rt).toBe(0); + expect(performance.total_trials).toBe(0); + }); + + it("should calculate accuracy correctly", () => { + const mockData = [ + { task: 'flanker', correct: true, rt: 500 }, + { task: 'flanker', correct: false, rt: 600 }, + { task: 'flanker', correct: true, rt: 550 }, + { task: 'flanker', correct: true, rt: 450 } + ]; + + const performance = calculatePerformance(mockData); + expect(performance.accuracy).toBe(75); // 3/4 = 75% + expect(performance.total_trials).toBe(4); + expect(performance.correct_trials).toBe(3); + expect(performance.mean_rt).toBeCloseTo(500, 1); // Mean of correct trials: (500+550+450)/3 + }); + + it("should filter only flanker task data", () => { + const mockData = [ + { task: 'flanker', correct: true, rt: 500 }, + { task: 'other', correct: false, rt: 600 }, + { task: 'flanker', correct: true, rt: 550 } + ]; + + const performance = calculatePerformance(mockData); + expect(performance.total_trials).toBe(2); // Only flanker trials + expect(performance.accuracy).toBe(100); // Both flanker trials correct + }); + + it("should handle all incorrect trials", () => { + const mockData = [ + { task: 'flanker', correct: false, rt: 500 }, + { task: 'flanker', correct: false, rt: 600 } + ]; + + const performance = calculatePerformance(mockData); + expect(performance.accuracy).toBe(0); + expect(performance.mean_rt).toBe(0); // No correct trials + expect(performance.correct_trials).toBe(0); + }); + }); + + describe("Stimulus Constants", () => { + it("should provide default_stimuli with left and right", () => { + expect(default_stimuli).toBeDefined(); + expect(default_stimuli.left).toBeDefined(); + expect(default_stimuli.right).toBeDefined(); + expect(typeof default_stimuli.left).toBe('string'); + expect(typeof default_stimuli.right).toBe('string'); + }); + + it("should provide fish_stimuli with left and right", () => { + expect(fish_stimuli).toBeDefined(); + expect(fish_stimuli.left).toBeDefined(); + expect(fish_stimuli.right).toBeDefined(); + expect(typeof fish_stimuli.left).toBe('string'); + expect(typeof fish_stimuli.right).toBe('string'); + }); + + it("should provide arrow_stimuli with left and right", () => { + expect(arrow_stimuli).toBeDefined(); + expect(arrow_stimuli.left).toBeDefined(); + expect(arrow_stimuli.right).toBeDefined(); + expect(typeof arrow_stimuli.left).toBe('string'); + expect(typeof arrow_stimuli.right).toBe('string'); + }); + + it("should have different stimuli for left and right directions", () => { + expect(default_stimuli.left).not.toBe(default_stimuli.right); + expect(fish_stimuli.left).not.toBe(fish_stimuli.right); + expect(arrow_stimuli.left).not.toBe(arrow_stimuli.right); + }); + }); + + describe("Text and Instructions", () => { + it("should provide trial_text object", () => { + expect(trial_text).toBeDefined(); + expect(trial_text.continue_button).toBeDefined(); + expect(trial_text.left_button).toBeDefined(); + expect(trial_text.right_button).toBeDefined(); + expect(trial_text.fixation_cross).toBeDefined(); + expect(trial_text.correct_feedback).toBeDefined(); + expect(trial_text.incorrect_feedback).toBeDefined(); + }); + + it("should provide instruction_pages array", () => { + expect(instruction_pages).toBeDefined(); + expect(Array.isArray(instruction_pages)).toBe(true); + expect(instruction_pages.length).toBeGreaterThan(0); + }); + + it("should have properly structured instruction pages", () => { + instruction_pages.forEach((page, index) => { + expect(page).toBeDefined(); + expect(typeof page).toBe('string'); + expect(page.length).toBeGreaterThan(0); + }); + }); + }); + + describe("Timeline Units", () => { + it("should provide timelineUnits object", () => { + expect(timelineUnits).toBeDefined(); + expect(timelineUnits.instructions).toBeDefined(); + expect(timelineUnits.practice).toBeDefined(); + expect(timelineUnits.main).toBeDefined(); + expect(timelineUnits.completion).toBeDefined(); + }); + }); + + describe("Utils Export", () => { + it("should provide utils object with functions", () => { + expect(utils).toBeDefined(); + expect(utils.calculatePerformance).toBeDefined(); + expect(utils.createFlankerStim).toBeDefined(); + expect(utils.createPracticeStim).toBeDefined(); + expect(typeof utils.calculatePerformance).toBe('function'); + expect(typeof utils.createFlankerStim).toBe('function'); + expect(typeof utils.createPracticeStim).toBe('function'); + }); + + it("should have utils functions work the same as direct exports", () => { + const mockData = [{ task: 'flanker', correct: true, rt: 500 }]; + + const directResult = calculatePerformance(mockData); + const utilsResult = utils.calculatePerformance(mockData); + + expect(directResult).toEqual(utilsResult); + }); + }); + + describe("FlankerConfig Interface", () => { + it("should accept valid config objects", () => { + const configs: FlankerConfig[] = [ + {}, + { stimuli_type: 'fish' }, + { stimuli_type: 'arrow' }, + { stimuli_type: 'layered' }, + { fixation_duration: 1000 }, + { show_instructions: false }, + { show_practice: true }, + { num_practice: 10 }, + { num_trials: 25 }, + { svg: ['test'] } + ]; + + configs.forEach(config => { + expect(() => createTimeline(jsPsych, config)).not.toThrow(); + }); + }); + }); + + describe("SVG Flipping Functionality", () => { + it("should generate different left and right stimuli from same input", () => { + // Test with a simple SVG array input + const testSVG = ['']; + + const leftHtml = createFlankerStim('left', true, testSVG); + const rightHtml = createFlankerStim('right', true, testSVG); + + // Should be different due to flipping + expect(leftHtml).not.toBe(rightHtml); + expect(leftHtml).toContain('flanker-stim'); + expect(rightHtml).toContain('flanker-stim'); + }); + + it("should handle SVGs with existing transforms", () => { + // Test with SVG that already has transform (like the fish) + const transformedSVG = ['']; + + const leftHtml = createFlankerStim('left', true, transformedSVG); + const rightHtml = createFlankerStim('right', true, transformedSVG); + + expect(leftHtml).not.toBe(rightHtml); + expect(leftHtml).toContain('flanker-stim'); + expect(rightHtml).toContain('flanker-stim'); + }); + + it("should properly handle fish SVG alignment - no double transforms", () => { + // Test specifically with fish stimuli to ensure alignment fix works + const fishLeftHtml = createFlankerStim('left', true, fish_stimuli); + const fishRightHtml = createFlankerStim('right', true, fish_stimuli); + + // Both should contain flanker stimulus + expect(fishLeftHtml).toContain('flanker-stim'); + expect(fishRightHtml).toContain('flanker-stim'); + + // They should be different (one flipped) + expect(fishLeftHtml).not.toBe(fishRightHtml); + + // Test that the fish SVG original has a transform + expect(fish_stimuli.right).toContain('transform='); + + // Test that flipping twice gets back to original orientation + // This validates the fix for double-transform issues + const twice_flipped_left = createFlankerStim('left', true, fish_stimuli); + const twice_flipped_right = createFlankerStim('right', true, fish_stimuli); + expect(twice_flipped_left).not.toBe(twice_flipped_right); + }); + + it("should handle fish incongruent stimuli alignment correctly", () => { + // Test incongruent fish (center different from flankers) - key alignment test + const leftIncongruent = createFlankerStim('left', false, fish_stimuli); + const rightIncongruent = createFlankerStim('right', false, fish_stimuli); + + // Should produce valid HTML + expect(leftIncongruent).toContain('flanker-stim'); + expect(rightIncongruent).toContain('flanker-stim'); + + // Should be different + expect(leftIncongruent).not.toBe(rightIncongruent); + + // The key test: each should contain both orientations (center vs flankers) + // This is where alignment issues would be most visible + expect(leftIncongruent).toContain(''); + expect(rightIncongruent).toContain(''); + }); + + it("should maintain consistent alignment with layered fish+arrow stimuli", () => { + // Test the default layered stimuli (fish + arrow) for alignment + const leftLayered = createFlankerStim('left', true, default_stimuli); + const rightLayered = createFlankerStim('right', true, default_stimuli); + + expect(leftLayered).toContain('flanker-stim'); + expect(rightLayered).toContain('flanker-stim'); + expect(leftLayered).not.toBe(rightLayered); + + // Should contain position styling for layered elements + expect(leftLayered).toContain('position:'); + expect(rightLayered).toContain('position:'); + }); + }); + + describe("Layered SVG Functionality", () => { + it("should handle multiple SVG layers", () => { + const layeredSVGs = [ + '', + '' + ]; + + const html = createFlankerStim('left', true, layeredSVGs); + + expect(html).toContain('flanker-stim'); + expect(html).toContain('position: relative'); + }); + + it("should handle single SVG in array", () => { + const singleSVG = ['']; + + const html = createFlankerStim('left', true, singleSVG); + + expect(html).toContain('flanker-stim'); + }); + }); + + describe("Stimuli Amount Functionality", () => { + + it("should default to 5 stimuli (2 left + 1 center + 2 right)", () => { + const html = createFlankerStim('left', true); + const spanCount = countFlankerPositions(html); + expect(spanCount).toBe(5); + }); + + it("should create correct number of stimuli for custom amounts", () => { + const testAmounts = [3, 5, 7, 9]; + + testAmounts.forEach(amount => { + const html = createFlankerStim('left', true, layered_stimuli, amount); + const spanCount = countFlankerPositions(html); + expect(spanCount).toBe(amount); + }); + }); + + it("should handle even numbers by rounding up to next odd", () => { + const html4 = createFlankerStim('left', true, layered_stimuli, 4); + const html6 = createFlankerStim('left', true, layered_stimuli, 6); + const html8 = createFlankerStim('left', true, layered_stimuli, 8); + + expect(countFlankerPositions(html4)).toBe(5); // 4 -> 5 + expect(countFlankerPositions(html6)).toBe(7); // 6 -> 7 + expect(countFlankerPositions(html8)).toBe(9); // 8 -> 9 + }); + + it("should enforce minimum of 3 stimuli", () => { + const html1 = createFlankerStim('left', true, layered_stimuli, 1); + const html2 = createFlankerStim('left', true, layered_stimuli, 2); + const htmlNegative = createFlankerStim('left', true, layered_stimuli, -5); + + expect(countFlankerPositions(html1)).toBe(3); + expect(countFlankerPositions(html2)).toBe(3); + expect(countFlankerPositions(htmlNegative)).toBe(3); + }); + + it("should maintain center stimulus with highlighting in practice", () => { + const html = createPracticeStim('left', true, layered_stimuli, 7); + const spanCount = countFlankerPositions(html); + expect(spanCount).toBe(7); + expect(html).toContain('center highlighted'); + }); + + it("should work with different stimuli types", () => { + const fishHtml = createFlankerStim('left', true, fish_stimuli, 7); + const arrowHtml = createFlankerStim('left', true, arrow_stimuli, 7); + + expect(countFlankerPositions(fishHtml)).toBe(7); + expect(countFlankerPositions(arrowHtml)).toBe(7); + }); + + it("should be configurable via timeline config", () => { + const timeline = createTimeline(jsPsych, { + stimuli_amount: 7, + show_instructions: false, + show_practice: false + }); + + expect(timeline.timeline.length).toBeGreaterThan(0); + }); + + it("should validate stimuli_amount in different function calls", () => { + // Test with simple SVGs (no inner spans) + const testSVGs = ['']; + const html1 = createFlankerStim('left', true, testSVGs, 9); + expect(countFlankerPositions(html1)).toBe(9); + + // Test with objects + const testObj = { left: [''], right: [''] }; + const html2 = createFlankerStim('left', true, testObj, 7); + expect(countFlankerPositions(html2)).toBe(7); + }); + + it("should handle invalid amounts by using valid fallbacks", () => { + // NaN should fall back to default (5) + const htmlNaN = createFlankerStim('left', true, layered_stimuli, NaN); + expect(countFlankerPositions(htmlNaN)).toBe(5); + + // Very large numbers should still work + const htmlLarge = createFlankerStim('left', true, layered_stimuli, 15); + expect(countFlankerPositions(htmlLarge)).toBe(15); + }); + }); + + describe("Error Handling", () => { + it("should handle invalid direction gracefully", () => { + expect(() => createFlankerStim('invalid' as any, true)).not.toThrow(); + }); + + it("should handle undefined stimuli", () => { + expect(() => createFlankerStim('left', true, undefined as any)).not.toThrow(); + }); + + it("should handle empty stimuli array", () => { + expect(() => createFlankerStim('left', true, [])).not.toThrow(); + }); + + it("should handle malformed SVG strings", () => { + const badSVG = ['not an svg']; + expect(() => createFlankerStim('left', true, badSVG)).not.toThrow(); + }); + + it("should handle invalid stimuli_amount gracefully", () => { + expect(() => createFlankerStim('left', true, layered_stimuli, NaN)).not.toThrow(); + expect(() => createFlankerStim('left', true, layered_stimuli, Infinity)).not.toThrow(); + + // Should fall back to default value (5) for invalid inputs + const htmlNaN = createFlankerStim('left', true, layered_stimuli, NaN); + const htmlInfinity = createFlankerStim('left', true, layered_stimuli, Infinity); + + expect(countFlankerPositions(htmlNaN)).toBe(5); + expect(countFlankerPositions(htmlInfinity)).toBe(5); + }); + }); + + describe("Edge Cases and Timeline Variable Errors", () => { + it("should handle num_practice=0 with show_practice=true without errors", () => { + expect(() => { + const timeline = createTimeline(jsPsych, { + num_practice: 0, + show_practice: true, + show_instructions: false, + num_trials: 1 + }); + expect(timeline.timeline.length).toBeGreaterThan(0); + }).not.toThrow(); + }); + + it("should handle num_trials=0 without errors", () => { + expect(() => { + const timeline = createTimeline(jsPsych, { + num_trials: 0, + show_instructions: false, + show_practice: false + }); + expect(timeline.timeline.length).toBeGreaterThan(0); + }).not.toThrow(); + }); + + it("should handle both num_practice=0 and num_trials=0 without errors", () => { + expect(() => { + const timeline = createTimeline(jsPsych, { + num_practice: 0, + show_practice: true, + num_trials: 0, + show_instructions: false + }); + // Should still have completion screen + expect(timeline.timeline.length).toBeGreaterThan(0); + }).not.toThrow(); + }); + + it("should handle negative numbers for practice trials", () => { + expect(() => { + const timeline = createTimeline(jsPsych, { + num_practice: -5, + show_practice: true, + show_instructions: false, + num_trials: 1 + }); + expect(timeline.timeline.length).toBeGreaterThan(0); + }).not.toThrow(); + }); + + it("should handle negative numbers for main trials", () => { + expect(() => { + const timeline = createTimeline(jsPsych, { + num_trials: -10, + show_instructions: false, + show_practice: false + }); + expect(timeline.timeline.length).toBeGreaterThan(0); + }).not.toThrow(); + }); + + it("should handle very large numbers without breaking", () => { + expect(() => { + const timeline = createTimeline(jsPsych, { + num_practice: 1000, + show_practice: true, + num_trials: 1000, + show_instructions: false + }); + expect(timeline.timeline.length).toBeGreaterThan(0); + }).not.toThrow(); + }); + + it("should handle NaN values for trial counts", () => { + expect(() => { + const timeline = createTimeline(jsPsych, { + num_practice: NaN, + show_practice: true, + num_trials: NaN, + show_instructions: false + }); + expect(timeline.timeline.length).toBeGreaterThan(0); + }).not.toThrow(); + }); + + it("should handle all combinations of false/0 settings", () => { + const edgeCaseConfigs = [ + { show_instructions: false, show_practice: false, num_practice: 0, num_trials: 0 }, + { show_instructions: true, show_practice: false, num_practice: 0, num_trials: 0 }, + { show_instructions: false, show_practice: true, num_practice: 0, num_trials: 0 }, + { show_instructions: false, show_practice: false, num_practice: 5, num_trials: 0 }, + { show_instructions: false, show_practice: false, num_practice: 0, num_trials: 5 }, + ]; + + edgeCaseConfigs.forEach((config, index) => { + expect(() => { + const timeline = createTimeline(jsPsych, config); + expect(timeline.timeline.length).toBeGreaterThan(0); + }).not.toThrow(); + }); + }); + + it("should always include completion screen even with no trials", () => { + const timeline = createTimeline(jsPsych, { + show_instructions: false, + show_practice: false, + num_practice: 0, + num_trials: 0 + }); + + // Should have at least the main intro and completion screens + expect(timeline.timeline.length).toBeGreaterThanOrEqual(2); + + // Check that completion screen is present + const completionScreen = timeline.timeline.find(trial => + trial.data && trial.data.task === 'complete' + ); + expect(completionScreen).toBeDefined(); + }); + }); + + describe("Responsive Sizing", () => { + it("should add CSS custom property for stimuli count", () => { + const html3 = createFlankerStim('left', true, fish_stimuli, 3); + const html7 = createFlankerStim('left', true, fish_stimuli, 7); + const html15 = createFlankerStim('left', true, fish_stimuli, 15); + + expect(html3).toContain('--stimuli-count: 3'); + expect(html7).toContain('--stimuli-count: 7'); + expect(html15).toContain('--stimuli-count: 15'); + }); + + it("should add CSS custom property to practice stimuli", () => { + const practiceHtml = createPracticeStim('right', false, arrow_stimuli, 9); + + expect(practiceHtml).toContain('--stimuli-count: 9'); + expect(practiceHtml).toContain('practice'); + }); + + it("should handle different stimuli counts with custom property", () => { + const testCounts = [3, 5, 7, 9, 11, 15, 21]; + + testCounts.forEach(count => { + const html = createFlankerStim('left', true, default_stimuli, count); + expect(html).toContain(`--stimuli-count: ${count}`); + expect(html).toContain('flanker-stim'); + }); + }); + + it("should work with edge cases for responsive sizing", () => { + // Test with very large count + const htmlLarge = createFlankerStim('left', true, fish_stimuli, 25); + expect(htmlLarge).toContain('--stimuli-count: 25'); + + // Test with minimum count (enforced to 3) + const htmlMin = createFlankerStim('left', true, fish_stimuli, 1); + expect(htmlMin).toContain('--stimuli-count: 3'); + + // Test with invalid input (should fall back to default) + const htmlInvalid = createFlankerStim('left', true, fish_stimuli, NaN); + expect(htmlInvalid).toContain('--stimuli-count: 5'); + }); + }); +}); \ No newline at end of file diff --git a/packages/flanker-inhibitory/src/index.ts b/packages/flanker-inhibitory/src/index.ts new file mode 100644 index 00000000..cc5b5904 --- /dev/null +++ b/packages/flanker-inhibitory/src/index.ts @@ -0,0 +1,532 @@ +import { JsPsych } from "jspsych"; +import jsPsychHtmlButtonResponse from '@jspsych/plugin-html-button-response'; +import jsPsychInstructions from '@jspsych/plugin-instructions'; +import { trial_text, instruction_pages } from './text'; +import { layered_stimuli, fish_only, arrow_only, custom_stimulus } from './stimuli'; + +function createInstructions(instruction_pages_data = instruction_pages) { + return { + type: jsPsychInstructions, + pages: instruction_pages_data.map(page => `

${page}

`), + show_clickable_nav: true, + allow_keys: true, + key_forward: 'ArrowRight', + key_backward: 'ArrowLeft', + button_label_previous: trial_text.back_button, + button_label_next: trial_text.next_button, + on_finish: function(data: any) { + data.phase = 'instructions'; + } + }; +} + +// ============================================================================ +// SVG UTILITIES +// ============================================================================ + +// Robust SVG flipping with proper alignment handling +function flipSVG(svgString: string): string { + if (!svgString || typeof svgString !== 'string') return svgString; + + // Check if SVG already contains a flip transform + const hasExistingTransform = svgString.includes('matrix(-1,') || + svgString.includes('matrix(-1 ') || + svgString.includes('scaleX(-1)') || + svgString.includes('scale(-1'); + + // If already flipped, remove the flip transform to restore original orientation + if (hasExistingTransform) { + return svgString + .replace(/transform="matrix\(-1,\s*0,\s*0,\s*1,\s*0,\s*0\)"/g, '') + .replace(/transform="matrix\(-1\s+0\s+0\s+1\s+0\s+0\)"/g, '') + .replace(/transform="scaleX\(-1\)"/g, '') + .replace(/transform="scale\(-1,\s*1\)"/g, '') + .replace(/transform="scale\(-1\s+1\)"/g, '') + .replace(/style="[^"]*transform:\s*scaleX\(-1\)[^"]*"/g, (match) => { + return match.replace(/transform:\s*scaleX\(-1\);?/, '').replace(/style=""/, ''); + }); + } + + // Parse SVG to get dimensions for proper transform calculation + const viewBoxMatch = svgString.match(/viewBox="([^"]+)"/); + const widthMatch = svgString.match(/width="([^"]+)"/); + + let translateX = 0; + if (viewBoxMatch) { + const viewBox = viewBoxMatch[1].split(/\s+/); + if (viewBox.length >= 3) { + translateX = parseFloat(viewBox[2]) || 0; // width from viewBox + } + } else if (widthMatch) { + translateX = parseFloat(widthMatch[1]) || 0; + } + + // Apply proper SVG transform that maintains alignment + if (translateX > 0) { + // Insert transform into the SVG element itself for better precision + return svgString.replace( + /]*)>/, + `` + ).replace(/<\/svg>$/, ''); + } else { + // Fallback to CSS transform with inline-block container + return `${svgString}`; + } +} + +// Simple SVG layering using CSS +function layerSVGs(svgArray: string[], flip: boolean = false): string { + if (!svgArray || !Array.isArray(svgArray) || svgArray.length === 0) return ''; + if (svgArray.length === 1) return flip ? flipSVG(svgArray[0]) : svgArray[0]; + + // Layer SVGs using CSS positioning, flip each SVG individually if needed + return ` + ${svgArray.map((svg, index) => + `${flip ? flipSVG(svg) : svg}` + ).join('')} + `; +} + +// Simplified stimulus processing +function processStimuli(stimuli: { left?: string[], right?: string[] } | string[] | { left: string, right: string }) { + // If already processed (has left/right strings), return as-is + if (stimuli && typeof stimuli === 'object' && !Array.isArray(stimuli) && + typeof stimuli.left === 'string' && typeof stimuli.right === 'string') { + return stimuli as { left: string, right: string }; + } + + // If simple array, treat as right-facing and generate left by flipping + if (Array.isArray(stimuli)) { + return { + right: layerSVGs(stimuli, false), + left: layerSVGs(stimuli, true) + }; + } + + // If object with arrays + if (stimuli && typeof stimuli === 'object') { + const obj = stimuli as { left?: string[], right?: string[] }; + if (obj.left && obj.right) { + return { + left: layerSVGs(obj.left, false), + right: layerSVGs(obj.right, false) + }; + } else if (obj.right) { + return { + right: layerSVGs(obj.right, false), + left: layerSVGs(obj.right, true) + }; + } else if (obj.left) { + return { + left: layerSVGs(obj.left, false), + right: layerSVGs(obj.left, true) + }; + } + } + + // Default fallback + return { + right: layerSVGs(layered_stimuli, false), + left: layerSVGs(layered_stimuli, true) + }; +} + +// Pre-processed stimuli for convenience (lazy-loaded) +export const default_stimuli = processStimuli(layered_stimuli); +export const fish_stimuli = processStimuli(fish_only); +export const arrow_stimuli = processStimuli(arrow_only); + +// ============================================================================ +// STIMULUS CREATION FUNCTIONS +// ============================================================================ + +export function createFlankerStim(direction, congruent, stimuli: string[] | { left?: string[]; right?: string[] } | { left: string; right: string } = layered_stimuli, stimuli_amount: number = 5) { + // Ensure stimuli_amount is valid (odd number ≥3) + let safeAmount = typeof stimuli_amount === 'number' && !isNaN(stimuli_amount) && isFinite(stimuli_amount) ? stimuli_amount : 5; + const validAmount = Math.max(3, safeAmount % 2 === 0 ? safeAmount + 1 : safeAmount); + const halfFlankers = Math.floor(validAmount / 2); + + // Process stimuli + const processedStimuli = processStimuli(stimuli); + const center = processedStimuli[direction]; + const flanker = congruent ? processedStimuli[direction] : processedStimuli[direction === 'left' ? 'right' : 'left']; + + let html = `
`; + + // Add left flankers + for (let i = 0; i < halfFlankers; i++) { + html += `${flanker}`; + } + + // Add center stimulus + html += `${center}`; + + // Add right flankers + for (let i = 0; i < halfFlankers; i++) { + html += `${flanker}`; + } + + html += `
`; + return html; +} + +export function createPracticeStim(direction, congruent, stimuli: string[] | { left?: string[]; right?: string[] } | { left: string; right: string } = layered_stimuli, stimuli_amount: number = 5) { + // Ensure stimuli_amount is valid (odd number ≥3) + let safeAmount = typeof stimuli_amount === 'number' && !isNaN(stimuli_amount) && isFinite(stimuli_amount) ? stimuli_amount : 5; + const validAmount = Math.max(3, safeAmount % 2 === 0 ? safeAmount + 1 : safeAmount); + const halfFlankers = Math.floor(validAmount / 2); + + // Process stimuli + const processedStimuli = processStimuli(stimuli); + const center = processedStimuli[direction]; + const flanker = congruent ? processedStimuli[direction] : processedStimuli[direction === 'left' ? 'right' : 'left']; + + let html = `
`; + + // Add left flankers + for (let i = 0; i < halfFlankers; i++) { + html += `${flanker}`; + } + + // Add center stimulus with highlighting + html += `${center}`; + + // Add right flankers + for (let i = 0; i < halfFlankers; i++) { + html += `${flanker}`; + } + + html += `
`; + return html; +} + +// ============================================================================ +// INTERFACES +// ============================================================================ + +export interface FlankerConfig { + stimuli_type?: 'fish' | 'arrow' | 'layered' | 'custom'; + svg?: string[]; // Override parameter - array of SVGs to layer + stimuli_amount?: number; // Number of stimuli in flanker display (default: 5, must be odd ≥3) + fixation_duration?: number; + stimulus_duration?: number; // Maximum time for stimulus display before auto-advancing (undefined = no timeout) + show_instructions?: boolean; + show_practice?: boolean; + num_practice?: number; + num_trials?: number; +} + +// ============================================================================ +// UTILITY FUNCTIONS +// ============================================================================ + +export function calculatePerformance(data: any[]) { + const trial_data = data.filter(d => d.task === 'flanker'); + + if (trial_data.length === 0) { + return { accuracy: 0, mean_rt: 0, total_trials: 0 }; + } + + const correct_trials = trial_data.filter(d => d.correct); + const accuracy = (correct_trials.length / trial_data.length) * 100; + const mean_rt = correct_trials.length > 0 + ? correct_trials.reduce((sum, d) => sum + d.rt, 0) / correct_trials.length + : 0; + + return { + accuracy, + mean_rt, + total_trials: trial_data.length, + correct_trials: correct_trials.length + }; +} + +// ============================================================================ +// MAIN TIMELINE FUNCTION +// ============================================================================ + +export function createTimeline(jsPsych: JsPsych, config: FlankerConfig = {}) { + const { + stimuli_type = 'layered', + svg, + stimuli_amount = 5, + fixation_duration = 500, + stimulus_duration, + show_instructions = true, + show_practice = true, + num_practice = 8, + num_trials = 20, + } = config; + + // Determine which stimuli to use based on priority: + // 1. svg override parameter (highest priority) + // 2. stimuli_type selection + let stimuli; + if (svg) { + stimuli = processStimuli(svg); // Process the SVG override array to handle flipping + } else if (stimuli_type === 'custom') { + stimuli = processStimuli(custom_stimulus); + } else if (stimuli_type === 'arrow') { + stimuli = arrow_stimuli; + } else if (stimuli_type === 'fish') { + stimuli = fish_stimuli; + } else { + stimuli = default_stimuli; // layered by default + } + const timeline = []; + + // Instructions + if (show_instructions) { + timeline.push(createInstructions(instruction_pages)); + } + + // Practice + if (show_practice) { + // Practice intro + timeline.push({ + type: jsPsychHtmlButtonResponse, + stimulus: ` +
+

${trial_text.practice_intro}

+
+ `, + choices: [trial_text.start_button], + button_html: (choice) => ``, + data: { + task: 'practice-intro', + phase: 'practice' + } + }); + + // Only add practice trials if num_practice > 0 + if (num_practice > 0) { + // Practice trials + const practice_variables = [ + {direction: 'left', congruent: true}, + {direction: 'left', congruent: false}, + {direction: 'right', congruent: true}, + {direction: 'right', congruent: false}, + ]; + + const practice_trials = jsPsych.randomization.repeat(practice_variables, Math.floor(num_practice/4)) + .concat(jsPsych.randomization.sampleWithoutReplacement(practice_variables, num_practice%4)); + + // Fixation + const practice_fixation = { + type: jsPsychHtmlButtonResponse, + stimulus: `
${trial_text.fixation_cross}
`, + choices: [], + trial_duration: fixation_duration, + data: { + task: 'fixation', + phase: 'practice' + } + }; + + // Practice trial + const practice_trial = { + type: jsPsychHtmlButtonResponse, + stimulus: function() { + const direction = jsPsych.evaluateTimelineVariable('direction'); + const congruent = jsPsych.evaluateTimelineVariable('congruent'); + return ` +
+
+
${trial_text.main_task_prompt}
+ ${createPracticeStim(direction, congruent, stimuli, stimuli_amount)} +
+
+ `; + }, + choices: [trial_text.left_button, trial_text.right_button], + button_html: (choice) => ``, + trial_duration: stimulus_duration, + data: { + task: 'flanker', + phase: 'practice', + direction: jsPsych.timelineVariable('direction'), + congruent: jsPsych.timelineVariable('congruent') + }, + on_finish: function(data: any) { + const correct_response = data.direction === 'left' ? 0 : 1; + data.correct = data.response === correct_response; + } + }; + + // Feedback + const practice_feedback = { + type: jsPsychHtmlButtonResponse, + stimulus: () => { + const last_trial = jsPsych.data.get().last(1).values()[0]; + const feedback = last_trial.correct ? trial_text.correct_feedback : trial_text.incorrect_feedback; + return ``; + }, + choices: [], + trial_duration: 1000, + data: { + task: 'feedback', + phase: 'practice' + } + }; + + // Practice timeline + timeline.push({ + timeline: [practice_fixation, practice_trial, practice_feedback], + timeline_variables: practice_trials, + randomize_order: true + }); + } + + // Practice completion + timeline.push({ + type: jsPsychHtmlButtonResponse, + stimulus: () => { + + return ` +
+

${trial_text.practice_outro}

+
+ `; + }, + choices: [trial_text.continue_button], + button_html: (choice) => ``, + data: { + task: 'practice-complete', + phase: 'practice' + } + }); + } + + // Main task + timeline.push({ + type: jsPsychHtmlButtonResponse, + stimulus: ` +
+

${trial_text.main_task_intro}

+
+ `, + choices: [trial_text.ready_button], + button_html: (choice) => ``, + data: { + task: 'main-intro', + phase: 'main' + } + }); + + // Main trials - only add if num_trials > 0 + if (num_trials > 0) { + const trial_variables = [ + {direction: 'left', congruent: true}, + {direction: 'left', congruent: false}, + {direction: 'right', congruent: true}, + {direction: 'right', congruent: false}, + ]; + + const main_trials = jsPsych.randomization.repeat(trial_variables, Math.floor(num_trials/4)) + .concat(jsPsych.randomization.sampleWithoutReplacement(trial_variables, num_trials%4)); + + // Fixation + const fixation = { + type: jsPsychHtmlButtonResponse, + stimulus: `
${trial_text.fixation_cross}
`, + choices: [], + trial_duration: fixation_duration, + data: { + task: 'fixation', + phase: 'main' + } + }; + + // Main trial + const flanker_trial = { + type: jsPsychHtmlButtonResponse, + stimulus: function() { + const direction = jsPsych.evaluateTimelineVariable('direction'); + const congruent = jsPsych.evaluateTimelineVariable('congruent'); + return ` +
+
+
${trial_text.main_task_prompt}
+ ${createFlankerStim(direction, congruent, stimuli, stimuli_amount)} +
+
+ `; + }, + choices: [trial_text.left_button, trial_text.right_button], + button_html: (choice) => ``, + trial_duration: stimulus_duration, + data: { + task: 'flanker', + phase: 'main', + direction: jsPsych.timelineVariable('direction'), + congruent: jsPsych.timelineVariable('congruent') + }, + on_finish: function(data: any) { + const correct_response = data.direction === 'left' ? 0 : 1; + data.correct = data.response === correct_response; + } + }; + + // Main task timeline + timeline.push({ + timeline: [fixation, flanker_trial], + timeline_variables: main_trials, + randomize_order: true + }); + } + + // Completion + timeline.push({ + type: jsPsychHtmlButtonResponse, + stimulus: () => { + const main_data = jsPsych.data.get().filter({task: 'flanker', phase: 'main'}).values(); + const performance = calculatePerformance(main_data); + + return ` +
+

${trial_text.task_complete}

+
+

${trial_text.performance_title}

+

${trial_text.accuracy_label} ${performance.accuracy.toFixed(1)}%

+

${trial_text.response_time_label} ${performance.mean_rt.toFixed(0)}ms

+
+
+ `; + }, + choices: [trial_text.end_button], + button_html: (choice) => ``, + data: { + task: 'complete', + phase: 'completion' + } + }); + + return { + timeline: timeline + }; +} + +// ============================================================================ +// EXPORTS +// ============================================================================ + +export const timelineUnits = { + instructions: "Instructions for the flanker inhibitory control task", + practice: "Practice trials with feedback", + main: "Main flanker task trials", + completion: "Task completion screen" +}; + +export const utils = { + calculatePerformance, + createFlankerStim, + createPracticeStim, + createInstructions +}; + +// Export text and createInstructions for external use +export { trial_text, instruction_pages } from './text'; +export { createInstructions }; + +// Default export +export default { createTimeline, timelineUnits, utils }; \ No newline at end of file diff --git a/packages/flanker-inhibitory/src/stimuli.ts b/packages/flanker-inhibitory/src/stimuli.ts new file mode 100644 index 00000000..a232937e --- /dev/null +++ b/packages/flanker-inhibitory/src/stimuli.ts @@ -0,0 +1,53 @@ +// ============================================================================ +// LAYERED SVG STIMULI +// ============================================================================ + +// Fish SVG (base layer) +const fish_svg = ` + + + + + + + + +`; + +// Arrow SVG (overlay layer) +const arrow_svg = ` + +`; + +// Default layered stimuli - fish with arrow overlay +export const layered_stimuli = [ + fish_svg, + arrow_svg +]; + +// Fish only +export const fish_only = [ + fish_svg +]; + +// Arrow only +export const arrow_only = [ + arrow_svg +]; + +// Custom stimulus - you layer SVGs and add the directional svgs separately. +// If you add only one direction, it will be transformed to the other direction automatically. +export const custom_stimulus = { + left: [ + '', + '' + + '' + + '' + ], + right: [ + '', + '' + + '' + + '' + ] +}; \ No newline at end of file diff --git a/packages/flanker-inhibitory/src/styles.css b/packages/flanker-inhibitory/src/styles.css new file mode 100644 index 00000000..9528cfb6 --- /dev/null +++ b/packages/flanker-inhibitory/src/styles.css @@ -0,0 +1,475 @@ +/* GENERAL STYLES */ + +h1, h2, h3 { + line-height: 1.6; + margin: 3vmin; + word-wrap: break-word; + overflow-wrap: break-word; + hyphens: auto; + max-width: 100%; + box-sizing: border-box; +} + +p { + margin: 5vmin 1vmin; + font-size: clamp(15px, 8vmin, 40px); + line-height: 1.6; + /* Prevent horizontal overflow */ + word-wrap: break-word; + overflow-wrap: break-word; + hyphens: auto; + max-width: 100%; + box-sizing: border-box; + text-align: center; +} + +/* Styles for any normal HTML button */ +.continue-button { + padding: clamp(20px, 8vh, 50px) clamp(25px, 12vmin, 60px); /* Bigger padding */ + @media (orientation: landscape) and (max-width: 1024px) { + padding: clamp(0px, 3vh, 22px); /* Smaller padding for landscape */ + } + font-size: clamp(18px, 8vmin, 45px); /* Bigger font */ + border: 3px solid #00000061; + border-radius: 5%; + cursor: pointer; + margin: 2vmin; + margin-top: 10vh; + transition: background-color 0.3s; + min-height: 60px; /* Bigger minimum height */ + flex: 1; /* Buttons will grow to fill available space */ + max-width: min(45vw, 400px); /* Responsive max-width */ + /* Disable double-click zoom */ + touch-action: manipulation !important; + user-select: none !important; +} + +/* INSTRUCTIONS TRIAL STYLES */ + +.instructions-container { + max-width: 90vw; /* Limit width for better readability */ + margin: 0 auto; /* Center the container */ + padding: 2vmin; /* Add some padding */ + box-sizing: border-box; /* Include padding in width calculations */ + overflow-x: hidden; /* Prevent horizontal overflow */ +} + +/* Mobile-friendly instruction navigation buttons */ +.jspsych-instructions-nav { + display: flex; + gap: 25vw; + justify-content: center; + margin-top: auto; + padding: clamp(20px, 5vmin, 40px) !important; + min-height: clamp(20px, 50vmin, 250px); + align-items: flex-end; +} + +/* Instruction navigation button styles */ +.jspsych-instructions-nav .jspsych-btn { + background-color: #fff; + color: #000; + width: 20vmin; + height: 20vmin; + border: 3px solid #ccc; + border-radius: 50%; + font-size: clamp(15px, 7vmin, 35px); + cursor: pointer; + min-height: 100px; + min-width: 100px; + font-weight: 600; + + /* Disable double-click zoom */ + touch-action: manipulation !important; + user-select: none !important; +} + +.jspsych-instructions-nav .jspsych-btn:active { + background-color: #f0f0f0; + border-color: #999; +} + +.jspsych-instructions-nav .jspsych-btn:disabled { + visibility: hidden; /* Keeps the space but hides the button */ +} + +/* PRACTICE TRIAL STYLES */ + +/* pre-practice and post-practice round pages */ +.practice-instructions, .ready-screen { + max-width: 90vw; /* Limit width for better readability */ + margin: 0 auto; /* Center the container */ + padding: 2vmin; /* Add some padding */ + box-sizing: border-box; /* Include padding in width calculations */ + overflow-x: hidden; /* Prevent horizontal overflow */ + padding-bottom: 5vh; +} + +/* EXPERIMENT TRIAL STYLES */ + +/* Fixation cross (inter-trial), set to invisible by default */ +.fixation { + display: flex; + align-items: center; + justify-content: center; + font-size: 8vmin; + color: #ffffff; /* White color for invisibility */ + font-weight: bold; + min-height: 20vmin; +} +.trial-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; + font-family: Arial, sans-serif; + padding: 2vmin 0; /* Add vertical padding */ +} + +/* Task instructions in trial container, text on top. Usually there is none. */ +.task-instructions { + text-align: center; + p { + font-size: clamp(15px, 6vmin, 35px); + margin: 2vmin; + } +} + +/* Target stimulus*/ +.target-stimulus { + width: 30vmin; + height: 30vmin; + border: 3.5px solid #000000bd; + border-radius: 2vmin; + background-color: white; + display: inline-flex; + align-items: center; + justify-content: center; + box-shadow: 0 2vmin 3vmin rgba(0, 0, 0, 0.15); + margin: 2vmin; + svg { + width: 100%; + height: 100%; + } +} + +.target-stimulus.flash { + animation: flash-border 0.75s ease-in-out 3; +} + +@keyframes flash-border { + 0%, 100% { + border-color: #2c3e50; + box-shadow: 0 2vmin 3vmin rgba(0, 0, 0, 0.15); + } + 50% { + border-color: #e74c3c; + box-shadow: 0 2vmin 4vmin rgba(231, 76, 60, 0.4); + } +} + +/* Button layout - flexible responsive grid */ +#jspsych-html-button-response-btngroup { + display: flex; + flex-wrap: wrap; + gap: 30vw; /* Smaller gap for better centering */ + max-width: 75%; /* Wider container */ + margin: auto; /* Auto margins for centering */ + justify-content: center; + align-items: center; +} + + +/* Choice buttons - dynamic sizing based on available space */ +.choice-option { + border: 2px solid #bdc3c7; + border-radius: 2vmin; + background-color: white !important; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer !important; + transition: all 0.3s ease; + box-shadow: 0 1vmin 2vmin rgba(0, 0, 0, 0.1); + padding: 1.5vmin; + margin: 2px 5px; + /* Disable double-click zoom */ + touch-action: manipulation !important; + user-select: none !important; + /* Fix the flexbox sizing */ + flex: 0 0 auto; /* Don't grow, don't shrink, use basis */ + flex-basis: clamp(20vmin, calc(163vmin / var(--num-choices, 4)), 90vmin); + height: clamp(20vmin, calc(80vh / var(--num-choices, 4)), 33vmin); + + /* Container-linked SVG scaling */ + svg { + width: 100%; + height: 100%; + } +} + +/* Hover effect for choice options, only visible on desktop */ +.choice-option:hover { + border-color: #3498db; + transform: translateY(-0.5vmin); + box-shadow: 0 2vmin 3vmin rgba(0, 0, 0, 0.15); +} + +/* Flashing for practice round */ +.choice-option.flash-choices { + animation: flash-choices 1s ease-in-out infinite; +} + +@keyframes flash-choices { + 0%, 100% { + border-color: #bdc3c7; + box-shadow: 0 1vmin 2vmin rgba(0, 0, 0, 0.1); + } + 50% { + border-color: #f39c12; + box-shadow: 0 2vmin 3vmin rgba(243, 156, 18, 0.3); + } +} + +/* FLANKER-SPECIFIC STYLES - Keep existing functionality */ + +/* Trial display */ +.flanker-trial { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 50vh; + padding: 3vmin; + gap: 4vmin; + max-width: 92vw; +} + +.trial-prompt { + font-size: 3vmin; + font-weight: 200; + color: #2c3e50; + text-align: center; + max-width: 90vw; + line-height: normal; +} + +/* Flanker stimulus */ +.flanker-stim { + display: flex; + align-items: center; + justify-content: center; + margin: 3vmin 0; + padding: 3vmin; + width: 100%; + gap: max(0.5vmin, calc(90vw / var(--stimuli-count, 5) * 0.1)); + + /* Dynamic sizing based on number of stimuli and screen space */ + --base-size: min( + calc(85vw / var(--stimuli-count, 5)), + calc(70vh / 1), + 20vmin + ); + --min-size: 4vmin; + --max-size: 25vmin; + --svg-size: max(var(--min-size), min(var(--max-size), var(--base-size))); + + /* Special handling for many stimuli (> 7) to ensure readability */ + --many-stimuli-adjustment: calc(max(0, var(--stimuli-count, 5) - 7) * 0.5vmin); + --adjusted-svg-size: max(calc(var(--svg-size) + var(--many-stimuli-adjustment)), var(--min-size)); + max-width: 95vw; + +} + +.flanker-stim span { + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + vertical-align: middle; + line-height: 0; +} + +.flanker-stim svg { + width: var(--adjusted-svg-size, var(--svg-size)); + height: var(--adjusted-svg-size, var(--svg-size)); + display: block; +} + +/* Practice highlighting */ +.flanker-stim.practice .center.highlighted { + background-color: #fff3cd; + border: 3px solid #ffc107; + border-radius: 50%; + padding: 1.5vmin; + animation: pulse 1.5s infinite; +} + +@keyframes pulse { + 0% { + box-shadow: 0 0 0 0 rgba(255, 193, 7, 0.7); + } + 70% { + box-shadow: 0 0 0 2vmin rgba(255, 193, 7, 0); + } + 100% { + box-shadow: 0 0 0 0 rgba(255, 193, 7, 0); + } +} + +/* Feedback */ +.feedback { + display: flex; + align-items: center; + justify-content: center; + min-height: 20vh; + font-size: 6vmin; + font-weight: bold; + text-align: center; + padding: 3vmin; +} + +.feedback.correct { + color: #27ae60; +} + +.feedback.incorrect { + color: #e74c3c; +} + +/* Performance summary */ +.performance-summary { + background-color: #f8f9fa; + border: 1px solid #dee2e6; + border-radius: 2vmin; + padding: 4vmin; + margin: 4vmin 0; + text-align: center; +} + +.performance-summary h3 { + color: #495057; + margin-bottom: 3vmin; + text-align: center; + font-size: 4.5vmin; +} + +.performance-summary p { + margin: 2vmin 0; + font-size: 3.5vmin; + color: #6c757d; + text-align: center; +} + +/* Styles for tablets */ +@media (max-width: 768px) { + .jspsych-instructions-nav { + position: fixed !important; + bottom: 30vmin !important; + left: 50% !important; + transform: translateX(-50%) !important; + } + + /* Add padding to prevent content from going under fixed buttons */ + .instructions-container { + padding-bottom: 45vmin; /* 30vmin for button position + 5vmin safety */ + } + + /* Experiment-specific, adjusting sizes of images */ + .target-stimulus { + width: 35vmin; + height: 35vmin; + + } + .choice-option { + height: clamp(20vmin, calc(85vh / var(--num-choices, 4)), 30vh); + flex-basis: clamp(15vmin, calc(165vw / var(--num-choices, 4)), 43vw); + /* width: clamp(30vmin, calc(150vw / var(--num-choices, 4)), 43vmin); */ + /* aspect-ratio: 1;*/ + } + + /* Flanker-specific tablet adjustments */ + .flanker-stim { + gap: max(0.3vmin, calc(85vw / var(--stimuli-count, 5) * 0.08)); + padding: 2vmin; + + /* Tighter sizing for tablets */ + --base-size: min( + calc(80vw / var(--stimuli-count, 5)), + calc(50vh / 1), + 15vmin + ); + --min-size: 3vmin; + --max-size: 12vmin; + } +} + +/* Styles for phones to override tablet */ +@media (max-width: 480px) { + /* Smaller padding for phone to allow more text */ + .instructions-container { + padding-bottom: 40vmin; + } + .target-container { + margin-bottom: 4vmin; /* Adjusted for better spacing on small screens */ + } + /* Experiment-specific, adjusting sizes of images */ + .target-stimulus { + width: 45vmin; + height: 45vmin; + } + + /* Flanker-specific phone adjustments */ + .trial-prompt { + font-size: 4vmin; + } + + .flanker-stim { + gap: max(0.3vmin, calc(85vw / var(--stimuli-count, 5) * 0.08)); + padding: 2vmin; + + /* Tighter sizing for small screens */ + --base-size: min( + calc(80vw / var(--stimuli-count, 5)), + calc(50vh / 1), + 15vmin + ); + --min-size: 3vmin; + --max-size: 12vmin; + } +} + +/* Landscape mode adjustments */ +@media (orientation: landscape) { + .jspsych-instructions-nav { + bottom: 3vh; + gap: 30vw; + } + + .target-container { + margin-bottom: 1vmin; + } + + .target-stimulus { + width: 30vmin; + height: 30vmin; + border: 4px solid #000000bd; + } + + .choice-option { + margin: 0; + flex-basis: clamp(15vmin, calc(85vw / var(--num-choices, 4)), 25vw) !important; + height: clamp(15vmin, calc(135vh / var(--num-choices, 4)), 35vmin) !important; + margin: -10px 2px; + } + + /* Flanker-specific landscape adjustments */ + .flanker-stim { + gap: max(0.4vw, calc(88vw / var(--stimuli-count, 5) * 0.09)); + + /* Medium screen sizing - use vw for landscape */ + --base-size: calc(80vw / var(--stimuli-count, 5)); + --min-size: 3.5vw; + --max-size: 18.5vw; + } +} \ No newline at end of file diff --git a/packages/flanker-inhibitory/src/text.ts b/packages/flanker-inhibitory/src/text.ts new file mode 100644 index 00000000..dc18957d --- /dev/null +++ b/packages/flanker-inhibitory/src/text.ts @@ -0,0 +1,78 @@ +/* This file contains the configurable text used in the flanker inhibitory timeline. + * Researchers can modify these texts to change the language or instructions. + */ + +export const trial_text = { + // Button texts + continue_button: "Continue", + start_button: "Start", + ready_button: "I'm Ready", + end_button: "End", + // Instruction pages buttons text, these will always have arrows < and > + // these do not work right now due to CSS fixed position, might fix later + next_button: "", + back_button: "", + practice_button: "Practice Again", + left_button: "←", + right_button: "→", + + // Task completion messages + task_complete: "Thank you for participating in the flanker inhibitory control task.", + + // Practice phase text: pre-practice intro and post-practice message. + practice_intro: "Let's practice! Look at the middle fish and click the button that shows which way it's swimming.", + practice_outro: "

Practice Complete!

Great job! Now you're ready for the real task.", + + // Main task instructions and inter-task prompt + main_task_intro: "

Main Task

Now we'll start the real task. Are you ready?", + main_task_prompt: "", + + // Performance summary labels + performance_title: "Your Performance:", + accuracy_label: "Accuracy:", + response_time_label: "Average Response Time:", + + // Fixation and inter-trial + fixation_cross: "+", + + // Feedback messages (optional) + correct_feedback: "Correct", + incorrect_feedback: "Incorrect", +}; + +/* + * This is an array of page objects that have configurable texts for the instruction pages + * before the actual trials. Researchers can modify these instructions to change the + * task description, add new instruction pages, or translate to different languages. + * + * Each page object can contain: + * - header: Main title (displays in

) + * - header2: Subtitle (displays in

) + * - description: Main description text (displays in

) + * - task_explanation: Detailed task explanation (displays in

) + * - performance_note: Performance tips (displays in

) + * - strategy_title: Strategy section title (displays in

) + * - strategy_intro: Introduction to strategy (displays in

) + * - strategy_points: Array of strategy bullet points (displays as

  • ) + * - start_prompt: Final instruction before starting (displays in

    ) + * - buttons: Array of button text options + * - button_html: Custom HTML for buttons (optional) + */ + +/* + * This is an array of HTML strings for instruction pages displayed before the actual trials. + * Researchers can modify these instructions to change the task description, add new instruction + * pages, or translate to different languages. + * + * Each string should contain valid HTML that will be displayed as an instruction page. + * You can add more pages by adding more strings to the array, or modify existing pages + * by editing the HTML content. + */ + +export const instruction_pages = [ + "Look at the middle fish only - ignore the other fish.", + "Click the ← button if it's swimming left, click the → button if it's swimming right.", + "Try to respond as quickly and accurately as possible.", + "Continue to start the practice round." +]; + diff --git a/packages/false-memory/tsconfig.json b/packages/flanker-inhibitory/tsconfig.json similarity index 100% rename from packages/false-memory/tsconfig.json rename to packages/flanker-inhibitory/tsconfig.json diff --git a/packages/spatial-cueing/CHANGELOG.md b/packages/spatial-cueing/CHANGELOG.md deleted file mode 100644 index a9dc3552..00000000 --- a/packages/spatial-cueing/CHANGELOG.md +++ /dev/null @@ -1,16 +0,0 @@ -# @jspsych-timelines/spatial-cueing - -## 0.2.0 - -### Minor Changes - -- 22c0236: delete -task suffix - -## 0.1.0 - -### Minor Changes - -- d6a0e3b: upgrade jspsych depedencies to v8 -- 0de5056: update all jspsych dependencies to v8 -- abfa815: add spatial cueing task package -- 0b18198: upgrade to use jspsych v8 diff --git a/packages/spatial-cueing/README.md b/packages/spatial-cueing/README.md deleted file mode 100644 index 4e97e752..00000000 --- a/packages/spatial-cueing/README.md +++ /dev/null @@ -1,51 +0,0 @@ -# spatial-cueing - -## Overview - -A shareable timeline of the Posner spatial cueing task. - -## Loading - -### In browser - -```html - - - - - - - - - - diff --git a/packages/spatial-cueing/jest.config.js b/packages/spatial-cueing/jest.config.js deleted file mode 100644 index bd1aacef..00000000 --- a/packages/spatial-cueing/jest.config.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require("@jspsych/config/jest").makePackageConfig(__dirname); \ No newline at end of file diff --git a/packages/spatial-cueing/package.json b/packages/spatial-cueing/package.json deleted file mode 100644 index 35490e62..00000000 --- a/packages/spatial-cueing/package.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "name": "@jspsych-timelines/spatial-cueing", - "version": "0.2.0", - "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": { - "name": "Cherrie Chang", - "url": "https://github.com/cchang-vassar" - }, - "license": "MIT", - "bugs": { - "url": "https://github.com/jspsych/jspsych-timelines/issues" - }, - "homepage": "https://github.com/jspsych/jspsych-timelines/packages/spatial-cueing#readme", - "peerDependencies": { - "jspsych": "^8.0.1" - }, - "dependencies": {}, - "devDependencies": { - "tsup": "^8.2.4", - "typescript": "^5.5.4" - } -} \ No newline at end of file diff --git a/packages/spatial-cueing/src/index.ts b/packages/spatial-cueing/src/index.ts deleted file mode 100644 index e979524f..00000000 --- a/packages/spatial-cueing/src/index.ts +++ /dev/null @@ -1,377 +0,0 @@ -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.evaluateTimelineVariable("validity"); - const stimulus_direction = jsPsych.evaluateTimelineVariable("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.evaluateTimelineVariable("validity"); - const stimulus_direction = jsPsych.evaluateTimelineVariable("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.evaluateTimelineVariable("validity"); - const stimulus_direction = jsPsych.evaluateTimelineVariable("stimulus_direction"); - const cue_direction = cueDirectionMapper(validity, stimulus_direction); - - var stimulus = `
    `; - if (cue_direction == Direction.Left || cue_direction == Direction.Bi) { - stimulus += jsPsych.evaluateTimelineVariable("stimulus_direction") == Direction.Left ? StimulusBox.WithStimulusHighlighted : StimulusBox.Highlighted; - } - else if (jsPsych.evaluateTimelineVariable("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.evaluateTimelineVariable("stimulus_direction") == Direction.Right ? StimulusBox.WithStimulusHighlighted : StimulusBox.Highlighted; - } - else if (jsPsych.evaluateTimelineVariable("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.evaluateTimelineVariable("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.evaluateTimelineVariable("validity"); - const stimulus_direction = jsPsych.evaluateTimelineVariable("stimulus_direction"); - const cue_direction = cueDirectionMapper(validity, stimulus_direction); - - var stimulus = `
    ` + (jsPsych.evaluateTimelineVariable("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.evaluateTimelineVariable("stimulus_direction") == Direction.Right ? StimulusBox.WithStimulus : StimulusBox.Default) + `
    `; - - return stimulus; - }, - choices: ['f', 'j'], - data: { - task: "stimulus", - cue_type: "endogenous", - correct_response: () => { - return jsPsych.evaluateTimelineVariable("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/src/styles.css b/packages/spatial-cueing/src/styles.css deleted file mode 100644 index 97040407..00000000 --- a/packages/spatial-cueing/src/styles.css +++ /dev/null @@ -1,35 +0,0 @@ -.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/tsconfig.json b/packages/spatial-cueing/tsconfig.json deleted file mode 100644 index 33655f70..00000000 --- a/packages/spatial-cueing/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "@jspsych/config/tsconfig.contrib.json", - "compilerOptions": { - "baseUrl": "." - }, - "include": [ - "src", - "../cli/src/build.js" - ] -} \ No newline at end of file