diff --git a/.changeset/rotten-coins-drop.md b/.changeset/rotten-coins-drop.md new file mode 100644 index 00000000..1a57af7c --- /dev/null +++ b/.changeset/rotten-coins-drop.md @@ -0,0 +1,5 @@ +--- +"@jspsych-contrib/plugin-chat": major +--- + +Initial version of chat-plugin diff --git a/.gitignore b/.gitignore index 7fd1fb21..72a7ac96 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ coverage/ .vscode/ dist.zip .idea +.env \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index f6ae9ad6..a2fd64e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,10 @@ "packages/*", "templates/cli" ], + "dependencies": { + "axios": "^1.7.2", + "openai": "^4.47.3" + }, "devDependencies": { "@changesets/changelog-github": "^0.4.0", "@changesets/cli": "^2.16.0", @@ -3087,6 +3091,10 @@ "resolved": "packages/plugin-audio-swipe-response", "link": true }, + "node_modules/@jspsych-contrib/plugin-chat": { + "resolved": "packages/plugin-chat", + "link": true + }, "node_modules/@jspsych-contrib/plugin-copying-task": { "resolved": "packages/plugin-copying-task", "link": true @@ -3945,6 +3953,16 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==" }, + "node_modules/@types/node-fetch": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz", + "integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, "node_modules/@types/normalize-package-data": { "version": "2.4.4", "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", @@ -4085,6 +4103,18 @@ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "dev": true }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/acorn": { "version": "8.11.3", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", @@ -4128,6 +4158,18 @@ "node": ">= 6.0.0" } }, + "node_modules/agentkeepalive": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", + "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/aggregate-error": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", @@ -4712,8 +4754,7 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/atob": { "version": "2.1.2", @@ -4755,6 +4796,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/axios": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", + "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/babel-core": { "version": "7.0.0-bridge.0", "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-7.0.0-bridge.0.tgz", @@ -6045,7 +6097,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -6652,7 +6703,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "engines": { "node": ">=0.4.0" } @@ -7064,6 +7114,15 @@ "resolved": "https://registry.npmjs.org/eve/-/eve-0.5.4.tgz", "integrity": "sha512-aqprQ9MAOh1t66PrHxDFmMXPlgNO6Uv1uqvxmwjprQV50jaQ2RqO7O1neY4PJwC+hMnkyMDphu2AQPOPZdjQog==" }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -7712,6 +7771,26 @@ "readable-stream": "^2.3.6" } }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -7744,7 +7823,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -7754,6 +7832,34 @@ "node": ">= 6" } }, + "node_modules/form-data-encoder": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", + "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", + "license": "MIT" + }, + "node_modules/formdata-node": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "license": "MIT", + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 12.20" + } + }, + "node_modules/formdata-node/node_modules/web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/fragment-cache": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", @@ -8880,6 +8986,15 @@ "node": ">=10.17.0" } }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, "node_modules/husky": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/husky/-/husky-7.0.4.tgz", @@ -12909,7 +13024,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "engines": { "node": ">= 0.6" } @@ -12918,7 +13032,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "dependencies": { "mime-db": "1.52.0" }, @@ -13071,8 +13184,7 @@ "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/mute-stdout": { "version": "1.0.1", @@ -13165,11 +13277,29 @@ "node": ">= 0.10.5" } }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dev": true, "dependencies": { "whatwg-url": "^5.0.0" }, @@ -13188,20 +13318,17 @@ "node_modules/node-fetch/node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "dev": true + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, "node_modules/node-fetch/node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "dev": true + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, "node_modules/node-fetch/node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dev": true, "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" @@ -13497,6 +13624,34 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/openai": { + "version": "4.47.3", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.47.3.tgz", + "integrity": "sha512-470d4ibH5kizXflCzgur22GpM4nOjrg7WQ9jTOa3dNKEn248oBy4+pjOyfcFR4V4YUn/YlDNjp6h83PbviCCKQ==", + "license": "Apache-2.0", + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7", + "web-streams-polyfill": "^3.2.1" + }, + "bin": { + "openai": "bin/cli" + } + }, + "node_modules/openai/node_modules/@types/node": { + "version": "18.19.34", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.34.tgz", + "integrity": "sha512-eXF4pfBNV5DAMKGbI02NnDtWrQ40hAN558/2vvS4gMpMIxaf6JmD7YjnZbq0Q9TDSSkKBamime8ewRoomHdt4g==", + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, "node_modules/ora": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", @@ -14087,6 +14242,12 @@ "node": ">= 6" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", @@ -16681,6 +16842,12 @@ "node": ">= 0.10" } }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT" + }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", @@ -17075,6 +17242,15 @@ "defaults": "^1.0.3" } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", @@ -17607,6 +17783,19 @@ "jspsych": ">=7.0.0" } }, + "packages/plugin-chat": { + "name": "@jspsych-contrib/plugin-chat", + "version": "0.0.1", + "license": "MIT", + "devDependencies": { + "@jspsych/config": "^2.0.0", + "@jspsych/test-utils": "^1.0.0", + "jspsych": "^7.0.0" + }, + "peerDependencies": { + "jspsych": ">=7.0.0" + } + }, "packages/plugin-copying-task": { "name": "@jspsych-contrib/plugin-copying-task", "version": "1.0.0", diff --git a/package.json b/package.json index 7f95ab67..0b44b046 100644 --- a/package.json +++ b/package.json @@ -42,5 +42,9 @@ "projects": [ "/packages/*" ] + }, + "dependencies": { + "axios": "^1.7.2", + "openai": "^4.47.3" } } diff --git a/packages/extension-countdown/examples/example.html b/packages/extension-countdown/examples/example.html index 8f6d3eca..b8e1a4bc 100644 --- a/packages/extension-countdown/examples/example.html +++ b/packages/extension-countdown/examples/example.html @@ -6,59 +6,22 @@ Countdown Extension Example - - + \ No newline at end of file diff --git a/packages/plugin-chat/README.md b/packages/plugin-chat/README.md new file mode 100644 index 00000000..a02d83d9 --- /dev/null +++ b/packages/plugin-chat/README.md @@ -0,0 +1,33 @@ +# chat + +## Overview + +Chat interface for running experiments using LLMs. For protecting your OpenAI key, this plugin features a backend that needs to be deployed prior to running online experiments. + +This chat-plugin offers a variety of customizability and prompting strategies including: simple prompting, variable prompting, chain prompting and selection prompting. Each one is built-in to the system and data collection. See [documentation](https://github.com/jspsych/jspsych-contrib/blob/main/packages/plugin-chat/docs/jspsych-chat.md) + +## Setup + +Using this plugin is a two-step process that requires setting up a backend in addition to loading the plugin. + +## Running locally + +To run this package locally, you need to follow these steps below. + + 1. Run ```npm install``. + 2. Run ```npm run build```. + 3. Setup the backend server (instructions in the backend dir). + +After these steps you should be able to run example.html. + +## Compatibility + +jsPsych 7.0.0 + +## Documentation + +See [documentation](https://github.com/jspsych/jspsych-contrib/blob/main/packages/plugin-chat/docs/jspsych-chat.md) + +## Author / Citation + +Victor Zhang and Niranjan Baskaran diff --git a/packages/plugin-chat/backend/README.md b/packages/plugin-chat/backend/README.md new file mode 100644 index 00000000..94ce99d7 --- /dev/null +++ b/packages/plugin-chat/backend/README.md @@ -0,0 +1,19 @@ +# Chat backend + +## Overview + +This is a simple backend setup that is necessary to protect your OpenAI keys. This server routes the calls from the chat messages in the browser to the OpenAI API endpoint. + +## Local Setup + + 1. First you need to install the necessary packages from node by running ```npm install```. This will download all the necessary packages to run the backend locally. + 2. Then you will need to create a .env file in the root of the backend directory with the line ```OPENAI_KEY="personal-key"```. + 3. Then you can run ```node server.js``` in the backend directory, and you should get the message ```Server is running on http://localhost:3000```, indicating the server is working as intended. + +### Hosting server for deploying experiment + +For hosting we recommend using a online hosting service or a local server that you trust and using a process manager for node.js environments such as pm2. + +## Author / Citation + +Victor Zhang and Niranjan Baskaran diff --git a/packages/plugin-chat/backend/package-lock.json b/packages/plugin-chat/backend/package-lock.json new file mode 100644 index 00000000..e3999c34 --- /dev/null +++ b/packages/plugin-chat/backend/package-lock.json @@ -0,0 +1,1041 @@ +{ + "name": "backend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "backend", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "axios": "^1.7.2", + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^4.19.2", + "openai": "^4.52.1" + } + }, + "node_modules/@types/node": { + "version": "18.19.39", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.39.tgz", + "integrity": "sha512-nPwTRDKUctxw3di5b4TfT3I0sWDiWoPQCZjXhvdkINntwr8lcoVCKsTgnXeRubKIlfnV+eN/HYk6Jb40tbcEAQ==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/node-fetch": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz", + "integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agentkeepalive": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", + "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", + "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.2", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.6.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data-encoder": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", + "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==" + }, + "node_modules/formdata-node": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 12.20" + } + }, + "node_modules/formdata-node/node_modules/web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "engines": { + "node": ">= 14" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", + "license": "MIT" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/openai": { + "version": "4.52.1", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.52.1.tgz", + "integrity": "sha512-kv2hevAWZZ3I/vd2t8znGO2rd8wkowncsfcYpo8i+wU9ML+JEcdqiViANXXjWWGjIhajFNixE6gOY1fEgqILAg==", + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7", + "web-streams-polyfill": "^3.2.1" + }, + "bin": { + "openai": "bin/cli" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "license": "MIT", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + } + } +} diff --git a/packages/plugin-chat/backend/package.json b/packages/plugin-chat/backend/package.json new file mode 100644 index 00000000..299c39f0 --- /dev/null +++ b/packages/plugin-chat/backend/package.json @@ -0,0 +1,20 @@ +{ + "name": "backend", + "version": "1.0.0", + "main": "index.js", + "type": "module", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "axios": "^1.7.2", + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^4.19.2", + "openai": "^4.52.1" + } +} diff --git a/packages/plugin-chat/backend/server.js b/packages/plugin-chat/backend/server.js new file mode 100644 index 00000000..8520a569 --- /dev/null +++ b/packages/plugin-chat/backend/server.js @@ -0,0 +1,43 @@ +import bodyParser from "body-parser"; +import cors from "cors"; +import dotenv from "dotenv"; +import express from "express"; +import OpenAI from "openai"; + +dotenv.config(); + +const app = express(); +const port = process.env.PORT || 3000; +const OPENAI_API_KEY = process.env.OPENAI_KEY; + +const openai = new OpenAI({ + apiKey: OPENAI_API_KEY, +}); + +app.use(bodyParser.json()); +app.use(cors()); + +app.post("/api/chat", async (req, res) => { + try { + const { messages, ai_model } = req.body; + + const stream = await openai.chat.completions.create({ + model: ai_model, + messages: messages, + stream: true, + }); + + res.header("Content-Type", "text/plain"); + for await (const chunk of stream.toReadableStream()) { + res.write(chunk); + } + res.end(); + } catch (error) { + console.error(error); + res.status(500).send("Error processing request"); + } +}); + +app.listen(port, () => { + console.log(`Server is running on http://localhost:${port}`); +}); diff --git a/packages/plugin-chat/docs/chat.md b/packages/plugin-chat/docs/chat.md new file mode 100644 index 00000000..d5e1c641 --- /dev/null +++ b/packages/plugin-chat/docs/chat.md @@ -0,0 +1,151 @@ +# chat + +Chat interface for running experiments using LLMs. To find an example usage of how to setup and declare within the context of JsPsych, see example.html. + +## Parameters + +In addition to the [parameters available in all plugins](https://jspsych.org/latest/overview/plugins.md#parameters-available-in-all-plugins), this plugin accepts the following parameters. Parameters with a default value of undefined must be specified. Other parameters can be left unspecified if the default value is acceptable. Additional details on using the prompts and which prompts are the best for different tasks can be found below. + +| Parameter | Type | Default Value | Description | +| ------------------- | ---------------- | ------------------ | ---------------------------------------- | +| ai_prompt | String | undefined | System prompt that will be fed to GPT in order to prompt the chatbot. This is the most simple prompting parameter and should be used in cases where you want to | +| ai_model | String | gpt-4o-mini | Refers to the OpenAI model that will be called during the chats. | +| continue_button | Object | { message_trigger: 0 } | Allows you to customize when the continue_button appears. The three options that can be passed in the object are "timer_trigger", "message_trigger", and "message". "Timer_trigger" referes to the milliseconds from initialization, "message_trigger" refers to the total number of messages sent by the user and "message" is the message to be displayed when the button appears. This is an optional parameter that should be used to customize the length of conversation. | +| ai_model | String | gpt-4o-mini | OpenAI model that will be called during the chats. | +| additional_prompts | Array of objects | undefined | This array holds the various prompts that you can use to set messages to display on the screen or dynamically change the prompt being used to prompt the chatbot. Further details on prompting and the parameters are detailed below along with the exact properties that each object may contain.| +| prompt_chain | Object | {} | Allows you to customize the prompt chain feature that takes the user's message and passes through a pipeline of multiple prompts. In addition to the message and timer trigger has a prompts attribute that is an array of the prompts in the chain to be passed chronologically.| +| selection_prompt | Object | {} | Allows you to customize the selection prompt feature that takes the user's message and passes through a pipeline of multiple prompts. Similar to the chain prompts feature, except instead of being a pipeline, makes seperate calls to OpenAI and has a "selection_prompt" that is then used to select the best bot response. | + +## Data Generated + +In addition to the [default data collected by all plugins](https://jspsych.org/latest/overview/plugins.md#data-collected-by-all-plugins), this plugin collects the following data for each trial. + +| Name | Type | Value | +| --------- | ------- | ---------------------------------------- | +| Logs | Array of objects | This is an all encompassing object that captures the entire user conversation. It lists each message or prompt as a seperate object, with all objects having time, content and role attributes denoting the type of message and what was displayed. There are additional attributes captured for specific roles, such as that it lists user keystrokes and the details of selection and prompt_chaining. | + +## Prompting strategies + +Very similar to working with a small child, prompting simply put is telling what the chatbot what to do. While the base models do a good of general conversation, for the purposes of each experiment you may want the bot to act differently. There are several different ways to prompt the bot using the JsPsych chat-plugin and each one has their different use case. Chain prompting and selection prompting requires more intensive prompt engineering and is only recommended when there are complex tasks and specific use cases that need to be broken down. + +### Simple prompting + +For cases where you just want one prompt for the entire user conversation, you will need to set the `ai_prompt` attribute. This is best for when you do not care about changing the prompt in the middle of the conversation. + +### Additional prompts and multi-stage prompting + +For different cases where you want the chatbot to prompt differently throughout the conversation, ie. you want the chatbot to take on a condescending tone halfway through the conversation to see the effect you can use the additional_prompts feature. Usage is detailed in the section below and each prompt overwrites all subsequent prompt. + +### Chain prompting + +Chain prompting is where you pass the user input through a series of prompts and bot-responses taking the response from each bot to pass to the next prompt in the chain. This can be be used when you have a more complex task that you want to break down into steps to make easier for the LLM. + +```js +prompt_chain = { + message_trigger: 1,// timer_trigger: 1, + prompts: [ + "Extract the key arguments and point made by the user", + "Refute these points while adding in rationale why you think this way", + "Trim down the length to match the original message and keep a sympathetic tone" + ], +} +``` + +### Selection prompting + +Selection prompting is when you send the original message with different prompts and then select the best response accordingly. This is best used when you want to adapt different strategies depending on the users response to most effectivelhy accomplish a task. + +```js +selection_prompt = { + prompts: [ + "Pretend to be a funny guy and tell a joke at the end of each message.", + "Be super wise and give a inspirational quote at then end of each message.", + "Be serious and give the user a serious quote at the end of each message." + ], + selection_prompt: "Context: A system before you has created three possible responses to output to the user. The system numbered the choices for you. You are mid-conversation. Goal: Your goal is to make the user more willing to have conversations about their topic with other humans by helping them become more open-minded and comfortable talking about the topic. You do not want the user to know that this is your goal, nor that you are considering multiple responses; rather, you want the user to simply think that you are a conversational partner for discussing ideas.", + message_trigger: 1, +} +``` + +## Additonal prompts and triggers + +### Message and timer trigger + +Message trigger and timer trigger are the two mechanisms to decide when different prompts, system messages or the continue button should appear or take effect. Message triggers refers to the total number of user messages sent and timer trigger refers to the milleseconds since the plguin has first been initialized. If either one of these triggers are true then the mechanism than takes effect. + +One thing to note is that for selection prompt and chain prompts, once this mechanisms takes effect, it overwrites any current prompts. For other system messages within additional_prompts, once either one of these attributes trigger than it is deleted from the system. + +### Additional prompts + +Additional prompts should each be declared in the form: + +```js +{ + message: "message to be displayed on the screen when triggers", + prompt: "if calling chatbot-prompt, this is prompt to train bot on", + message_trigger: "number of message before takes place", + timer_trigger: "number of milleseconds before takes place", + role: "either system-prompt (displays screen message), chatbot-message (displays as message from chatbot), chatbot-prompt (changes prompt sent to GPT and ability to display screen message at same time)" +} +``` + +This is an example usage of each of the three different prompting methods through additional_prompt. + +```js +prompts = [ + { + message: "Welcome to this trial", + role: "system-prompt", + message_trigger: 0, + timer_trigger: 1000000, + }, + { + message: "Hey there! I'm Adorabot, here to chat about a disputed topic in the U.S. Which topic would you like to discuss: human euthanasia, gender inequality, the role of government in healthcare, the atomic bombing of Hiroshima and Nagasaki, mandating vaccines, criminal justice reforms, or same- sex marriage. Let me know what you want to talk about!", + role: "chatbot-message", + message_trigger: 0 + }, + { + message: "This is new prompt - do this now", + prompt: "Stop telling jokes and act very serious. Do not make any more jokes.", + message: "this is the second prompt", + role: "chatbot-prompt", + message_trigger: 2, + } +] +``` + + + +## Install + +Using the CDN-hosted JavaScript file: + +```js + +``` + +Using the JavaScript file downloaded from a GitHub release dist archive: + +```js + +``` + +Using NPM: + +``` +npm install @jspsych-contrib/plugin-chat +``` + +```js +import Chat from '@jspsych-contrib/plugin-chat'; +``` + +## Examples + +### Title of Example + +```javascript +var trial = { + type: jsPsychChat +} +``` \ No newline at end of file diff --git a/packages/plugin-chat/example/example.html b/packages/plugin-chat/example/example.html new file mode 100644 index 00000000..ff4508a7 --- /dev/null +++ b/packages/plugin-chat/example/example.html @@ -0,0 +1,92 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/plugin-chat/example/styles.css b/packages/plugin-chat/example/styles.css new file mode 100644 index 00000000..23ff2fb9 --- /dev/null +++ b/packages/plugin-chat/example/styles.css @@ -0,0 +1,247 @@ +/* styles.css */ + +html, body { + margin: 0; + padding: 0; + width: 100%; + height: 100%; + overflow-x: hidden; +} + +.chat-page { + /* width: 100vw; + height: 100vh; */ + display: flex; + justify-content: center; + align-items: center; + overflow: hidden; /* Ensure no overflow */ + /* padding-top: 60px; Adjust based on the height of .bot-title */ + /* box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); Add a subtle box shadow */ + background-color: #000000; +} + +.bot-title { + /* height: 80px; */ + height: 8vh; + width: 100%; + background-color: #706EA0; + padding: 10px; + position: fixed; /* Fix the position to the top */ + top: 0; + left: 0; + z-index: 1000; /* Ensure it stays above other content */ + display: flex; + justify-content: center; /* Center horizontally */ + align-items: center; /* Center vertically */ +} + +.bot-title-text { + color: black; + font-family: "Cantarell", sans-serif; + font-weight: 700; + font-style: normal; + text-align: center; +} + + +/* Chat container */ +.chat-container { + position: absolute; + bottom: 0; + left: 0; + right: 0; + max-width: 1200px; + margin: 0 auto; + /* border-radius: 10px; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); */ + overflow: hidden; /* Prevent overflow */ + padding: 20px; + box-sizing: border-box; +} + +/* Chat box */ +.chat-box { + display: flex; + flex-direction: column; + padding: 10px 30px 20px 20px; /* Not sure about the direction of these */ + margin-top: 10px; + overflow-y: auto; + /* max-height: 80vh; for title sizing */ + max-height: 85vh; + box-sizing: border-box; + display: flex; + flex-direction: column; +} + +.chat-box::before { + content: ''; + display: block; + height: 20px; /* Adjust this value to control the extra space */ + width: 100%; + flex-shrink: 0; /* Ensure the space doesn't collapse */ +} + +/* Chat fields */ +.chat-fields { + padding: 30px; + display: flex; + /* overflow: hidden; + width: calc(100% - 40px); + box-sizing: border-box; */ + width: 100%; /* Occupy full width of .chat-box */ + box-sizing: border-box; + align-items: center; /* Center items vertically */ + background: #F1F1F1; + box-shadow: 0px 4px 4px 0px #00000040; + border-radius: 20px; +} + +/* User input */ +#user-input { + padding: 10px; + border: none; + border-radius: 10px; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); + width: 100%; + resize: none; + height: auto; + overflow-y: auto; + flex: 1; + + font-family: "Cantarell", sans-serif; + font-weight: 500; + font-size: 18px; + line-height: 26px; + /* background: #FFFFFF; */ +} + +#send-btn, #continue-btn { + font-family: "Exo", sans-serif; + font-weight: 200; + font-size: 22px; + line-height: 33.23px; + border: none; + padding: 10px 30px; + box-shadow: 0px 4px 4px 0px #00000040; + border-radius: 20px; +} + +/* Send button */ +#send-btn { + background-color: #D1D0E5;; + color: #000000; + cursor: pointer; + margin-left: 10px; + width: auto; + transition: background-color 0.3s ease; /* Add a transition for hover effect */ +} + +/* Hover effect for send button */ +#send-btn:hover { + background-color: #cac8e3; ; /* Slightly darker shade on hover */ +} + +/* Submit button */ +#continue-btn { + background-color: #FFFAC180; /* Different background color */ + color: black; + cursor: pointer; + margin-left: 10px; + width: auto; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); /* Add a shadow for distinction */ + transition: background-color 0.3s ease; /* Add a transition for hover effect */ +} + +/* Hover effect for submit button */ +#continue-btn:hover { + background-color: #f9f2a1a8; /* Darker shade on hover */ +} + +/* .system-prompt-message */ +.user-message, .chatbot-message { + padding: 10px 15px; + margin: 5px; + margin-top: 30px; + border-radius: 15px; + position: relative; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); +} + +.user-message, .chatbot-message { + max-width: 70%; + word-wrap: break-word; + text-align: left; + font-family: "Cantarell", sans-serif; + font-weight: 500; + font-size: 18px; + line-height: 26px; + font-style: normal; + /* border: 1px solid #000000; */ +} + +/* User message */ +.user-message { + align-self: flex-end; + background-color: #e7e7e7; + /* background-color: #D1D0E5; */ + color: #000000; + border-radius: 15px 15px 0px 15px; +} + +/* Chatbot message */ +.chatbot-message { + align-self: flex-start; + background-color: #d1e7dd; /* Change this color to something contrasting */ + color: black; + border-radius: 15px 15px 15px 0px; +} + +.system-prompt-message { + /* background: #FFFAC180; */ + margin-top: 30px; + color: #3e3e51c5; + + font-family: Exo; + font-size: 18px; + font-weight: 500; + line-height: 26px; + text-align: center; +} + +/* Add pseudo-elements for the speech bubble tails */ +.user-message::after, .chatbot-message::after { + content: ''; + position: absolute; + bottom: 0; + width: 0; + height: 0; + border: 20px solid transparent; + /* border: 1px solid #000000; */ +} + +/* Tail for user message */ +.user-message::after { + right: -10px; + bottom: 0px; /* Adjust this value to align the tail */ + border-left-color: #e7e7e7; + /* border-left-color: #D1D0E5; */ + border-right: 0; + border-bottom: 0; + border-top: 20px solid transparent; +} + +/* Tail for chatbot message */ +.chatbot-message::after { + left: -10px; + bottom: 0px; /* Adjust this value to align the tail */ + border-right-color: #d1e7dd; + border-left: 0; + border-bottom: 0; +} +/* Data display */ +#jspsych-data-display { + overflow-wrap: break-word; + white-space: pre-wrap; + width: 100%; + overflow-x: auto; +} \ No newline at end of file diff --git a/packages/plugin-chat/jest.config.cjs b/packages/plugin-chat/jest.config.cjs new file mode 100644 index 00000000..6ac19d5c --- /dev/null +++ b/packages/plugin-chat/jest.config.cjs @@ -0,0 +1 @@ +module.exports = require("@jspsych/config/jest").makePackageConfig(__dirname); diff --git a/packages/plugin-chat/package.json b/packages/plugin-chat/package.json new file mode 100644 index 00000000..16534062 --- /dev/null +++ b/packages/plugin-chat/package.json @@ -0,0 +1,44 @@ +{ + "name": "@jspsych-contrib/plugin-chat", + "version": "0.0.1", + "description": "Chat interface for running experiments using LLMs", + "type": "module", + "main": "dist/index.cjs", + "exports": { + "import": "./dist/index.js", + "require": "./dist/index.cjs" + }, + "typings": "dist/index.d.ts", + "unpkg": "dist/index.browser.min.js", + "files": [ + "src", + "dist" + ], + "source": "src/index.ts", + "scripts": { + "test": "jest", + "test:watch": "npm test -- --watch", + "tsc": "tsc", + "build": "rollup --config", + "build:watch": "npm run build -- --watch" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/jspsych/jspsych-contrib.git", + "directory": "packages/plugin-chat" + }, + "author": "Victor Zhang and Niranjan Baskaran", + "license": "MIT", + "bugs": { + "url": "https://github.com/jspsych/jspsych-contrib/issues" + }, + "homepage": "https://github.com/jspsych/jspsych-contrib/tree/main/packages/plugin-chat", + "peerDependencies": { + "jspsych": ">=7.0.0" + }, + "devDependencies": { + "@jspsych/config": "^2.0.0", + "@jspsych/test-utils": "^1.0.0", + "jspsych": "^7.0.0" + } +} diff --git a/packages/plugin-chat/rollup.config.mjs b/packages/plugin-chat/rollup.config.mjs new file mode 100644 index 00000000..c1f9637f --- /dev/null +++ b/packages/plugin-chat/rollup.config.mjs @@ -0,0 +1,3 @@ +import { makeRollupConfig } from "@jspsych/config/rollup"; + +export default makeRollupConfig("jsPsychChat"); diff --git a/packages/plugin-chat/src/ChatLog.ts b/packages/plugin-chat/src/ChatLog.ts new file mode 100644 index 00000000..69cda993 --- /dev/null +++ b/packages/plugin-chat/src/ChatLog.ts @@ -0,0 +1,103 @@ +export class ChatLog { + private conversation_log: { role: string; content: string }[]; // keeps track of previous conversation to send to GPT + private final_data: { role: string; content: string; time: number; keyPressLog?: any }[]; // keeps track of data to print + private prompt: string; + + constructor() { + this.conversation_log = []; + this.final_data = []; + } + + setPrompt(prompt) { + const time = Math.round(performance.now()); + + const newMessage = { + role: "system", + content: prompt, + time: time, + }; + + this.final_data.push(newMessage); + this.prompt = prompt; + } + + getPrompt() { + const newElement = { role: "system", content: this.prompt }; + const tempConversationLog = [...this.conversation_log, newElement]; + return tempConversationLog; + } + + getChatLogs() { + return this.final_data; + } + + // updates current prompt, possibly replace to pass in objct + updateConversationLog(content, role, keyPressLog?, message?): void { + if (role === "system") + console.log( + "WARNING: this case is not caught and is incorrectly trigerring outadated method", + "content:", + content, + "role:", + role + ); + + const time = Math.round(performance.now()); + const newPrompt = { role: role, content: content }; + this.conversation_log.push(newPrompt); + + const newMessage = { + role: role, + content: content, + time: time, + ...(message ? { message: message } : {}), + ...(keyPressLog ? { keyPressLog: keyPressLog } : {}), + }; + this.final_data.push(newMessage); + } + + // logMessage adds message to final data but not conversatin log + logMessage(message, role) { + const time = Math.round(performance.now()); + + const newMessage = { + role: role, + content: message, + time: time, + }; + this.final_data.push(newMessage); + } + + // Chain Condition -- have it be a list of the chain and what happens before and after + // called when temporary chainPrompting + cleanConversation(): {}[] { + const res = this.conversation_log.filter((message: any, index: number, array: any[]) => { + if ("role" in message && message["role"] === "system") { + return false; + } + // Exclude the last message because will be user message - only want existing conversation + if (index === array.length - 1) { + return false; + } + return true; + }); + + return res; + } + + // call when adding a new prompt + cleanSystem(prompt, message?) { + // cleans existing prompts + const res = this.conversation_log.filter((message: any) => { + if ("role" in message && message["role"] === "system") { + return false; + } + return true; + }); + + this.conversation_log = res; + + this.setPrompt(prompt); + return this.getPrompt(); + } +} diff --git a/packages/plugin-chat/src/assets/bot-icon.png b/packages/plugin-chat/src/assets/bot-icon.png new file mode 100644 index 00000000..8a0557f2 Binary files /dev/null and b/packages/plugin-chat/src/assets/bot-icon.png differ diff --git a/packages/plugin-chat/src/index.spec.ts b/packages/plugin-chat/src/index.spec.ts new file mode 100644 index 00000000..043a5222 --- /dev/null +++ b/packages/plugin-chat/src/index.spec.ts @@ -0,0 +1,19 @@ +import { startTimeline } from "@jspsych/test-utils"; + +import jsPsychChat from "."; + +jest.useFakeTimers(); + +describe("my plugin", () => { + it("should load", async () => { + const { expectFinished, getHTML, getData, displayElement, jsPsych } = await startTimeline([ + { + type: jsPsychChat, + parameter_name: 1, + parameter_name2: "img.png", + }, + ]); + + await expectFinished(); + }); +}); diff --git a/packages/plugin-chat/src/index.ts b/packages/plugin-chat/src/index.ts new file mode 100644 index 00000000..3d818a56 --- /dev/null +++ b/packages/plugin-chat/src/index.ts @@ -0,0 +1,541 @@ +import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; +import { ChatCompletionStream } from "openai/lib/ChatCompletionStream"; + +import { ChatLog } from "./ChatLog"; + +const info = { + name: "chat", + parameters: { + // BOOL, STRING, INT, FLOAT, FUNCTION, KEY, KEYS, SELECT, HTML_STRING, IMAGE, AUDIO, VIDEO, OBJECT, COMPLEX + ai_prompt: { + type: ParameterType.STRING, + default: undefined, + }, + ai_model: { + type: ParameterType.STRING, + default: "gpt-4o-mini", + }, + chat_field_placeholder: { + type: ParameterType.STRING, + default: "Type your message here...", + }, + // bot_name: { + // type: ParameterType.STRING, + // default: undefined, + // }, + continue_button: { + type: ParameterType.COMPLEX, + default: { message_trigger: 0 }, + nested: { + timer_trigger: { + type: ParameterType.INT, + }, + message_trigger: { + type: ParameterType.INT, + }, + message: { + type: ParameterType.STRING, + }, + }, + }, + additional_prompts: { + type: ParameterType.COMPLEX, + array: true, + default: undefined, + nested: { + message: { + // messages to display on screen + type: ParameterType.STRING, + default: "", + }, + prompt: { + // prompting to pass in + type: ParameterType.STRING, + default: null, + }, + role: { + // "prompt" ("system-prompt"), "chatbot-message","chatbot-prompt" + type: ParameterType.STRING, + default: "system-prompt", + }, + message_trigger: { + type: ParameterType.INT, + default: null, + }, + timer_trigger: { + type: ParameterType.INT, + default: null, + }, + }, + }, + // when triggers it doesn't stop, do we want to give it a stop? + prompt_chain: { + type: ParameterType.COMPLEX, + default: {}, + nested: { + prompts: { + type: ParameterType.STRING, + array: true, + default: [], + }, + message_trigger: { + type: ParameterType.INT, + default: 99999999999999999999999, // silencing error message + }, + timer_trigger: { + type: ParameterType.INT, + default: null, + }, + }, + }, + selection_prompt: { + type: ParameterType.COMPLEX, + default: {}, + nested: { + prompts: { + type: ParameterType.STRING, + array: true, + default: [], + }, + selection_prompt: { + type: ParameterType.STRING, + default: "Select one of these prompts:", + }, + message_trigger: { + type: ParameterType.INT, + default: 99999999999999999999999, // silencing error message + }, + timer_trigger: { + type: ParameterType.INT, + default: null, + }, + }, + }, + }, +}; + +type Info = typeof info; + +/** + * **chat** + * + * Chat interface for running experiments using LLMs + * + * @author Victor Zhang and Niranjan Baskaran + * @see {@link https://github.com/jspsych/jspsych-contrib/packages/plugin-chat/README.md}} + */ +class ChatPlugin implements JsPsychPlugin { + static info = info; + private researcher_prompts: {}[]; // keeps track of researcher's prompts that need to be displayed + private prompt_chain: {}; + private selection_prompt: {}; + private messages_sent: number; // notes number of messages sent to calculate prompts + private timer_start: number; // notes beginning of session in order to calculate prompts + private ai_model: string; // keeps track of model + private chatLog: ChatLog; + + constructor(private jsPsych: JsPsych) {} + + trial(display_element: HTMLElement, trial: TrialType) { + this.initializeTrialVariables(trial); + // var botTitle = trial.bot_name + // ? `
+ //

