From d69ae2773ea31373b607383619606b9e1f704001 Mon Sep 17 00:00:00 2001 From: Victor Zhang Date: Mon, 3 Jun 2024 09:35:29 -0400 Subject: [PATCH 01/62] created chat plugin template --- packages/plugin-chat/README.md | 35 +++++++++++++++++ packages/plugin-chat/docs/chat.md | 53 ++++++++++++++++++++++++++ packages/plugin-chat/jest.config.cjs | 1 + packages/plugin-chat/package.json | 44 +++++++++++++++++++++ packages/plugin-chat/rollup.config.mjs | 3 ++ packages/plugin-chat/src/index.spec.ts | 19 +++++++++ packages/plugin-chat/src/index.ts | 43 +++++++++++++++++++++ packages/plugin-chat/tsconfig.json | 7 ++++ 8 files changed, 205 insertions(+) create mode 100644 packages/plugin-chat/README.md create mode 100644 packages/plugin-chat/docs/chat.md create mode 100644 packages/plugin-chat/jest.config.cjs create mode 100644 packages/plugin-chat/package.json create mode 100644 packages/plugin-chat/rollup.config.mjs create mode 100644 packages/plugin-chat/src/index.spec.ts create mode 100644 packages/plugin-chat/src/index.ts create mode 100644 packages/plugin-chat/tsconfig.json diff --git a/packages/plugin-chat/README.md b/packages/plugin-chat/README.md new file mode 100644 index 00000000..2df9fab3 --- /dev/null +++ b/packages/plugin-chat/README.md @@ -0,0 +1,35 @@ +# chat + +## Overview + +Chat interface for running experiments using LLMs + +## Loading + +### In browser + +```js + +``` + +### Via NPM + +``` +npm install @jspsych-contrib/plugin-chat +``` + +```js +import jsPsychChat from '@jspsych-contrib/plugin-chat'; +``` + +## 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/docs/chat.md b/packages/plugin-chat/docs/chat.md new file mode 100644 index 00000000..439ab81b --- /dev/null +++ b/packages/plugin-chat/docs/chat.md @@ -0,0 +1,53 @@ +# chat + +Chat interface for running experiments using LLMs + +## 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. + +| Parameter | Type | Default Value | Description | +| ------------------- | ---------------- | ------------------ | ---------------------------------------- | +| | | | | + +## 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 | +| --------- | ------- | ---------------------------------------- | +| | | | + +## 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/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/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..4caeab17 --- /dev/null +++ b/packages/plugin-chat/src/index.ts @@ -0,0 +1,43 @@ +import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; + +const info = { + name: "chat", + parameters: { + parameter_name: { + type: ParameterType.INT, // BOOL, STRING, INT, FLOAT, FUNCTION, KEY, KEYS, SELECT, HTML_STRING, IMAGE, AUDIO, VIDEO, OBJECT, COMPLEX + default: undefined, + }, + parameter_name2: { + type: ParameterType.IMAGE, + default: undefined, + }, + }, +}; + +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; + + constructor(private jsPsych: JsPsych) {} + + trial(display_element: HTMLElement, trial: TrialType) { + // data saving + var trial_data = { + parameter_name: "parameter value", + }; + + // end trial + this.jsPsych.finishTrial(trial_data); + } +} + +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"] +} From 6e290b8be05b28191a9260ed44cb77210aaea2ae Mon Sep 17 00:00:00 2001 From: Victor Zhang Date: Mon, 3 Jun 2024 10:21:36 -0400 Subject: [PATCH 02/62] finished example html case for testing --- packages/plugin-chat/example/example.html | 34 +++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 packages/plugin-chat/example/example.html diff --git a/packages/plugin-chat/example/example.html b/packages/plugin-chat/example/example.html new file mode 100644 index 00000000..3a8a664c --- /dev/null +++ b/packages/plugin-chat/example/example.html @@ -0,0 +1,34 @@ + + + + + + + + + + \ No newline at end of file From b0ccb020610388a77892d5070362dd958cb0ce97 Mon Sep 17 00:00:00 2001 From: Bankminer78 Date: Mon, 3 Jun 2024 11:04:51 -0400 Subject: [PATCH 03/62] Initial barebones implementation of chat interface --- packages/plugin-chat/src/index.ts | 73 ++++++++++++++++++++++++++++--- 1 file changed, 67 insertions(+), 6 deletions(-) diff --git a/packages/plugin-chat/src/index.ts b/packages/plugin-chat/src/index.ts index 4caeab17..b7013e64 100644 --- a/packages/plugin-chat/src/index.ts +++ b/packages/plugin-chat/src/index.ts @@ -7,10 +7,6 @@ const info = { type: ParameterType.INT, // BOOL, STRING, INT, FLOAT, FUNCTION, KEY, KEYS, SELECT, HTML_STRING, IMAGE, AUDIO, VIDEO, OBJECT, COMPLEX default: undefined, }, - parameter_name2: { - type: ParameterType.IMAGE, - default: undefined, - }, }, }; @@ -35,8 +31,73 @@ class ChatPlugin implements JsPsychPlugin { parameter_name: "parameter value", }; - // end trial - this.jsPsych.finishTrial(trial_data); + this.jsPsych.pluginAPI.setTimeout(() => { + display_element.querySelector( + "#jspsych-html-keyboard-response-stimulus" + ).style.visibility = "hidden"; + }, 5000); + + var transcript = []; + var html = `
+
+ + + +
`; + + display_element.innerHTML = html; + + 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 submitButton = display_element.querySelector("#submit-btn") as HTMLButtonElement; + + // Function to add user message to the chat box + function addUserMessage(message) { + const userMessage = document.createElement("div"); + userMessage.className = "user-message"; + userMessage.textContent = message; + chatBox.appendChild(userMessage); + chatBox.scrollTop = chatBox.scrollHeight; + } + + // Function to add chatbot message to the chat box + function addChatbotMessage(message) { + const chatbotMessage = document.createElement("div"); + chatbotMessage.className = "chatbot-message"; + chatbotMessage.textContent = message; + chatBox.appendChild(chatbotMessage); + chatBox.scrollTop = chatBox.scrollHeight; + } + + // Function to handle sending user message + function sendMessage() { + const message = userInput.value.trim(); + if (message !== "") { + addUserMessage(message); + // Here you would typically send the user message to your backend server for processing + // and receive a response from the chatbot, then call addChatbotMessage with the response + // For demonstration purposes, let's just mimic a simple response from the chatbot + setTimeout(function () { + addChatbotMessage("This is a response from the chatbot."); + }, 500); + userInput.value = ""; + } + } + + // Event listener for send button click + sendButton.addEventListener("click", sendMessage); + + // Event listener for Enter key press + userInput.addEventListener("keypress", function (event) { + if (event.key === "Enter") { + sendMessage(); + } + }); + + submitButton.addEventListener("click", () => { + this.jsPsych.finishTrial(trial_data); + }); } } From 4e26a02ebae18e8257c443203afe1fdf95188ba1 Mon Sep 17 00:00:00 2001 From: Victor Zhang Date: Mon, 3 Jun 2024 14:18:03 -0400 Subject: [PATCH 04/62] finished chat interface styling --- packages/plugin-chat/example/example.html | 7 +- packages/plugin-chat/example/styles.css | 91 +++++++++++++++++++++++ packages/plugin-chat/src/index.ts | 20 +++-- 3 files changed, 106 insertions(+), 12 deletions(-) create mode 100644 packages/plugin-chat/example/styles.css diff --git a/packages/plugin-chat/example/example.html b/packages/plugin-chat/example/example.html index 3a8a664c..b47a63a2 100644 --- a/packages/plugin-chat/example/example.html +++ b/packages/plugin-chat/example/example.html @@ -1,6 +1,7 @@ + @@ -17,11 +18,7 @@ type: jsPsychChat, parameter_name: "TESTING", parameter_name2: "abc", - // type: jsPsychHtmlKeyboardResponseRaf, - // stimulus: 'Hello world!', - // stimulus_duration: 16.6, - // trial_duration: 50, - // choices: ['a', 'b', 'c'], + } // const loop = { diff --git a/packages/plugin-chat/example/styles.css b/packages/plugin-chat/example/styles.css new file mode 100644 index 00000000..6f524f66 --- /dev/null +++ b/packages/plugin-chat/example/styles.css @@ -0,0 +1,91 @@ +/* styles.css */ + +/* Chat container */ +.chat-container { + position: fixed; + bottom: 0; + left: 0; + right: 0; + max-width: 1200px; + margin: 0 auto; /* Center the container horizontally */ + background-color: #f7f7f7; + border-radius: 10px; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); +} + +/* Chat box */ +.chat-box { + display: flex; + flex-direction: column; + padding: 20px; + overflow-y: auto; + max-height: 90vh; /* Set a maximum height relative to the viewport height */ +} + +/* Chat fields */ +.chat-fields { + padding: 10px; + border-top: 1px solid #ddd; + display: flex; + max-height: 10vh; + margin-bottom: 20px; + margin-left: 20px; + margin-right: 20px; +} + +/* User input */ +#user-input { + padding: 10px; + border: none; + border-radius: 10px; /* Reduced border radius */ + background-color: #fff; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); + width: 100%; /* Stretch input field to fill available space */ + min-width: 0; /* Ensure input field can shrink if needed */ + font-family: inherit; /* Use the same font as the parent element */ + font-size: inherit; /* Use the same font size as the parent element */ + resize: none; /* Disable textarea resizing */ + height: auto; /* Automatically adjust height based on content */ + overflow-y: auto; /* Enable vertical scrolling if needed */ + flex: 1; /* Allow textarea to grow to fill remaining space */ +} + +/* Send button */ +#send-btn { + background-color: #4CAF50; + color: white; + padding: 10px 20px; + border: none; + border-radius: 30px; + cursor: pointer; + margin-left: 10px; + width: auto; /* Auto width to fit content */ + font-family: inherit; /* Use the same font as the parent element */ + font-size: inherit; /* Use the same font size as the parent element */ +} + +/* User message */ +.user-message { + align-self: flex-end; /* Aligns user message to the right */ + background-color: #4CAF50; /* Example background color for user message */ + color: white; /* Example text color for user message */ + padding: 10px; + margin: 5px; + border-radius: 10px; + max-width: 70%; /* Set a maximum width for the message container */ + word-wrap: break-word; /* Allow long words to break onto the next line */ + text-align: left; +} + +/* Chatbot message */ +.chatbot-message { + align-self: flex-start; /* Aligns chatbot message to the left */ + background-color: #f7f7f7; /* Example background color for chatbot message */ + color: black; /* Example text color for chatbot message */ + padding: 10px; + margin: 5px; + border-radius: 10px; + max-width: 70%; /* Set a maximum width for the message container */ + word-wrap: break-word; /* Allow long words to break onto the next line */ + text-align: left; +} \ No newline at end of file diff --git a/packages/plugin-chat/src/index.ts b/packages/plugin-chat/src/index.ts index b7013e64..79a6e3fc 100644 --- a/packages/plugin-chat/src/index.ts +++ b/packages/plugin-chat/src/index.ts @@ -39,11 +39,14 @@ class ChatPlugin implements JsPsychPlugin { var transcript = []; var html = `
-
- - - -
`; +
+ +
+ + + +
+ `; display_element.innerHTML = html; @@ -89,9 +92,12 @@ class ChatPlugin implements JsPsychPlugin { sendButton.addEventListener("click", sendMessage); // Event listener for Enter key press - userInput.addEventListener("keypress", function (event) { + userInput.addEventListener("keydown", function (event) { if (event.key === "Enter") { - sendMessage(); + if (!event.shiftKey) { + event.preventDefault(); // Prevent default behavior of adding new line + sendMessage(); + } } }); From 20ec85bf4e823c495ec26e71108c67c230e20220 Mon Sep 17 00:00:00 2001 From: Victor Zhang Date: Mon, 3 Jun 2024 14:24:07 -0400 Subject: [PATCH 05/62] fixed displaying of newlines and text processing --- packages/plugin-chat/example/styles.css | 4 +++- packages/plugin-chat/src/index.ts | 5 ++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/plugin-chat/example/styles.css b/packages/plugin-chat/example/styles.css index 6f524f66..3a7ae867 100644 --- a/packages/plugin-chat/example/styles.css +++ b/packages/plugin-chat/example/styles.css @@ -17,9 +17,11 @@ .chat-box { display: flex; flex-direction: column; - padding: 20px; + padding: 10px 20px 20px 20px; /* Reduced top padding to 10px */ + margin-top: 10px; /* Add top margin to create space */ overflow-y: auto; max-height: 90vh; /* Set a maximum height relative to the viewport height */ + box-sizing: border-box; /* Include padding and border in the element's total width and height */ } /* Chat fields */ diff --git a/packages/plugin-chat/src/index.ts b/packages/plugin-chat/src/index.ts index 79a6e3fc..5a654829 100644 --- a/packages/plugin-chat/src/index.ts +++ b/packages/plugin-chat/src/index.ts @@ -59,7 +59,7 @@ class ChatPlugin implements JsPsychPlugin { function addUserMessage(message) { const userMessage = document.createElement("div"); userMessage.className = "user-message"; - userMessage.textContent = message; + userMessage.innerHTML = message.replace(/\n/g, "
"); // Replace newline characters with
tags chatBox.appendChild(userMessage); chatBox.scrollTop = chatBox.scrollHeight; } @@ -68,11 +68,10 @@ class ChatPlugin implements JsPsychPlugin { function addChatbotMessage(message) { const chatbotMessage = document.createElement("div"); chatbotMessage.className = "chatbot-message"; - chatbotMessage.textContent = message; + chatbotMessage.innerHTML = message.replace(/\n/g, "
"); // Replace newline characters with
tags chatBox.appendChild(chatbotMessage); chatBox.scrollTop = chatBox.scrollHeight; } - // Function to handle sending user message function sendMessage() { const message = userInput.value.trim(); From 0dd071acc3f892622ba2d7fa72e21026efc948f3 Mon Sep 17 00:00:00 2001 From: Bankminer78 Date: Mon, 3 Jun 2024 14:38:14 -0400 Subject: [PATCH 06/62] Added data/transcript with rt recording functionality for the chat --- packages/plugin-chat/src/index.ts | 58 ++++++++++++++++++++++++------- 1 file changed, 45 insertions(+), 13 deletions(-) diff --git a/packages/plugin-chat/src/index.ts b/packages/plugin-chat/src/index.ts index b7013e64..d4137ac7 100644 --- a/packages/plugin-chat/src/index.ts +++ b/packages/plugin-chat/src/index.ts @@ -27,9 +27,19 @@ class ChatPlugin implements JsPsychPlugin { trial(display_element: HTMLElement, trial: TrialType) { // data saving - var trial_data = { - parameter_name: "parameter value", - }; + + let startTime = performance.now(); + + /*function getResponseTime(){ + if (startTime) { + var endTime = performance.now(); + var difference = endTime - startTime; + return difference; + } + else { + startTime = performance.now(); + } + }*/ this.jsPsych.pluginAPI.setTimeout(() => { display_element.querySelector( @@ -68,22 +78,44 @@ class ChatPlugin implements JsPsychPlugin { chatbotMessage.textContent = message; chatBox.appendChild(chatbotMessage); chatBox.scrollTop = chatBox.scrollHeight; + return message; } - // Function to handle sending user message - function sendMessage() { + // Function to handle sending user message, and recording times + const sendMessage = () => { const message = userInput.value.trim(); + + var subtrialDataParticipant = { + interlocutor: "Participant", + response: message, + rt: performance.now() - startTime, + }; + + var subtrialDataBot = {}; + if (message !== "") { addUserMessage(message); - // Here you would typically send the user message to your backend server for processing - // and receive a response from the chatbot, then call addChatbotMessage with the response - // For demonstration purposes, let's just mimic a simple response from the chatbot - setTimeout(function () { - addChatbotMessage("This is a response from the chatbot."); - }, 500); + + startTime = performance.now(); + + var botMessage = addChatbotMessage(`I reject ${message}! You are wrong!`); + + subtrialDataBot = { + interlocutor: "Bot", + response: botMessage, + rt: performance.now() - startTime, + }; + userInput.value = ""; } - } + + //jumps over the finishTrial function to write data for each response for better readability. + this.jsPsych.data.write(subtrialDataParticipant); + this.jsPsych.data.write(subtrialDataBot); + + //resets startTime to mark the beginning of the participant's response period. + startTime = performance.now(); + }; // Event listener for send button click sendButton.addEventListener("click", sendMessage); @@ -96,7 +128,7 @@ class ChatPlugin implements JsPsychPlugin { }); submitButton.addEventListener("click", () => { - this.jsPsych.finishTrial(trial_data); + this.jsPsych.finishTrial({ endOfTrial: true }); }); } } From 74878b0852521eddc72aebc616de5b872127e5cf Mon Sep 17 00:00:00 2001 From: Bankminer78 Date: Mon, 3 Jun 2024 15:00:21 -0400 Subject: [PATCH 07/62] Added function to streamline conversation data collection and rounded response time --- packages/plugin-chat/src/index.ts | 32 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/packages/plugin-chat/src/index.ts b/packages/plugin-chat/src/index.ts index c7191350..4ee59ae2 100644 --- a/packages/plugin-chat/src/index.ts +++ b/packages/plugin-chat/src/index.ts @@ -84,39 +84,37 @@ class ChatPlugin implements JsPsychPlugin { return message; } + function getResponseData(message, interlocutorName, startTimeData) { + return { + interlocutor: interlocutorName, + response: message, + rt: Math.round(performance.now() - startTimeData), + }; + } + // Function to handle sending user message, and recording times const sendMessage = () => { const message = userInput.value.trim(); - var subtrialDataParticipant = { - interlocutor: "Participant", - response: message, - rt: performance.now() - startTime, - }; - - var subtrialDataBot = {}; + //jumps over the finishTrial function to write data for each response for better readability. + this.jsPsych.data.write(getResponseData(message, "Participant", startTime)); if (message !== "") { addUserMessage(message); + //resets startTime reference point for bot. startTime = performance.now(); + //updates html text while also returning the bot message var botMessage = addChatbotMessage(`I reject ${message}! You are wrong!`); - subtrialDataBot = { - interlocutor: "Bot", - response: botMessage, - rt: performance.now() - startTime, - }; + //jumps over the finishTrial function to write data for each response for better readability. + this.jsPsych.data.write(getResponseData(botMessage, "Bot", startTime)); userInput.value = ""; } - //jumps over the finishTrial function to write data for each response for better readability. - this.jsPsych.data.write(subtrialDataParticipant); - this.jsPsych.data.write(subtrialDataBot); - - //resets startTime to mark the beginning of the participant's response period. + //resets startTime reference point to mark the beginning of the participant's next response period. startTime = performance.now(); }; From 63b669e0289b97d2a636add256c518bef96be7c4 Mon Sep 17 00:00:00 2001 From: Victor Zhang Date: Mon, 3 Jun 2024 15:01:18 -0400 Subject: [PATCH 08/62] added parameters --- packages/plugin-chat/src/index.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/plugin-chat/src/index.ts b/packages/plugin-chat/src/index.ts index c7191350..bdac2a19 100644 --- a/packages/plugin-chat/src/index.ts +++ b/packages/plugin-chat/src/index.ts @@ -7,6 +7,18 @@ const info = { type: ParameterType.INT, // BOOL, STRING, INT, FLOAT, FUNCTION, KEY, KEYS, SELECT, HTML_STRING, IMAGE, AUDIO, VIDEO, OBJECT, COMPLEX default: undefined, }, + open_ai_api_key: { + type: ParameterType.KEY, + default: undefined, + }, + subject_prompt: { + type: ParameterType.STRING, + default: "", + }, + ai_prompt: { + type: ParameterType.STRING, + default: "", + }, }, }; From fa35f651957621ab750ebbce090dd22dee0da765 Mon Sep 17 00:00:00 2001 From: Victor Zhang Date: Tue, 4 Jun 2024 10:02:04 -0400 Subject: [PATCH 09/62] cleaning up code --- packages/plugin-chat/example/example.html | 5 --- packages/plugin-chat/example/styles.css | 53 +++++++++++------------ packages/plugin-chat/src/index.ts | 20 +-------- 3 files changed, 27 insertions(+), 51 deletions(-) diff --git a/packages/plugin-chat/example/example.html b/packages/plugin-chat/example/example.html index b47a63a2..e68c977f 100644 --- a/packages/plugin-chat/example/example.html +++ b/packages/plugin-chat/example/example.html @@ -21,11 +21,6 @@ } - // const loop = { - // timeline: [trial], - // loop_function: () => true, - // } - jsPsych.run([trial]); \ No newline at end of file diff --git a/packages/plugin-chat/example/styles.css b/packages/plugin-chat/example/styles.css index 3a7ae867..e2e17e07 100644 --- a/packages/plugin-chat/example/styles.css +++ b/packages/plugin-chat/example/styles.css @@ -7,7 +7,7 @@ left: 0; right: 0; max-width: 1200px; - margin: 0 auto; /* Center the container horizontally */ + margin: 0 auto; background-color: #f7f7f7; border-radius: 10px; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); @@ -17,11 +17,11 @@ .chat-box { display: flex; flex-direction: column; - padding: 10px 20px 20px 20px; /* Reduced top padding to 10px */ - margin-top: 10px; /* Add top margin to create space */ + padding: 10px 20px 20px 20px; + margin-top: 10px; overflow-y: auto; - max-height: 90vh; /* Set a maximum height relative to the viewport height */ - box-sizing: border-box; /* Include padding and border in the element's total width and height */ + max-height: 90vh; + box-sizing: border-box; } /* Chat fields */ @@ -39,17 +39,16 @@ #user-input { padding: 10px; border: none; - border-radius: 10px; /* Reduced border radius */ + border-radius: 10px; background-color: #fff; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); - width: 100%; /* Stretch input field to fill available space */ - min-width: 0; /* Ensure input field can shrink if needed */ - font-family: inherit; /* Use the same font as the parent element */ - font-size: inherit; /* Use the same font size as the parent element */ - resize: none; /* Disable textarea resizing */ - height: auto; /* Automatically adjust height based on content */ - overflow-y: auto; /* Enable vertical scrolling if needed */ - flex: 1; /* Allow textarea to grow to fill remaining space */ + width: 100%; + font-family: inherit; + font-size: inherit; + resize: none; + height: auto; + overflow-y: auto; + flex: 1; } /* Send button */ @@ -61,33 +60,33 @@ border-radius: 30px; cursor: pointer; margin-left: 10px; - width: auto; /* Auto width to fit content */ - font-family: inherit; /* Use the same font as the parent element */ - font-size: inherit; /* Use the same font size as the parent element */ + width: auto; + font-family: inherit; + font-size: inherit; } /* User message */ .user-message { - align-self: flex-end; /* Aligns user message to the right */ - background-color: #4CAF50; /* Example background color for user message */ - color: white; /* Example text color for user message */ + align-self: flex-end; + background-color: #4CAF50; + color: white; padding: 10px; margin: 5px; border-radius: 10px; - max-width: 70%; /* Set a maximum width for the message container */ - word-wrap: break-word; /* Allow long words to break onto the next line */ + max-width: 70%; + word-wrap: break-word; text-align: left; } /* Chatbot message */ .chatbot-message { - align-self: flex-start; /* Aligns chatbot message to the left */ - background-color: #f7f7f7; /* Example background color for chatbot message */ - color: black; /* Example text color for chatbot message */ + align-self: flex-start; + background-color: #f7f7f7; + color: black; padding: 10px; margin: 5px; border-radius: 10px; - max-width: 70%; /* Set a maximum width for the message container */ - word-wrap: break-word; /* Allow long words to break onto the next line */ + max-width: 70%; + word-wrap: break-word; text-align: left; } \ No newline at end of file diff --git a/packages/plugin-chat/src/index.ts b/packages/plugin-chat/src/index.ts index 08456d70..c737f235 100644 --- a/packages/plugin-chat/src/index.ts +++ b/packages/plugin-chat/src/index.ts @@ -42,24 +42,6 @@ class ChatPlugin implements JsPsychPlugin { let startTime = performance.now(); - /*function getResponseTime(){ - if (startTime) { - var endTime = performance.now(); - var difference = endTime - startTime; - return difference; - } - else { - startTime = performance.now(); - } - }*/ - - this.jsPsych.pluginAPI.setTimeout(() => { - display_element.querySelector( - "#jspsych-html-keyboard-response-stimulus" - ).style.visibility = "hidden"; - }, 5000); - - var transcript = []; var html = `
@@ -118,7 +100,7 @@ class ChatPlugin implements JsPsychPlugin { startTime = performance.now(); //updates html text while also returning the bot message - var botMessage = addChatbotMessage(`I reject ${message}! You are wrong!`); + var botMessage = addChatbotMessage(`Responding to ${message}, I think...`); //jumps over the finishTrial function to write data for each response for better readability. this.jsPsych.data.write(getResponseData(botMessage, "Bot", startTime)); From 61007fc8868ab178e57bb137596cf349ecdf5438 Mon Sep 17 00:00:00 2001 From: Victor Zhang Date: Tue, 4 Jun 2024 14:14:12 -0400 Subject: [PATCH 10/62] consolidated code and implemented prompting features. Beginning to work on imbedding open ai calls --- packages/plugin-chat/example/example.html | 7 +- packages/plugin-chat/example/styles.css | 14 +++ packages/plugin-chat/src/index.ts | 112 +++++++++++++--------- 3 files changed, 83 insertions(+), 50 deletions(-) diff --git a/packages/plugin-chat/example/example.html b/packages/plugin-chat/example/example.html index e68c977f..ea613667 100644 --- a/packages/plugin-chat/example/example.html +++ b/packages/plugin-chat/example/example.html @@ -16,9 +16,10 @@ const trial = { type: jsPsychChat, - parameter_name: "TESTING", - parameter_name2: "abc", - + chat_field_placeholder: "be nice...", + subject_prompt: "Welcome to this trial.", + additional_prompts: "Now do this...", + additional_prompt_trigger: 4, } jsPsych.run([trial]); diff --git a/packages/plugin-chat/example/styles.css b/packages/plugin-chat/example/styles.css index e2e17e07..1d165912 100644 --- a/packages/plugin-chat/example/styles.css +++ b/packages/plugin-chat/example/styles.css @@ -1,5 +1,12 @@ /* styles.css */ + +/* .chat-page { + background-color: #f7f7f7; + width: 100vh; + height: 100vh; +} */ + /* Chat container */ .chat-container { position: fixed; @@ -89,4 +96,11 @@ max-width: 70%; word-wrap: break-word; text-align: left; +} + +.prompt-message { + border-radius: 10px; + background-color: #faf39d; + padding: 10px; + margin: 5px; } \ No newline at end of file diff --git a/packages/plugin-chat/src/index.ts b/packages/plugin-chat/src/index.ts index c737f235..2f69a79b 100644 --- a/packages/plugin-chat/src/index.ts +++ b/packages/plugin-chat/src/index.ts @@ -3,22 +3,32 @@ import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; const info = { name: "chat", parameters: { - parameter_name: { - type: ParameterType.INT, // BOOL, STRING, INT, FLOAT, FUNCTION, KEY, KEYS, SELECT, HTML_STRING, IMAGE, AUDIO, VIDEO, OBJECT, COMPLEX - default: undefined, - }, + // BOOL, STRING, INT, FLOAT, FUNCTION, KEY, KEYS, SELECT, HTML_STRING, IMAGE, AUDIO, VIDEO, OBJECT, COMPLEX open_ai_api_key: { type: ParameterType.KEY, default: undefined, }, + ai_prompt: { + type: ParameterType.STRING, + default: "", + }, subject_prompt: { type: ParameterType.STRING, default: "", }, - ai_prompt: { + chat_field_placeholder: { + type: ParameterType.STRING, + default: "Type your message...", + }, + additional_prompts: { + // need to figure out how to implement this type: ParameterType.STRING, default: "", }, + additional_prompt_trigger: { + type: ParameterType.INT, + default: 10000, + }, }, }; @@ -38,17 +48,25 @@ class ChatPlugin implements JsPsychPlugin { constructor(private jsPsych: JsPsych) {} trial(display_element: HTMLElement, trial: TrialType) { - // data saving - + // Setting up Variables let startTime = performance.now(); - - var html = `
-
- -
- - - + let messages_sent = 0; + // Setting up HTML + // might want to fix the chat page backgrond to stay similar + // include a prompting feature that displays a prompt in the middle of the page + // create a chat buble around the other message from bot + var html = + `
+
+
+ +
+ + + +
`; @@ -59,53 +77,33 @@ class ChatPlugin implements JsPsychPlugin { const sendButton = display_element.querySelector("#send-btn") as HTMLButtonElement; const submitButton = display_element.querySelector("#submit-btn") as HTMLButtonElement; - // Function to add user message to the chat box - function addUserMessage(message) { - const userMessage = document.createElement("div"); - userMessage.className = "user-message"; - userMessage.innerHTML = message.replace(/\n/g, "
"); // Replace newline characters with
tags - chatBox.appendChild(userMessage); - chatBox.scrollTop = chatBox.scrollHeight; - } - - // Function to add chatbot message to the chat box - function addChatbotMessage(message) { - const chatbotMessage = document.createElement("div"); - chatbotMessage.className = "chatbot-message"; - chatbotMessage.innerHTML = message.replace(/\n/g, "
"); // Replace newline characters with
tags - chatBox.appendChild(chatbotMessage); - chatBox.scrollTop = chatBox.scrollHeight; - return message; - } - - function getResponseData(message, interlocutorName, startTimeData) { - return { - interlocutor: interlocutorName, - response: message, - rt: Math.round(performance.now() - startTimeData), - }; - } - - // Function to handle sending user message, and recording times + // Setting up Trial Logic + // Function to handle logic of sending user message, and data collection const sendMessage = () => { const message = userInput.value.trim(); //jumps over the finishTrial function to write data for each response for better readability. - this.jsPsych.data.write(getResponseData(message, "Participant", startTime)); + this.jsPsych.data.write(this.getResponseData(message, "Participant", startTime)); if (message !== "") { - addUserMessage(message); + this.addMessage("user", message, chatBox); //resets startTime reference point for bot. startTime = performance.now(); //updates html text while also returning the bot message - var botMessage = addChatbotMessage(`Responding to ${message}, I think...`); + const botResponse = `Responding to ${message}, I think...`; + this.addMessage("chatbot", botResponse, chatBox); //jumps over the finishTrial function to write data for each response for better readability. - this.jsPsych.data.write(getResponseData(botMessage, "Bot", startTime)); + this.jsPsych.data.write(this.getResponseData(botResponse, "Bot", startTime)); userInput.value = ""; + + messages_sent++; // testing one possible implementation, will need to adjust how this is implemented + if (messages_sent === trial.additional_prompt_trigger) { + this.addMessage("prompt", trial.additional_prompts, chatBox); + } } //resets startTime reference point to mark the beginning of the participant's next response period. @@ -128,6 +126,26 @@ class ChatPlugin implements JsPsychPlugin { submitButton.addEventListener("click", () => { this.jsPsych.finishTrial({ endOfTrial: true }); }); + + // Setting up Trial + this.addMessage("prompt", trial.subject_prompt, chatBox); + } + + // user, chatbot, prompt + addMessage(sender, message, chatBox) { + const newMessage = document.createElement("div"); + newMessage.className = sender + "-message"; + newMessage.innerHTML = message.replace(/\n/g, "
"); // Replace newline characters with
tags + chatBox.appendChild(newMessage); + chatBox.scrollTop = chatBox.scrollHeight; // scroll a little bit higher + } + + getResponseData(message, interlocutorName, startTimeData) { + return { + interlocutor: interlocutorName, + response: message, + rt: Math.round(performance.now() - startTimeData), + }; } } From 70221227e8a81a62bab0532561bd786c7690ef31 Mon Sep 17 00:00:00 2001 From: Victor Zhang Date: Tue, 4 Jun 2024 16:11:16 -0400 Subject: [PATCH 11/62] finished up initial parameters and chatting with ai functionality --- .gitignore | 1 + packages/plugin-chat/example/example.html | 2 ++ packages/plugin-chat/src/index.ts | 34 +++++++++++++++++++---- 3 files changed, 31 insertions(+), 6 deletions(-) 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/packages/plugin-chat/example/example.html b/packages/plugin-chat/example/example.html index ea613667..ae400dba 100644 --- a/packages/plugin-chat/example/example.html +++ b/packages/plugin-chat/example/example.html @@ -16,6 +16,8 @@ const trial = { type: jsPsychChat, + openai_key: OPENAI_KEY, + ai_prompt: "Pretend you are a baby and say googoo gaga when responding to this plebian: ", chat_field_placeholder: "be nice...", subject_prompt: "Welcome to this trial.", additional_prompts: "Now do this...", diff --git a/packages/plugin-chat/src/index.ts b/packages/plugin-chat/src/index.ts index 2f69a79b..6ece51fa 100644 --- a/packages/plugin-chat/src/index.ts +++ b/packages/plugin-chat/src/index.ts @@ -1,10 +1,11 @@ import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; +import OpenAI from "openai"; const info = { name: "chat", parameters: { // BOOL, STRING, INT, FLOAT, FUNCTION, KEY, KEYS, SELECT, HTML_STRING, IMAGE, AUDIO, VIDEO, OBJECT, COMPLEX - open_ai_api_key: { + openai_key: { type: ParameterType.KEY, default: undefined, }, @@ -12,6 +13,10 @@ const info = { type: ParameterType.STRING, default: "", }, + ai_model: { + type: ParameterType.STRING, + default: "gpt-3.5-turbo-16k", + }, subject_prompt: { type: ParameterType.STRING, default: "", @@ -51,6 +56,7 @@ class ChatPlugin implements JsPsychPlugin { // Setting up Variables let startTime = performance.now(); let messages_sent = 0; + const openai = new OpenAI({ apiKey: trial.openai_key, dangerouslyAllowBrowser: true }); // Setting up HTML // might want to fix the chat page backgrond to stay similar // include a prompting feature that displays a prompt in the middle of the page @@ -71,7 +77,6 @@ class ChatPlugin implements JsPsychPlugin {
`; display_element.innerHTML = html; - 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; @@ -79,7 +84,7 @@ class ChatPlugin implements JsPsychPlugin { // Setting up Trial Logic // Function to handle logic of sending user message, and data collection - const sendMessage = () => { + const sendMessage = async () => { const message = userInput.value.trim(); //jumps over the finishTrial function to write data for each response for better readability. @@ -87,19 +92,26 @@ class ChatPlugin implements JsPsychPlugin { if (message !== "") { this.addMessage("user", message, chatBox); + userInput.value = ""; //resets startTime reference point for bot. startTime = performance.now(); //updates html text while also returning the bot message const botResponse = `Responding to ${message}, I think...`; - this.addMessage("chatbot", botResponse, chatBox); + const gptPrompt = trial.ai_prompt + message; + try { + const response = await this.fetchGPT(gptPrompt, openai, trial.ai_model); + const responseContent = response.message.content; + this.addMessage("chatbot", responseContent, chatBox); + } catch (error) { + console.error("Error:", error); + this.addMessage("chatbot", "Error: Failed to get response from ChatGPT", chatBox); + } //jumps over the finishTrial function to write data for each response for better readability. this.jsPsych.data.write(this.getResponseData(botResponse, "Bot", startTime)); - userInput.value = ""; - messages_sent++; // testing one possible implementation, will need to adjust how this is implemented if (messages_sent === trial.additional_prompt_trigger) { this.addMessage("prompt", trial.additional_prompts, chatBox); @@ -131,6 +143,16 @@ class ChatPlugin implements JsPsychPlugin { this.addMessage("prompt", trial.subject_prompt, chatBox); } + async fetchGPT(prompt, openai, ai_model) { + const completion = await openai.chat.completions.create({ + messages: [{ role: "system", content: prompt }], + model: ai_model, + }); + + console.log(completion.choices[0]); + return completion.choices[0]; + } + // user, chatbot, prompt addMessage(sender, message, chatBox) { const newMessage = document.createElement("div"); From f5a6a435b7d01a66d9fe2cc1329839bbfbf18b26 Mon Sep 17 00:00:00 2001 From: Victor Zhang Date: Tue, 4 Jun 2024 16:55:11 -0400 Subject: [PATCH 12/62] finished inital backend server implemenetation and tested --- backend/package-lock.json | 873 ++++++++++++++++++++++ backend/package.json | 18 + backend/server.js | 42 ++ packages/plugin-chat/example/example.html | 2 +- packages/plugin-chat/src/index.ts | 34 +- 5 files changed, 959 insertions(+), 10 deletions(-) create mode 100644 backend/package-lock.json create mode 100644 backend/package.json create mode 100644 backend/server.js diff --git a/backend/package-lock.json b/backend/package-lock.json new file mode 100644 index 00000000..09ad1e72 --- /dev/null +++ b/backend/package-lock.json @@ -0,0 +1,873 @@ +{ + "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" + } + }, + "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/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/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/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/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/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/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/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/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" + } + } + } +} diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 00000000..f2b50989 --- /dev/null +++ b/backend/package.json @@ -0,0 +1,18 @@ +{ + "name": "backend", + "version": "1.0.0", + "main": "index.js", + "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" + } +} diff --git a/backend/server.js b/backend/server.js new file mode 100644 index 00000000..4bb5074b --- /dev/null +++ b/backend/server.js @@ -0,0 +1,42 @@ +const express = require("express"); +const axios = require("axios"); +const bodyParser = require("body-parser"); +const cors = require("cors"); +require("dotenv").config(); + +const app = express(); +const port = process.env.PORT || 3000; + +const OPENAI_API_KEY = process.env.OPENAI_KEY; + +app.use(bodyParser.json()); +app.use(cors()); + +app.post("/api/chat", async (req, res) => { + try { + const { prompt, ai_model } = req.body; + + const response = await axios.post( + "https://api.openai.com/v1/chat/completions", + { + model: ai_model, + messages: [{ role: "system", content: prompt }], + }, + { + headers: { + Authorization: `Bearer ${OPENAI_API_KEY}`, + "Content-Type": "application/json", + }, + } + ); + + res.json(response.data.choices[0]); + } 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/example/example.html b/packages/plugin-chat/example/example.html index ae400dba..5e1e3e20 100644 --- a/packages/plugin-chat/example/example.html +++ b/packages/plugin-chat/example/example.html @@ -16,7 +16,7 @@ const trial = { type: jsPsychChat, - openai_key: OPENAI_KEY, + openai_key: 'abc', ai_prompt: "Pretend you are a baby and say googoo gaga when responding to this plebian: ", chat_field_placeholder: "be nice...", subject_prompt: "Welcome to this trial.", diff --git a/packages/plugin-chat/src/index.ts b/packages/plugin-chat/src/index.ts index 6ece51fa..6d3d7a9e 100644 --- a/packages/plugin-chat/src/index.ts +++ b/packages/plugin-chat/src/index.ts @@ -1,5 +1,5 @@ import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; -import OpenAI from "openai"; +// import OpenAI from "openai"; archived backend server const info = { name: "chat", @@ -56,7 +56,7 @@ class ChatPlugin implements JsPsychPlugin { // Setting up Variables let startTime = performance.now(); let messages_sent = 0; - const openai = new OpenAI({ apiKey: trial.openai_key, dangerouslyAllowBrowser: true }); + // const openai = new OpenAI({ apiKey: trial.openai_key, dangerouslyAllowBrowser: true }); ARCHIVED - backend server // Setting up HTML // might want to fix the chat page backgrond to stay similar // include a prompting feature that displays a prompt in the middle of the page @@ -101,7 +101,7 @@ class ChatPlugin implements JsPsychPlugin { const botResponse = `Responding to ${message}, I think...`; const gptPrompt = trial.ai_prompt + message; try { - const response = await this.fetchGPT(gptPrompt, openai, trial.ai_model); + const response = await this.fetchGPT(gptPrompt, trial.ai_model); const responseContent = response.message.content; this.addMessage("chatbot", responseContent, chatBox); } catch (error) { @@ -143,14 +143,30 @@ class ChatPlugin implements JsPsychPlugin { this.addMessage("prompt", trial.subject_prompt, chatBox); } - async fetchGPT(prompt, openai, ai_model) { - const completion = await openai.chat.completions.create({ - messages: [{ role: "system", content: prompt }], - model: ai_model, + // ARCHIVED - no backend server code + // async fetchGPT(prompt, openai, ai_model) { + // const completion = await openai.chat.completions.create({ + // messages: [{ role: "system", content: prompt }], + // model: ai_model, + // }); + + // console.log(completion.choices[0]); + // return completion.choices[0]; + // } + + async fetchGPT(prompt, ai_model) { + // do nothing with openai + const response = await fetch("http://localhost:3000/api/chat", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ prompt, ai_model }), }); - console.log(completion.choices[0]); - return completion.choices[0]; + const data = await response.json(); + console.log(data); + return data; } // user, chatbot, prompt From 920bf091c3f9295cf544dfd382f9f057e245b77b Mon Sep 17 00:00:00 2001 From: Victor Zhang Date: Wed, 5 Jun 2024 10:01:46 -0400 Subject: [PATCH 13/62] Finalized parameters and ChatGPT prompting --- backend/server.js | 6 ++-- packages/plugin-chat/example/example.html | 1 - packages/plugin-chat/src/index.ts | 39 ++++++++++------------- 3 files changed, 20 insertions(+), 26 deletions(-) diff --git a/backend/server.js b/backend/server.js index 4bb5074b..841166cd 100644 --- a/backend/server.js +++ b/backend/server.js @@ -6,7 +6,6 @@ require("dotenv").config(); const app = express(); const port = process.env.PORT || 3000; - const OPENAI_API_KEY = process.env.OPENAI_KEY; app.use(bodyParser.json()); @@ -14,13 +13,14 @@ app.use(cors()); app.post("/api/chat", async (req, res) => { try { - const { prompt, ai_model } = req.body; + const { messages, ai_model } = req.body; const response = await axios.post( "https://api.openai.com/v1/chat/completions", { model: ai_model, - messages: [{ role: "system", content: prompt }], + messages: messages, + // messages: [{ role: "system", content: prompt }], }, { headers: { diff --git a/packages/plugin-chat/example/example.html b/packages/plugin-chat/example/example.html index 5e1e3e20..e72d34e2 100644 --- a/packages/plugin-chat/example/example.html +++ b/packages/plugin-chat/example/example.html @@ -16,7 +16,6 @@ const trial = { type: jsPsychChat, - openai_key: 'abc', ai_prompt: "Pretend you are a baby and say googoo gaga when responding to this plebian: ", chat_field_placeholder: "be nice...", subject_prompt: "Welcome to this trial.", diff --git a/packages/plugin-chat/src/index.ts b/packages/plugin-chat/src/index.ts index 6d3d7a9e..e1409be2 100644 --- a/packages/plugin-chat/src/index.ts +++ b/packages/plugin-chat/src/index.ts @@ -5,7 +5,9 @@ const info = { name: "chat", parameters: { // BOOL, STRING, INT, FLOAT, FUNCTION, KEY, KEYS, SELECT, HTML_STRING, IMAGE, AUDIO, VIDEO, OBJECT, COMPLEX + openai_key: { + // unnecessary parameter because need to include within type: ParameterType.KEY, default: undefined, }, @@ -49,6 +51,7 @@ type Info = typeof info; */ class ChatPlugin implements JsPsychPlugin { static info = info; + private messages: {}[]; constructor(private jsPsych: JsPsych) {} @@ -56,6 +59,8 @@ class ChatPlugin implements JsPsychPlugin { // Setting up Variables let startTime = performance.now(); let messages_sent = 0; + this.messages = [{ role: "system", content: trial.ai_prompt }]; + // const openai = new OpenAI({ apiKey: trial.openai_key, dangerouslyAllowBrowser: true }); ARCHIVED - backend server // Setting up HTML // might want to fix the chat page backgrond to stay similar @@ -93,25 +98,23 @@ class ChatPlugin implements JsPsychPlugin { if (message !== "") { this.addMessage("user", message, chatBox); userInput.value = ""; + this.updatePrompt(message, "user"); //resets startTime reference point for bot. startTime = performance.now(); - - //updates html text while also returning the bot message - const botResponse = `Responding to ${message}, I think...`; - const gptPrompt = trial.ai_prompt + message; try { - const response = await this.fetchGPT(gptPrompt, trial.ai_model); + const response = await this.fetchGPT(this.messages, trial.ai_model); const responseContent = response.message.content; + this.addMessage("chatbot", responseContent, chatBox); + this.updatePrompt(responseContent, "assistant"); + //jumps over the finishTrial function to write data for each response for better readability. + this.jsPsych.data.write(this.getResponseData(responseContent, "Bot", startTime)); } catch (error) { console.error("Error:", error); this.addMessage("chatbot", "Error: Failed to get response from ChatGPT", chatBox); } - //jumps over the finishTrial function to write data for each response for better readability. - this.jsPsych.data.write(this.getResponseData(botResponse, "Bot", startTime)); - messages_sent++; // testing one possible implementation, will need to adjust how this is implemented if (messages_sent === trial.additional_prompt_trigger) { this.addMessage("prompt", trial.additional_prompts, chatBox); @@ -143,29 +146,21 @@ class ChatPlugin implements JsPsychPlugin { this.addMessage("prompt", trial.subject_prompt, chatBox); } - // ARCHIVED - no backend server code - // async fetchGPT(prompt, openai, ai_model) { - // const completion = await openai.chat.completions.create({ - // messages: [{ role: "system", content: prompt }], - // model: ai_model, - // }); - - // console.log(completion.choices[0]); - // return completion.choices[0]; - // } + updatePrompt(message, role): void { + const newMessage = { role: role, content: message }; + this.messages.push(newMessage); + } - async fetchGPT(prompt, ai_model) { - // do nothing with openai + async fetchGPT(messages, ai_model) { const response = await fetch("http://localhost:3000/api/chat", { method: "POST", headers: { "Content-Type": "application/json", }, - body: JSON.stringify({ prompt, ai_model }), + body: JSON.stringify({ messages, ai_model }), }); const data = await response.json(); - console.log(data); return data; } From e406340745f40358513727b21be2a35caba3d703 Mon Sep 17 00:00:00 2001 From: Victor Zhang Date: Wed, 5 Jun 2024 10:24:47 -0400 Subject: [PATCH 14/62] Finishing edits, moving to implementing prompting system --- packages/plugin-chat/src/index.ts | 53 +++++++++++++++++++------------ 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/packages/plugin-chat/src/index.ts b/packages/plugin-chat/src/index.ts index e1409be2..21c342b8 100644 --- a/packages/plugin-chat/src/index.ts +++ b/packages/plugin-chat/src/index.ts @@ -5,12 +5,6 @@ const info = { name: "chat", parameters: { // BOOL, STRING, INT, FLOAT, FUNCTION, KEY, KEYS, SELECT, HTML_STRING, IMAGE, AUDIO, VIDEO, OBJECT, COMPLEX - - openai_key: { - // unnecessary parameter because need to include within - type: ParameterType.KEY, - default: undefined, - }, ai_prompt: { type: ParameterType.STRING, default: "", @@ -51,7 +45,7 @@ type Info = typeof info; */ class ChatPlugin implements JsPsychPlugin { static info = info; - private messages: {}[]; + private prompt: {}[]; constructor(private jsPsych: JsPsych) {} @@ -59,7 +53,7 @@ class ChatPlugin implements JsPsychPlugin { // Setting up Variables let startTime = performance.now(); let messages_sent = 0; - this.messages = [{ role: "system", content: trial.ai_prompt }]; + this.prompt = [{ role: "system", content: trial.ai_prompt }]; // const openai = new OpenAI({ apiKey: trial.openai_key, dangerouslyAllowBrowser: true }); ARCHIVED - backend server // Setting up HTML @@ -98,16 +92,14 @@ class ChatPlugin implements JsPsychPlugin { if (message !== "") { this.addMessage("user", message, chatBox); userInput.value = ""; - this.updatePrompt(message, "user"); //resets startTime reference point for bot. startTime = performance.now(); try { - const response = await this.fetchGPT(this.messages, trial.ai_model); + const response = await this.fetchGPT(this.prompt, trial.ai_model); const responseContent = response.message.content; this.addMessage("chatbot", responseContent, chatBox); - this.updatePrompt(responseContent, "assistant"); //jumps over the finishTrial function to write data for each response for better readability. this.jsPsych.data.write(this.getResponseData(responseContent, "Bot", startTime)); } catch (error) { @@ -146,11 +138,7 @@ class ChatPlugin implements JsPsychPlugin { this.addMessage("prompt", trial.subject_prompt, chatBox); } - updatePrompt(message, role): void { - const newMessage = { role: role, content: message }; - this.messages.push(newMessage); - } - + // Call to backend async fetchGPT(messages, ai_model) { const response = await fetch("http://localhost:3000/api/chat", { method: "POST", @@ -164,13 +152,36 @@ class ChatPlugin implements JsPsychPlugin { return data; } - // user, chatbot, prompt - addMessage(sender, message, chatBox) { + // updates prompts behind the scenes when we add messages to the screen + private updatePrompt(message, role): void { + const newMessage = { role: role, content: message }; + this.prompt.push(newMessage); + } + + // Handles updates to system with the prompt and to the screen + addMessage(role, message, chatBox) { const newMessage = document.createElement("div"); - newMessage.className = sender + "-message"; - newMessage.innerHTML = message.replace(/\n/g, "
"); // Replace newline characters with
tags + + // Handles logic of updating prompts and error checking + switch (role) { + case "user": + this.updatePrompt(message, "user"); + break; + case "chatbot": + this.updatePrompt(message, "assistant"); + break; + case "prompt": // no need to update prompt + break; + default: + console.error("Incorrect role"); + return; + } + + // Handles shared logic of printing to screen + newMessage.className = role + "-message"; + newMessage.innerHTML = message.replace(/\n/g, "
"); chatBox.appendChild(newMessage); - chatBox.scrollTop = chatBox.scrollHeight; // scroll a little bit higher + chatBox.scrollTop = chatBox.scrollHeight; } getResponseData(message, interlocutorName, startTimeData) { From a0ca273172012571e5f198e0a67a61ae30e0904d Mon Sep 17 00:00:00 2001 From: Victor Zhang Date: Wed, 5 Jun 2024 14:01:11 -0400 Subject: [PATCH 15/62] Built out researcher_prompts feature, need to implement option of bot calls --- packages/plugin-chat/example/example.html | 18 +++++- packages/plugin-chat/src/index.ts | 79 +++++++++++++++++------ 2 files changed, 74 insertions(+), 23 deletions(-) diff --git a/packages/plugin-chat/example/example.html b/packages/plugin-chat/example/example.html index e72d34e2..c4a846ea 100644 --- a/packages/plugin-chat/example/example.html +++ b/packages/plugin-chat/example/example.html @@ -14,13 +14,25 @@ }, }); + // Declaring the prompts necessary + prompts = [ + { + prompt: "Welcome to this trial", + role: "prompt", + message_trigger: 0, + }, + { + prompt: "Triggers on 10000 timer_trigger", + message_trigger: 10000, + timer_trigger: 10000, + } + ] + const trial = { type: jsPsychChat, ai_prompt: "Pretend you are a baby and say googoo gaga when responding to this plebian: ", chat_field_placeholder: "be nice...", - subject_prompt: "Welcome to this trial.", - additional_prompts: "Now do this...", - additional_prompt_trigger: 4, + additional_prompts: prompts, } jsPsych.run([trial]); diff --git a/packages/plugin-chat/src/index.ts b/packages/plugin-chat/src/index.ts index 21c342b8..44a3f9d9 100644 --- a/packages/plugin-chat/src/index.ts +++ b/packages/plugin-chat/src/index.ts @@ -1,5 +1,7 @@ import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; -// import OpenAI from "openai"; archived backend server + +// thinking about using an enum to define +// -> system, user, bot const info = { name: "chat", @@ -13,22 +15,35 @@ const info = { type: ParameterType.STRING, default: "gpt-3.5-turbo-16k", }, - subject_prompt: { - type: ParameterType.STRING, - default: "", - }, chat_field_placeholder: { type: ParameterType.STRING, default: "Type your message...", }, additional_prompts: { - // need to figure out how to implement this - type: ParameterType.STRING, - default: "", - }, - additional_prompt_trigger: { - type: ParameterType.INT, - default: 10000, + type: ParameterType.COMPLEX, + array: true, + pretty_name: "Additional Prompts", + default: undefined, + nested: { + prompt: { + // prompt to pass into + type: ParameterType.STRING, + default: "", + }, + role: { + // "prompt", "bot", "bot-fetch" + type: ParameterType.STRING, + default: "prompt", + }, + message_trigger: { + type: ParameterType.INT, + default: undefined, + }, + timer_trigger: { + type: ParameterType.INT, + default: undefined, + }, + }, }, }, }; @@ -46,19 +61,22 @@ type Info = typeof info; class ChatPlugin implements JsPsychPlugin { static info = info; private prompt: {}[]; + private researcher_prompts: {}[]; + private messages_sent: number; + private timer_start: number; constructor(private jsPsych: JsPsych) {} trial(display_element: HTMLElement, trial: TrialType) { // Setting up Variables let startTime = performance.now(); - let messages_sent = 0; + this.timer_start = performance.now(); this.prompt = [{ role: "system", content: trial.ai_prompt }]; + this.researcher_prompts = trial.additional_prompts; + this.messages_sent = 0; - // const openai = new OpenAI({ apiKey: trial.openai_key, dangerouslyAllowBrowser: true }); ARCHIVED - backend server // Setting up HTML // might want to fix the chat page backgrond to stay similar - // include a prompting feature that displays a prompt in the middle of the page // create a chat buble around the other message from bot var html = `
@@ -95,6 +113,8 @@ class ChatPlugin implements JsPsychPlugin { //resets startTime reference point for bot. startTime = performance.now(); + + // Error catching for chatGPT response try { const response = await this.fetchGPT(this.prompt, trial.ai_model); const responseContent = response.message.content; @@ -107,10 +127,9 @@ class ChatPlugin implements JsPsychPlugin { this.addMessage("chatbot", "Error: Failed to get response from ChatGPT", chatBox); } - messages_sent++; // testing one possible implementation, will need to adjust how this is implemented - if (messages_sent === trial.additional_prompt_trigger) { - this.addMessage("prompt", trial.additional_prompts, chatBox); - } + // inc messages and check researcher prompts + this.messages_sent += 1; + this.checkResearcherPrompts(chatBox); } //resets startTime reference point to mark the beginning of the participant's next response period. @@ -135,7 +154,7 @@ class ChatPlugin implements JsPsychPlugin { }); // Setting up Trial - this.addMessage("prompt", trial.subject_prompt, chatBox); + this.checkResearcherPrompts(chatBox); } // Call to backend @@ -184,6 +203,26 @@ class ChatPlugin implements JsPsychPlugin { chatBox.scrollTop = chatBox.scrollHeight; } + checkResearcherPrompts(chatBox): void { + this.researcher_prompts = this.researcher_prompts.filter((researcher_prompt) => { + const time_elapsed = performance.now() - this.timer_start; + if (!("message_trigger" in researcher_prompt) && !("timer_trigger" in researcher_prompt)) { + console.error("Missing required property in researcher prompt:", researcher_prompt); + return false; // Remove this item from the array to prevent future errors + } + + if ( + this.messages_sent >= researcher_prompt["message_trigger"] || + time_elapsed >= researcher_prompt["timer_trigger"] + ) { + // if case to check if it's equal to bot or prompt or bot_fetch + this.addMessage(researcher_prompt["role"], researcher_prompt["prompt"], chatBox); + return false; // Remove this item from the array + } + return true; // Keep this item in the array + }); + } + getResponseData(message, interlocutorName, startTimeData) { return { interlocutor: interlocutorName, From f7961c3f648affb4781628e80f13cc6d6d3ea9a1 Mon Sep 17 00:00:00 2001 From: Victor Zhang Date: Wed, 5 Jun 2024 14:51:48 -0400 Subject: [PATCH 16/62] Finished researchers_prompt features and tested, plugin complete outside of data collection features --- backend/server.js | 1 - packages/plugin-chat/example/example.html | 12 +++- packages/plugin-chat/src/index.ts | 78 ++++++++++++++++------- 3 files changed, 63 insertions(+), 28 deletions(-) diff --git a/backend/server.js b/backend/server.js index 841166cd..adf1022d 100644 --- a/backend/server.js +++ b/backend/server.js @@ -20,7 +20,6 @@ app.post("/api/chat", async (req, res) => { { model: ai_model, messages: messages, - // messages: [{ role: "system", content: prompt }], }, { headers: { diff --git a/packages/plugin-chat/example/example.html b/packages/plugin-chat/example/example.html index c4a846ea..274beb6a 100644 --- a/packages/plugin-chat/example/example.html +++ b/packages/plugin-chat/example/example.html @@ -22,15 +22,21 @@ message_trigger: 0, }, { - prompt: "Triggers on 10000 timer_trigger", + prompt: "Triggers on 100 'timer_trigger'", + role: "chatbot", message_trigger: 10000, - timer_trigger: 10000, + timer_trigger: 100, + }, + { + prompt: "YELL AT ME AND SAY SOMETHING MEAN", + role: "chatbot-fetch", + message_trigger: 3, } ] const trial = { type: jsPsychChat, - ai_prompt: "Pretend you are a baby and say googoo gaga when responding to this plebian: ", + ai_prompt: "Pretend to be a comedian and include a joke at the end of every response", chat_field_placeholder: "be nice...", additional_prompts: prompts, } diff --git a/packages/plugin-chat/src/index.ts b/packages/plugin-chat/src/index.ts index 44a3f9d9..abdda779 100644 --- a/packages/plugin-chat/src/index.ts +++ b/packages/plugin-chat/src/index.ts @@ -9,7 +9,7 @@ const info = { // BOOL, STRING, INT, FLOAT, FUNCTION, KEY, KEYS, SELECT, HTML_STRING, IMAGE, AUDIO, VIDEO, OBJECT, COMPLEX ai_prompt: { type: ParameterType.STRING, - default: "", + default: undefined, }, ai_model: { type: ParameterType.STRING, @@ -17,7 +17,7 @@ const info = { }, chat_field_placeholder: { type: ParameterType.STRING, - default: "Type your message...", + default: "Type your message here...", }, additional_prompts: { type: ParameterType.COMPLEX, @@ -60,10 +60,11 @@ type Info = typeof info; */ class ChatPlugin implements JsPsychPlugin { static info = info; - private prompt: {}[]; - private researcher_prompts: {}[]; - private messages_sent: number; - private timer_start: number; + private prompt: {}[]; // keeps track of prompt to send to GPT + private researcher_prompts: {}[]; // keeps track of researcher's prompts that need to be displayed + 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 constructor(private jsPsych: JsPsych) {} @@ -74,6 +75,7 @@ class ChatPlugin implements JsPsychPlugin { this.prompt = [{ role: "system", content: trial.ai_prompt }]; this.researcher_prompts = trial.additional_prompts; this.messages_sent = 0; + this.ai_model = trial.ai_model; // Setting up HTML // might want to fix the chat page backgrond to stay similar @@ -116,10 +118,7 @@ class ChatPlugin implements JsPsychPlugin { // Error catching for chatGPT response try { - const response = await this.fetchGPT(this.prompt, trial.ai_model); - const responseContent = response.message.content; - - this.addMessage("chatbot", responseContent, chatBox); + const responseContent = await this.updateAndProcessGPT(chatBox); //jumps over the finishTrial function to write data for each response for better readability. this.jsPsych.data.write(this.getResponseData(responseContent, "Bot", startTime)); } catch (error) { @@ -158,17 +157,26 @@ class ChatPlugin implements JsPsychPlugin { } // Call to backend - async fetchGPT(messages, ai_model) { - const response = await fetch("http://localhost:3000/api/chat", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ messages, ai_model }), - }); + async fetchGPT(messages) { + try { + const response = await fetch("http://localhost:3000/api/chat", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ messages, ai_model: this.ai_model }), // Corrected JSON structure + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } - const data = await response.json(); - return data; + const data = await response.json(); + return data; + } catch (error) { + console.error("Error fetching GPT data:", error); + throw error; // Rethrow the error after logging it + } } // updates prompts behind the scenes when we add messages to the screen @@ -203,20 +211,42 @@ class ChatPlugin implements JsPsychPlugin { chatBox.scrollTop = chatBox.scrollHeight; } + async updateAndProcessGPT(chatBox) { + const response = await this.fetchGPT(this.prompt); + const responseContent = response.message.content; + console.log(responseContent); + this.addMessage("chatbot", responseContent, chatBox); + return responseContent; + } + checkResearcherPrompts(chatBox): void { this.researcher_prompts = this.researcher_prompts.filter((researcher_prompt) => { - const time_elapsed = performance.now() - this.timer_start; + // Removes prompts with no possibility of triggering if (!("message_trigger" in researcher_prompt) && !("timer_trigger" in researcher_prompt)) { console.error("Missing required property in researcher prompt:", researcher_prompt); - return false; // Remove this item from the array to prevent future errors + return false; } + // Checking conditions to trigger the prompt + const time_elapsed = performance.now() - this.timer_start; if ( this.messages_sent >= researcher_prompt["message_trigger"] || time_elapsed >= researcher_prompt["timer_trigger"] ) { - // if case to check if it's equal to bot or prompt or bot_fetch - this.addMessage(researcher_prompt["role"], researcher_prompt["prompt"], chatBox); + // Checking with prompt to trigger + switch (researcher_prompt["role"]) { + case "chatbot": + case "prompt": // want these cases to have the same functionality + this.addMessage(researcher_prompt["role"], researcher_prompt["prompt"], chatBox); + break; + case "chatbot-fetch": + this.addMessage("user", researcher_prompt["prompt"], chatBox); + this.updateAndProcessGPT(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 From 20f74488453879ee4a0d65c3fad6fb2ab3cd614c Mon Sep 17 00:00:00 2001 From: vzhang03 Date: Tue, 18 Jun 2024 16:50:12 -0400 Subject: [PATCH 17/62] Pushing updates for initial testing period --- package-lock.json | 219 ++++++++++++++++-- package.json | 4 + .../extension-countdown/examples/example.html | 45 +--- packages/plugin-chat/example/example.html | 14 +- packages/plugin-chat/src/index.ts | 5 +- 5 files changed, 223 insertions(+), 64 deletions(-) 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/example/example.html b/packages/plugin-chat/example/example.html index 274beb6a..5346097f 100644 --- a/packages/plugin-chat/example/example.html +++ b/packages/plugin-chat/example/example.html @@ -20,17 +20,19 @@ prompt: "Welcome to this trial", role: "prompt", message_trigger: 0, + timer_trigger: 1000000, }, - { - prompt: "Triggers on 100 'timer_trigger'", - role: "chatbot", - message_trigger: 10000, - timer_trigger: 100, - }, + // { + // prompt: "Triggers on 100 'timer_trigger'", + // role: "chatbot", + // message_trigger: 10000, + // timer_trigger: 100, + // }, { prompt: "YELL AT ME AND SAY SOMETHING MEAN", role: "chatbot-fetch", message_trigger: 3, + timer_trigger: 1000000, } ] diff --git a/packages/plugin-chat/src/index.ts b/packages/plugin-chat/src/index.ts index abdda779..a10d54eb 100644 --- a/packages/plugin-chat/src/index.ts +++ b/packages/plugin-chat/src/index.ts @@ -2,6 +2,7 @@ import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; // thinking about using an enum to define // -> system, user, bot +// naming convention a little weird because of chatGPT calling models "assistants" const info = { name: "chat", @@ -31,7 +32,7 @@ const info = { default: "", }, role: { - // "prompt", "bot", "bot-fetch" + // "prompt", "chatbot", "chatbot-fetch" type: ParameterType.STRING, default: "prompt", }, @@ -41,7 +42,7 @@ const info = { }, timer_trigger: { type: ParameterType.INT, - default: undefined, + default: 1000000, }, }, }, From 2602a96024a41186b2653647d32cb4ad250053ae Mon Sep 17 00:00:00 2001 From: vzhang03 Date: Thu, 20 Jun 2024 11:33:06 -0400 Subject: [PATCH 18/62] Fixing issues with styling, moving on to implement a continue feature that triggers after a certain number of messages or response time prompting the user and creating a continue button --- packages/plugin-chat/example/styles.css | 54 ++++++++++++++++++------- packages/plugin-chat/src/index.ts | 2 +- 2 files changed, 41 insertions(+), 15 deletions(-) diff --git a/packages/plugin-chat/example/styles.css b/packages/plugin-chat/example/styles.css index 1d165912..e8333dc9 100644 --- a/packages/plugin-chat/example/styles.css +++ b/packages/plugin-chat/example/styles.css @@ -1,15 +1,27 @@ /* styles.css */ - -/* .chat-page { +html, body { + margin: 0; + padding: 0; + width: 100%; + height: 100%; + font-family: Arial, sans-serif; background-color: #f7f7f7; - width: 100vh; - height: 100vh; -} */ + overflow-x: hidden; +} + +.chat-page { + /* width: 100vw; + height: 100vh; */ + display: flex; + justify-content: center; + align-items: center; + overflow: hidden; /* Ensure no overflow */ +} /* Chat container */ .chat-container { - position: fixed; + position: absolute; bottom: 0; left: 0; right: 0; @@ -18,28 +30,42 @@ background-color: #f7f7f7; border-radius: 10px; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); + overflow: hidden; /* Prevent overflow */ + padding: 20px; + } /* Chat box */ .chat-box { display: flex; flex-direction: column; - padding: 10px 20px 20px 20px; + padding: 10px 30px 20px 20px; /* Not sure about the direction of these */ margin-top: 10px; overflow-y: auto; max-height: 90vh; 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: 10px; - border-top: 1px solid #ddd; display: flex; - max-height: 10vh; - margin-bottom: 20px; - margin-left: 20px; - margin-right: 20px; + /* 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 */ } /* User input */ @@ -88,7 +114,7 @@ /* Chatbot message */ .chatbot-message { align-self: flex-start; - background-color: #f7f7f7; + background-color: #d1e7dd; /* Change this color to something contrasting */ color: black; padding: 10px; margin: 5px; @@ -103,4 +129,4 @@ background-color: #faf39d; padding: 10px; margin: 5px; -} \ No newline at end of file +} diff --git a/packages/plugin-chat/src/index.ts b/packages/plugin-chat/src/index.ts index a10d54eb..9984120a 100644 --- a/packages/plugin-chat/src/index.ts +++ b/packages/plugin-chat/src/index.ts @@ -91,7 +91,7 @@ class ChatPlugin implements JsPsychPlugin { trial.chat_field_placeholder + `"> - +
`; From 6cec0bd02fcc7aa7564c7e3bf1f87d9109e1b802 Mon Sep 17 00:00:00 2001 From: vzhang03 Date: Thu, 20 Jun 2024 14:44:31 -0400 Subject: [PATCH 19/62] Figuring out best way to display timeout, fixed error checking and data handling --- packages/plugin-chat/example/example.html | 13 +-- packages/plugin-chat/src/index.ts | 97 ++++++++++++----------- 2 files changed, 57 insertions(+), 53 deletions(-) diff --git a/packages/plugin-chat/example/example.html b/packages/plugin-chat/example/example.html index 5346097f..df68e45f 100644 --- a/packages/plugin-chat/example/example.html +++ b/packages/plugin-chat/example/example.html @@ -22,12 +22,7 @@ message_trigger: 0, timer_trigger: 1000000, }, - // { - // prompt: "Triggers on 100 'timer_trigger'", - // role: "chatbot", - // message_trigger: 10000, - // timer_trigger: 100, - // }, + { prompt: "YELL AT ME AND SAY SOMETHING MEAN", role: "chatbot-fetch", @@ -36,10 +31,16 @@ } ] + continue_button = { + timer_trigger: 5000, + prompt: "does this work?", + } + const trial = { type: jsPsychChat, ai_prompt: "Pretend to be a comedian and include a joke at the end of every response", chat_field_placeholder: "be nice...", + continue_button: continue_button, additional_prompts: prompts, } diff --git a/packages/plugin-chat/src/index.ts b/packages/plugin-chat/src/index.ts index 9984120a..ed0e6044 100644 --- a/packages/plugin-chat/src/index.ts +++ b/packages/plugin-chat/src/index.ts @@ -20,12 +20,25 @@ const info = { type: ParameterType.STRING, default: "Type your message here...", }, + continue_button: { + type: ParameterType.COMPLEX, + default: {}, + nested: { + timer_trigger: { + type: ParameterType.INT, + }, + prompt: { + type: ParameterType.STRING, + }, + }, + }, additional_prompts: { type: ParameterType.COMPLEX, array: true, pretty_name: "Additional Prompts", default: undefined, nested: { + // at one point used nested others used parameters prompt: { // prompt to pass into type: ParameterType.STRING, @@ -70,28 +83,19 @@ class ChatPlugin implements JsPsychPlugin { constructor(private jsPsych: JsPsych) {} trial(display_element: HTMLElement, trial: TrialType) { - // Setting up Variables - let startTime = performance.now(); - this.timer_start = performance.now(); - this.prompt = [{ role: "system", content: trial.ai_prompt }]; - this.researcher_prompts = trial.additional_prompts; - this.messages_sent = 0; - this.ai_model = trial.ai_model; + this.initializeTrialVariables(trial); - // Setting up HTML - // might want to fix the chat page backgrond to stay similar - // create a chat buble around the other message from bot var html = `
-
+
- +
`; @@ -107,33 +111,15 @@ class ChatPlugin implements JsPsychPlugin { const sendMessage = async () => { const message = userInput.value.trim(); - //jumps over the finishTrial function to write data for each response for better readability. - this.jsPsych.data.write(this.getResponseData(message, "Participant", startTime)); - if (message !== "") { this.addMessage("user", message, chatBox); userInput.value = ""; - //resets startTime reference point for bot. - startTime = performance.now(); - - // Error catching for chatGPT response - try { - const responseContent = await this.updateAndProcessGPT(chatBox); - //jumps over the finishTrial function to write data for each response for better readability. - this.jsPsych.data.write(this.getResponseData(responseContent, "Bot", startTime)); - } catch (error) { - console.error("Error:", error); - this.addMessage("chatbot", "Error: Failed to get response from ChatGPT", chatBox); - } - + await this.updateAndProcessGPT(chatBox); // inc messages and check researcher prompts this.messages_sent += 1; this.checkResearcherPrompts(chatBox); } - - //resets startTime reference point to mark the beginning of the participant's next response period. - startTime = performance.now(); }; // Event listener for send button click @@ -149,14 +135,34 @@ class ChatPlugin implements JsPsychPlugin { } }); + setTimeout(() => { + console.log("timeout passes"); + submitButton.style.display = "block"; + }, trial.continue_button.timer_trigger); + submitButton.addEventListener("click", () => { - this.jsPsych.finishTrial({ endOfTrial: true }); + this.jsPsych.finishTrial({ chatLogs: this.prompt }); }); // Setting up Trial this.checkResearcherPrompts(chatBox); } + initializeTrialVariables(trial: TrialType) { + this.timer_start = performance.now(); + this.messages_sent = 0; + this.ai_model = trial.ai_model; + + // sets prompt + this.prompt = []; + this.updatePrompt(trial.ai_prompt, "system"); + // sets researcher prompts + this.researcher_prompts = trial.additional_prompts; + const continue_button = trial.continue_button; + continue_button["role"] = "prompt"; + this.researcher_prompts.push(continue_button); + } + // Call to backend async fetchGPT(messages) { try { @@ -182,7 +188,8 @@ class ChatPlugin implements JsPsychPlugin { // updates prompts behind the scenes when we add messages to the screen private updatePrompt(message, role): void { - const newMessage = { role: role, content: message }; + const time = Math.round(performance.now()); + const newMessage = { role: role, content: message, time: time }; this.prompt.push(newMessage); } @@ -199,6 +206,7 @@ class ChatPlugin implements JsPsychPlugin { this.updatePrompt(message, "assistant"); break; case "prompt": // no need to update prompt + // this.updatePrompt(message, "researcher"); not sure if this would mess up logs break; default: console.error("Incorrect role"); @@ -213,11 +221,14 @@ class ChatPlugin implements JsPsychPlugin { } async updateAndProcessGPT(chatBox) { - const response = await this.fetchGPT(this.prompt); - const responseContent = response.message.content; - console.log(responseContent); - this.addMessage("chatbot", responseContent, chatBox); - return responseContent; + try { + const response = await this.fetchGPT(this.prompt); + const responseContent = response.message.content; + this.addMessage("chatbot", responseContent, chatBox); + return responseContent; + } catch (error) { + this.addMessage("chatbot", "error fetching bot response", chatBox); + } } checkResearcherPrompts(chatBox): void { @@ -229,7 +240,7 @@ class ChatPlugin implements JsPsychPlugin { } // Checking conditions to trigger the prompt - const time_elapsed = performance.now() - this.timer_start; + const time_elapsed = performance.now() - this.timer_start; // could instead keep subtracting from time_elapsed if ( this.messages_sent >= researcher_prompt["message_trigger"] || time_elapsed >= researcher_prompt["timer_trigger"] @@ -253,14 +264,6 @@ class ChatPlugin implements JsPsychPlugin { return true; // Keep this item in the array }); } - - getResponseData(message, interlocutorName, startTimeData) { - return { - interlocutor: interlocutorName, - response: message, - rt: Math.round(performance.now() - startTimeData), - }; - } } export default ChatPlugin; From 0ba582053a87099c9c1bb289b11c51321da6a94e Mon Sep 17 00:00:00 2001 From: vzhang03 Date: Thu, 20 Jun 2024 15:14:49 -0400 Subject: [PATCH 20/62] Finished final edits, refactored error checking and 'continue parameter' finalized --- packages/plugin-chat/example/example.html | 5 +- packages/plugin-chat/example/styles.css | 27 +++++++++++ packages/plugin-chat/src/index.ts | 59 ++++++++++++++--------- 3 files changed, 65 insertions(+), 26 deletions(-) diff --git a/packages/plugin-chat/example/example.html b/packages/plugin-chat/example/example.html index df68e45f..bd830b14 100644 --- a/packages/plugin-chat/example/example.html +++ b/packages/plugin-chat/example/example.html @@ -32,8 +32,9 @@ ] continue_button = { - timer_trigger: 5000, - prompt: "does this work?", + timer_trigger: 1000000, + message_trigger: 1, + prompt: "The continue button will appear in the bottom right corner, please continue when you feel ready", } const trial = { diff --git a/packages/plugin-chat/example/styles.css b/packages/plugin-chat/example/styles.css index e8333dc9..1db3029c 100644 --- a/packages/plugin-chat/example/styles.css +++ b/packages/plugin-chat/example/styles.css @@ -96,6 +96,33 @@ html, body { width: auto; font-family: inherit; font-size: inherit; + transition: background-color 0.3s ease; /* Add a transition for hover effect */ +} + +/* Hover effect for send button */ +#send-btn:hover { + background-color: #45A049; /* Slightly darker shade on hover */ +} + +/* Submit button */ +#continue-btn { + background-color: #FF5722; /* Different background color */ + color: white; + padding: 10px 20px; + border: none; + border-radius: 30px; + cursor: pointer; + margin-left: 10px; + width: auto; + font-family: inherit; + font-size: inherit; + 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: #E64A19; /* Darker shade on hover */ } /* User message */ diff --git a/packages/plugin-chat/src/index.ts b/packages/plugin-chat/src/index.ts index ed0e6044..c84217e7 100644 --- a/packages/plugin-chat/src/index.ts +++ b/packages/plugin-chat/src/index.ts @@ -27,6 +27,9 @@ const info = { timer_trigger: { type: ParameterType.INT, }, + message_trigger: { + type: ParameterType.INT, + }, prompt: { type: ParameterType.STRING, }, @@ -95,7 +98,7 @@ class ChatPlugin implements JsPsychPlugin { trial.chat_field_placeholder + `"> - +
`; @@ -104,7 +107,7 @@ class ChatPlugin implements JsPsychPlugin { 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 submitButton = display_element.querySelector("#submit-btn") as HTMLButtonElement; + const continueButton = display_element.querySelector("#continue-btn") as HTMLButtonElement; // Setting up Trial Logic // Function to handle logic of sending user message, and data collection @@ -118,7 +121,7 @@ class ChatPlugin implements JsPsychPlugin { await this.updateAndProcessGPT(chatBox); // inc messages and check researcher prompts this.messages_sent += 1; - this.checkResearcherPrompts(chatBox); + this.checkResearcherPrompts(chatBox, continueButton); } }; @@ -135,17 +138,12 @@ class ChatPlugin implements JsPsychPlugin { } }); - setTimeout(() => { - console.log("timeout passes"); - submitButton.style.display = "block"; - }, trial.continue_button.timer_trigger); - - submitButton.addEventListener("click", () => { + continueButton.addEventListener("click", () => { this.jsPsych.finishTrial({ chatLogs: this.prompt }); }); // Setting up Trial - this.checkResearcherPrompts(chatBox); + this.checkResearcherPrompts(chatBox, continueButton); } initializeTrialVariables(trial: TrialType) { @@ -156,10 +154,20 @@ class ChatPlugin implements JsPsychPlugin { // sets prompt this.prompt = []; this.updatePrompt(trial.ai_prompt, "system"); - // sets researcher prompts - this.researcher_prompts = trial.additional_prompts; + // sets researcher prompts and removes any that can't trigger + this.researcher_prompts = trial.additional_prompts.filter((researcher_prompt) => { + if (!("message_trigger" in researcher_prompt) && !("timer_trigger" in researcher_prompt)) { + console.error("Missing required property in researcher prompt:", researcher_prompt); + return false; + } + return true; + }); + const continue_button = trial.continue_button; - continue_button["role"] = "prompt"; + if (!("message_trigger" in continue_button) && !("timer_trigger" in continue_button)) { + console.error("Missing required property in continue prompt, will never display"); + } + continue_button["role"] = "continue"; this.researcher_prompts.push(continue_button); } @@ -194,9 +202,8 @@ class ChatPlugin implements JsPsychPlugin { } // Handles updates to system with the prompt and to the screen - addMessage(role, message, chatBox) { + addMessage(role, message, chatBox, continueButton?) { const newMessage = document.createElement("div"); - // Handles logic of updating prompts and error checking switch (role) { case "user": @@ -205,8 +212,15 @@ class ChatPlugin implements JsPsychPlugin { case "chatbot": this.updatePrompt(message, "assistant"); break; - case "prompt": // no need to update prompt - // this.updatePrompt(message, "researcher"); not sure if this would mess up logs + case "continue": + if (!continueButton) { + console.error("No continue button to display"); + return; + } + role = "prompt"; // use same style as promp + continueButton.style.display = "block"; + break; + case "prompt": break; default: console.error("Incorrect role"); @@ -231,14 +245,8 @@ class ChatPlugin implements JsPsychPlugin { } } - checkResearcherPrompts(chatBox): void { + checkResearcherPrompts(chatBox, continueButton): void { this.researcher_prompts = this.researcher_prompts.filter((researcher_prompt) => { - // Removes prompts with no possibility of triggering - if (!("message_trigger" in researcher_prompt) && !("timer_trigger" in researcher_prompt)) { - console.error("Missing required property in researcher prompt:", researcher_prompt); - return false; - } - // Checking conditions to trigger the prompt const time_elapsed = performance.now() - this.timer_start; // could instead keep subtracting from time_elapsed if ( @@ -255,6 +263,9 @@ class ChatPlugin implements JsPsychPlugin { this.addMessage("user", researcher_prompt["prompt"], chatBox); this.updateAndProcessGPT(chatBox); break; + case "continue": + this.addMessage("continue", researcher_prompt["prompt"], chatBox, continueButton); + break; default: console.error("Incorrect role for prompting"); } From 02bc77d3a7ea44f06ac280903c8e863ab5d44c3b Mon Sep 17 00:00:00 2001 From: vzhang03 Date: Tue, 25 Jun 2024 16:43:25 -0400 Subject: [PATCH 21/62] Adjusting styling, moving onto building out functionality with prompting and continue button --- packages/plugin-chat/example/styles.css | 31 +++++++++++++++++++++---- packages/plugin-chat/src/index.ts | 3 +++ 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/packages/plugin-chat/example/styles.css b/packages/plugin-chat/example/styles.css index 1db3029c..f1249a6a 100644 --- a/packages/plugin-chat/example/styles.css +++ b/packages/plugin-chat/example/styles.css @@ -17,8 +17,29 @@ html, body { justify-content: center; align-items: center; overflow: hidden; /* Ensure no overflow */ + padding-top: 60px; /* Adjust based on the height of .bot-title */ } + +.bot-title { + height: 80px; + 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 */ +} + +.bot-title-text { + color: black; + font-weight: 1000; + font-size: 40px; + text-align: center; +} + + /* Chat container */ .chat-container { position: absolute; @@ -32,7 +53,7 @@ html, body { box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); overflow: hidden; /* Prevent overflow */ padding: 20px; - + box-sizing: border-box; } /* Chat box */ @@ -106,8 +127,8 @@ html, body { /* Submit button */ #continue-btn { - background-color: #FF5722; /* Different background color */ - color: white; + background-color: #FF914D; /* Different background color */ + color: black; padding: 10px 20px; border: none; border-radius: 30px; @@ -118,11 +139,12 @@ html, body { font-size: inherit; 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 */ + font-style: italic; } /* Hover effect for submit button */ #continue-btn:hover { - background-color: #E64A19; /* Darker shade on hover */ + background-color: #e28043; /* Darker shade on hover */ } /* User message */ @@ -154,6 +176,7 @@ html, body { .prompt-message { border-radius: 10px; background-color: #faf39d; + color: #6B503F; padding: 10px; margin: 5px; } diff --git a/packages/plugin-chat/src/index.ts b/packages/plugin-chat/src/index.ts index c84217e7..ff4f1c22 100644 --- a/packages/plugin-chat/src/index.ts +++ b/packages/plugin-chat/src/index.ts @@ -90,6 +90,9 @@ class ChatPlugin implements JsPsychPlugin { var html = `
+
+

Adorabot

+
From 1786b901da677f43f4f0a8a088f49f316123919d Mon Sep 17 00:00:00 2001 From: vzhang03 Date: Wed, 26 Jun 2024 11:23:10 -0400 Subject: [PATCH 22/62] Implemented own streaming method, moving to implement native streaming method --- packages/plugin-chat/example/styles.css | 39 ++++++++------- packages/plugin-chat/src/assets/bot-icon.png | Bin 0 -> 13694 bytes packages/plugin-chat/src/index.ts | 49 ++++++++++++++----- 3 files changed, 58 insertions(+), 30 deletions(-) create mode 100644 packages/plugin-chat/src/assets/bot-icon.png diff --git a/packages/plugin-chat/example/styles.css b/packages/plugin-chat/example/styles.css index f1249a6a..800a70c5 100644 --- a/packages/plugin-chat/example/styles.css +++ b/packages/plugin-chat/example/styles.css @@ -6,7 +6,7 @@ html, body { width: 100%; height: 100%; font-family: Arial, sans-serif; - background-color: #f7f7f7; + background-color: #d1d0e5; overflow-x: hidden; } @@ -18,11 +18,13 @@ html, body { 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*/ } .bot-title { - height: 80px; + /* height: 80px; */ + height: 8vh; width: 100%; background-color: #706EA0; padding: 10px; @@ -30,6 +32,9 @@ html, body { 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 { @@ -46,7 +51,7 @@ html, body { bottom: 0; left: 0; right: 0; - max-width: 1200px; + max-width: 900px; margin: 0 auto; background-color: #f7f7f7; border-radius: 10px; @@ -63,7 +68,7 @@ html, body { padding: 10px 30px 20px 20px; /* Not sure about the direction of these */ margin-top: 10px; overflow-y: auto; - max-height: 90vh; + max-height: 80vh; box-sizing: border-box; display: flex; flex-direction: column; @@ -147,36 +152,34 @@ html, body { background-color: #e28043; /* Darker shade on hover */ } -/* User message */ -.user-message { - align-self: flex-end; - background-color: #4CAF50; - color: white; +.user-message, .chatbot-message, .prompt-message { padding: 10px; margin: 5px; + margin-top: 15px; border-radius: 10px; +} + +.user-message, .chatbot-message { max-width: 70%; word-wrap: break-word; text-align: left; } +/* User message */ +.user-message { + align-self: flex-end; + background-color: #4CAF50; + color: white; +} + /* Chatbot message */ .chatbot-message { align-self: flex-start; background-color: #d1e7dd; /* Change this color to something contrasting */ color: black; - padding: 10px; - margin: 5px; - border-radius: 10px; - max-width: 70%; - word-wrap: break-word; - text-align: left; } .prompt-message { - border-radius: 10px; background-color: #faf39d; color: #6B503F; - padding: 10px; - margin: 5px; } 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 0000000000000000000000000000000000000000..8a0557f2edbf71efa1e07235d48b5a86fa57772d GIT binary patch literal 13694 zcmeHuXH-*L@b5`NF9Af5UPBiU=^#yN=)EXS6fhtiq(iXMq=a4sgdn{ry$HD%)GG*R z=skjf^eDZ*!@d9a^{w~meR{0L%ACDtX3s8X@7eR46K86yOH0K@1pojoQcueq0H7cV z1t`eCpWTp=WAKM0;5O2d0{n}hcn}LdQ~K)J1polN_wpZ-AqHm$3t0oTZ38WQ+yaB| z`MUza!NC$JZ_j}H_k3L?eEi+>wr;Tj01trF(y$E4-<%C@bh8RQ{Js4_x+jn&Y}*wm zoub!fp!PtpS=vjbvfPhj6NnRZNjObJ25OhEjKtD1Bsx4HB_Sa(D30a9zmPC;cFuOp z&N(@`KcClTygc~fAhNLGKxBUCPB^c^PG@*wb78}fVmKX7Ma@10PQK4>^_avBx`UqV ziS||`3fEr=0a}ABkC=RQ$L<*PxfBJGBA!peLKu9%`cBFUZ+*Zq;WBW+F&$4A^f6ej zO`1-oL#cW$mde&@?(>=!K-u+SAp*&^q(^w^i(|tH`8+Si_dq(IE7tGkb$A%1pKwU|K zdcTc$Ip^8N8KQG&oraZnspEo}*fgKrWhqm>rRMKiw>9WEve3gMGC@ zZEtck*k1V_y?e6+H0v=7k$y>ewX**y?v2K@AqK0XobMfY&cDP!h~vF$jKRK8!i?}{ zZlwogJ;$1XDwN!>5xDw-Kb7`H;=Ch$w2>IL2XJ57CFLLZC6D|YdYBOg7%nt@J7V3u zE6yOzU@{bsJo-R-81iX8oc@r3D26W!35F)1OM5OZ?43QGC7o4QOMCCfY)X&)Ho}$N zJTE{X?Ko+wi;g?iI%1-EKRg^aC{|O~Cs9@4s9HOg5E9_VAeshc2w9fXspnThL~gLy zZvXWo+s4NJpb%yOTNg)~i=WDl1{HJ*1mUgN07soBerU9nA3Fls=b>DAG#9P18u=vO z2R{TEr11W%XGifUtxg1ku&RoO?=TPq6xrXUGREl2o7F}2uPUn8Z zZ{I^D3q{>5k;N7KTf?Mk{D?f1x?XpT`O#bfuBNy7_kFv&JV^6;FSKiKP$C_0;_mfV z?}ldXlzipuezaFZvpNQ=zRg=}VXtv4*Ik=LVtr+N&SJ;f34>K=p&#e%k_(s(|DDyV zx|V{d@GCI1cRw8X8nmtZQ;FQ!s6R^wSu&bkEBe7;yU z*>jteKG+!Dly-=Rt!#U=xbD~U~InR7vN-o#7B9I^2U!N;?al6$8b@s1DD<3Kr zSwgrWdcWQ1cgD^`AyWD$kJS@D#H>R65U*UIJRhf%vjf)gXqHk3X8QY7T@=F4r6DN` z+FwVz>05buj8WsMU1Yz;PthzG9VxJ4laqc?2fwrZmYuJu)QMr*5-%(Wn0JDTz2UKD z*QlW`kC^B+|0@)Sxy)RnFOk@yjAeLdB=B!@3BRgy6^@2%Gf8_NE8@8&_N-N=vJf6y$T%a&K}_ zPi&u%OuQ~oPTLbHU!lu3I< zyxd(fWx8Z}USQ1(0c=PMX)gp@{)$nvd>yZVrBpWm`iu^h}cIOE=QYvaZpL(z# zQxV9@57c_pKXxrLzi$jd+|vXhYr)F-g&hm#*A$>UR*OOqDL0r9S+B^_QZM3$8g(qV zKADG#!HMG~gyrUx(*ffQ4YWyLE|*G}Tw3;zZC=91oOP}rS}s#v&$26?2_XBH#d-su zP(K`-0)}JqVo@iG|1BU#9YY}nl1{OJ*^}RNy-gi)CEOTnQl3#*np!cxJtaqK%dtu* z6t&b<{^KGiv`P|bo@l}tEZ>RF&e*Gb&xHEaRlXm^=d&k(j1Ra`U(jm&`{l?^BpQW# z-rE$Q;_E!dtcSU};j*CgiI>oiORpB6=oG<4Uzz?y{r>W;pt_YjG3ZBeD7eQ$nGK0Z zINaB+;RRnMvn={~py{0^z z>aR>>C1l_N)8^nN97;L*!D#;3d$_`z z?HXaEIqZyd)(|=d^P!oD!XRYFMAElY0@SGXNdGrP2 ztujV&U(DDIFjzUoBoFs#F)J=e(kpO-M>Gw^;Od0uQMwsb=rhfh6a-RSmR8}_ZiW3S zymS0Lm%{6TAyr`{M(#%_&MSTUtI%EVH(zgl1O-3I;LDyE2+zMuw#iJ@k#Mto$BRI& z6+0jkcB_jcbi4iRC{M{zDi9>bGKLcwJQ;L&$C+|f@uJpB&K864s%IkJA>91ab-fkd zdNqW$-gT)a9f7<*#Tmlx3t3f+2rTQqdZf2}Q|>KBs}|!s?*s zEQPZM=A+iodD1SDh(eh=MO@jyt{a?4M!4#H@OMxuw)R-uSBZ#n4*k(ClSGO8`IXcW zmQS@|*(!m-KHw!%n%}H1_jO?FA#rwThMdDg80%eswCiEyyrg`|hzy1#pJYn|Nj1 z!)`YiH&m5;!>U;ngH_$2?9AN#wf`BLLm~T>0X-kgLzE??;F(ep&)XRkC=ZQR<42w+ zK)if!`Z7H}$BgKZL)3&maPx3LfNaW$d>KOo(xY1v;)7ibAP-c5OwhIhArY(bdYJde zvP3AM3(q7{+E0m#!O`jU|JKK}uSnp9=H#<#^(Y-8$q7gt)5MX1dDbT7X?ml5_)4H3 z2_b}#haEawTW};q5zO#x6aBtTV#rOYgvJ+r9L`+z()FI-+0MFLv7`2q5)P^P?U}$W z@c8kaU1P^Bj>K6+2=2*{MBp~Jp2f;Dm8qhr=@@fMf1UfV~^KqoruQ=Ql zd}E4YKR-63(h>T|AyiDDqODobK}^l4{qb^_qa;~Ub!SH38bHwa!>Q$o-igWjaSXwX zB&=dRe;r{xJ!;nvw$W!d8%1B495u`GVo6Oq$Ea*&><1ep9>56lnwr9J12?&woNIl6wo5)Btew(LP z*OM#uajn#cC68)S5ndZwgci&2n&{BxG2z}QY>Q%*63<|d{INge?Lw3wew53H%tb>8HCWF;rtzqG@`tg*<;y^8p-dJUQBhE!b(~liuS!s1oqof$ zp)2e2at^*x_>L2Uo!czwc;|JAqN z59x=mBgIiasF3CjU-=z+N=}|%TY2>ClRSHC-r>Dy_a_+aa;1ylgfg;7~NzGi9HV<3H?_^&Gz@`?mqx+o@p&A8CYfvScx)Ir$L%4pYrg0g3D|){CO(? zyd?kFCUdFyNP;?kxLmS7HxuBvLcbVAc#Bp5NAr*tMbYiV5NRoAWB|ZvV*NrdRfN+( z)Tm+uOw|(~f&3ec^o!%5kBQv$2R+FTFqa3zjU>)>Xcl?$-V{WWf!4e;)HJmXXASXV zCi$-fT2FgJna7yfNFU%+y|_z0!$z-*8IfoK)?o`WZkV=O0U$xI0no?DOzAr#)$_eT zrQfk6&i!bXeyEEcW=ez)l6{SyKS59ckcPTkrv*JjkFr=*mjpD6os0-1cA^|SA+hhY z5&^B5YPU;Kef7kQe~oP9F*I$FZNPDJHNa>KFtm=%*tpo*Dw+ zdC$WLfVaSZz+x=?=Kq`GqKZn*=Cf zzra&u^z%86T5qLanzf^ZyWi4z`iK8dor`&&7=xGn&vBZOqn4EwSuoAW%!G#VcFwmj zA;1YX_U$4n`(YcE0_jBUv7r8&Wk?-)lp)(_pI=wCfWx9OnVZ#ouXTI&`9TkI(sLFf z2pnxfPuYpWM@nZ#85uY`>>KlnK{#ZP)Y@V{Ka}KLi244E%FeO^)chtWCt7cKv*drpLF>1taiB2Lg{9iMFG zUzSkQ1!gcdbLa4%0-$~=7J!K0K7s*Mj{G*Fo$v!QM z2-+#brN!6)<5z19raR#w`wG!+v?xrOHr5QAdStp))9evS3pIhxX&mbWQMxub>H>0y zVtw5e91df!i}+-ct3IMc%0BMJ@2&#GYcbOmt9&}Zu}A_kpy*U()pbS3cs!bZ)%E?f zNHjWGLJ+v2J|$mzpq;TajYY+lQVhrVn4h=SzK^)^$W!+2Dv9%=Ki%rmKu^`9!1NWE zjcHWLE35Vbzz{NsJ&pRuOoG2ggJPpOS-tQAa@LOe-v^Q8gz9srN z>+0Juf~QnN`uw>--_oIidYXKi%qySO#TKn2OWeL^r62;}UsAq$db~P{!w*>2CY4rE z;>bn1Hw`i`{(fU*G$TU@PgnCc$UNBpz?4^kq{Pv$emu6ZdN1(lM%uEf?v@LbsMF_j zpp-s*GMloNIAnSrQloj)5GDTrCMynM5phqum-*QNwJ-e++>Lb2qwdT%VV@({h+ls6 zA5Z76q0>a7^J@iPOM6m}-&Z8Ip3Au}%E&@k_y$($cOI}Q=iK`qiB5QUj+JtFCxF5f zuI5DfFszD?ip8+&rQMk^(O1_oSP3t9)Ye%Qv3|81WV;a9`C4`Mzv0p*+;?w^Q-|LWSE;&x{8^k{+aTZ*4 zvG}+jp16B+Hj}C4&05A-CM?`~hD`Qz0rq}z{CzK0l<00{_w5`b{l2QO@nyF^D#q6< z#KD>K<*)auV|PiBiH=^?(aMxS0NZ7`UcoR_xoVk1`PQ?*?>w){*V4MVZ+yvm59QEk zC)#{32zGmH$FVGGYiHCU-AMHVAZAc(097;4> zXt8r(rc_Kl{JH#hwYVv#cAdlf+{RvS&d#TY>wl@adTB&?83J7Vkzb^EE%XZYGin2w zN(U%$`&b$k|L`-TG3LbWkcAc55c4tSa^nKyjdzXlGR3AsKK#wE5|Gr0CM$MEa3t@j zT)5}#8hyzAyNAFMC3}-}zT&td@t5`MmYs zsgf?n?YYYRfar%@MB!0aUYj&{fumrQq6pz&S_yiCHU!;z7U(^L;I6fAH+X}6! zW1AqiHJS0-ZSEW`Ooy1`IY!6ACwgZe7=Y(*XXnfLS=taWVikIxyXw~83RNXtEydhj zR3YAU?5;A5=y$KxXS&*-uc_^F(Yu9rAnp!l8rF7(rek8l9=&#lM5PGAzj-jD@3~KV#{=m_T?mFV_?6`YvM;{xH z%9BKe$6Uq9J~h(eRDMpw5h4Mz0Tr51pkgRO`S*^}t$$dOL?`0Qy-*rKio>l?j0y#;Kq%CPZNwnOTF#N|`w{4b{ zY-YO7fz>Bdh-CVfg*PjPT-4}mDa=vv#H(`c8p-w){7NURKPYf-XVovJT?GbS|L{za zF$wLSc9umQSu{;&_8R&>=3jC^;r>E0tCC$;zqj(Qz2k~p>F`_AXto$PTd1#lq(thqQ4T3Vn4_?|P4>#Ji zS{S<#DhIk+%u+oURlYf+Gf6zltUo8gn5ojs?1bNpP?z$#B%0WFNykGOQU$RshvffA zqw?hr@8Tc2b{9~6z1+Er3}O8JE$r> zr>MPt5}s}o(vP91JW|0bMfiZN9-QS=?)g-x|-t%t3_6;tE9SF(+af+p5U(LQrzy=LzUI zD}^M=d_i_|Kpxh`Ms}n$y6LcM<)0Ktx@zsV1(aMtzm3wIdted63h~P%>n%@+Z4S}! zyyU#jqvN2JFWK;L8;=WO{=PEEnL}RWBnEYP=wIwsbC>mM89bA`cjBkMod;=MP`Bhl z1S>A!?Xy^|z(F3VVLQ1Xooc~^c6#kZzLDqGLcEN%-YZLAs+`pd8T@(e`wCnxNW!yp zC51uyb`$C!}D|MNKxnU9aIAO!Q@NzQMC{0S_xQrFz3G zCw++cV6RsVtP$$S4Zq!IiTtF+xLl4M% z&3M7qgYDE4?QnOY3+cp{xJBr~DO3%$_Cz z;jx3K3+3m<`+H_G+Yaah#+_ClCcn6J5X}@;EZ{D@YC4o4J<{;ZhoOO+8wT&c+%joz zsT+3ql!ONRugwC!LQC6@%0`;F00 zqb?MQgSe1XN0JD{jXAoQyRgLyR&T+5*8R_DHHyS*tFyg>d%qR&i?^H9wRC<_sD~;; z#9}{4M@3$qu`JGt?}ELDzD_|$zxefX8T7~HIH}>;^;Y~Ju$ORs7&R-?0Vt?kpr&;0 z^x+zJO(MH!6k-ksFUsuh7AYM|z56^OO6aW?XPFl1eSEHo(`{(xQDa1N#Z#)QY(T07 z_n(1LGwn9gr)21E0a`_!mh~ereiC*J1HFmQdar#dBn{jE_oPd2Le#kh^d|lyDS8RE z#?6g5@8yO@A;bJc%Qn$F(6z1#e~nL%%jftcj`Kw4`3CAzhhwlg@T!MH^&g)fpWLJ1YQ8?`~*nJmNaG#baUuaH7Wg!a)S} zajx{JAIbkr1E+SFcj3CrJHODxU7Fp;_}BVYLqbA#9EazZEy+6z-hcij3f_d3S7#~~ z*0bU=M{#K<)A_H4D@brT!1QK!j0N@ixg|cPU4B*K?MT_P6dj>8)sOG`l+zlybVP=c zD}8g=0C$hS+D|!&J5Rdn;&Xt;`R?!MUz9%uuLi1Pv zxVn9r3m{hf$&RX%-qy68FUJ8mWkpflyOas8=8fz>$-pyx`6)_Z#eR8)b-V>|7=>lM zrS8(s)$GkUSNfZz=Z9UL`Upj51Q%vL8X`Da3@n0ZVSw&iU8NzeJ~`_OL=+M}1O-A5D1OrrXy2qFP!RscGp!E$=t);Zz75nK2?K$> z&uv|S+Q++*__k=*)%bD_MnFZ2?WS7XF5V)on&g}tWN>B5j^5V zU0u}1{rz{48LgHf>#8Mg^G0%G>(HG(BQ>S(e>k1%^qZf^_;o;>%1)1NINIs5eVAy@ z9r(s(Oj zml%Gw=OfnSxxYhiExB>en<&{K3GDaAsxQ;Oon%it;Clz3{(Z&2afU;pwR2@R?zu=h z_9GMO^Qky8D^$!!;pB73_VepP`}uYh5sV?y_3pJ~O}1rM&=>=w%A7640NZ1-RWVEt z(&q7)26BtW^yc{ez1dCdu!A+e`ErIVN{*>7`eJ*7sq&6lMN<{kAc0nP`0F4Y3B z!{yTkvq*80dMmFO56guFfwj+Jc>0yM)<$`hUm?qwE(u#D`^@j1CS_RRD&&rj{kyme zA2|D9P8&AMUw1GkqfNAY>juQs${L>a5(Ei6VpD$}27^=VO_WyuzYBKvfbW(RZ`S#KNa5@r9!Lv#o6cqD$p5oSmk z8?eYjWONqhnGnHMgHcSx>P0SXr!3E47(vJax!!a^rhpwnD+AjkbJOk|6>k zneT&rI4v>K+3c?{*~wdkBWzY;FbQ+ooLxJ8YmM^+dhTtQXYB=Pnl zYI%uibd$n4Nd4>uIyn)3x?5DFiOr&_%4Pw>j6plM@v~=ILkVa7mJMe6PNhtfd`e$)AtTBP7XI9#j3CWs zb1YfTql$R?Wk^(sRv|CD?R#naBy;PtS6dS&f$)l<3zd%40vjDyR!!U{F72u4CIxzJ zp=LLjBP!+xjlKF5v-|gXxwDg*3~RAb6bV5XUw-dFVQ2in*U11kVlpknGiDf>W2gpN zd`+$7;{I>ORt|aWc8;3H#q>_@43mx6C@*9JuJCjuSm)sL#k)4{xoaWSPq;bxKGg4+ zg&UBfPae;8? zJV^olRUdStk2ht!(4GhPC$(C4NC2|xn3IabV6SQ|>PqYVPPIk>U+&QdJf#Wzz}mQR zFL#y!TK@a%1L%2nT(qiU&in8|o$?GY^tNWZ<$fg~^%O}fYOgH?G3|bf(W$OfP?ap1 z<#zxZE6w$4;h5lavKTS`Kz+!AvaTDPL1j7VOA-nN(O9Q)|xw6k9jb}%T8Q9#eOa*fSumXLbWplKVgZxJJ{#>XJ z-bfVugj26)USL3Ameyx2DTEf)OVDuUB>o~sb04kzbMV*Q)LP2vv?I8AfL{?{HyekLftA5jzWU}W^|TRZvZ`a!oDG!g z*S&3TlDWP*xy&xuE{)}qk%9;}bL>-M1)>dn&~e~&S#1hf%3C;R^l zmo3!oY^@2L&#ao5Ik2%FfC7HdTN5fXKQ4bps*qs4BKrPCi?f=kvf zNt`KBqhDJfCpME4`8)0=0!BPH3wPW8?!x-KTA|7MJ?D~dl$(y_&Z1XPDV9MBrMw{S zgD7lnJ9&uwYK>dPvxJzHUDibiUfD0NA%FLJG%G49X?o&D+;|<*sqCqq;yft#_VB(Heijtjn_cQFPb``2yj;#wJ716%DWZrL}=VLpL58Kcv;3Hd*u_1q#1AymFB zLV%GKMMJ0J?cMLp0M5;j8v)ug`4jfKIvm}%D z{!V`DuvOw2B7qoV*>|`pWBNTW(1v)4Hi(pQ&XEW1(CMkj@169xk4i=rMG06!G1&pr(sOXGFyrr|F`3go^2T%B+77E^hRe+^SkL za0j~llz+1T9MxyOUCFO>)~DpCE!}0YAo1uOxi>Cn=1+GUHzLZf^|zSc$=mAS_~r80Wd|U!G}rsXU!% zxux58q%huqko_x=z}|ZRTaUTFBdhsAA?uJ+jl@1qj9lORwHNF(+v6DB@CU4zirW=& zVj{c*hu$ zX}&3Y?(eK$ASiNYcQ^Td#yZlI;?PzdQz;E}W_zd}&9$DwClpN@EHlbdmc43^omu(B zr9*GJC4wm|DS>H0Orf<){V^pCjfNS@F}r^plVI)?t#LAU( zH5}#4M*)>Nh=&2L;wYmSHPYs)t~ks{$TXM>6{X((V!6|;j{DrJ>6`LD;Wu7MT&7$_ z!yKPE(d%6Sw?MP0Wav2NLdPpGH2kAiO@>5Q ze}j8pwJGOl9xE1E(JU}vELZm^2wU6?UB+ZCzcy%xf{jo1FC~HzE(g5%L^zUF7tfel zs4bP@b{6I11X1(~T!&NxDw)?aC9*Pb2BtzP2{I#&$2=QNr`MuTrhVLy7SnTZI`hFhTbKSEoFVpAzG0UgtfXHQFB%(vaFsyTw|zh1BR<>?+Ia;f4Tqk;{6p?3 z>B>7ON&lrIDxs8fsSIlZFZH8^`T$&SPh*rP|MY;k^|jD(pLa6d*GU`=+TR+ay;oVC zMPsBb_RNhdUG4`y3qE5u*y&dyNMqw#v0b-lTQPsg_q6)=vVZUYeW~?m$shmB-n`r$ zq}$)vA{6*S=nGR*(L6iw5Mf8TPgZLQ}huK0;ayR11cVrz)L^ zp$h>qee3c~`?^l>0Cj@f%dB+|@Crb}Vd2o})GnLZ%tjM84D;*BrlI+Q>6Lk(r}*GA zv6wu)G<~E)Q+%KVc=M*Fet8jk&G)mfon!T5^MQ!m^TM3hxgW;%%>US3Ea4K+C@Xn$ zOxOfg81q4&4aS zO9Gq(1gRfL5H5)T@D8;C5>_|{U^FB63}eZw3s~x2%6|mqUx$G5ISSCCcHzkJi!eeL zv|Ajh4L;-7{KtJ(Br7aHCge+q6ObfKoCc-VWI*x1e|4PCyzU0IJ>v)ZhCr(-A-X_% z!liotBtZD3Cer6H9MmieT3z`QY<@JIAE=DKtOxTEg>yk-1ON%lB|#4oPcDfYVD#7N z(lVGLw8-(YXSB-{=&&E4ylg%{kV~@-=D7)QK)z{R)`>4rpg&#qx+#SpnBussD+TM` zrgyG8fr}essi+GyzSE461az6f0mdjrD}#1s^8;*B$F*v~D{B7Z(NHjvG5t9|0KJqB z;DG#+yCh0LLgJF>0SPXUKqdi!iJ(dZjsabpec27H83^5UYw9%|U7BJJi(q3}Yp|!T zri_)I{6kNSKQ!V)nxAMR0D-<>w1emb7x-;N7pzEFbv+A9_L~#XCfb3WN(3*HFE4v; z3ijOQvgc31o`3mM>x8)j&X>`P%epH`z(c-HP%aak)WzSIb^kB`uRemK?jZ$sQRGst zAC&uk$&>^x0scz@4su`CkSjvJd0i5^icnLhb+N=VdLq>&!=OJ&Q5Un-)dO~1UJSWd zyd?Nx1(C{Gxc(MJetHur7>L0)L#^qDkD@szcrSC`&FfSz4MGGBTH`n`*NyoKhgO)y zJZK0;$t{?`CpZ7Qj(qUaFwC75TS5SC0Cy8_@MIjyO%9i|0rS&`Pory@>pQ5;+K#|) Pr2wS1u~zME=O_OQl5|kw literal 0 HcmV?d00001 diff --git a/packages/plugin-chat/src/index.ts b/packages/plugin-chat/src/index.ts index ff4f1c22..549b9ffb 100644 --- a/packages/plugin-chat/src/index.ts +++ b/packages/plugin-chat/src/index.ts @@ -205,16 +205,17 @@ class ChatPlugin implements JsPsychPlugin { } // Handles updates to system with the prompt and to the screen - addMessage(role, message, chatBox, continueButton?) { - const newMessage = document.createElement("div"); + async addMessage(role, message, chatBox, continueButton?) { // Handles logic of updating prompts and error checking switch (role) { case "user": this.updatePrompt(message, "user"); break; - case "chatbot": + case "chatbot": // make this appear a bit at a time this.updatePrompt(message, "assistant"); break; + case "prompt": // same with this + break; case "continue": if (!continueButton) { console.error("No continue button to display"); @@ -223,28 +224,52 @@ class ChatPlugin implements JsPsychPlugin { role = "prompt"; // use same style as promp continueButton.style.display = "block"; break; - case "prompt": - break; default: console.error("Incorrect role"); return; } - // Handles shared logic of printing to screen - newMessage.className = role + "-message"; - newMessage.innerHTML = message.replace(/\n/g, "
"); - chatBox.appendChild(newMessage); - chatBox.scrollTop = chatBox.scrollHeight; + await this.typeMessage(role, message, chatBox); + } + + private typeMessage(role, message, chatBox): Promise { + return new Promise((resolve) => { + const newMessage = document.createElement("div"); + newMessage.className = role + "-message"; + newMessage.innerHTML = ""; + chatBox.appendChild(newMessage); + + if (role === "continue" || role === "user" || role === "prompt") { + newMessage.innerHTML = message.replace(/\n/g, "
"); + chatBox.scrollTop = chatBox.scrollHeight; + resolve(); + } else { + const sentences = message.split(/(?<=[.!?])\s+/); // Split message into sentences + let index = 0; + + const interval = setInterval(() => { + if (index < sentences.length) { + newMessage.innerHTML += + (index > 0 ? " " : "") + sentences[index].replace(/\n/g, "
"); + chatBox.scrollTop = chatBox.scrollHeight; + index++; + } else { + clearInterval(interval); + resolve(); + } + }, 1000); // Adjust the interval time (in milliseconds) as needed + } + }); } async updateAndProcessGPT(chatBox) { try { const response = await this.fetchGPT(this.prompt); const responseContent = response.message.content; - this.addMessage("chatbot", responseContent, chatBox); + await this.addMessage("chatbot", responseContent, chatBox); return responseContent; } catch (error) { - this.addMessage("chatbot", "error fetching bot response", chatBox); + await this.addMessage("chatbot", "error fetching bot response", chatBox); } } From 7834e77304140cd3cf37411ca4e4ce198ba6a556 Mon Sep 17 00:00:00 2001 From: vzhang03 Date: Wed, 26 Jun 2024 12:18:07 -0400 Subject: [PATCH 23/62] Got streaming to work from backend to frontend --- backend/package-lock.json | 170 +++++++++++++++++++++++++++++- backend/package.json | 3 +- backend/server.js | 44 +++++--- packages/plugin-chat/src/index.ts | 30 +++++- 4 files changed, 227 insertions(+), 20 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index 09ad1e72..e3999c34 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -12,7 +12,36 @@ "axios": "^1.7.2", "cors": "^2.8.5", "dotenv": "^16.4.5", - "express": "^4.19.2" + "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": { @@ -28,6 +57,17 @@ "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", @@ -281,6 +321,14 @@ "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", @@ -375,6 +423,31 @@ "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", @@ -497,6 +570,14 @@ "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", @@ -596,6 +677,43 @@ "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", @@ -626,6 +744,24 @@ "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", @@ -829,6 +965,11 @@ "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", @@ -842,6 +983,11 @@ "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", @@ -868,6 +1014,28 @@ "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/backend/package.json b/backend/package.json index f2b50989..48557673 100644 --- a/backend/package.json +++ b/backend/package.json @@ -13,6 +13,7 @@ "axios": "^1.7.2", "cors": "^2.8.5", "dotenv": "^16.4.5", - "express": "^4.19.2" + "express": "^4.19.2", + "openai": "^4.52.1" } } diff --git a/backend/server.js b/backend/server.js index adf1022d..08b1715e 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,3 +1,4 @@ +const OpenAI = require("openai"); const express = require("express"); const axios = require("axios"); const bodyParser = require("body-parser"); @@ -7,6 +8,7 @@ require("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()); @@ -15,21 +17,33 @@ app.post("/api/chat", async (req, res) => { try { const { messages, ai_model } = req.body; - const response = await axios.post( - "https://api.openai.com/v1/chat/completions", - { - model: ai_model, - messages: messages, - }, - { - headers: { - Authorization: `Bearer ${OPENAI_API_KEY}`, - "Content-Type": "application/json", - }, - } - ); - - res.json(response.data.choices[0]); + 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(); + + // const response = await axios.post( + // "https://api.openai.com/v1/chat/completions", + // { + // model: ai_model, + // messages: messages, + // }, + // { + // headers: { + // Authorization: `Bearer ${OPENAI_API_KEY}`, + // "Content-Type": "application/json", + // }, + // } + // ); + + // res.json(response.data.choices[0]); } catch (error) { console.error(error); res.status(500).send("Error processing request"); diff --git a/packages/plugin-chat/src/index.ts b/packages/plugin-chat/src/index.ts index 549b9ffb..d221f3b5 100644 --- a/packages/plugin-chat/src/index.ts +++ b/packages/plugin-chat/src/index.ts @@ -1,4 +1,5 @@ import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; +import { ChatCompletionStream } from "openai/lib/ChatCompletionStream"; // thinking about using an enum to define // -> system, user, bot @@ -174,6 +175,12 @@ class ChatPlugin implements JsPsychPlugin { this.researcher_prompts.push(continue_button); } + private waitForFiveSeconds() { + return new Promise((resolve) => { + setTimeout(resolve, 5000); // Resolve the promise after 5 seconds + }); + } + // Call to backend async fetchGPT(messages) { try { @@ -189,8 +196,22 @@ class ChatPlugin implements JsPsychPlugin { throw new Error(`HTTP error! status: ${response.status}`); } - const data = await response.json(); - return data; + const runner = ChatCompletionStream.fromReadableStream(response.body); + console.log(runner); + + runner.on("content", (delta, snapshot) => { + console.log(delta); + // or, in a browser, you might display like this: + // document.body.innerText += delta; // or: + // document.body.innerText = snapshot; + }); + await this.waitForFiveSeconds(); + console.log(runner); + console.dir(await runner.finalChatCompletion(), { depth: null }); + + // const data = await response.json(); + // return data; + return ""; } catch (error) { console.error("Error fetching GPT data:", error); throw error; // Rethrow the error after logging it @@ -265,11 +286,14 @@ class ChatPlugin implements JsPsychPlugin { async updateAndProcessGPT(chatBox) { try { const response = await this.fetchGPT(this.prompt); - const responseContent = response.message.content; + console.log(response); + // const responseContent = response.message.content; + const responseContent = "filling while testing"; await this.addMessage("chatbot", responseContent, chatBox); return responseContent; } catch (error) { await this.addMessage("chatbot", "error fetching bot response", chatBox); + return "failing"; } } From b0d59f15c92e1b31f079e64a0f9b0c02f20d42d8 Mon Sep 17 00:00:00 2001 From: vzhang03 Date: Wed, 26 Jun 2024 14:05:53 -0400 Subject: [PATCH 24/62] Finishing streaming and added more styling, implementing more features for customizing the flow of conversation --- backend/server.js | 16 ----- packages/plugin-chat/example/example.html | 1 + packages/plugin-chat/src/index.ts | 78 +++++++---------------- 3 files changed, 24 insertions(+), 71 deletions(-) diff --git a/backend/server.js b/backend/server.js index 08b1715e..5777c290 100644 --- a/backend/server.js +++ b/backend/server.js @@ -28,22 +28,6 @@ app.post("/api/chat", async (req, res) => { res.write(chunk); } res.end(); - - // const response = await axios.post( - // "https://api.openai.com/v1/chat/completions", - // { - // model: ai_model, - // messages: messages, - // }, - // { - // headers: { - // Authorization: `Bearer ${OPENAI_API_KEY}`, - // "Content-Type": "application/json", - // }, - // } - // ); - - // res.json(response.data.choices[0]); } catch (error) { console.error(error); res.status(500).send("Error processing request"); diff --git a/packages/plugin-chat/example/example.html b/packages/plugin-chat/example/example.html index bd830b14..30366566 100644 --- a/packages/plugin-chat/example/example.html +++ b/packages/plugin-chat/example/example.html @@ -11,6 +11,7 @@ const jsPsych = initJsPsych({ on_finish: function() { jsPsych.data.displayData(); + // jsPsych.data.get().localSave('csv','mydata.csv'); }, }); diff --git a/packages/plugin-chat/src/index.ts b/packages/plugin-chat/src/index.ts index d221f3b5..fe69ec79 100644 --- a/packages/plugin-chat/src/index.ts +++ b/packages/plugin-chat/src/index.ts @@ -182,7 +182,7 @@ class ChatPlugin implements JsPsychPlugin { } // Call to backend - async fetchGPT(messages) { + async fetchGPT(messages, newMessage) { try { const response = await fetch("http://localhost:3000/api/chat", { method: "POST", @@ -197,21 +197,13 @@ class ChatPlugin implements JsPsychPlugin { } const runner = ChatCompletionStream.fromReadableStream(response.body); - console.log(runner); runner.on("content", (delta, snapshot) => { - console.log(delta); - // or, in a browser, you might display like this: - // document.body.innerText += delta; // or: - // document.body.innerText = snapshot; + newMessage.innerHTML = snapshot.replace(/\n/g, "
"); }); - await this.waitForFiveSeconds(); - console.log(runner); - console.dir(await runner.finalChatCompletion(), { depth: null }); - // const data = await response.json(); - // return data; - return ""; + 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 @@ -226,15 +218,16 @@ class ChatPlugin implements JsPsychPlugin { } // Handles updates to system with the prompt and to the screen - async addMessage(role, message, chatBox, continueButton?) { + addMessage(role, message, chatBox, continueButton?) { + const newMessage = document.createElement("div"); // Handles logic of updating prompts and error checking switch (role) { + case "chatbot": // writing to screen handled caller function + this.updatePrompt(message, "assistant"); + return; case "user": this.updatePrompt(message, "user"); break; - case "chatbot": // make this appear a bit at a time - this.updatePrompt(message, "assistant"); - break; case "prompt": // same with this break; case "continue": @@ -250,50 +243,25 @@ class ChatPlugin implements JsPsychPlugin { return; } - await this.typeMessage(role, message, chatBox); - } - - private typeMessage(role, message, chatBox): Promise { - return new Promise((resolve) => { - const newMessage = document.createElement("div"); - newMessage.className = role + "-message"; - newMessage.innerHTML = ""; - chatBox.appendChild(newMessage); - - if (role === "continue" || role === "user" || role === "prompt") { - newMessage.innerHTML = message.replace(/\n/g, "
"); - chatBox.scrollTop = chatBox.scrollHeight; - resolve(); - } else { - const sentences = message.split(/(?<=[.!?])\s+/); // Split message into sentences - let index = 0; - - const interval = setInterval(() => { - if (index < sentences.length) { - newMessage.innerHTML += - (index > 0 ? " " : "") + sentences[index].replace(/\n/g, "
"); - chatBox.scrollTop = chatBox.scrollHeight; - index++; - } else { - clearInterval(interval); - resolve(); - } - }, 1000); // Adjust the interval time (in milliseconds) as needed - } - }); + newMessage.className = role + "-message"; + newMessage.innerHTML = ""; + chatBox.appendChild(newMessage); + newMessage.innerHTML = message.replace(/\n/g, "
"); + chatBox.scrollTop = chatBox.scrollHeight; } async updateAndProcessGPT(chatBox) { + const newMessage = document.createElement("div"); + newMessage.className = "chatbot" + "-message"; + newMessage.innerHTML = ""; + chatBox.appendChild(newMessage); + try { - const response = await this.fetchGPT(this.prompt); - console.log(response); - // const responseContent = response.message.content; - const responseContent = "filling while testing"; - await this.addMessage("chatbot", responseContent, chatBox); - return responseContent; + const response = await this.fetchGPT(this.prompt, newMessage); + chatBox.scrollTop = chatBox.scrollHeight; + this.addMessage("chatbot", response, chatBox); // saves to prompt } catch (error) { - await this.addMessage("chatbot", "error fetching bot response", chatBox); - return "failing"; + this.addMessage("chatbot", "error fetching bot response", chatBox); } } From 454ff5593a18e8c6b265e69bd4a992e057c0757f Mon Sep 17 00:00:00 2001 From: vzhang03 Date: Wed, 26 Jun 2024 16:56:46 -0400 Subject: [PATCH 25/62] Finished inital version of prompt_chaining --- packages/plugin-chat/example/example.html | 12 ++- packages/plugin-chat/src/index.ts | 105 ++++++++++++++++++---- 2 files changed, 96 insertions(+), 21 deletions(-) diff --git a/packages/plugin-chat/example/example.html b/packages/plugin-chat/example/example.html index 30366566..e33d926f 100644 --- a/packages/plugin-chat/example/example.html +++ b/packages/plugin-chat/example/example.html @@ -18,14 +18,14 @@ // Declaring the prompts necessary prompts = [ { - prompt: "Welcome to this trial", + message: "Welcome to this trial", role: "prompt", message_trigger: 0, timer_trigger: 1000000, }, { - prompt: "YELL AT ME AND SAY SOMETHING MEAN", + message: "YELL AT ME AND SAY SOMETHING MEAN", role: "chatbot-fetch", message_trigger: 3, timer_trigger: 1000000, @@ -35,7 +35,12 @@ continue_button = { timer_trigger: 1000000, message_trigger: 1, - prompt: "The continue button will appear in the bottom right corner, please continue when you feel ready", + message: "The continue button will appear in the bottom right corner, please continue when you feel ready", + } + + prompt_chain = { + message_trigger: 1, + prompts: ["Summarize this response in Spanish", "Switch half of the sentences to English", "Add another paragraph onto the output explaining why you like bananas"], } const trial = { @@ -43,6 +48,7 @@ ai_prompt: "Pretend to be a comedian and include a joke at the end of every response", chat_field_placeholder: "be nice...", continue_button: continue_button, + prompt_chain: prompt_chain, additional_prompts: prompts, } diff --git a/packages/plugin-chat/src/index.ts b/packages/plugin-chat/src/index.ts index fe69ec79..8211e7be 100644 --- a/packages/plugin-chat/src/index.ts +++ b/packages/plugin-chat/src/index.ts @@ -1,5 +1,7 @@ import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; +import { deepCopy } from "jspsych/dist/modules/utils"; import { ChatCompletionStream } from "openai/lib/ChatCompletionStream"; +import { MessagesPage } from "openai/resources/beta/threads/messages"; // thinking about using an enum to define // -> system, user, bot @@ -31,7 +33,7 @@ const info = { message_trigger: { type: ParameterType.INT, }, - prompt: { + message: { type: ParameterType.STRING, }, }, @@ -39,11 +41,10 @@ const info = { additional_prompts: { type: ParameterType.COMPLEX, array: true, - pretty_name: "Additional Prompts", default: undefined, nested: { // at one point used nested others used parameters - prompt: { + message: { // prompt to pass into type: ParameterType.STRING, default: "", @@ -63,6 +64,25 @@ const info = { }, }, }, + prompt_chain: { + type: ParameterType.COMPLEX, + default: undefined, + nested: { + prompts: { + type: ParameterType.STRING, + array: true, + default: [], + }, + message_trigger: { + type: ParameterType.INT, + default: undefined, + }, + timer_trigger: { + type: ParameterType.INT, + default: 10000000, + }, + }, + }, }, }; @@ -80,6 +100,7 @@ class ChatPlugin implements JsPsychPlugin { static info = info; private prompt: {}[]; // keeps track of prompt to send to GPT private researcher_prompts: {}[]; // keeps track of researcher's prompts that need to be displayed + private prompt_chain: {}; 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 @@ -118,7 +139,13 @@ class ChatPlugin implements JsPsychPlugin { const sendMessage = async () => { const message = userInput.value.trim(); - if (message !== "") { + if (this.chainCondition() && message !== "") { + this.addMessage("user", message, chatBox); + userInput.value = ""; + await this.chainPrompts(message, chatBox); + this.messages_sent += 1; + this.checkResearcherPrompts(chatBox, continueButton); + } else if (message !== "") { this.addMessage("user", message, chatBox); userInput.value = ""; @@ -173,16 +200,12 @@ class ChatPlugin implements JsPsychPlugin { } continue_button["role"] = "continue"; this.researcher_prompts.push(continue_button); - } - private waitForFiveSeconds() { - return new Promise((resolve) => { - setTimeout(resolve, 5000); // Resolve the promise after 5 seconds - }); + this.prompt_chain = trial.prompt_chain; } // Call to backend - async fetchGPT(messages, newMessage) { + async fetchGPT(messages, newMessage?) { try { const response = await fetch("http://localhost:3000/api/chat", { method: "POST", @@ -198,9 +221,12 @@ class ChatPlugin implements JsPsychPlugin { const runner = ChatCompletionStream.fromReadableStream(response.body); - runner.on("content", (delta, snapshot) => { - newMessage.innerHTML = snapshot.replace(/\n/g, "
"); - }); + if (newMessage) { + // prints to screen if specified, otherwise only fetch + runner.on("content", (delta, snapshot) => { + newMessage.innerHTML = snapshot.replace(/\n/g, "
"); + }); + } await runner.finalChatCompletion(); // waits before returning the actual content return runner["messages"][0]["content"]; @@ -250,18 +276,25 @@ class ChatPlugin implements JsPsychPlugin { chatBox.scrollTop = chatBox.scrollHeight; } - async updateAndProcessGPT(chatBox) { + // updates and processes to the screen + async updateAndProcessGPT(chatBox, prompt?) { const newMessage = document.createElement("div"); newMessage.className = "chatbot" + "-message"; newMessage.innerHTML = ""; chatBox.appendChild(newMessage); try { - const response = await this.fetchGPT(this.prompt, newMessage); + var response = undefined; + if (prompt) response = await this.fetchGPT(prompt, newMessage); + // special case when wanting to prompt with own thing + else response = await this.fetchGPT(this.prompt, newMessage); + chatBox.scrollTop = chatBox.scrollHeight; this.addMessage("chatbot", response, chatBox); // saves to prompt + return response; } catch (error) { this.addMessage("chatbot", "error fetching bot response", chatBox); + return "error fetching response"; } } @@ -277,14 +310,14 @@ class ChatPlugin implements JsPsychPlugin { switch (researcher_prompt["role"]) { case "chatbot": case "prompt": // want these cases to have the same functionality - this.addMessage(researcher_prompt["role"], researcher_prompt["prompt"], chatBox); + this.addMessage(researcher_prompt["role"], researcher_prompt["message"], chatBox); break; case "chatbot-fetch": - this.addMessage("user", researcher_prompt["prompt"], chatBox); + this.addMessage("user", researcher_prompt["message"], chatBox); this.updateAndProcessGPT(chatBox); break; case "continue": - this.addMessage("continue", researcher_prompt["prompt"], chatBox, continueButton); + this.addMessage("continue", researcher_prompt["message"], chatBox, continueButton); break; default: console.error("Incorrect role for prompting"); @@ -295,6 +328,42 @@ class ChatPlugin implements JsPsychPlugin { return true; // Keep this item in the array }); } + + private chainCondition() { + const time_elapsed = performance.now() - this.timer_start; // could instead keep subtracting from time_elapsed + if ( + this.messages_sent >= this.prompt_chain["message_trigger"] || + time_elapsed >= this.prompt_chain["timer_trigger"] + ) { + return true; + } else return false; + } + + private async chainPrompts(message, chatBox) { + for (let i = 0; i < this.prompt_chain["prompts"].length; i++) { + var temp_prompt = []; + + const prompt = this.prompt_chain["prompts"][i]; + console.log("prompt", prompt); + const new_sys = { + role: "system", + content: prompt, + }; + temp_prompt.push(new_sys); + + const user_message = { + role: "user", + content: message, + }; + temp_prompt.push(user_message); + + if (i === this.prompt_chain["prompts"].length - 1) { + await this.updateAndProcessGPT(chatBox, temp_prompt); + } else message = await this.fetchGPT(temp_prompt); // Ensure to await if fetchGPT is asynchronous + + console.log("asssistant message:", message); + } + } } export default ChatPlugin; From 5380a64a369ce528d43b405983beb9abfe43fb09 Mon Sep 17 00:00:00 2001 From: vzhang03 Date: Wed, 26 Jun 2024 17:10:22 -0400 Subject: [PATCH 26/62] Fixed error that requires prompt_chain parameter --- packages/plugin-chat/example/example.html | 2 +- packages/plugin-chat/src/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/plugin-chat/example/example.html b/packages/plugin-chat/example/example.html index e33d926f..4227498c 100644 --- a/packages/plugin-chat/example/example.html +++ b/packages/plugin-chat/example/example.html @@ -48,7 +48,7 @@ ai_prompt: "Pretend to be a comedian and include a joke at the end of every response", chat_field_placeholder: "be nice...", continue_button: continue_button, - prompt_chain: prompt_chain, + // prompt_chain: prompt_chain, additional_prompts: prompts, } diff --git a/packages/plugin-chat/src/index.ts b/packages/plugin-chat/src/index.ts index 8211e7be..b60fd48b 100644 --- a/packages/plugin-chat/src/index.ts +++ b/packages/plugin-chat/src/index.ts @@ -139,7 +139,7 @@ class ChatPlugin implements JsPsychPlugin { const sendMessage = async () => { const message = userInput.value.trim(); - if (this.chainCondition() && message !== "") { + if (message !== "" && this.prompt_chain && this.chainCondition()) { this.addMessage("user", message, chatBox); userInput.value = ""; await this.chainPrompts(message, chatBox); From 7b6a03b374dd6610da430910231417e4e22d6fe7 Mon Sep 17 00:00:00 2001 From: vzhang03 Date: Thu, 27 Jun 2024 09:35:39 -0400 Subject: [PATCH 27/62] Fixed errors crashing code when there wasn't chain of prompts defined --- packages/plugin-chat/example/example.html | 2 +- packages/plugin-chat/src/index.ts | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/plugin-chat/example/example.html b/packages/plugin-chat/example/example.html index 4227498c..e33d926f 100644 --- a/packages/plugin-chat/example/example.html +++ b/packages/plugin-chat/example/example.html @@ -48,7 +48,7 @@ ai_prompt: "Pretend to be a comedian and include a joke at the end of every response", chat_field_placeholder: "be nice...", continue_button: continue_button, - // prompt_chain: prompt_chain, + prompt_chain: prompt_chain, additional_prompts: prompts, } diff --git a/packages/plugin-chat/src/index.ts b/packages/plugin-chat/src/index.ts index b60fd48b..b8b694f4 100644 --- a/packages/plugin-chat/src/index.ts +++ b/packages/plugin-chat/src/index.ts @@ -1,7 +1,5 @@ import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; -import { deepCopy } from "jspsych/dist/modules/utils"; import { ChatCompletionStream } from "openai/lib/ChatCompletionStream"; -import { MessagesPage } from "openai/resources/beta/threads/messages"; // thinking about using an enum to define // -> system, user, bot From cefe7e814a535cf36ddfdcebeea4cef980e5a74a Mon Sep 17 00:00:00 2001 From: vzhang03 Date: Thu, 27 Jun 2024 10:27:35 -0400 Subject: [PATCH 28/62] Fixing bot message and making print statements more informative --- packages/plugin-chat/example/example.html | 6 +++++- packages/plugin-chat/src/index.ts | 14 +++++++++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/plugin-chat/example/example.html b/packages/plugin-chat/example/example.html index e33d926f..5123af40 100644 --- a/packages/plugin-chat/example/example.html +++ b/packages/plugin-chat/example/example.html @@ -23,7 +23,11 @@ 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: "YELL AT ME AND SAY SOMETHING MEAN", role: "chatbot-fetch", diff --git a/packages/plugin-chat/src/index.ts b/packages/plugin-chat/src/index.ts index b8b694f4..eae1d789 100644 --- a/packages/plugin-chat/src/index.ts +++ b/packages/plugin-chat/src/index.ts @@ -48,7 +48,7 @@ const info = { default: "", }, role: { - // "prompt", "chatbot", "chatbot-fetch" + // "prompt", "chatbot-message", "chatbot-fetch" type: ParameterType.STRING, default: "prompt", }, @@ -252,6 +252,9 @@ class ChatPlugin implements JsPsychPlugin { case "user": this.updatePrompt(message, "user"); break; + case "chatbot-message": + role = "chatbot"; + break; case "prompt": // same with this break; case "continue": @@ -306,11 +309,11 @@ class ChatPlugin implements JsPsychPlugin { ) { // Checking with prompt to trigger switch (researcher_prompt["role"]) { - case "chatbot": + case "chatbot-message": case "prompt": // want these cases to have the same functionality this.addMessage(researcher_prompt["role"], researcher_prompt["message"], chatBox); break; - case "chatbot-fetch": + case "chatbot-fetch": // want to phase it out this.addMessage("user", researcher_prompt["message"], chatBox); this.updateAndProcessGPT(chatBox); break; @@ -342,7 +345,6 @@ class ChatPlugin implements JsPsychPlugin { var temp_prompt = []; const prompt = this.prompt_chain["prompts"][i]; - console.log("prompt", prompt); const new_sys = { role: "system", content: prompt, @@ -355,11 +357,13 @@ class ChatPlugin implements JsPsychPlugin { }; temp_prompt.push(user_message); + console.log("current prompt and input:", temp_prompt); + if (i === this.prompt_chain["prompts"].length - 1) { await this.updateAndProcessGPT(chatBox, temp_prompt); } else message = await this.fetchGPT(temp_prompt); // Ensure to await if fetchGPT is asynchronous - console.log("asssistant message:", message); + console.log("assistant message:", message); } } } From 7f230cb6df36068fd260149d8786e36c6dc2463b Mon Sep 17 00:00:00 2001 From: vzhang03 Date: Thu, 27 Jun 2024 11:43:41 -0400 Subject: [PATCH 29/62] Adjusting naming (system-prompt instead of prompt), deleted chatbot-fetch, fixed error checking for null value message/timer trigger, implemented more comprehensive null checking, implemented dynamic prompting in addition to chaining that allow to change gpt prompts --- packages/plugin-chat/example/example.html | 13 +- packages/plugin-chat/example/styles.css | 4 +- packages/plugin-chat/src/index.ts | 140 ++++++++++++++-------- 3 files changed, 96 insertions(+), 61 deletions(-) diff --git a/packages/plugin-chat/example/example.html b/packages/plugin-chat/example/example.html index 5123af40..4518f60b 100644 --- a/packages/plugin-chat/example/example.html +++ b/packages/plugin-chat/example/example.html @@ -19,7 +19,7 @@ prompts = [ { message: "Welcome to this trial", - role: "prompt", + role: "system-prompt", message_trigger: 0, timer_trigger: 1000000, }, @@ -29,10 +29,10 @@ message_trigger: 0 }, { - message: "YELL AT ME AND SAY SOMETHING MEAN", - role: "chatbot-fetch", - message_trigger: 3, - timer_trigger: 1000000, + message: "This is new prompt - do this now", + prompt: "Stop telling jokes and act very serious. Do not make any more jokes.", + role: "chatbot-prompt", + message_trigger: 2, } ] @@ -43,7 +43,8 @@ } prompt_chain = { - message_trigger: 1, + message_trigger: 100, + // timer_trigger: 1, prompts: ["Summarize this response in Spanish", "Switch half of the sentences to English", "Add another paragraph onto the output explaining why you like bananas"], } diff --git a/packages/plugin-chat/example/styles.css b/packages/plugin-chat/example/styles.css index 800a70c5..0f01feb9 100644 --- a/packages/plugin-chat/example/styles.css +++ b/packages/plugin-chat/example/styles.css @@ -152,7 +152,7 @@ html, body { background-color: #e28043; /* Darker shade on hover */ } -.user-message, .chatbot-message, .prompt-message { +.user-message, .chatbot-message, .system-prompt-message { padding: 10px; margin: 5px; margin-top: 15px; @@ -179,7 +179,7 @@ html, body { color: black; } -.prompt-message { +.system-prompt-message { background-color: #faf39d; color: #6B503F; } diff --git a/packages/plugin-chat/src/index.ts b/packages/plugin-chat/src/index.ts index eae1d789..82ba2fd1 100644 --- a/packages/plugin-chat/src/index.ts +++ b/packages/plugin-chat/src/index.ts @@ -1,9 +1,11 @@ import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; import { ChatCompletionStream } from "openai/lib/ChatCompletionStream"; -// thinking about using an enum to define -// -> system, user, bot -// naming convention a little weird because of chatGPT calling models "assistants" +// CHANGES FOR IAN +// system-prompt instead of prompt for the prompts that want to display in yellow +// deleted chatbot-fetch +// fixed error checking for null values message/timer_trigger, implemented null checking within trigger methods that check prompts +// implementing dynamic prompting const info = { name: "chat", @@ -41,27 +43,32 @@ const info = { array: true, default: undefined, nested: { - // at one point used nested others used parameters message: { - // prompt to pass into + // messages to display on screen type: ParameterType.STRING, default: "", }, + prompt: { + // prompting to pass in + type: ParameterType.STRING, + default: null, + }, role: { - // "prompt", "chatbot-message", "chatbot-fetch" + // "prompt" ("system-prompt"), "chatbot-message","chatbot-prompt" type: ParameterType.STRING, - default: "prompt", + default: "system-prompt", }, message_trigger: { type: ParameterType.INT, - default: undefined, + default: null, }, timer_trigger: { type: ParameterType.INT, - default: 1000000, + default: null, }, }, }, + // when triggers it doesn't stop, do we want to give it a stop? prompt_chain: { type: ParameterType.COMPLEX, default: undefined, @@ -73,11 +80,11 @@ const info = { }, message_trigger: { type: ParameterType.INT, - default: undefined, + default: null, }, timer_trigger: { type: ParameterType.INT, - default: 10000000, + default: null, }, }, }, @@ -136,22 +143,18 @@ class ChatPlugin implements JsPsychPlugin { // Function to handle logic of sending user message, and data collection const sendMessage = async () => { const message = userInput.value.trim(); + this.addMessage("user", message, chatBox); + userInput.value = ""; + // prompt chaining or simple requests if (message !== "" && this.prompt_chain && this.chainCondition()) { - this.addMessage("user", message, chatBox); - userInput.value = ""; await this.chainPrompts(message, chatBox); - this.messages_sent += 1; - this.checkResearcherPrompts(chatBox, continueButton); } else if (message !== "") { - this.addMessage("user", message, chatBox); - userInput.value = ""; - await this.updateAndProcessGPT(chatBox); - // inc messages and check researcher prompts - this.messages_sent += 1; - this.checkResearcherPrompts(chatBox, continueButton); } + // inc messages and check researcher prompts + this.messages_sent += 1; + this.checkResearcherPrompts(chatBox, continueButton); }; // Event listener for send button click @@ -185,24 +188,37 @@ class ChatPlugin implements JsPsychPlugin { this.updatePrompt(trial.ai_prompt, "system"); // sets researcher prompts and removes any that can't trigger this.researcher_prompts = trial.additional_prompts.filter((researcher_prompt) => { - if (!("message_trigger" in researcher_prompt) && !("timer_trigger" in 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 (!("message_trigger" in continue_button) && !("timer_trigger" in continue_button)) { + if (continue_button["message_trigger"] === null && continue_button["timer_trigger"] === null) { console.error("Missing required property in continue prompt, will never display"); + } else { + continue_button["role"] = "continue"; + this.researcher_prompts.push(continue_button); } - continue_button["role"] = "continue"; - this.researcher_prompts.push(continue_button); - this.prompt_chain = trial.prompt_chain; + // sets prompt chain and removes any that can't trigger + if ( + trial.prompt_chain["message_trigger"] === null && + trial.prompt_chain["timer_trigger"] === null + ) { + console.error("Missing required property in prompt_chain, will never trigger"); + } else { + this.prompt_chain = trial.prompt_chain; + } } - // Call to backend + // Call to backend, newMessage is the document item to print (optional because when chaining don't want them to display) async fetchGPT(messages, newMessage?) { try { const response = await fetch("http://localhost:3000/api/chat", { @@ -242,7 +258,7 @@ class ChatPlugin implements JsPsychPlugin { } // Handles updates to system with the prompt and to the screen - addMessage(role, message, chatBox, continueButton?) { + addMessage(role, message, chatBox) { const newMessage = document.createElement("div"); // Handles logic of updating prompts and error checking switch (role) { @@ -252,18 +268,10 @@ class ChatPlugin implements JsPsychPlugin { case "user": this.updatePrompt(message, "user"); break; - case "chatbot-message": + case "chatbot-message": // set by researcher, needs be seperate case because doesn't update prompts role = "chatbot"; break; - case "prompt": // same with this - break; - case "continue": - if (!continueButton) { - console.error("No continue button to display"); - return; - } - role = "prompt"; // use same style as promp - continueButton.style.display = "block"; + case "system-prompt": // set by researcher break; default: console.error("Incorrect role"); @@ -277,7 +285,7 @@ class ChatPlugin implements JsPsychPlugin { chatBox.scrollTop = chatBox.scrollHeight; } - // updates and processes to the screen + // 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"; @@ -286,8 +294,12 @@ class ChatPlugin implements JsPsychPlugin { try { var response = undefined; - if (prompt) response = await this.fetchGPT(prompt, newMessage); - // special case when wanting to prompt with own thing + // allows to pass in non defined prompts + if (prompt) + response = await this.fetchGPT( + prompt, + newMessage + ); // special case when wanting to prompt with own thing else response = await this.fetchGPT(this.prompt, newMessage); chatBox.scrollTop = chatBox.scrollHeight; @@ -301,24 +313,43 @@ class ChatPlugin implements JsPsychPlugin { checkResearcherPrompts(chatBox, continueButton): void { this.researcher_prompts = this.researcher_prompts.filter((researcher_prompt) => { - // Checking conditions to trigger the 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 ( - this.messages_sent >= researcher_prompt["message_trigger"] || - time_elapsed >= researcher_prompt["timer_trigger"] + (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 "prompt": // want these cases to have the same functionality + 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-fetch": // want to phase it out - this.addMessage("user", researcher_prompt["message"], chatBox); - this.updateAndProcessGPT(chatBox); + case "chatbot-prompt": // checkes 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.updatePrompt(prompt, "system"); + } else + console.error( + researcher_prompt, + "is missing prompt field or it isn't in the correct format" + ); + + if (message !== null && typeof prompt === "string") { + this.addMessage("system-prompt", message, chatBox); + } break; - case "continue": - this.addMessage("continue", researcher_prompt["message"], chatBox, continueButton); + 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"; + this.addMessage("system-prompt", researcher_prompt["message"], chatBox); break; default: console.error("Incorrect role for prompting"); @@ -332,9 +363,12 @@ class ChatPlugin implements JsPsychPlugin { private chainCondition() { const time_elapsed = performance.now() - this.timer_start; // could instead keep subtracting from time_elapsed + const message_trigger = this.prompt_chain["message_trigger"]; + const timer_trigger = this.prompt_chain["timer_trigger"]; + if ( - this.messages_sent >= this.prompt_chain["message_trigger"] || - time_elapsed >= this.prompt_chain["timer_trigger"] + (message_trigger !== null && this.messages_sent >= message_trigger) || + (timer_trigger !== null && time_elapsed >= timer_trigger) ) { return true; } else return false; From 64766ecb799265e0414ed40b43a2cc5b109e0cd7 Mon Sep 17 00:00:00 2001 From: Bankminer78 Date: Thu, 27 Jun 2024 11:47:49 -0400 Subject: [PATCH 30/62] Added functionality to display wait message above loading graphics during data save --- packages/plugin-pipe/src/index.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) 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} `; From 8ca96c8aa25a9beea11f1c84d12add62333ff3ad Mon Sep 17 00:00:00 2001 From: vzhang03 Date: Thu, 27 Jun 2024 12:10:02 -0400 Subject: [PATCH 31/62] Message bubble styling, and small bug fixes and tests --- packages/plugin-chat/example/styles.css | 35 +++++++++++++++++++++++-- packages/plugin-chat/src/index.ts | 16 ++++++----- 2 files changed, 43 insertions(+), 8 deletions(-) diff --git a/packages/plugin-chat/example/styles.css b/packages/plugin-chat/example/styles.css index 0f01feb9..b79f56ad 100644 --- a/packages/plugin-chat/example/styles.css +++ b/packages/plugin-chat/example/styles.css @@ -153,10 +153,12 @@ html, body { } .user-message, .chatbot-message, .system-prompt-message { - padding: 10px; + padding: 10px 15px; margin: 5px; margin-top: 15px; - border-radius: 10px; + border-radius: 15px; + position: relative; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); } .user-message, .chatbot-message { @@ -183,3 +185,32 @@ html, body { background-color: #faf39d; color: #6B503F; } + +/* 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; +} + +/* Tail for user message */ +.user-message::after { + right: -10px; + bottom: 1px; /* Adjust this value to align the tail */ + border-left-color: #4CAF50; + border-right: 0; + border-bottom: 0; + border-top: 20px solid transparent; +} + +/* Tail for chatbot message */ +.chatbot-message::after { + left: -10px; + bottom: 1px; /* Adjust this value to align the tail */ + border-right-color: #d1e7dd; + border-left: 0; + border-bottom: 0; +} \ No newline at end of file diff --git a/packages/plugin-chat/src/index.ts b/packages/plugin-chat/src/index.ts index 82ba2fd1..8fbf74e9 100644 --- a/packages/plugin-chat/src/index.ts +++ b/packages/plugin-chat/src/index.ts @@ -6,6 +6,7 @@ import { ChatCompletionStream } from "openai/lib/ChatCompletionStream"; // deleted chatbot-fetch // fixed error checking for null values message/timer_trigger, implemented null checking within trigger methods that check prompts // implementing dynamic prompting +// bot naming feature const info = { name: "chat", @@ -23,6 +24,10 @@ const info = { type: ParameterType.STRING, default: "Type your message here...", }, + bot_name: { + type: ParameterType.STRING, + default: "Adora-bot", + }, continue_button: { type: ParameterType.COMPLEX, default: {}, @@ -118,7 +123,9 @@ class ChatPlugin implements JsPsychPlugin { var html = `
-

Adorabot

+

` + + trial.bot_name + + `

@@ -295,11 +302,8 @@ class ChatPlugin implements JsPsychPlugin { try { var response = undefined; // allows to pass in non defined prompts - if (prompt) - response = await this.fetchGPT( - prompt, - newMessage - ); // special case when wanting to prompt with own thing + if (prompt) response = await this.fetchGPT(prompt, newMessage); + // special case when wanting to prompt with own thing else response = await this.fetchGPT(this.prompt, newMessage); chatBox.scrollTop = chatBox.scrollHeight; From 58b72001e8f9c05b4cb7cf96b4055d2dd96d7393 Mon Sep 17 00:00:00 2001 From: vzhang03 Date: Thu, 27 Jun 2024 15:46:05 -0400 Subject: [PATCH 32/62] Fixing errors with missing parameters that aren't required, making errors more descriptive --- packages/plugin-chat/example/example.html | 6 ++--- packages/plugin-chat/src/index.ts | 28 +++++++++++++---------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/packages/plugin-chat/example/example.html b/packages/plugin-chat/example/example.html index 4518f60b..3cb2ee06 100644 --- a/packages/plugin-chat/example/example.html +++ b/packages/plugin-chat/example/example.html @@ -52,9 +52,9 @@ type: jsPsychChat, ai_prompt: "Pretend to be a comedian and include a joke at the end of every response", chat_field_placeholder: "be nice...", - continue_button: continue_button, - prompt_chain: prompt_chain, - additional_prompts: prompts, + // continue_button: continue_button, + // prompt_chain: prompt_chain, + // additional_prompts: prompts, } jsPsych.run([trial]); diff --git a/packages/plugin-chat/src/index.ts b/packages/plugin-chat/src/index.ts index 8fbf74e9..7ad6e3c1 100644 --- a/packages/plugin-chat/src/index.ts +++ b/packages/plugin-chat/src/index.ts @@ -30,7 +30,7 @@ const info = { }, continue_button: { type: ParameterType.COMPLEX, - default: {}, + default: { message_trigger: 0 }, nested: { timer_trigger: { type: ParameterType.INT, @@ -194,16 +194,18 @@ class ChatPlugin implements JsPsychPlugin { this.prompt = []; this.updatePrompt(trial.ai_prompt, "system"); // sets researcher prompts and removes any that can't trigger - this.researcher_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; - }); + 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; @@ -216,6 +218,7 @@ class ChatPlugin implements JsPsychPlugin { // 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 ) { @@ -310,7 +313,7 @@ class ChatPlugin implements JsPsychPlugin { this.addMessage("chatbot", response, chatBox); // saves to prompt return response; } catch (error) { - this.addMessage("chatbot", "error fetching bot response", chatBox); + newMessage.innerHTML = "error fetching bot response"; return "error fetching response"; } } @@ -353,6 +356,7 @@ class ChatPlugin implements JsPsychPlugin { return false; } continueButton.style.display = "block"; + // implement check here this.addMessage("system-prompt", researcher_prompt["message"], chatBox); break; default: From 2a03ea9b00774a14f58423dac096649de912671d Mon Sep 17 00:00:00 2001 From: vzhang03 Date: Thu, 27 Jun 2024 16:46:53 -0400 Subject: [PATCH 33/62] Adjusting the chain prompting to keep track of conversations --- packages/plugin-chat/example/example.html | 20 +++++++-------- packages/plugin-chat/src/index.ts | 31 +++++++++++++++++++---- 2 files changed, 36 insertions(+), 15 deletions(-) diff --git a/packages/plugin-chat/example/example.html b/packages/plugin-chat/example/example.html index 3cb2ee06..d0843526 100644 --- a/packages/plugin-chat/example/example.html +++ b/packages/plugin-chat/example/example.html @@ -28,12 +28,12 @@ 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.", - role: "chatbot-prompt", - message_trigger: 2, - } + // { + // message: "This is new prompt - do this now", + // prompt: "Stop telling jokes and act very serious. Do not make any more jokes.", + // role: "chatbot-prompt", + // message_trigger: 2, + // } ] continue_button = { @@ -43,7 +43,7 @@ } prompt_chain = { - message_trigger: 100, + message_trigger: 1, // timer_trigger: 1, prompts: ["Summarize this response in Spanish", "Switch half of the sentences to English", "Add another paragraph onto the output explaining why you like bananas"], } @@ -52,9 +52,9 @@ type: jsPsychChat, ai_prompt: "Pretend to be a comedian and include a joke at the end of every response", chat_field_placeholder: "be nice...", - // continue_button: continue_button, - // prompt_chain: prompt_chain, - // additional_prompts: prompts, + continue_button: continue_button, + prompt_chain: prompt_chain, + additional_prompts: prompts, } jsPsych.run([trial]); diff --git a/packages/plugin-chat/src/index.ts b/packages/plugin-chat/src/index.ts index 7ad6e3c1..f07bab26 100644 --- a/packages/plugin-chat/src/index.ts +++ b/packages/plugin-chat/src/index.ts @@ -383,9 +383,10 @@ class ChatPlugin implements JsPsychPlugin { } private async chainPrompts(message, chatBox) { - for (let i = 0; i < this.prompt_chain["prompts"].length; i++) { - var temp_prompt = []; + const cleaned_prompt = this.cleanConversation(); + for (let i = 0; i < this.prompt_chain["prompts"].length; i++) { + const temp_prompt = [...cleaned_prompt]; const prompt = this.prompt_chain["prompts"][i]; const new_sys = { role: "system", @@ -399,15 +400,35 @@ class ChatPlugin implements JsPsychPlugin { }; temp_prompt.push(user_message); - console.log("current prompt and input:", temp_prompt); - if (i === this.prompt_chain["prompts"].length - 1) { await this.updateAndProcessGPT(chatBox, temp_prompt); } else message = await this.fetchGPT(temp_prompt); // Ensure to await if fetchGPT is asynchronous - console.log("assistant message:", message); + console.log("current prompt and input:", temp_prompt); + + // temp_prompt.splice(temp_prompt.length - 2, 1); + + // const assistant_response = { + // role: "assistant", + // content: message, + // }; } } + + private cleanConversation(): {}[] { + const res = this.prompt.filter((message: any, index: number, array: any[]) => { + if ("role" in message && message["role"] === "system") { + return false; + } + // Exclude the last message + if (index === array.length - 1) { + return false; + } + return true; + }); + + return res; + } } export default ChatPlugin; From 21eac68c0aa2c98363984c638bf2495e8ba282e4 Mon Sep 17 00:00:00 2001 From: vzhang03 Date: Thu, 27 Jun 2024 16:58:20 -0400 Subject: [PATCH 34/62] Allow for multiple system prompts to overwrite the others, need to work on representation within the final data --- packages/plugin-chat/example/example.html | 14 +++++++------- packages/plugin-chat/src/index.ts | 18 +++++++++++------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/packages/plugin-chat/example/example.html b/packages/plugin-chat/example/example.html index d0843526..594c6ea2 100644 --- a/packages/plugin-chat/example/example.html +++ b/packages/plugin-chat/example/example.html @@ -28,12 +28,12 @@ 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.", - // role: "chatbot-prompt", - // message_trigger: 2, - // } + { + message: "This is new prompt - do this now", + prompt: "Stop telling jokes and act very serious. Do not make any more jokes.", + role: "chatbot-prompt", + message_trigger: 2, + } ] continue_button = { @@ -53,7 +53,7 @@ ai_prompt: "Pretend to be a comedian and include a joke at the end of every response", chat_field_placeholder: "be nice...", continue_button: continue_button, - prompt_chain: prompt_chain, + // prompt_chain: prompt_chain, additional_prompts: prompts, } diff --git a/packages/plugin-chat/src/index.ts b/packages/plugin-chat/src/index.ts index f07bab26..18ca5dfe 100644 --- a/packages/plugin-chat/src/index.ts +++ b/packages/plugin-chat/src/index.ts @@ -339,6 +339,7 @@ class ChatPlugin implements JsPsychPlugin { const message = researcher_prompt["message"]; if (prompt !== null && typeof prompt === "string") { + this.prompt = this.cleanConversation(); this.updatePrompt(prompt, "system"); } else console.error( @@ -405,13 +406,6 @@ class ChatPlugin implements JsPsychPlugin { } else message = await this.fetchGPT(temp_prompt); // Ensure to await if fetchGPT is asynchronous console.log("current prompt and input:", temp_prompt); - - // temp_prompt.splice(temp_prompt.length - 2, 1); - - // const assistant_response = { - // role: "assistant", - // content: message, - // }; } } @@ -429,6 +423,16 @@ class ChatPlugin implements JsPsychPlugin { return res; } + + private cleanPrompt() { + for (let i = 0; i < this.prompt.length; i++) { + const message = this.prompt[i]; + + if (message["role"] === "system") { + delete message["role"]; + } + } + } } export default ChatPlugin; From f810420af5088ebe6536960e59d6e7d3f57aa2d2 Mon Sep 17 00:00:00 2001 From: vzhang03 Date: Fri, 28 Jun 2024 11:36:48 -0400 Subject: [PATCH 35/62] Finishing the initial key log functionality, building out into seperate classes --- packages/plugin-chat/src/index.ts | 43 +++++++++++++++---------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/packages/plugin-chat/src/index.ts b/packages/plugin-chat/src/index.ts index 18ca5dfe..b0b05257 100644 --- a/packages/plugin-chat/src/index.ts +++ b/packages/plugin-chat/src/index.ts @@ -1,12 +1,8 @@ import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; import { ChatCompletionStream } from "openai/lib/ChatCompletionStream"; -// CHANGES FOR IAN -// system-prompt instead of prompt for the prompts that want to display in yellow -// deleted chatbot-fetch -// fixed error checking for null values message/timer_trigger, implemented null checking within trigger methods that check prompts -// implementing dynamic prompting -// bot naming feature +// NEED TO FIX +// if prompting bot but not message shoudl not pop up with message const info = { name: "chat", @@ -145,12 +141,14 @@ class ChatPlugin implements JsPsychPlugin { 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); + this.addMessage("user", message, chatBox, keyPressLog); + keyPressLog = []; userInput.value = ""; // prompt chaining or simple requests @@ -177,8 +175,19 @@ class ChatPlugin implements JsPsychPlugin { } }); + // Function to log all keypresses + const logKeypress = (event) => { + console.log(`Key pressed: ${event.key}`); + keyPressLog.push(event.key); + }; + + // Event listener for all keypresses on userInput + userInput.addEventListener("keydown", logKeypress); + continueButton.addEventListener("click", () => { - this.jsPsych.finishTrial({ chatLogs: this.prompt }); + this.jsPsych.finishTrial({ + chatLogs: this.prompt, + }); }); // Setting up Trial @@ -261,14 +270,14 @@ class ChatPlugin implements JsPsychPlugin { } // updates prompts behind the scenes when we add messages to the screen - private updatePrompt(message, role): void { + private updatePrompt(message, role, keyPressLog?): void { const time = Math.round(performance.now()); - const newMessage = { role: role, content: message, time: time }; + const newMessage = { role: role, content: message, time: time, keyPressLog: keyPressLog }; this.prompt.push(newMessage); } // Handles updates to system with the prompt and to the screen - addMessage(role, message, chatBox) { + addMessage(role, message, chatBox, keyPressLog?) { const newMessage = document.createElement("div"); // Handles logic of updating prompts and error checking switch (role) { @@ -276,7 +285,7 @@ class ChatPlugin implements JsPsychPlugin { this.updatePrompt(message, "assistant"); return; case "user": - this.updatePrompt(message, "user"); + this.updatePrompt(message, "user", keyPressLog); break; case "chatbot-message": // set by researcher, needs be seperate case because doesn't update prompts role = "chatbot"; @@ -423,16 +432,6 @@ class ChatPlugin implements JsPsychPlugin { return res; } - - private cleanPrompt() { - for (let i = 0; i < this.prompt.length; i++) { - const message = this.prompt[i]; - - if (message["role"] === "system") { - delete message["role"]; - } - } - } } export default ChatPlugin; From b605fd1a5b65acba17099f1b9da3d9bc96bb2bda Mon Sep 17 00:00:00 2001 From: vzhang03 Date: Fri, 28 Jun 2024 12:21:53 -0400 Subject: [PATCH 36/62] Implemented chatLog to keep track of logs and prompts and updated to work for everything except chainPrompts --- packages/plugin-chat/example/example.html | 6 ++ packages/plugin-chat/src/ChatLog.ts | 77 +++++++++++++++++++++++ packages/plugin-chat/src/index.ts | 42 +++++-------- 3 files changed, 97 insertions(+), 28 deletions(-) create mode 100644 packages/plugin-chat/src/ChatLog.ts diff --git a/packages/plugin-chat/example/example.html b/packages/plugin-chat/example/example.html index 594c6ea2..a99e18ae 100644 --- a/packages/plugin-chat/example/example.html +++ b/packages/plugin-chat/example/example.html @@ -33,6 +33,12 @@ prompt: "Stop telling jokes and act very serious. Do not make any more jokes.", role: "chatbot-prompt", message_trigger: 2, + }, + { + message: "This is third prompt - do this now", + prompt: "Be super funny, just like a comedian like kevin hart.", + role: "chatbot-prompt", + message_trigger: 4, } ] diff --git a/packages/plugin-chat/src/ChatLog.ts b/packages/plugin-chat/src/ChatLog.ts new file mode 100644 index 00000000..1a83a161 --- /dev/null +++ b/packages/plugin-chat/src/ChatLog.ts @@ -0,0 +1,77 @@ +export class ChatLog { + private prompt: { role: string; content: string }[]; // keeps track of prompt to send to GPT + private chatLogs: { role: string; content: string; time: number; keyPressLog?: any }[]; // keeps track of all the chat logs + + constructor() { + this.prompt = []; + this.chatLogs = []; + } + + getPrompt() { + return this.prompt; + } + + getChatLogs() { + return this.chatLogs; + } + + // replaces current update prompt + updatePrompt(message, role, keyPressLog?): void { + const time = Math.round(performance.now()); + const newPrompt = { role: role, content: message }; + this.prompt.push(newPrompt); + + const newMessage = { + role: role, + content: message, + time: time, + ...(keyPressLog ? { keyPressLog: keyPressLog } : {}), + }; + this.chatLogs.push(newMessage); + } + + // addMessage, can ahve this replace updatePrompt + // work in functionaltiy to tie it in + logMessage(message, role) { + const time = Math.round(performance.now()); + + const newMessage = { + role: role, + content: message, + time: time, + }; + this.chatLogs.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.prompt.filter((message: any, index: number, array: any[]) => { + if ("role" in message && message["role"] === "system") { + return false; + } + // Exclude the last message, why? - only called when needed + if (index === array.length - 1) { + return false; + } + return true; + }); + + return res; + } + + // call when adding a new prompt + cleanSystem(prompt): {}[] { + const res = this.prompt.filter((message: any) => { + if ("role" in message && message["role"] === "system") { + return false; + } + return true; + }); + + this.prompt = res; + this.updatePrompt(prompt, "system"); + return this.prompt; + } +} diff --git a/packages/plugin-chat/src/index.ts b/packages/plugin-chat/src/index.ts index b0b05257..3909e12f 100644 --- a/packages/plugin-chat/src/index.ts +++ b/packages/plugin-chat/src/index.ts @@ -1,5 +1,8 @@ import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; import { ChatCompletionStream } from "openai/lib/ChatCompletionStream"; +import { Chat } from "openai/resources"; + +import { ChatLog } from "./ChatLog"; // NEED TO FIX // if prompting bot but not message shoudl not pop up with message @@ -104,12 +107,12 @@ type Info = typeof info; */ class ChatPlugin implements JsPsychPlugin { static info = info; - private prompt: {}[]; // keeps track of prompt to send to GPT private researcher_prompts: {}[]; // keeps track of researcher's prompts that need to be displayed private prompt_chain: {}; 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) {} @@ -177,7 +180,6 @@ class ChatPlugin implements JsPsychPlugin { // Function to log all keypresses const logKeypress = (event) => { - console.log(`Key pressed: ${event.key}`); keyPressLog.push(event.key); }; @@ -186,7 +188,8 @@ class ChatPlugin implements JsPsychPlugin { continueButton.addEventListener("click", () => { this.jsPsych.finishTrial({ - chatLogs: this.prompt, + prompt: this.chatLog.getPrompt(), + logs: this.chatLog.getChatLogs(), }); }); @@ -196,11 +199,10 @@ class ChatPlugin implements JsPsychPlugin { initializeTrialVariables(trial: TrialType) { this.timer_start = performance.now(); + this.chatLog = new ChatLog(); this.messages_sent = 0; this.ai_model = trial.ai_model; - // sets prompt - this.prompt = []; this.updatePrompt(trial.ai_prompt, "system"); // sets researcher prompts and removes any that can't trigger this.researcher_prompts = trial.additional_prompts @@ -271,9 +273,7 @@ class ChatPlugin implements JsPsychPlugin { // updates prompts behind the scenes when we add messages to the screen private updatePrompt(message, role, keyPressLog?): void { - const time = Math.round(performance.now()); - const newMessage = { role: role, content: message, time: time, keyPressLog: keyPressLog }; - this.prompt.push(newMessage); + this.chatLog.updatePrompt(message, role, keyPressLog); } // Handles updates to system with the prompt and to the screen @@ -289,8 +289,10 @@ class ChatPlugin implements JsPsychPlugin { 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; default: console.error("Incorrect role"); @@ -316,7 +318,7 @@ class ChatPlugin implements JsPsychPlugin { // allows to pass in non defined prompts if (prompt) response = await this.fetchGPT(prompt, newMessage); // special case when wanting to prompt with own thing - else response = await this.fetchGPT(this.prompt, newMessage); + else response = await this.fetchGPT(this.chatLog.getPrompt(), newMessage); chatBox.scrollTop = chatBox.scrollHeight; this.addMessage("chatbot", response, chatBox); // saves to prompt @@ -348,15 +350,14 @@ class ChatPlugin implements JsPsychPlugin { const message = researcher_prompt["message"]; if (prompt !== null && typeof prompt === "string") { - this.prompt = this.cleanConversation(); - this.updatePrompt(prompt, "system"); + this.chatLog.cleanSystem(prompt); } else console.error( researcher_prompt, "is missing prompt field or it isn't in the correct format" ); - if (message !== null && typeof prompt === "string") { + if (message !== null && typeof prompt === "string" && message !== "") { this.addMessage("system-prompt", message, chatBox); } break; @@ -393,7 +394,7 @@ class ChatPlugin implements JsPsychPlugin { } private async chainPrompts(message, chatBox) { - const cleaned_prompt = this.cleanConversation(); + const cleaned_prompt = this.chatLog.cleanConversation(); for (let i = 0; i < this.prompt_chain["prompts"].length; i++) { const temp_prompt = [...cleaned_prompt]; @@ -417,21 +418,6 @@ class ChatPlugin implements JsPsychPlugin { console.log("current prompt and input:", temp_prompt); } } - - private cleanConversation(): {}[] { - const res = this.prompt.filter((message: any, index: number, array: any[]) => { - if ("role" in message && message["role"] === "system") { - return false; - } - // Exclude the last message - if (index === array.length - 1) { - return false; - } - return true; - }); - - return res; - } } export default ChatPlugin; From c83e8f5c7ef0c922f60a33d33bf49af54a66e886 Mon Sep 17 00:00:00 2001 From: vzhang03 Date: Fri, 28 Jun 2024 12:34:13 -0400 Subject: [PATCH 37/62] Finished new ChatLogging for the chainPrompting --- packages/plugin-chat/example/example.html | 4 ++-- packages/plugin-chat/src/index.ts | 15 +++++++++++---- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/plugin-chat/example/example.html b/packages/plugin-chat/example/example.html index a99e18ae..7991a0a4 100644 --- a/packages/plugin-chat/example/example.html +++ b/packages/plugin-chat/example/example.html @@ -59,8 +59,8 @@ ai_prompt: "Pretend to be a comedian and include a joke at the end of every response", chat_field_placeholder: "be nice...", continue_button: continue_button, - // prompt_chain: prompt_chain, - additional_prompts: prompts, + prompt_chain: prompt_chain, + // additional_prompts: prompts, } jsPsych.run([trial]); diff --git a/packages/plugin-chat/src/index.ts b/packages/plugin-chat/src/index.ts index 3909e12f..0e3ccaf0 100644 --- a/packages/plugin-chat/src/index.ts +++ b/packages/plugin-chat/src/index.ts @@ -395,6 +395,7 @@ class ChatPlugin implements JsPsychPlugin { 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 temp_prompt = [...cleaned_prompt]; @@ -404,19 +405,25 @@ class ChatPlugin implements JsPsychPlugin { content: prompt, }; temp_prompt.push(new_sys); + logChain.push(new_sys); const user_message = { role: "user", content: message, }; temp_prompt.push(user_message); + logChain.push(user_message); if (i === this.prompt_chain["prompts"].length - 1) { - await this.updateAndProcessGPT(chatBox, temp_prompt); - } else message = await this.fetchGPT(temp_prompt); // Ensure to await if fetchGPT is asynchronous - - console.log("current prompt and input:", temp_prompt); + message = await this.updateAndProcessGPT(chatBox, temp_prompt); + logChain.push({ role: "assistant", content: message }); + } else { + message = await this.fetchGPT(temp_prompt); // Ensure to await if fetchGPT is asynchronous + logChain.push({ role: "assistant", content: message }); + } } + + this.chatLog.logMessage(logChain, "chain-prompt"); } } From 79e18cb81df8b050f2ebbfc6d9e2c9d71a05d8e8 Mon Sep 17 00:00:00 2001 From: vzhang03 Date: Fri, 28 Jun 2024 12:36:47 -0400 Subject: [PATCH 38/62] Fixed empty string error and small fixes --- packages/plugin-chat/example/example.html | 6 +++--- packages/plugin-chat/src/index.ts | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/plugin-chat/example/example.html b/packages/plugin-chat/example/example.html index 7991a0a4..e0ad4750 100644 --- a/packages/plugin-chat/example/example.html +++ b/packages/plugin-chat/example/example.html @@ -29,7 +29,7 @@ message_trigger: 0 }, { - message: "This is new prompt - do this now", + // message: "This is new prompt - do this now", prompt: "Stop telling jokes and act very serious. Do not make any more jokes.", role: "chatbot-prompt", message_trigger: 2, @@ -59,8 +59,8 @@ ai_prompt: "Pretend to be a comedian and include a joke at the end of every response", chat_field_placeholder: "be nice...", continue_button: continue_button, - prompt_chain: prompt_chain, - // additional_prompts: prompts, + // prompt_chain: prompt_chain, + additional_prompts: prompts, } jsPsych.run([trial]); diff --git a/packages/plugin-chat/src/index.ts b/packages/plugin-chat/src/index.ts index 0e3ccaf0..b7f44f0b 100644 --- a/packages/plugin-chat/src/index.ts +++ b/packages/plugin-chat/src/index.ts @@ -1,6 +1,5 @@ import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; import { ChatCompletionStream } from "openai/lib/ChatCompletionStream"; -import { Chat } from "openai/resources"; import { ChatLog } from "./ChatLog"; From ddc04c9d567815ad8cef8dbf99bffc20e1fc0fb3 Mon Sep 17 00:00:00 2001 From: vzhang03 Date: Fri, 28 Jun 2024 15:32:21 -0400 Subject: [PATCH 39/62] Implementing minor changes to ChatLog to make it more convenient --- packages/plugin-chat/example/example.html | 1 + packages/plugin-chat/src/ChatLog.ts | 15 +++++++++------ packages/plugin-chat/src/index.ts | 20 +++++++++----------- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/packages/plugin-chat/example/example.html b/packages/plugin-chat/example/example.html index e0ad4750..8ecb3a29 100644 --- a/packages/plugin-chat/example/example.html +++ b/packages/plugin-chat/example/example.html @@ -31,6 +31,7 @@ { // 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, }, diff --git a/packages/plugin-chat/src/ChatLog.ts b/packages/plugin-chat/src/ChatLog.ts index 1a83a161..59c4f101 100644 --- a/packages/plugin-chat/src/ChatLog.ts +++ b/packages/plugin-chat/src/ChatLog.ts @@ -15,16 +15,17 @@ export class ChatLog { return this.chatLogs; } - // replaces current update prompt - updatePrompt(message, role, keyPressLog?): void { + // replaces current update prompt, possibly replace to pass in objct + updatePrompt(content, role, keyPressLog?, message?): void { const time = Math.round(performance.now()); - const newPrompt = { role: role, content: message }; + const newPrompt = { role: role, content: content }; this.prompt.push(newPrompt); const newMessage = { role: role, - content: message, + content: content, time: time, + ...(message ? { message: message } : {}), ...(keyPressLog ? { keyPressLog: keyPressLog } : {}), }; this.chatLogs.push(newMessage); @@ -62,7 +63,8 @@ export class ChatLog { } // call when adding a new prompt - cleanSystem(prompt): {}[] { + cleanSystem(prompt, message?): {}[] { + // cleans existing prompts const res = this.prompt.filter((message: any) => { if ("role" in message && message["role"] === "system") { return false; @@ -70,8 +72,9 @@ export class ChatLog { return true; }); + // sets the prompts equal to the new one this.prompt = res; - this.updatePrompt(prompt, "system"); + this.updatePrompt(prompt, "system", undefined, message); return this.prompt; } } diff --git a/packages/plugin-chat/src/index.ts b/packages/plugin-chat/src/index.ts index b7f44f0b..b30ceac3 100644 --- a/packages/plugin-chat/src/index.ts +++ b/packages/plugin-chat/src/index.ts @@ -149,7 +149,7 @@ class ChatPlugin implements JsPsychPlugin { // 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); + this.addMessage("user", message, chatBox, (keyPressLog = keyPressLog)); keyPressLog = []; userInput.value = ""; @@ -202,7 +202,7 @@ class ChatPlugin implements JsPsychPlugin { this.messages_sent = 0; this.ai_model = trial.ai_model; - this.updatePrompt(trial.ai_prompt, "system"); + this.chatLog.updatePrompt(trial.ai_prompt, "system"); // sets researcher prompts and removes any that can't trigger this.researcher_prompts = trial.additional_prompts ? trial.additional_prompts.filter((researcher_prompt) => { @@ -270,21 +270,16 @@ class ChatPlugin implements JsPsychPlugin { } } - // updates prompts behind the scenes when we add messages to the screen - private updatePrompt(message, role, keyPressLog?): void { - this.chatLog.updatePrompt(message, role, keyPressLog); - } - // 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.updatePrompt(message, "assistant"); + this.chatLog.updatePrompt(message, "assistant"); return; case "user": - this.updatePrompt(message, "user", keyPressLog); + this.chatLog.updatePrompt(message, "user", keyPressLog); break; case "chatbot-message": // set by researcher, needs be seperate case because doesn't update prompts role = "chatbot"; @@ -293,6 +288,9 @@ class ChatPlugin implements JsPsychPlugin { 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; @@ -349,7 +347,7 @@ class ChatPlugin implements JsPsychPlugin { const message = researcher_prompt["message"]; if (prompt !== null && typeof prompt === "string") { - this.chatLog.cleanSystem(prompt); + this.chatLog.cleanSystem(prompt, message); } else console.error( researcher_prompt, @@ -357,7 +355,7 @@ class ChatPlugin implements JsPsychPlugin { ); if (message !== null && typeof prompt === "string" && message !== "") { - this.addMessage("system-prompt", message, chatBox); + this.addMessage(researcher_prompt["role"], message, chatBox); } break; case "continue": // displays continue button, error checking that pipelining is working From 1eaf93249cfb27518b492e11c4d12d545fa9b222 Mon Sep 17 00:00:00 2001 From: vzhang03 Date: Fri, 28 Jun 2024 16:40:07 -0400 Subject: [PATCH 40/62] Small edits reducing code --- packages/plugin-chat/src/index.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/plugin-chat/src/index.ts b/packages/plugin-chat/src/index.ts index b30ceac3..11b4e678 100644 --- a/packages/plugin-chat/src/index.ts +++ b/packages/plugin-chat/src/index.ts @@ -177,13 +177,10 @@ class ChatPlugin implements JsPsychPlugin { } }); - // Function to log all keypresses - const logKeypress = (event) => { - keyPressLog.push(event.key); - }; - // Event listener for all keypresses on userInput - userInput.addEventListener("keydown", logKeypress); + userInput.addEventListener("keydown", function (event) { + keyPressLog.push(event.key); + }); continueButton.addEventListener("click", () => { this.jsPsych.finishTrial({ @@ -196,6 +193,7 @@ class ChatPlugin implements JsPsychPlugin { this.checkResearcherPrompts(chatBox, continueButton); } + // includes error checking to minimize error checking later initializeTrialVariables(trial: TrialType) { this.timer_start = performance.now(); this.chatLog = new ChatLog(); From 32edb17dfea29ba1fe78f04ea967395d77c418dc Mon Sep 17 00:00:00 2001 From: vzhang03 Date: Fri, 28 Jun 2024 16:49:02 -0400 Subject: [PATCH 41/62] Additional small comments - preparing for next week --- packages/plugin-chat/src/ChatLog.ts | 1 - packages/plugin-chat/src/index.ts | 7 +++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/plugin-chat/src/ChatLog.ts b/packages/plugin-chat/src/ChatLog.ts index 59c4f101..c44bf527 100644 --- a/packages/plugin-chat/src/ChatLog.ts +++ b/packages/plugin-chat/src/ChatLog.ts @@ -45,7 +45,6 @@ export class ChatLog { } // Chain Condition -- have it be a list of the chain and what happens before and after - // called when temporary chainPrompting cleanConversation(): {}[] { const res = this.prompt.filter((message: any, index: number, array: any[]) => { diff --git a/packages/plugin-chat/src/index.ts b/packages/plugin-chat/src/index.ts index 11b4e678..a3122208 100644 --- a/packages/plugin-chat/src/index.ts +++ b/packages/plugin-chat/src/index.ts @@ -3,9 +3,6 @@ import { ChatCompletionStream } from "openai/lib/ChatCompletionStream"; import { ChatLog } from "./ChatLog"; -// NEED TO FIX -// if prompting bot but not message shoudl not pop up with message - const info = { name: "chat", parameters: { @@ -184,7 +181,6 @@ class ChatPlugin implements JsPsychPlugin { continueButton.addEventListener("click", () => { this.jsPsych.finishTrial({ - prompt: this.chatLog.getPrompt(), logs: this.chatLog.getChatLogs(), }); }); @@ -324,6 +320,7 @@ class ChatPlugin implements JsPsychPlugin { } } + // logic for triggering logic checkResearcherPrompts(chatBox, continueButton): void { this.researcher_prompts = this.researcher_prompts.filter((researcher_prompt) => { const message_trigger = researcher_prompt["message_trigger"]; @@ -375,6 +372,7 @@ class ChatPlugin implements JsPsychPlugin { }); } + // checking whether chain prompts can trigger private chainCondition() { const time_elapsed = performance.now() - this.timer_start; // could instead keep subtracting from time_elapsed const message_trigger = this.prompt_chain["message_trigger"]; @@ -388,6 +386,7 @@ class ChatPlugin implements JsPsychPlugin { } else return false; } + // triggering prompts in chain and prompting/logging logic private async chainPrompts(message, chatBox) { const cleaned_prompt = this.chatLog.cleanConversation(); const logChain = []; From f750460dac13833d1fce1b8cbd07945f7d03e54f Mon Sep 17 00:00:00 2001 From: vzhang03 Date: Fri, 28 Jun 2024 16:50:12 -0400 Subject: [PATCH 42/62] Cleaning comments --- packages/plugin-chat/src/ChatLog.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugin-chat/src/ChatLog.ts b/packages/plugin-chat/src/ChatLog.ts index c44bf527..3f60e2bf 100644 --- a/packages/plugin-chat/src/ChatLog.ts +++ b/packages/plugin-chat/src/ChatLog.ts @@ -51,7 +51,7 @@ export class ChatLog { if ("role" in message && message["role"] === "system") { return false; } - // Exclude the last message, why? - only called when needed + // Exclude the last message because will be user message - only want existing conversation if (index === array.length - 1) { return false; } From a8797bd6f33ad458a5cb2400b34980c6642dd2dd Mon Sep 17 00:00:00 2001 From: vzhang03 Date: Tue, 9 Jul 2024 12:31:26 -0400 Subject: [PATCH 43/62] Manually built adding system prompts at end - transfering from ian-chat branch --- packages/plugin-chat/src/ChatLog.ts | 40 +++++++++++++++++++++-------- packages/plugin-chat/src/index.ts | 25 +++++++++++------- 2 files changed, 45 insertions(+), 20 deletions(-) diff --git a/packages/plugin-chat/src/ChatLog.ts b/packages/plugin-chat/src/ChatLog.ts index 3f60e2bf..104a8f6e 100644 --- a/packages/plugin-chat/src/ChatLog.ts +++ b/packages/plugin-chat/src/ChatLog.ts @@ -1,14 +1,21 @@ export class ChatLog { - private prompt: { role: string; content: string }[]; // keeps track of prompt to send to GPT + private conversationLog: { role: string; content: string }[]; // keeps track of prompt to send to GPT private chatLogs: { role: string; content: string; time: number; keyPressLog?: any }[]; // keeps track of all the chat logs + private prompt: string; constructor() { - this.prompt = []; + this.conversationLog = []; this.chatLogs = []; } + setPrompt(prompt) { + this.prompt = prompt; + } + getPrompt() { - return this.prompt; + const newElement = { role: "system", content: this.prompt }; + const tempConversationLog = [...this.conversationLog, newElement]; + return tempConversationLog; } getChatLogs() { @@ -16,10 +23,19 @@ export class ChatLog { } // replaces current update prompt, possibly replace to pass in objct - updatePrompt(content, role, keyPressLog?, message?): void { + 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.prompt.push(newPrompt); + this.conversationLog.push(newPrompt); const newMessage = { role: role, @@ -47,7 +63,7 @@ export class ChatLog { // Chain Condition -- have it be a list of the chain and what happens before and after // called when temporary chainPrompting cleanConversation(): {}[] { - const res = this.prompt.filter((message: any, index: number, array: any[]) => { + const res = this.conversationLog.filter((message: any, index: number, array: any[]) => { if ("role" in message && message["role"] === "system") { return false; } @@ -62,9 +78,9 @@ export class ChatLog { } // call when adding a new prompt - cleanSystem(prompt, message?): {}[] { + cleanSystem(prompt, message?) { // cleans existing prompts - const res = this.prompt.filter((message: any) => { + const res = this.conversationLog.filter((message: any) => { if ("role" in message && message["role"] === "system") { return false; } @@ -72,8 +88,10 @@ export class ChatLog { }); // sets the prompts equal to the new one - this.prompt = res; - this.updatePrompt(prompt, "system", undefined, message); - return this.prompt; + this.conversationLog = res; + // this.updateConversationLog(prompt, "system", undefined, message); + this.prompt = prompt; + + return this.getPrompt(); } } diff --git a/packages/plugin-chat/src/index.ts b/packages/plugin-chat/src/index.ts index a3122208..ae2cebd6 100644 --- a/packages/plugin-chat/src/index.ts +++ b/packages/plugin-chat/src/index.ts @@ -196,8 +196,9 @@ class ChatPlugin implements JsPsychPlugin { this.messages_sent = 0; this.ai_model = trial.ai_model; - this.chatLog.updatePrompt(trial.ai_prompt, "system"); - // sets researcher prompts and removes any that can't trigger + // 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 ( @@ -270,10 +271,10 @@ class ChatPlugin implements JsPsychPlugin { // Handles logic of updating prompts and error checking switch (role) { case "chatbot": // writing to screen handled caller function - this.chatLog.updatePrompt(message, "assistant"); + this.chatLog.updateConversationLog(message, "assistant"); return; case "user": - this.chatLog.updatePrompt(message, "user", keyPressLog); + this.chatLog.updateConversationLog(message, "user", keyPressLog); break; case "chatbot-message": // set by researcher, needs be seperate case because doesn't update prompts role = "chatbot"; @@ -306,10 +307,16 @@ class ChatPlugin implements JsPsychPlugin { try { var response = undefined; - // allows to pass in non defined prompts - if (prompt) response = await this.fetchGPT(prompt, newMessage); - // special case when wanting to prompt with own thing - else response = await this.fetchGPT(this.chatLog.getPrompt(), newMessage); + + if (prompt) { + // allows to pass in non defined prompts + response = await this.fetchGPT(prompt, newMessage); + console.log(prompt); + } else { + // special case when wanting to prompt with own thing + response = await this.fetchGPT(this.chatLog.getPrompt(), newMessage); + console.log(this.chatLog.getPrompt()); + } chatBox.scrollTop = chatBox.scrollHeight; this.addMessage("chatbot", response, chatBox); // saves to prompt @@ -337,7 +344,7 @@ class ChatPlugin implements JsPsychPlugin { case "system-prompt": // want these cases to have the same functionality this.addMessage(researcher_prompt["role"], researcher_prompt["message"], chatBox); break; - case "chatbot-prompt": // checkes messages, updates prompt and prints sytem message if exists + case "chatbot-prompt": // checks messages, updates prompt and prints sytem message if exists const prompt = researcher_prompt["prompt"]; const message = researcher_prompt["message"]; From 7be8e4eb730e1f3593f877a6d74bc91d37972241 Mon Sep 17 00:00:00 2001 From: vzhang03 Date: Tue, 9 Jul 2024 12:34:29 -0400 Subject: [PATCH 44/62] Fixed api calls to be consistent with server --- packages/plugin-chat/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugin-chat/src/index.ts b/packages/plugin-chat/src/index.ts index ae2cebd6..42f4766a 100644 --- a/packages/plugin-chat/src/index.ts +++ b/packages/plugin-chat/src/index.ts @@ -236,7 +236,7 @@ class ChatPlugin implements JsPsychPlugin { // Call to backend, newMessage is the document item to print (optional because when chaining don't want them to display) async fetchGPT(messages, newMessage?) { try { - const response = await fetch("http://localhost:3000/api/chat", { + const response = await fetch("/api/chat", { method: "POST", headers: { "Content-Type": "application/json", From de4070194588337a39e94e4f2268fb433145f8fc Mon Sep 17 00:00:00 2001 From: vzhang03 Date: Tue, 9 Jul 2024 14:14:21 -0400 Subject: [PATCH 45/62] Fixing chatLog generation --- backend/server.js | 6 +++--- packages/plugin-chat/src/ChatLog.ts | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/backend/server.js b/backend/server.js index 5777c290..dbd6c0c1 100644 --- a/backend/server.js +++ b/backend/server.js @@ -34,6 +34,6 @@ app.post("/api/chat", async (req, res) => { } }); -app.listen(port, () => { - console.log(`Server is running on http://localhost:${port}`); -}); +// app.listen(port, () => { +// console.log(`Server is running on http://localhost:${port}`); +// }); diff --git a/packages/plugin-chat/src/ChatLog.ts b/packages/plugin-chat/src/ChatLog.ts index 104a8f6e..b01d62a1 100644 --- a/packages/plugin-chat/src/ChatLog.ts +++ b/packages/plugin-chat/src/ChatLog.ts @@ -9,6 +9,8 @@ export class ChatLog { } setPrompt(prompt) { + const newElement = { role: "system", content: this.prompt }; + this.conversationLog.push(newElement); this.prompt = prompt; } From fffdf61a1f0b93585dea3997588a0ecd349ba3fe Mon Sep 17 00:00:00 2001 From: vzhang03 Date: Tue, 9 Jul 2024 14:41:36 -0400 Subject: [PATCH 46/62] Fixing the errors with printing and prompt error --- packages/plugin-chat/src/ChatLog.ts | 39 ++++++++++++++++------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/packages/plugin-chat/src/ChatLog.ts b/packages/plugin-chat/src/ChatLog.ts index b01d62a1..ef898501 100644 --- a/packages/plugin-chat/src/ChatLog.ts +++ b/packages/plugin-chat/src/ChatLog.ts @@ -1,27 +1,34 @@ export class ChatLog { - private conversationLog: { role: string; content: string }[]; // keeps track of prompt to send to GPT - private chatLogs: { role: string; content: string; time: number; keyPressLog?: any }[]; // keeps track of all the chat logs + 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.conversationLog = []; - this.chatLogs = []; + this.conversation_log = []; + this.final_data = []; } setPrompt(prompt) { - const newElement = { role: "system", content: this.prompt }; - this.conversationLog.push(newElement); + 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.conversationLog, newElement]; + const tempConversationLog = [...this.conversation_log, newElement]; return tempConversationLog; } getChatLogs() { - return this.chatLogs; + return this.final_data; } // replaces current update prompt, possibly replace to pass in objct @@ -37,7 +44,7 @@ export class ChatLog { const time = Math.round(performance.now()); const newPrompt = { role: role, content: content }; - this.conversationLog.push(newPrompt); + this.conversation_log.push(newPrompt); const newMessage = { role: role, @@ -46,7 +53,7 @@ export class ChatLog { ...(message ? { message: message } : {}), ...(keyPressLog ? { keyPressLog: keyPressLog } : {}), }; - this.chatLogs.push(newMessage); + this.final_data.push(newMessage); } // addMessage, can ahve this replace updatePrompt @@ -59,13 +66,13 @@ export class ChatLog { content: message, time: time, }; - this.chatLogs.push(newMessage); + 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.conversationLog.filter((message: any, index: number, array: any[]) => { + const res = this.conversation_log.filter((message: any, index: number, array: any[]) => { if ("role" in message && message["role"] === "system") { return false; } @@ -82,18 +89,16 @@ export class ChatLog { // call when adding a new prompt cleanSystem(prompt, message?) { // cleans existing prompts - const res = this.conversationLog.filter((message: any) => { + const res = this.conversation_log.filter((message: any) => { if ("role" in message && message["role"] === "system") { return false; } return true; }); - // sets the prompts equal to the new one - this.conversationLog = res; - // this.updateConversationLog(prompt, "system", undefined, message); - this.prompt = prompt; + this.conversation_log = res; + this.setPrompt(prompt); return this.getPrompt(); } } From 70ec0ad62404dd0d8cd2662b85fa338dc6a43786 Mon Sep 17 00:00:00 2001 From: vzhang03 Date: Wed, 10 Jul 2024 10:54:04 -0400 Subject: [PATCH 47/62] Compability for server and local testing --- backend/server.js | 7 +++---- packages/plugin-chat/src/index.ts | 25 ++++++++++++++++++------- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/backend/server.js b/backend/server.js index dbd6c0c1..2836861f 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,6 +1,5 @@ const OpenAI = require("openai"); const express = require("express"); -const axios = require("axios"); const bodyParser = require("body-parser"); const cors = require("cors"); require("dotenv").config(); @@ -34,6 +33,6 @@ app.post("/api/chat", async (req, res) => { } }); -// app.listen(port, () => { -// console.log(`Server is running on http://localhost:${port}`); -// }); +app.listen(port, () => { + console.log(`Server is running on http://localhost:${port}`); +}); diff --git a/packages/plugin-chat/src/index.ts b/packages/plugin-chat/src/index.ts index 42f4766a..b7da31b3 100644 --- a/packages/plugin-chat/src/index.ts +++ b/packages/plugin-chat/src/index.ts @@ -236,13 +236,24 @@ class ChatPlugin implements JsPsychPlugin { // Call to backend, newMessage is the document item to print (optional because when chaining don't want them to display) async fetchGPT(messages, newMessage?) { try { - const response = await fetch("/api/chat", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ messages, ai_model: this.ai_model }), // Corrected JSON structure - }); + var response; + if (window.location.href.includes("127.0.0.1")) { + response = await fetch("http://localhost:3000/api/chat", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ messages, ai_model: this.ai_model }), // Corrected JSON structure + }); + } else { + response = await fetch("/api/chat", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ messages, ai_model: this.ai_model }), // Corrected JSON structure + }); + } if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); From 77e211861aaf9dfc5bc5d07db6bbac9f0b0b170e Mon Sep 17 00:00:00 2001 From: vzhang03 Date: Wed, 10 Jul 2024 11:47:07 -0400 Subject: [PATCH 48/62] Beginning changing fonts, fixing the displaying of data, allowing for chat with no title --- packages/plugin-chat/example/example.html | 7 +++++ packages/plugin-chat/example/styles.css | 31 ++++++++++++++++++++--- packages/plugin-chat/src/index.ts | 19 ++++++++------ 3 files changed, 45 insertions(+), 12 deletions(-) diff --git a/packages/plugin-chat/example/example.html b/packages/plugin-chat/example/example.html index 8ecb3a29..08ba2bcb 100644 --- a/packages/plugin-chat/example/example.html +++ b/packages/plugin-chat/example/example.html @@ -6,6 +6,13 @@ + -``` +Using this plugin is a two-step process that requires setting up a backend in addition to loading the plugin. -### Via NPM +## Running locally -``` -npm install @jspsych-contrib/plugin-chat -``` +To run this package locally, you need to follow these steps below. -```js -import jsPsychChat from '@jspsych-contrib/plugin-chat'; -``` + 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 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/backend/package-lock.json b/packages/plugin-chat/backend/package-lock.json similarity index 100% rename from backend/package-lock.json rename to packages/plugin-chat/backend/package-lock.json diff --git a/backend/package.json b/packages/plugin-chat/backend/package.json similarity index 100% rename from backend/package.json rename to packages/plugin-chat/backend/package.json diff --git a/backend/server.js b/packages/plugin-chat/backend/server.js similarity index 95% rename from backend/server.js rename to packages/plugin-chat/backend/server.js index 2836861f..4acec231 100644 --- a/backend/server.js +++ b/packages/plugin-chat/backend/server.js @@ -1,4 +1,4 @@ -const OpenAI = require("openai"); +const OpenAI = require("openai/index.mjs"); const express = require("express"); const bodyParser = require("body-parser"); const cors = require("cors"); diff --git a/packages/plugin-chat/docs/chat.md b/packages/plugin-chat/docs/chat.md index 439ab81b..d5e1c641 100644 --- a/packages/plugin-chat/docs/chat.md +++ b/packages/plugin-chat/docs/chat.md @@ -1,14 +1,20 @@ # chat -Chat interface for running experiments using LLMs +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. +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 @@ -16,7 +22,99 @@ In addition to the [default data collected by all plugins](https://jspsych.org/l | 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 diff --git a/packages/plugin-chat/example/example.html b/packages/plugin-chat/example/example.html index f9ef1aec..ff4508a7 100644 --- a/packages/plugin-chat/example/example.html +++ b/packages/plugin-chat/example/example.html @@ -60,8 +60,6 @@ message_trigger: 1, } - - continue_button = { timer_trigger: 1000000, message_trigger: 1, diff --git a/packages/plugin-chat/src/index.ts b/packages/plugin-chat/src/index.ts index 14ebdc98..3d818a56 100644 --- a/packages/plugin-chat/src/index.ts +++ b/packages/plugin-chat/src/index.ts @@ -13,7 +13,7 @@ const info = { }, ai_model: { type: ParameterType.STRING, - default: "gpt-3.5-turbo-16k", + default: "gpt-4o-mini", }, chat_field_placeholder: { type: ParameterType.STRING, @@ -71,7 +71,7 @@ const info = { // when triggers it doesn't stop, do we want to give it a stop? prompt_chain: { type: ParameterType.COMPLEX, - default: [], + default: {}, nested: { prompts: { type: ParameterType.STRING, @@ -290,7 +290,7 @@ class ChatPlugin implements JsPsychPlugin { headers: { "Content-Type": "application/json", }, - body: JSON.stringify({ messages, ai_model: this.ai_model }), // Corrected JSON structure + body: JSON.stringify({ messages, ai_model: this.ai_model }), }); } else { response = await fetch("/api/chat", { @@ -298,7 +298,7 @@ class ChatPlugin implements JsPsychPlugin { headers: { "Content-Type": "application/json", }, - body: JSON.stringify({ messages, ai_model: this.ai_model }), // Corrected JSON structure + body: JSON.stringify({ messages, ai_model: this.ai_model }), }); } @@ -534,7 +534,7 @@ class ChatPlugin implements JsPsychPlugin { { role: "user", content: bot_responses }, ]; - const response_message = await this.updateAndProcessGPT(chatBox, prompt_select); + const response_message = await this.updateAndProcessGPT(chatBox, prompt_select); // in case need to something with this } } From 100f2b7b700830301a8f4b963e4ff40d7054c809 Mon Sep 17 00:00:00 2001 From: vzhang03 Date: Thu, 15 Aug 2024 18:22:15 -0700 Subject: [PATCH 61/62] Updated to use esmodules and import syntax --- packages/plugin-chat/backend/package.json | 1 + packages/plugin-chat/backend/server.js | 17 +++++++++++------ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/plugin-chat/backend/package.json b/packages/plugin-chat/backend/package.json index 48557673..299c39f0 100644 --- a/packages/plugin-chat/backend/package.json +++ b/packages/plugin-chat/backend/package.json @@ -2,6 +2,7 @@ "name": "backend", "version": "1.0.0", "main": "index.js", + "type": "module", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, diff --git a/packages/plugin-chat/backend/server.js b/packages/plugin-chat/backend/server.js index 4acec231..8520a569 100644 --- a/packages/plugin-chat/backend/server.js +++ b/packages/plugin-chat/backend/server.js @@ -1,13 +1,18 @@ -const OpenAI = require("openai/index.mjs"); -const express = require("express"); -const bodyParser = require("body-parser"); -const cors = require("cors"); -require("dotenv").config(); +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 }); + +const openai = new OpenAI({ + apiKey: OPENAI_API_KEY, +}); app.use(bodyParser.json()); app.use(cors()); From 80b5c609b1a69517cce190d8745e93dbbfeac76f Mon Sep 17 00:00:00 2001 From: vzhang03 Date: Thu, 15 Aug 2024 18:50:10 -0700 Subject: [PATCH 62/62] Adding changeset --- .changeset/rotten-coins-drop.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/rotten-coins-drop.md 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