diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 00000000..ffb44bec --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,16 @@ +{ + "permissions": { + "allow": [ + "WebFetch(domain:nihtoolbox.org)", + "WebFetch(domain:pmc.ncbi.nlm.nih.gov)", + "WebFetch(domain:www.svgrepo.com)", + "Bash(rg:*)", + "Bash(find:*)", + "Bash(ls:*)", + "WebFetch(domain:resources.nihtoolbox.org)", + "Bash(npm run build:*)", + "Bash(npm test)" + ], + "deny": [] + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index d80ce910..1f82da5c 100644 --- a/.gitignore +++ b/.gitignore @@ -91,7 +91,6 @@ out # Nuxt.js build / generate output .nuxt -dist # Gatsby files .cache/ @@ -132,4 +131,4 @@ dist .yarn/unplugged .yarn/build-state.yml .yarn/install-state.gz -.pnp.* +.pnp.* \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 940e6ed0..d0756fef 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,6 +2484,18 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@jspsych-contrib/plugin-spatial-nback": { + "name": "@jspsych/jspsych-contrib", + "resolved": "git+ssh://git@github.com/farhat60/jspsych-contrib.git#814d4f3cfb134bbcc4357b7c25b4221ffcb1674c", + "workspaces": [ + "packages/*", + "templates/cli" + ], + "dependencies": { + "gulp": "^5.0.0", + "gulp-replace": "^1.1.4" + } + }, "node_modules/@jspsych-timelines/arrow-flanker": { "resolved": "packages/arrow-flanker", "link": true @@ -2495,12 +2504,8 @@ "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/speeded-matching": { + "resolved": "packages/speeded-matching", "link": true }, "node_modules/@jspsych/config": { @@ -2572,22 +2577,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 +3077,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 +3203,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 +3217,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 +3225,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 +3281,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 +3344,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 +3351,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 +4053,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 +4218,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 +4842,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 +5167,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 +7267,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 +7281,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 +7384,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 +7430,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 +7444,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 +7459,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 +7478,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 +7741,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 +7757,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 +7890,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 +8075,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 +9133,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 +9185,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 +9259,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 +9272,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 +9375,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 +9944,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 +10076,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 +10087,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 +11617,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 +11633,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 +11647,34 @@ "jspsych": "^8.0.1" } }, + "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 +11684,575 @@ "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 + }, + "packages/speeded-matching": { + "name": "@jspsych-timelines/speeded-matching", + "version": "0.0.1", + "license": "MIT", + "dependencies": { + "@jspsych/plugin-html-button-response": "^2.1.0", + "@jspsych/plugin-instructions": "^2.1.0" + }, + "devDependencies": { + "@jspsych/config": "^3.2.2", + "jspsych": "^8.0.0", + "tsup": "^6.7.0", + "typescript": "^5.0.2" + } + }, + "packages/speeded-matching/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/speeded-matching/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/speeded-matching/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/speeded-matching/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/speeded-matching/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/speeded-matching/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/speeded-matching/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/speeded-matching/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/speeded-matching/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/speeded-matching/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/speeded-matching/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/speeded-matching/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/speeded-matching/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/speeded-matching/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/speeded-matching/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/speeded-matching/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/speeded-matching/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/speeded-matching/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/speeded-matching/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/speeded-matching/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/speeded-matching/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/speeded-matching/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/speeded-matching/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/speeded-matching/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/speeded-matching/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==", + "deprecated": "The work that was done in this beta branch won't be included in future versions", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "whatwg-url": "^7.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "packages/speeded-matching/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/speeded-matching/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/speeded-matching/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/speeded-matching/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" + } + }, "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/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 diff --git a/packages/speeded-matching/README.md b/packages/speeded-matching/README.md new file mode 100644 index 00000000..b97cdb9a --- /dev/null +++ b/packages/speeded-matching/README.md @@ -0,0 +1,100 @@ +# speeded-matching + +## Overview + +An assessment of processing speed. Participants are asked to identify which of four pictures matches the target picture at the top of the screen. + +## Loading + +### In browser + +```html + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/false-memory/jest.config.js b/packages/speeded-matching/jest.config.cjs similarity index 100% rename from packages/false-memory/jest.config.js rename to packages/speeded-matching/jest.config.cjs diff --git a/packages/speeded-matching/package.json b/packages/speeded-matching/package.json new file mode 100644 index 00000000..4c063cad --- /dev/null +++ b/packages/speeded-matching/package.json @@ -0,0 +1,40 @@ +{ + "name": "@jspsych-timelines/speeded-matching", + "version": "0.0.1", + "description": "An assessment of processing speed. Participants are asked to identify which of four pictures matches the target picture at the top of the screen.", + "type": "module", + "main": "dist/index.mjs", + "types": "dist/index.d.ts", + "unpkg": "dist/index.browser.min.js", + "scripts": { + "build": "tsup src/index.ts --format esm,iife --sourcemap --dts --treeshake --clean --global-name jsPsychTimelineSpeededMatching", + "test": "jest" + }, + "repository": { + "type": "git", + "url": "https://github.com/farhat60/jspsych-timelines/", + "directory": "packages" + }, + "keywords": [ + "jsPsych" + ], + "author": { + "name": "Hunter Farhat", + "url": "github.com/farhat60" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/farhat60/jspsych-timelines//issues" + }, + "homepage": "https://github.com/jspsych/jspsych-timelines/packages/speeded-matching/README.md", + "devDependencies": { + "jspsych": "^8.0.0", + "@jspsych/config": "^3.2.2", + "tsup": "^6.7.0", + "typescript": "^5.0.2" + }, + "dependencies": { + "@jspsych/plugin-html-button-response": "^2.1.0", + "@jspsych/plugin-instructions": "^2.1.0" + } + } \ No newline at end of file diff --git a/packages/speeded-matching/src/index.spec.ts b/packages/speeded-matching/src/index.spec.ts new file mode 100644 index 00000000..06ea77f6 --- /dev/null +++ b/packages/speeded-matching/src/index.spec.ts @@ -0,0 +1,617 @@ +import { JsPsych } from "jspsych"; +import { createTimeline, utils, timelineUnits, trial_text, instruction_pages } from "./index"; +import { test_items } from "./test-items"; + +// Mock jsPsych instance +const mockJsPsych = {} as JsPsych; + +// Mock Math.random for predictable tests +const mockMath = Object.create(global.Math); +mockMath.random = jest.fn(); +global.Math = mockMath; + +// Mock DOM methods +const mockQuerySelector = jest.fn(); +const mockQuerySelectorAll = jest.fn(); +const mockAddEventListener = jest.fn(); +const mockClassList = { + add: jest.fn(), + remove: jest.fn() +}; + +global.document = { + querySelector: mockQuerySelector, + querySelectorAll: mockQuerySelectorAll +} as any; + +describe("Speeded Matching Task", () => { + beforeEach(() => { + jest.clearAllMocks(); + mockMath.random.mockReturnValue(0.5); + mockQuerySelector.mockReturnValue({ + style: { setProperty: jest.fn() } + }); + mockQuerySelectorAll.mockReturnValue([ + { addEventListener: mockAddEventListener, classList: mockClassList }, + { addEventListener: mockAddEventListener, classList: mockClassList } + ]); + }); + + describe("createTimeline", () => { + it("should create a basic timeline with default configuration", () => { + const timeline = createTimeline(mockJsPsych); + + expect(timeline).toBeDefined(); + expect(timeline.timeline).toBeInstanceOf(Array); + expect(timeline.timeline.length).toBeGreaterThan(0); + }); + + it("should create timeline with custom number of trials", () => { + const customTrials = 5; + const timeline = createTimeline(mockJsPsych, { num_trials: customTrials }); + + // Count actual speeded-matching-trial items + const mainTrials = timeline.timeline.filter( + (item: any) => item.data?.task === 'speeded-matching-trial' + ); + expect(mainTrials).toHaveLength(customTrials); + }); + + it("should include instructions when show_instructions is true", () => { + const timeline = createTimeline(mockJsPsych, { show_instructions: true }); + + // Should contain instruction trial + const hasInstructions = timeline.timeline.some( + (item: any) => item.data?.task === 'instruction-pages' + ); + expect(hasInstructions).toBe(true); + }); + + it("should include practice round when show_practice is true", () => { + const timeline = createTimeline(mockJsPsych, { show_practice: true }); + + // Should contain practice trials + const hasPractice = timeline.timeline.some( + (item: any) => item.data?.task?.includes('practice') + ); + expect(hasPractice).toBe(true); + }); + + it("should use custom number of choices", () => { + const customChoices = 6; + const timeline = createTimeline(mockJsPsych, { + num_choices: customChoices, + num_trials: 1, + show_instructions: false, + show_practice: false + }); + + const mainTrial = timeline.timeline.find( + (item: any) => item.data?.task === 'speeded-matching-trial' + ); + expect(mainTrial.choices).toHaveLength(customChoices); + }); + + it("should set custom trial timeout", () => { + const customTimeout = 5000; + const timeline = createTimeline(mockJsPsych, { + trial_timeout: customTimeout, + num_trials: 1, + show_instructions: false, + show_practice: false + }); + + const mainTrial = timeline.timeline.find( + (item: any) => item.data?.task === 'speeded-matching-trial' + ); + expect(mainTrial.trial_duration).toBe(customTimeout); + }); + + it("should not set trial timeout when undefined", () => { + const timeline = createTimeline(mockJsPsych, { + trial_timeout: undefined, + num_trials: 1, + show_instructions: false, + show_practice: false + }); + + const mainTrial = timeline.timeline.find( + (item: any) => item.data?.task === 'speeded-matching-trial' + ); + expect(mainTrial.trial_duration).toBeUndefined(); + }); + + it("should not set trial timeout when not specified", () => { + const timeline = createTimeline(mockJsPsych, { + num_trials: 1, + show_instructions: false, + show_practice: false + }); + + const mainTrial = timeline.timeline.find( + (item: any) => item.data?.task === 'speeded-matching-trial' + ); + expect(mainTrial.trial_duration).toBeUndefined(); + }); + + it("should use custom test items", () => { + const customItems = [ + '', + '' + ]; + + const timeline = createTimeline(mockJsPsych, { + test_items: customItems, + num_trials: 1, + show_instructions: false, + show_practice: false + }); + + const mainTrial = timeline.timeline.find( + (item: any) => item.data?.task === 'speeded-matching-trial' + ); + expect(customItems).toContain(mainTrial.data.target); + }); + + it("should set custom inter-trial interval", () => { + const customInterval = 1000; + const timeline = createTimeline(mockJsPsych, { + inter_trial_interval: customInterval, + num_trials: 2, + show_instructions: false, + show_practice: false + }); + + const intervalTrial = timeline.timeline.find( + (item: any) => item.data?.task === 'inter-trial-interval' + ); + expect(intervalTrial.trial_duration).toBe(customInterval); + }); + + it("should skip inter-trial interval when set to 0", () => { + const timeline = createTimeline(mockJsPsych, { + inter_trial_interval: 0, + num_trials: 2, + show_instructions: false, + show_practice: false + }); + + const intervalTrials = timeline.timeline.filter( + (item: any) => item.data?.task === 'inter-trial-interval' + ); + expect(intervalTrials).toHaveLength(0); + }); + + it("should skip inter-trial interval when undefined", () => { + const timeline = createTimeline(mockJsPsych, { + inter_trial_interval: undefined, + num_trials: 2, + show_instructions: false, + show_practice: false + }); + + const intervalTrials = timeline.timeline.filter( + (item: any) => item.data?.task === 'inter-trial-interval' + ); + expect(intervalTrials).toHaveLength(0); + }); + + it("should skip inter-trial interval when not specified", () => { + const timeline = createTimeline(mockJsPsych, { + num_trials: 2, + show_instructions: false, + show_practice: false + }); + + const intervalTrials = timeline.timeline.filter( + (item: any) => item.data?.task === 'inter-trial-interval' + ); + expect(intervalTrials).toHaveLength(0); + }); + + it("should include end screen", () => { + const timeline = createTimeline(mockJsPsych, { + num_trials: 1, + show_instructions: false, + show_practice: false + }); + + const endScreen = timeline.timeline.find( + (item: any) => item.data?.task === 'end-screen' + ); + expect(endScreen).toBeDefined(); + expect(endScreen.stimulus).toContain(trial_text.task_complete_header); + }); + }); + + describe("utils.generateTrials", () => { + it("should generate the correct number of trials", () => { + const trials = utils.generateTrials({ num_trials: 10 }); + expect(trials).toHaveLength(10); + }); + + it("should generate trials with required properties", () => { + const trials = utils.generateTrials({ num_trials: 1 }); + const trial = trials[0]; + + expect(trial).toHaveProperty("target"); + expect(trial).toHaveProperty("choices"); + expect(trial).toHaveProperty("correct_answer"); + expect(trial).toHaveProperty("target_index"); + expect(trial).toHaveProperty("trial_number"); + }); + + it("should use default test items when none provided", () => { + const trials = utils.generateTrials({ num_trials: 1 }); + const trial = trials[0]; + + expect(test_items).toContain(trial.target); + expect(trial.choices.every((choice: string) => test_items.includes(choice))).toBe(true); + }); + + it("should use custom test items", () => { + const customItems = [ + 'item1', + 'item2', + 'item3', + 'item4' + ]; + + const trials = utils.generateTrials({ + test_items: customItems, + num_trials: 1 + }); + + const trial = trials[0]; + expect(customItems).toContain(trial.target); + expect(trial.choices.every((choice: string) => customItems.includes(choice))).toBe(true); + }); + + it("should ensure target is included in choices", () => { + const trials = utils.generateTrials({ num_trials: 5 }); + + trials.forEach(trial => { + expect(trial.choices).toContain(trial.target); + expect(trial.choices[trial.correct_answer]).toBe(trial.target); + }); + }); + + it("should generate trials with variety in targets", () => { + // Mock Math.random to return different values for variety + const mockMath = jest.spyOn(Math, 'random'); + + // Provide plenty of different random values for target selection and shuffling + const randomValues = [ + 0.1, 0.5, 0.9, 0.2, 0.7, // Target indices + 0.3, 0.8, 0.4, 0.6, 0.9, // Shuffling values + 0.2, 0.7, 0.1, 0.8, 0.4, // More shuffling values + 0.6, 0.3, 0.9, 0.1, 0.5 // Additional values + ]; + + randomValues.forEach(value => { + mockMath.mockReturnValueOnce(value); + }); + + try { + const trials = utils.generateTrials({ num_trials: 5 }); + + // Check that we get variety in targets + const uniqueTargets = new Set(trials.map(t => t.target)); + expect(uniqueTargets.size).toBeGreaterThan(1); + } finally { + // Always restore the mock + mockMath.mockRestore(); + } + }); + }); + + describe("utils.createTrialSet", () => { + const testItems = ['item1', 'item2', 'item3', 'item4', 'item5']; + + it("should create trial set with correct structure", () => { + const trialSet = utils.createTrialSet(testItems, 0, 4); + + expect(trialSet).toHaveProperty("target"); + expect(trialSet).toHaveProperty("choices"); + expect(trialSet).toHaveProperty("correct_answer"); + expect(trialSet).toHaveProperty("target_index"); + }); + + it("should use specified target index", () => { + const targetIndex = 2; + const trialSet = utils.createTrialSet(testItems, targetIndex, 4); + + expect(trialSet.target).toBe(testItems[targetIndex]); + expect(trialSet.target_index).toBe(targetIndex); + }); + + it("should create correct number of choices", () => { + const numChoices = 3; + const trialSet = utils.createTrialSet(testItems, 0, numChoices); + + expect(trialSet.choices).toHaveLength(numChoices); + }); + + it("should include target in choices", () => { + const trialSet = utils.createTrialSet(testItems, 1, 4); + + expect(trialSet.choices).toContain(trialSet.target); + expect(trialSet.choices[trialSet.correct_answer]).toBe(trialSet.target); + }); + + it("should handle invalid target index", () => { + const trialSet = utils.createTrialSet(testItems, 999, 4); + + expect(trialSet.target).toBe(testItems[0]); // Should fallback to index 0 + expect(trialSet.target_index).toBe(0); + }); + }); + + describe("utils.getRandomTestItems", () => { + const testItems = ['item1', 'item2', 'item3', 'item4', 'item5']; + + it("should return correct number of items", () => { + const items = utils.getRandomTestItems(testItems, 3); + expect(items).toHaveLength(3); + }); + + it("should return all items when count exceeds array length", () => { + const items = utils.getRandomTestItems(testItems, 10); + expect(items).toHaveLength(testItems.length); + }); + + it("should return items from original array", () => { + const items = utils.getRandomTestItems(testItems, 2); + items.forEach(item => { + expect(testItems).toContain(item); + }); + }); + }); + + describe("utils.calculatePerformance", () => { + const mockTrialData = [ + { task: 'speeded-matching-trial', correct: true, rt: 1000, target_index: 0 }, + { task: 'speeded-matching-trial', correct: false, rt: 1500, target_index: 0 }, + { task: 'speeded-matching-trial', correct: true, rt: 800, target_index: 1 }, + { task: 'speeded-matching-trial', correct: true, rt: null, target_index: 1 }, + { task: 'instruction-page', correct: true, rt: 500, target_index: 0 }, // Should be filtered out + ]; + + it("should calculate overall performance correctly", () => { + const performance = utils.calculatePerformance(mockTrialData); + + expect(performance.overall.total_trials).toBe(4); + expect(performance.overall.correct_trials).toBe(3); + expect(performance.overall.accuracy).toBe(75); + expect(performance.overall.mean_reaction_time).toBe(900); // (1000 + 800) / 2 + }); + + it("should calculate performance by target type", () => { + const performance = utils.calculatePerformance(mockTrialData); + + // Target 0: 1 correct out of 2 trials + expect(performance.by_target[0].correct).toBe(1); + expect(performance.by_target[0].total).toBe(2); + expect(performance.by_target[0].reaction_times).toEqual([1000]); + + // Target 1: 2 correct out of 2 trials (one with null rt) + expect(performance.by_target[1].correct).toBe(2); + expect(performance.by_target[1].total).toBe(2); + expect(performance.by_target[1].reaction_times).toEqual([800]); + }); + + it("should handle empty data", () => { + const performance = utils.calculatePerformance([]); + + expect(performance.overall.accuracy).toBe(0); + expect(performance.overall.mean_reaction_time).toBe(null); + expect(performance.overall.total_trials).toBe(0); + }); + + it("should filter out non-task data", () => { + const mixedData = [ + { task: 'speeded-matching-trial', correct: true, rt: 1000, target_index: 0 }, + { task: 'instruction-page', correct: true, rt: 500, target_index: 0 }, + { task: 'practice-demo', correct: false, rt: 2000, target_index: 0 }, + ]; + + const performance = utils.calculatePerformance(mixedData); + expect(performance.overall.total_trials).toBe(1); + }); + }); + + + describe("utils.createInstructions", () => { + it("should create instruction trial with default pages", () => { + const instructions = utils.createInstructions(); + + expect(instructions.pages).toBeInstanceOf(Array); + expect(instructions.pages.length).toBe(instruction_pages.length); + expect(instructions.data?.task).toBe('instruction-pages'); + }); + + it("should create instruction trial with custom pages", () => { + const customPages = [ + '

Custom Header

Custom description

' + ]; + + const instructions = utils.createInstructions(customPages); + + expect(instructions.pages).toHaveLength(1); + expect(instructions.pages[0]).toContain("Custom Header"); + expect(instructions.pages[0]).toContain("Custom description"); + }); + + it("should handle HTML pages correctly", () => { + const customPages = [ + '

Strategy

Follow these steps:

' + ]; + + const instructions = utils.createInstructions(customPages); + const page = instructions.pages[0]; + + expect(page).toContain("Strategy"); + expect(page).toContain("Step 1"); + expect(page).toContain("Step 2"); + expect(page).toContain("Step 3"); + }); + }); + + describe("utils.createPracticeRound", () => { + it("should create practice timeline with correct structure", () => { + const practice = utils.createPracticeRound(test_items); + + expect(practice).toBeInstanceOf(Array); + expect(practice.length).toBeGreaterThan(0); + }); + + it("should include practice instruction screen", () => { + const practice = utils.createPracticeRound(test_items); + + const instructionScreen = practice.find( + (item: any) => item.data?.task === 'practice-instruction' + ); + expect(instructionScreen).toBeDefined(); + }); + + it("should include practice demonstration trials", () => { + const practice = utils.createPracticeRound(test_items); + + const demoTrials = practice.filter( + (item: any) => item.data?.task?.includes('practice') + ); + expect(demoTrials.length).toBeGreaterThan(1); + }); + + it("should use first test item for practice", () => { + const practice = utils.createPracticeRound(test_items); + + const targetDemo = practice.find( + (item: any) => item.data?.task === 'practice-target-demo' + ); + expect(targetDemo.stimulus).toContain(test_items[0]); + }); + }); + + describe("utils.createReadyScreen", () => { + it("should create ready screen with correct structure", () => { + const readyScreen = utils.createReadyScreen(); + + expect(readyScreen).toBeDefined(); + expect(readyScreen.data.task).toBe('ready-screen'); + expect(readyScreen.stimulus).toContain(trial_text.practice_complete_header); + expect(readyScreen.choices).toEqual([trial_text.ready_button]); + }); + }); + + describe("Exports", () => { + it("should export trial_text configuration", () => { + expect(trial_text).toBeDefined(); + expect(trial_text.continue_button).toBe("Continue"); + expect(trial_text.practice_header).toBe("Practice Round"); + expect(trial_text.task_complete_header).toBe("Task Complete!"); + }); + + it("should export instruction_pages configuration", () => { + expect(instruction_pages).toBeDefined(); + expect(instruction_pages).toBeInstanceOf(Array); + expect(instruction_pages.length).toBeGreaterThan(0); + }); + + it("should export timelineUnits descriptions", () => { + expect(timelineUnits).toBeDefined(); + expect(timelineUnits.instructions).toBeDefined(); + expect(timelineUnits.practice).toBeDefined(); + expect(timelineUnits.trial).toBeDefined(); + expect(timelineUnits.interTrialInterval).toBeDefined(); + expect(timelineUnits.endScreen).toBeDefined(); + }); + + it("should export utils object with required functions", () => { + expect(utils).toBeDefined(); + expect(typeof utils.generateTrials).toBe("function"); + expect(typeof utils.createInstructions).toBe("function"); + expect(typeof utils.createPracticeRound).toBe("function"); + expect(typeof utils.createReadyScreen).toBe("function"); + expect(typeof utils.createTrialSet).toBe("function"); + expect(typeof utils.getRandomTestItems).toBe("function"); + expect(typeof utils.calculatePerformance).toBe("function"); + }); + }); + + describe("Trial Data Structure", () => { + it("should create trials with correct data structure", () => { + const timeline = createTimeline(mockJsPsych, { + num_trials: 1, + show_instructions: false, + show_practice: false + }); + + const trial = timeline.timeline.find( + (item: any) => item.data?.task === 'speeded-matching-trial' + ); + + expect(trial.data).toHaveProperty("task", "speeded-matching-trial"); + expect(trial.data).toHaveProperty("trial_number"); + expect(trial.data).toHaveProperty("correct_answer"); + expect(trial.data).toHaveProperty("target_index"); + expect(trial.data).toHaveProperty("target"); + expect(trial.data).toHaveProperty("choices"); + }); + + it("should set correct trial numbers", () => { + const timeline = createTimeline(mockJsPsych, { + num_trials: 3, + show_instructions: false, + show_practice: false + }); + + const trials = timeline.timeline.filter( + (item: any) => item.data?.task === 'speeded-matching-trial' + ); + + trials.forEach((trial: any, index: number) => { + expect(trial.data.trial_number).toBe(index + 1); + }); + }); + }); + + describe("Integration with test_items", () => { + it("should use default test items when none provided", () => { + const timeline = createTimeline(mockJsPsych, { + num_trials: 1, + show_instructions: false, + show_practice: false + }); + + const trial = timeline.timeline.find( + (item: any) => item.data?.task === 'speeded-matching-trial' + ); + + expect(test_items).toContain(trial.data.target); + expect(trial.data.target_index).toBeGreaterThanOrEqual(0); + expect(trial.data.target_index).toBeLessThan(test_items.length); + }); + + it("should handle custom instruction texts", () => { + const customInstructions = [ + '

Custom Task

This is a custom task

' + ]; + + const timeline = createTimeline(mockJsPsych, { + instruction_texts: customInstructions, + show_instructions: true, + num_trials: 1, + show_practice: false + }); + + const hasCustomInstructions = timeline.timeline.some((item: any) => + item.pages && item.pages.some((page: string) => + page.includes("Custom Task") + ) + ); + expect(hasCustomInstructions).toBe(true); + }); + }); + +}); \ No newline at end of file diff --git a/packages/speeded-matching/src/index.ts b/packages/speeded-matching/src/index.ts new file mode 100644 index 00000000..d7b544b2 --- /dev/null +++ b/packages/speeded-matching/src/index.ts @@ -0,0 +1,461 @@ +import { JsPsych } from "jspsych" +import jsPsychHtmlButtonResponse from "@jspsych/plugin-html-button-response" +import jsPsychInstructions from "@jspsych/plugin-instructions" +import { test_items } from "./test-items" +import { trial_text, instruction_pages } from "./text" + +interface SpeedMatchingConfig { + /** Array of test items (animal SVGs) to use as stimuli */ + test_items?: string[] + /** Number of trials to generate */ + num_trials?: number + /** Number of choice options per trial (default 4) */ + num_choices?: number + /** Maximum time allowed per trial (in ms) */ + trial_timeout?: number + /** Inter-trial interval (in ms) */ + inter_trial_interval?: number + /** Show instruction pages before the task */ + show_instructions?: boolean + /** Show practice round before main task */ + show_practice?: boolean + /** Number of practice rounds to show (default 1) */ + practice_rounds?: number + /** Custom instruction texts */ + instruction_texts?: typeof instruction_pages +} + +/** + * Function to get a random selection of test items for creating choice sets + * This ensures variety in the stimuli presented to participants + */ +function getRandomTestItems(items: string[], count: number = 4): string[] { + const shuffled = [...items].sort(() => 0.5 - Math.random()); + return shuffled.slice(0, Math.min(count, items.length)); +} + +/** + * Function to create a trial set with one target and distractors + * items: array of item strings (e.g., SVGs) + * target_index: index of the item to use as target + * num_choices: total number of choices to present (default 4) + * Returns an object with target and choices array + */ +function createTrialSet(items: string[], target_index: number = 0, num_choices: number = 4) { + if (target_index >= items.length) { + target_index = 0; // fallback to first item + } + + const target = items[target_index]; + const others = items.filter((_, index) => index !== target_index); + const shuffled_others = others.sort(() => 0.5 - Math.random()); + + // Create choices array with target and distractors + const choices = [target, ...shuffled_others.slice(0, num_choices - 1)]; + const shuffled_choices = choices.sort(() => 0.5 - Math.random()); + + // Find where the target ended up after shuffling + const correct_choice_index = shuffled_choices.findIndex(choice => choice === target); + + return { + target: target, + choices: shuffled_choices, + correct_answer: correct_choice_index, + target_index: target_index + }; +} + +/** + * Creates instruction pages with configurable text content + * Uses the jsPsych instructions plugin with simple HTML strings + */ +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, + data: { + task: 'instruction-pages' + }, + on_finish: function(data: any) { + data.phase = 'instructions'; + } + }; +} + +/** + * Creates practice rounds with voice instructions and visual demonstrations + * This helps participants understand the task before the actual trials + */ +function createPracticeRound(items: string[], num_choices: number = 4, practice_rounds: number = 1) { + const practice_timeline = []; + + // Practice instruction screen + practice_timeline.push({ + type: jsPsychHtmlButtonResponse, + stimulus: ` +
+

${trial_text.practice_header}

+

${trial_text.practice_intro_message}

+
+ `, + choices: [trial_text.continue_button], + button_html: function(choice) { + return ``; + }, + data: { + task: 'practice-instruction' + } + }); + + // Create multiple practice rounds + for (let round = 0; round < practice_rounds; round++) { + // Create a practice trial set using different target each round + const target_index = round % items.length; + const practice_set = createTrialSet(items, target_index, num_choices); + + // Practice trial with voice instruction practice_look_instruction + practice_timeline.push({ + type: jsPsychHtmlButtonResponse, + stimulus: ` +
+
+

${trial_text.practice_look_instruction}

+
+
+
+ ${practice_set.target} +
+
+
+ `, + choices: [], + trial_duration: 3000, // Show for 3 seconds + on_start: function() {}, + data: { + task: 'practice-target-demo', + practice_round: round + 1 + } + }); + + // Practice trial with voice instruction "We are going to tap the picture down here" + practice_timeline.push({ + type: jsPsychHtmlButtonResponse, + stimulus: ` +
+
+

${trial_text.practice_tap_instruction}

+
+
+
+ ${practice_set.target} +
+
+
+ `, + choices: practice_set.choices.map((_, i) => i.toString()), + button_html: function(choice, choice_index) { + const isCorrect = choice_index === practice_set.correct_answer; + const disabled = !isCorrect ? 'disabled' : ''; + const disabledClass = !isCorrect ? 'disabled-choice' : ''; + return ``; + }, + // No trial duration - only ends when correct choice is clicked + response_ends_trial: true, + on_start: function() {}, + on_load: function() { + // Set CSS custom property for number of choices for dynamic sizing + const btnGroup = document.querySelector('.jspsych-btn-group, #jspsych-html-button-response-btngroup') as HTMLElement; + if (btnGroup) { + btnGroup.style.setProperty('--num-choices', practice_set.choices.length.toString()); + } + + // Add flashing animation for practice demo (1.5 seconds total) + setTimeout(() => { + const buttons = document.querySelectorAll('.choice-option:not(.disabled-choice)'); + buttons.forEach(button => { + button.classList.add('flash-choices'); + }); + }, 500); + + // Disable incorrect choices by preventing click events + setTimeout(() => { + const incorrectButtons = document.querySelectorAll('.choice-option.disabled-choice'); + incorrectButtons.forEach(button => { + button.addEventListener('click', function(e) { + e.preventDefault(); + e.stopPropagation(); + return false; + }, true); + }); + }, 100); + }, + on_finish: function(data: any) { + // Mark if they selected the correct answer + data.correct = data.response === practice_set.correct_answer; + data.practice_round = round + 1; + data.correct_answer = practice_set.correct_answer; + }, + data: { + task: 'practice-choices-demo', + practice_round: round + 1, + correct_answer: practice_set.correct_answer + } + }); + } + + return practice_timeline; +} + +/** + * Creates ready screen asking if user is ready for the actual test + */ +function createReadyScreen() { + return { + type: jsPsychHtmlButtonResponse, + stimulus: ` +
+

${trial_text.practice_complete_header}

+

${trial_text.practice_complete_message}

+
+ `, + choices: [trial_text.ready_button], + button_html: function(choice) { + return ``; + }, + data: { + task: 'ready-screen' + } + }; +} + +/** + * Generates trials for the main speeded matching task + * Each trial presents a target and multiple choice options + */ +function generateTrials(config: SpeedMatchingConfig) { + const items = config.test_items || test_items; + const num_trials = config.num_trials || 20; + const num_choices = config.num_choices || 4; + const trials = []; + + for (let i = 0; i < num_trials; i++) { + // Randomly select a target item + const target_index = Math.floor(Math.random() * items.length); + const trial_set = createTrialSet(items, target_index, num_choices); + + trials.push({ + target: trial_set.target, + choices: trial_set.choices, + correct_answer: trial_set.correct_answer, + target_index: trial_set.target_index, + trial_number: i + 1 + }); + } + + return trials; +} + +/** + * Main function to create the complete speeded matching timeline + * Includes instructions, practice, and test phases based on configuration + */ +export function createTimeline(jsPsych: JsPsych, config: SpeedMatchingConfig = {}) { + const { + trial_timeout, + inter_trial_interval, + show_instructions = true, + show_practice = true, + practice_rounds = 1, + num_choices = 4, + instruction_texts = instruction_pages + } = config; + + const items = config.test_items || test_items; + const trials = generateTrials(config); + const timeline = []; + + // Add instructions if requested + if (show_instructions) { + const instructions = createInstructions(instruction_texts); + timeline.push(instructions); + } + + // Add practice round if requested + if (show_practice) { + const practice_round = createPracticeRound(items, num_choices, practice_rounds); + practice_round.forEach(trial => timeline.push(trial)); + + // Add ready screen after practice + timeline.push(createReadyScreen()); + } + + // Create main task trials + trials.forEach((trial, index) => { + // Create the main trial object + const mainTrial: any = { + type: jsPsychHtmlButtonResponse, + stimulus: ` +
+
+

${trial_text.main_task_prompt}

+
+
+
+ ${trial.target} +
+
+
+ `, + choices: trial.choices.map((_, i) => i.toString()), + button_html: function(choice, choice_index) { + return ``; + }, + on_load: function() { + // Set CSS custom property for number of choices for dynamic sizing + const btnGroup = document.querySelector('.jspsych-btn-group, #jspsych-html-button-response-btngroup') as HTMLElement; + if (btnGroup) { + btnGroup.style.setProperty('--num-choices', trial.choices.length.toString()); + } + + // Add click handlers for visual feedback + const choices = document.querySelectorAll('.choice-option'); + choices.forEach((choice, index) => { + choice.addEventListener('click', function() { + choice.classList.add('selected'); + }); + }); + }, + data: { + task: 'speeded-matching-trial', + trial_number: trial.trial_number, + correct_answer: trial.correct_answer, + target_index: trial.target_index, + target: trial.target, + choices: trial.choices + }, + on_finish: function(data: any) { + // Calculate accuracy and response time + data.correct = data.response === data.correct_answer; + data.reaction_time = data.rt; + } + }; + + // Only add trial_duration if trial_timeout is defined and not null + if (trial_timeout !== undefined && trial_timeout !== null) { + mainTrial.trial_duration = trial_timeout; + } + + timeline.push(mainTrial); + + // Inter-trial interval (fixation cross) - only if defined and > 0 + if (inter_trial_interval !== undefined && inter_trial_interval > 0 && index < trials.length - 1) { + timeline.push({ + type: jsPsychHtmlButtonResponse, + stimulus: `
${trial_text.fixation_cross}
`, + choices: [], + trial_duration: inter_trial_interval, + data: { + task: 'inter-trial-interval' + } + }); + } + }); + + // End screen + timeline.push({ + type: jsPsychHtmlButtonResponse, + stimulus: ` +
+

${trial_text.task_complete_header}

+

${trial_text.task_complete_message}

+
+ `, + choices: [trial_text.end_button], + button_html: function(choice) { + return ``; + }, + data: { + task: 'end-screen' + } + }); + + return { + timeline: timeline + }; +} + +/** + * Function to calculate accuracy and reaction time statistics + * just for exporting with utils +*/ +function calculatePerformance(data: any[]) { + const trial_data = data.filter(d => d.task === 'speeded-matching-trial'); + const correct = trial_data.filter(d => d.correct).length; + const total = trial_data.length; + const accuracy = total > 0 ? (correct / total) * 100 : 0; + + const valid_rts = trial_data.filter(d => d.correct && d.rt !== null).map(d => d.rt); + const mean_rt = valid_rts.length > 0 ? valid_rts.reduce((a, b) => a + b, 0) / valid_rts.length : null; + + // Calculate performance by target type + const target_performance = {}; + trial_data.forEach(trial => { + const target_index = trial.target_index; + if (!target_performance[target_index]) { + target_performance[target_index] = { + correct: 0, + total: 0, + reaction_times: [] + }; + } + target_performance[target_index].total++; + if (trial.correct) { + target_performance[target_index].correct++; + if (trial.rt !== null) { + target_performance[target_index].reaction_times.push(trial.rt); + } + } + }); + + return { + overall: { + accuracy, + mean_reaction_time: mean_rt, + total_trials: total, + correct_trials: correct + }, + by_target: target_performance + }; + } + + +export const timelineUnits = { + instructions: "Instructions for the speeded matching task", + practice: "Practice round with demonstrations", + readyScreen: "Confirmation screen before starting the main task", + trial: "Single speeded matching trial with target and choice options", + interTrialInterval: "Fixation cross between trials", + endScreen: "Task completion screen" +} + +export const utils = { + generateTrials, + createInstructions, + createPracticeRound, + createReadyScreen, + createTrialSet, + getRandomTestItems, + calculatePerformance +} + +// Export text and test items for external use +export { trial_text, instruction_pages, test_items, createInstructions } + +// Default export for convenience +export default { createTimeline, timelineUnits, utils } \ No newline at end of file diff --git a/packages/speeded-matching/src/styles.css b/packages/speeded-matching/src/styles.css new file mode 100644 index 00000000..6ab41ae1 --- /dev/null +++ b/packages/speeded-matching/src/styles.css @@ -0,0 +1,295 @@ +/* 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 { + 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/speeded-matching/src/test-items.ts b/packages/speeded-matching/src/test-items.ts new file mode 100644 index 00000000..084ef77e --- /dev/null +++ b/packages/speeded-matching/src/test-items.ts @@ -0,0 +1,116 @@ +/* This file contains the animal SVGs used in the speeded matching task. + * Researchers can modify these SVGs to change the visual stimuli used in the experiment. + * Each SVG should be a complete SVG element as a string. + * + * To add new animal SVGs: + * 1. Go to https://www.svgrepo.com/collection/animal-portrait-vectors/ + * 2. Select an animal SVG + * 3. Click "Edit SVG" + * 4. Copy the SVG code + * 5. Add it to the array below + * + * Make sure all SVGs have similar dimensions for consistent display. + */ + +export const test_items = [ + // Cat SVG + ` + + + + + + + `, + + // Dog SVG + ` + + + + + + + `, + + // Bird SVG + ` + + + + + + `, + + // Fish SVG + ` + + + + + + `, + + // Rabbit SVG + ` + + + + + + + + `, + + // Elephant SVG + ` + + + + + + + `, + + // Lion SVG + ` + + + + + + + `, + + // Bear SVG + ` + + + + + + + `, + + // Frog SVG + ` + + + + + + + `, + + // Butterfly SVG + ` + + + + + + + + + ` +]; \ No newline at end of file diff --git a/packages/speeded-matching/src/text.ts b/packages/speeded-matching/src/text.ts new file mode 100644 index 00000000..986b3dbd --- /dev/null +++ b/packages/speeded-matching/src/text.ts @@ -0,0 +1,57 @@ +/* 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 = { + // 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: "", + // 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. + */ + +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." +]; \ No newline at end of file diff --git a/packages/false-memory/tsconfig.json b/packages/speeded-matching/tsconfig.json similarity index 100% rename from packages/false-memory/tsconfig.json rename to packages/speeded-matching/tsconfig.json