` + + // trial.bot_name + + // `

+ //
` + // : ""; + + var html = + `
` + + // botTitle + + `
+
+ +
+ + + +
+
+
`; + + display_element.innerHTML = html; + document.body.style.backgroundColor = "#9c9ad05c"; + const chatBox = display_element.querySelector("#chat-box") as HTMLElement; + const userInput = display_element.querySelector("#user-input") as HTMLInputElement; + const sendButton = display_element.querySelector("#send-btn") as HTMLButtonElement; + const continueButton = display_element.querySelector("#continue-btn") as HTMLButtonElement; + var keyPressLog = []; + + // Setting up Trial Logic + // Function to handle logic of sending user message, and data collection + const sendMessage = async () => { + const message = userInput.value.trim(); + this.addMessage("user", message, chatBox, (keyPressLog = keyPressLog)); + keyPressLog = []; + userInput.value = ""; + + // prompt chaining or simple requests + if (message !== "" && this.selection_prompt && this.checkCondition("selection_prompt")) + await this.selectionPrompt(message, chatBox); + else if (message !== "" && this.prompt_chain && this.checkCondition("prompt_chain")) { + await this.chainPrompts(message, chatBox); + } else if (message !== "") { + await this.updateAndProcessGPT(chatBox); + } + + chatBox.scrollTop = chatBox.scrollHeight; + // inc messages and check researcher prompts + this.messages_sent += 1; + this.checkResearcherPrompts(chatBox, continueButton); + }; + + // Event listener for send button click + sendButton.addEventListener("click", function (event) { + if (userInput.value.trim() != "") { + sendMessage(); + } + }); + + // Event listener for Enter key press + userInput.addEventListener("keydown", function (event) { + if (event.key === "Enter") { + if (!event.shiftKey) { + event.preventDefault(); // Prevent default behavior of adding new line + sendMessage(); + } + } + }); + + // Event listener for all keypresses on userInput + userInput.addEventListener("keydown", function (event) { + keyPressLog.push(event.key); + }); + + continueButton.addEventListener("click", () => { + this.jsPsych.finishTrial({ + logs: this.chatLog.getChatLogs(), + }); + }); + + // Setting up Trial + this.checkResearcherPrompts(chatBox, continueButton); + } + + // includes error checking to minimize error checking later + initializeTrialVariables(trial: TrialType) { + this.timer_start = performance.now(); + this.chatLog = new ChatLog(); + this.messages_sent = 0; + this.ai_model = trial.ai_model; + + // this.chatLog.updateConversationLog(trial.ai_prompt, "system"); + this.chatLog.setPrompt(trial.ai_prompt); // sets researcher prompts and removes any that can't trigger + + this.researcher_prompts = trial.additional_prompts + ? trial.additional_prompts.filter((researcher_prompt) => { + if ( + researcher_prompt["message_trigger"] === null && + researcher_prompt["timer_trigger"] === null + ) { + console.error("Missing required property in researcher prompt:", researcher_prompt); + return false; + } + return true; + }) + : []; + + // sets continue button and removes any that can't trigger + const continue_button = trial.continue_button; + if (continue_button["message_trigger"] === null && continue_button["timer_trigger"] === null) { + console.error("Missing required trigger property in continue prompt, will never display"); + } else { + continue_button["role"] = "continue"; + this.researcher_prompts.push(continue_button); + } + + // sets prompt chain and removes any that can't trigger + if ( + trial.prompt_chain && + trial.prompt_chain["message_trigger"] === null && + trial.prompt_chain["timer_trigger"] === null + ) { + console.error("Missing required trigger property in prompt_chain, will never trigger"); + } else { + this.prompt_chain = trial.prompt_chain; + } + + if ( + trial.selection_prompt && + trial.selection_prompt["message_trigger"] === null && + trial.selection_prompt["timer_trigger"] === null + ) { + console.error("Missing required trigger property in selection_prompt, will never trigger"); + } else { + this.selection_prompt = trial.selection_prompt; + } + } + + // Call to backend, newMessage is the document item to print (optional because when chaining don't want them to display) + async fetchGPT(messages, chatBox, newMessage?) { + try { + var response; + if (window.location.href.includes("127.0.0.1")) { + // local chat vs hosting + response = await fetch("http://localhost:3000/api/chat", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ messages, ai_model: this.ai_model }), + }); + } else { + response = await fetch("/api/chat", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ messages, ai_model: this.ai_model }), + }); + } + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const runner = ChatCompletionStream.fromReadableStream(response.body); + + if (newMessage) { + // prints to screen if specified, otherwise only fetch + runner.on("content", (delta, snapshot) => { + newMessage.innerHTML += delta.replace(/\n/g, "
"); + chatBox.scrollTop = chatBox.scrollHeight; + }); + } + + await runner.finalChatCompletion(); // waits before returning the actual content + return runner["messages"][0]["content"]; + } catch (error) { + console.error("Error fetching GPT data:", error); + throw error; // Rethrow the error after logging it + } + } + + // Handles updates to system with the prompt and to the screen + addMessage(role, message, chatBox, keyPressLog?) { + const newMessage = document.createElement("div"); + // Handles logic of updating prompts and error checking + switch (role) { + case "chatbot": // writing to screen handled caller function + this.chatLog.updateConversationLog(message, "assistant"); + return; + case "user": + this.chatLog.updateConversationLog(message, "user", keyPressLog); + break; + case "chatbot-message": // set by researcher, needs be seperate case because doesn't update prompts + role = "chatbot"; + this.chatLog.logMessage(message, role); + break; + case "system-prompt": // set by researcher + this.chatLog.logMessage(message, role); + break; + case "chatbot-prompt": // logging already handled by "cleanSystem" + role = "system-prompt"; + break; + default: + console.error("Incorrect role"); + return; + } + + newMessage.className = role + "-message"; + newMessage.innerHTML = ""; + chatBox.appendChild(newMessage); + newMessage.innerHTML = message.replace(/\n/g, "
"); + chatBox.scrollTop = chatBox.scrollHeight; + } + + // updates and processes to the screen, workflow for one message (can be used in the process of workflow for mulitple messages) + async updateAndProcessGPT(chatBox, prompt?) { + const newMessage = document.createElement("div"); + newMessage.className = "chatbot" + "-message"; + newMessage.innerHTML = ""; + chatBox.appendChild(newMessage); + + try { + var response = undefined; + + if (prompt) { + // allows to pass in non defined prompts + response = await this.fetchGPT(prompt, chatBox, newMessage); + console.log(prompt); + } else { + // special case when wanting to prompt with own thing + response = await this.fetchGPT(this.chatLog.getPrompt(), chatBox, newMessage); + console.log(this.chatLog.getPrompt()); + } + + chatBox.scrollTop = chatBox.scrollHeight; + this.addMessage("chatbot", response, chatBox); // saves to prompt + return response; + } catch (error) { + newMessage.innerHTML = "error fetching bot response"; + return "error fetching response"; + } + } + + // logic for triggering logic + checkResearcherPrompts(chatBox, continueButton): void { + this.researcher_prompts = this.researcher_prompts.filter((researcher_prompt) => { + const message_trigger = researcher_prompt["message_trigger"]; + const timer_trigger = researcher_prompt["timer_trigger"]; + const time_elapsed = performance.now() - this.timer_start; // could instead keep subtracting from time_elapsed + + if ( + (message_trigger !== null && this.messages_sent >= message_trigger) || + (timer_trigger !== null && time_elapsed >= timer_trigger) + ) { + // Checking with prompt to trigger + switch (researcher_prompt["role"]) { + case "chatbot-message": // case is needed because of chatbot updating prompt + case "system-prompt": // want these cases to have the same functionality + this.addMessage(researcher_prompt["role"], researcher_prompt["message"], chatBox); + break; + case "chatbot-prompt": // checks messages, updates prompt and prints sytem message if exists + const prompt = researcher_prompt["prompt"]; + const message = researcher_prompt["message"]; + + if (prompt !== null && typeof prompt === "string") { + this.chatLog.cleanSystem(prompt, message); + } else + console.error( + researcher_prompt, + "is missing prompt field or it isn't in the correct format" + ); + + if (message !== null && typeof prompt === "string" && message !== "") { + this.addMessage(researcher_prompt["role"], message, chatBox); + } + break; + case "continue": // displays continue button, error checking that pipelining is working + if (!continueButton) { + console.error("No continue button to display"); + return false; + } + continueButton.style.display = "block"; + // implement check here + this.addMessage("system-prompt", researcher_prompt["message"], chatBox); + break; + default: + console.error("Incorrect role for prompting"); + } + + return false; // Remove this item from the array + } + return true; // Keep this item in the array + }); + } + + // checking whether chain prompts can trigger + private checkCondition(name) { + const time_elapsed = performance.now() - this.timer_start; // could instead keep subtracting from time_elapsed + const message_trigger = this[name]["message_trigger"]; + const timer_trigger = this[name]["timer_trigger"]; + + if ( + (message_trigger !== null && this.messages_sent >= message_trigger) || + (timer_trigger !== null && time_elapsed >= timer_trigger) + ) + return true; + else return false; + } + + // triggering prompts in chain and prompting/logging logic + private async chainPrompts(message, chatBox) { + const cleaned_prompt = this.chatLog.cleanConversation(); + const logChain = []; + + for (let i = 0; i < this.prompt_chain["prompts"].length; i++) { + const chain_prompt_system = this.prompt_chain["prompts"][i]; + + const temp_prompt = [ + ...cleaned_prompt, + { + role: "system", + content: chain_prompt_system, + }, + { + role: "user", + content: message, + }, + ]; + + logChain.push( + { + role: `chain-system-${i}`, + content: chain_prompt_system, + }, + { + role: `link-response-${i}`, + content: message, + } + ); + + if (i === this.prompt_chain["prompts"].length - 1) { + message = await this.updateAndProcessGPT(chatBox, temp_prompt); + logChain.push({ role: "assistant", content: message }); + } else { + message = await this.fetchGPT(temp_prompt, chatBox); // Ensure to await if fetchGPT is asynchronous + } + } + + this.chatLog.logMessage(logChain, "chain-prompt"); + } + + private async selectionPrompt(message, chatBox) { + const cleaned_prompt = this.chatLog.cleanConversation(); // maybe be able to refactor and cleanSystem() + var bot_responses = ""; + + for (var i = 0; i < this.selection_prompt["prompts"].length; i++) { + const input_prompt = this.selection_prompt["prompts"][i]; + const combined_prompt = [ + ...cleaned_prompt, + { role: "system", content: input_prompt }, + { role: "user", content: message }, + ]; + + console.log("individual_prompt:", combined_prompt); + const response = await this.fetchGPT(combined_prompt, chatBox); + + bot_responses = bot_responses + "(" + i + ") " + response + "\n\n"; + } + + const system_user = + this.selection_prompt["selection_prompt"] + + "Task: You should select one of the three possible responses that would best achieve your goal with the intention of outputting it to the user" + + "What you should output: You should output the choice that you have selected, but without any indication that this was a numbered choice. Do not tell the user that you selected a certain response. The user should not know that there were choices." + + " User message: `" + + message + + "`"; + + this.chatLog.logMessage( + [ + { role: "prompt-selection", content: system_user }, + { role: "content-choices/chatbot-answers", content: bot_responses }, + ], + "selection_prompt" + ); + + const prompt_select = [ + ...cleaned_prompt, + { role: "system", content: system_user }, + { role: "user", content: bot_responses }, + ]; + + const response_message = await this.updateAndProcessGPT(chatBox, prompt_select); // in case need to something with this + } +} + +export default ChatPlugin; diff --git a/packages/plugin-chat/tsconfig.json b/packages/plugin-chat/tsconfig.json new file mode 100644 index 00000000..3eabd0c2 --- /dev/null +++ b/packages/plugin-chat/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@jspsych/config/tsconfig.contrib.json", + "compilerOptions": { + "baseUrl": "." + }, + "include": ["src"] +} diff --git a/packages/plugin-pipe/src/index.ts b/packages/plugin-pipe/src/index.ts index d5724518..a7e86022 100644 --- a/packages/plugin-pipe/src/index.ts +++ b/packages/plugin-pipe/src/index.ts @@ -41,6 +41,13 @@ const info = { type: ParameterType.STRING, default: null, }, + /** + * An html message to be displayed above the loading graphics in the experiment during data save. + */ + wait_message: { + type: ParameterType.HTML_STRING, + default: `

Saving data. Please do not close this page.

`, + }, }, }; @@ -70,11 +77,6 @@ class PipePlugin implements JsPsychPlugin { const progressCSS = ` .spinner { animation: rotate 2s linear infinite; - z-index: 2; - position: absolute; - top: 50%; - left: 50%; - margin: -25px 0 0 -25px; width: 50px; height: 50px; } @@ -108,7 +110,8 @@ class PipePlugin implements JsPsychPlugin { `; const progressHTML = ` - + + ${trial.wait_message} `;