diff --git a/README.md b/README.md index 34fe06a0..f9379aa8 100755 --- a/README.md +++ b/README.md @@ -1,23 +1,17 @@

-
- -

Noora

-

Use AI to improve your social skills.

+

Noora

+

Use AI to improve your social conversation.

- A platform utilizing conversational AI to improve the social skills of individuals with Autism Spectrum Disorder (ASD). + A platform utilizing conversational AI to improve the social conversation of individuals with Autism Spectrum Disorder (ASD).

- - Stanford Medicine - - x Stanford University

- Powered by Vercel + Powered by Vercel

@@ -26,9 +20,9 @@ ## Overview -Noora is is a research project led by Prof. Monica Lam, from Stanford University, with the goal of improving the social skills of people with Autism Spectrum Disorder (ASD). The project is a collaboration between Stanford's Open Virtual Assistant Lab and Dr. Lynn Koegel in Stanford's Dept. of Psychiatry and Behavioral Sciences. +Noora is is a research project led by Prof. Monica Lam, from Stanford University, with the goal of improving the social conversation of people with Autism Spectrum Disorder (ASD). The project is a collaboration between Stanford's Open Virtual Assistant Lab and Dr. Lynn Koegel from Stanford's Dept. of Psychiatry and Behavioral Sciences. -Dr. Koegel's intervention method, pivotal response training, has been shown to help individuals make friends, have romantic relationships, collaborate in the workplace, and engage in leisure activities. Because pivotal response training's applicability is restricted by the lack of trained professionals, Noora's goal is to automate the intervention, the success of which can have a great impact on society, as 1 in 54 children is currently diagnosed with ASD. + This project built with Next.js, Tailwind CSS, OpenAI's GPT-3, and Microsoft Azure. @@ -52,4 +46,22 @@ For the speech-to-text and text-to-speech functionality, create an Azure speech OPENAI_API_KEY=[your API key] SPEECH_KEY=[your API key] SPEECH_REGION=[your API key] -``` \ No newline at end of file +``` + +## Repo Structure + +**`pages`**: all of the Next.js app's paths (e.g., `noora.tsx`, `_404.tsx`). These files simply return components, typically wrapped by the `` component. + +**`components`**: the bulk of the website code. Folders under this directory reference the pages of the website, with the exception of `components/global` and `components/interfaces`. +- `components/interfaces` contains the code behind the Noora Chat feature and the Ask Noora feature. + +**`data`**: all static or pre-written data, such as GPT-3 prompts, statement banks, routes. + +**`scripts`**: code that is called by components. +- `/scripts/noora-chat` (and, more specifically, `get-reply.ts`) contains the logic and flow for Noora's replies, and is used in `MessageBox.tsx` (where users submit their message in the Noora Chat). + +**`pages/api`**: the REST API endpoints. Rate limiting is in place for endpoints (implementation found in `scripts/rate-limit.ts`). +- `api/openai` calls OpenAI's Completion endpoint and returns the resulting `text` and `logprobs`. +- `api/get-speech-token.ts` retrieves an ephemeral authentication token for Microsoft Azure's speech service. + +**`public`**: the public assets including images and static files. diff --git a/package.json b/package.json index 4b01a58b..d1aa9b56 100644 --- a/package.json +++ b/package.json @@ -16,9 +16,12 @@ "@headlessui/react": "^1.6.6", "@heroicons/react": "^1.0.6", "@tailwindcss/forms": "^0.5.2", + "@types/dateformat": "^5.0.0", "@types/uuid": "^8.3.4", "clsx": "^1.2.1", + "dateformat": "^5.0.3", "fs": "^0.0.1-security", + "heroicons": "^2.0.10", "lru-cache": "^7.13.1", "microsoft-cognitiveservices-speech-sdk": "^1.22.0", "next": "12.2.2", @@ -26,9 +29,13 @@ "openai-api": "^1.3.1", "react": "18.2.0", "react-anchor-link-smooth-scroll": "^1.0.12", + "react-circular-progressbar": "^2.1.0", + "react-device-detect": "^2.2.2", "react-dom": "18.2.0", "react-joyride": "^2.5.0", "react-spinners": "^0.13.3", + "reactjs-percentage-circle": "^1.0.0", + "swr": "^1.3.0", "universal-cookie": "^4.0.4", "uuid": "^8.3.2" }, diff --git a/public/img/logos/stanford/engineering.png b/public/img/logos/stanford/engineering.png new file mode 100644 index 00000000..7bdc8afa Binary files /dev/null and b/public/img/logos/stanford/engineering.png differ diff --git a/src/components/ask-noora/AskNoora.tsx b/src/components/ask-noora/AskNoora.tsx index 5f98cdc0..45eb4248 100644 --- a/src/components/ask-noora/AskNoora.tsx +++ b/src/components/ask-noora/AskNoora.tsx @@ -1,5 +1,5 @@ import React, { useState } from "react"; -import AskNooraComponent from "../interfaces/ask-noora-interface/AskNooraComponent"; +import AskNooraComponent from "../interfaces/ask-noora/AskNooraComponent"; export default function AskNoora() { const [q, setQ] = useState(""); diff --git a/src/components/dashboard/AttemptHistory.tsx b/src/components/dashboard/AttemptHistory.tsx new file mode 100644 index 00000000..73f8f1b9 --- /dev/null +++ b/src/components/dashboard/AttemptHistory.tsx @@ -0,0 +1,70 @@ +import React from "react"; +import useSWR from 'swr' +import dateFormat from "dateformat"; +import { CircularProgressbarWithChildren } from 'react-circular-progressbar'; +import 'react-circular-progressbar/dist/styles.css'; +import clsx from "clsx"; +import Progress from "../interfaces/noora-chat/menu/sections/Progress"; + +export default function AttemptHistory() { + const { data } = useSWR("progress", key => { + const value = localStorage.getItem(key); + return !!value ? JSON.parse(value) : undefined; + }) + + return ( +
+ {data && data.map((attempt: any, idx: number) => { + let dateStr = dateFormat(new Date(attempt.timeCompleted), "mmmm dS, h:MM TT"); + return ( +
+
+

+ {dateStr} +

+

Attempt {data.length - idx}

+
+
+ +
+ {Object.keys(attempt.scores).map((k: any) => { + k = k as keyof typeof attempt.scores + if (k == "total" || attempt.scores[k][1] == 0) return
+ return + })} +
+
) + }) + } +
+ ); +} + +function ProgressCircle({ num, denom, title }: any) { + return
+ +
{`${num}/${denom}`}
+
{title}
+
+
+} diff --git a/src/components/dashboard/ProgressSummary.tsx b/src/components/dashboard/ProgressSummary.tsx index 322cdb2b..ce1a89c0 100644 --- a/src/components/dashboard/ProgressSummary.tsx +++ b/src/components/dashboard/ProgressSummary.tsx @@ -1,6 +1,7 @@ import React from "react"; import { ArrowSmRightIcon } from "@heroicons/react/solid"; import Link from "next/link"; +import AttemptHistory from "./AttemptHistory"; export default function ProgressSummary() { return ( @@ -8,15 +9,15 @@ export default function ProgressSummary() {

Practice your{" "} - social skills. + social conversation.

Noora helps you practice challenging aspects of social conversation, such as{" "} responding empathetically and positively to others.

-
-
+
+

Up Next

@@ -33,6 +34,7 @@ export default function ProgressSummary() {

+
); } diff --git a/src/components/dashboard/UserHero.tsx b/src/components/dashboard/UserInfo.tsx similarity index 100% rename from src/components/dashboard/UserHero.tsx rename to src/components/dashboard/UserInfo.tsx diff --git a/src/components/global/utility/Microphone.tsx b/src/components/global/utility/Microphone.tsx deleted file mode 100644 index d6a98bef..00000000 --- a/src/components/global/utility/Microphone.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import React from "react"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faMicrophone } from "@fortawesome/free-solid-svg-icons"; - -import { ResultReason } from "microsoft-cognitiveservices-speech-sdk"; -const speechsdk = require("microsoft-cognitiveservices-speech-sdk"); - -import { getTokenOrRefresh } from "../../../scripts/token_util"; - -export default function Microphone({ - className, - turn, - setTurn, - setText, - currText, - convoState -}: any) { - const microphoneHandler = () => { - console.log("In Microphone handler"); - sttFromMic(turn, setTurn, setText, currText); - }; - - return ( - - ); -} - -async function sttFromMic( - turn: string, - setTurn: any, - setText: any, - currText: string -) { - const tokenObj = await getTokenOrRefresh(); - - const speechConfig = speechsdk.SpeechConfig.fromAuthorizationToken( - tokenObj.authToken, - tokenObj.region - ); - speechConfig.speechRecognitionLanguage = "en-US"; - - const audioConfig = speechsdk.AudioConfig.fromDefaultMicrophoneInput(); - const recognizer = new speechsdk.SpeechRecognizer(speechConfig, audioConfig); - - if (turn.startsWith("user")) setTurn("user-answer-microphone") - else return; - - recognizer.recognizeOnceAsync((result: any) => { - setTurn(turn + "-edit"); - let transcribed; - if (result.reason === ResultReason.RecognizedSpeech) { - transcribed = `${result.text}`; - } else return; - - setText( - currText + - (currText.length > 0 && !currText.endsWith(" ") ? " " : "") + - transcribed - ); - }); -} diff --git a/src/components/utility/Page.tsx b/src/components/global/utility/Page.tsx similarity index 95% rename from src/components/utility/Page.tsx rename to src/components/global/utility/Page.tsx index 3a74913a..ac3a0474 100644 --- a/src/components/utility/Page.tsx +++ b/src/components/global/utility/Page.tsx @@ -1,6 +1,6 @@ import React from "react"; -import Header from "../global/header/Header"; -import Footer from "../global/footer/Footer"; +import Header from "../../global/header/Header"; +import Footer from "../../global/footer/Footer"; import Head from "next/head"; export default function Page({ fullTitle, title, desc, children }: PageProps) { diff --git a/src/components/global/utility/RateLimitingTest.tsx b/src/components/global/utility/RateLimitingTest.tsx deleted file mode 100644 index 360957a8..00000000 --- a/src/components/global/utility/RateLimitingTest.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import React, { useState } from "react"; - -export default function RateLimitingTest() { - const [response, setResponse] = useState | null>( - null - ); - - const makeRequest = async () => { - const res = await fetch("/api/openai", { - method: "POST", - body: JSON.stringify({ - model: "text-davinci-002", - prompt: "Give me a name for a dog.", - temperature: 0.9, - max_tokens: 5, - frequency_penalty: 0.2, - presence_penalty: 0.3, - }), - }); - - setResponse({ - status: res.status, - body: await res.json(), - limit: res.headers.get("X-RateLimit-Limit"), - remaining: res.headers.get("X-RateLimit-Remaining"), - }); - }; - - return ( -
- - {response && ( - -
{JSON.stringify(response, null, 2)}
-
- )} -
- ); -} diff --git a/src/components/home/CTA.tsx b/src/components/home/CTA.tsx deleted file mode 100644 index 07fa6215..00000000 --- a/src/components/home/CTA.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { faArrowRight } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import React from "react"; -import Link from "next/link"; - -export default function CTA() { - return ( -
-
-

- Ready to try it out? - - Explore the modules you can practice. - -

-
-
- - - -
-
-
-
- ); -} diff --git a/src/components/home/Features.tsx b/src/components/home/Features.tsx deleted file mode 100644 index 5a512b26..00000000 --- a/src/components/home/Features.tsx +++ /dev/null @@ -1,280 +0,0 @@ -import { - ChatAlt2Icon, - NewspaperIcon, - TerminalIcon, -} from "@heroicons/react/outline"; - -const nooraFeatures = [ - { - id: 1, - name: "Research-backed methods", - description: - "Dr. Koegel's intervention method, pivotal response training, has been shown to help individuals make friends, have romantic relationships, collaborate in the workplace, and engage in leisure activities.", - icon: NewspaperIcon, - }, - { - id: 2, - name: "AI for automation", - description: - "Because pivotal response training's applicability is restricted by the lack of trained professionals, Noora's goal is to automate the intervention using AI.", - icon: TerminalIcon, - }, - { - id: 3, - name: "Diverse scenarios", - description: - "Dr. Koegel's team has curated multiple modules in different contexts (e.g., workplace, general) and sentiments (e.g., positive news, neutral statements). ", - icon: ChatAlt2Icon, - }, -]; - -export default function Features() { - return ( -
-
- - - {/*
-

- Research-backed Methods -

-

- Lorem ipsum dolor sit amet consectetur adipisicing elit. Possimus - magnam voluptatum cupiditate veritatis in, accusamus quisquam. -

-
*/} - -
-
-

- About Noora -

-

- Noora is a conversational AI designed to help improve the social - skills of people with Autism Spectrum Disorder (ASD). Noora is - designed by{" "} - - Stanford's OVAL Lab - {" "} - in collaboration with{" "} - - Dr. Lynn Koegel - {" "} - in Stanford's Dept. of Psychiatry and Behavioral Sciences. -

- -
- {nooraFeatures.map((item) => ( -
-
-
-
-

- {item.name} -

-
-
- {item.description} -
-
- ))} -
-
- - -
- - - - {/*
-
-
-

- Always in the loop -

-

- Lorem ipsum dolor sit amet consectetur adipisicing elit. Impedit - ex obcaecati natus eligendi delectus, cum deleniti sunt in - labore nihil quod quibusdam expedita nemo. -

- -
- {communicationFeatures.map((item) => ( -
-
-
-
-

- {item.name} -

-
-
- {item.description} -
-
- ))} -
-
- -
- - -
-
-
*/} -
-
- ); -} diff --git a/src/components/home/Hero.tsx b/src/components/home/Hero.tsx index bc817c80..e7df6464 100644 --- a/src/components/home/Hero.tsx +++ b/src/components/home/Hero.tsx @@ -12,7 +12,7 @@ export default function Hero() {

- Improve your social skills using AI. + Improve your social conversation using AI.

Noora is a chatbot who guides you through a diverse set of social @@ -41,13 +41,13 @@ export default function Hero() {

Stanford Medicine Stanford
diff --git a/src/components/home/HomeChat.tsx b/src/components/home/HomeChat.tsx deleted file mode 100644 index 8ffe6526..00000000 --- a/src/components/home/HomeChat.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import React, { useState } from "react"; -import NooraChat from "../interfaces/chat-interface/chat/NooraChat"; -import Menu from "../interfaces/chat-interface/menu/Menu"; -import Summary from "../interfaces/chat-interface/summary/Summary"; - -export default function HomeChat() { - const [h, setH] = useState([]); - const [cs, setCs] = useState({ - draft: "", - turn: "user-answer", - modules: [ - { title: "general", active: true }, - { title: "work", active: true }, - ], - sentiments: [ - { title: "positive", active: true }, - { title: "neutral", active: false }, - { title: "negative", active: true }, - ], - model: { - name: "text-davinci-002", //"curie:ft-open-virtual-assistant-lab-stanford:dataset-v5-model-v4-2022-07-12-23-12-49", - temperature: 0.9, - frequencyPenalty: 0.6, - presencePenalty: 0.5, - goodReplyThreshold: 0.5, - }, - progress: [], - numProblems: 5, - }); - - const history = { - value: h, - setValue: setH, - }; - - const convoState = { - value: cs, - setValue: setCs, - }; - - // prop drilling beyond this point is intentional (for the re-usability of components) - - return ( -
-
-
- {convoState.value.turn == "summary" ? ( - - ) : ( - - )} -
-
- -
-
-
- ); -} diff --git a/src/components/interfaces/ask-noora-interface/AskNooraComponent.tsx b/src/components/interfaces/ask-noora/AskNooraComponent.tsx similarity index 100% rename from src/components/interfaces/ask-noora-interface/AskNooraComponent.tsx rename to src/components/interfaces/ask-noora/AskNooraComponent.tsx diff --git a/src/components/interfaces/ask-noora-interface/InputForm.tsx b/src/components/interfaces/ask-noora/InputForm.tsx similarity index 92% rename from src/components/interfaces/ask-noora-interface/InputForm.tsx rename to src/components/interfaces/ask-noora/InputForm.tsx index 3dbf0224..26111356 100644 --- a/src/components/interfaces/ask-noora-interface/InputForm.tsx +++ b/src/components/interfaces/ask-noora/InputForm.tsx @@ -1,5 +1,5 @@ import React, { useRef } from "react"; -import generateResult from "../../../scripts/generate-advice"; +import generateResult from "../../../scripts/gpt-3/generate-advice"; import { v4 as uuidv4 } from "uuid"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faComment } from "@fortawesome/free-solid-svg-icons"; @@ -42,7 +42,7 @@ export default function InputForm({ query, results, resultsQueue }: any) { type="submit" onClick={(e) => handleSubmit(e)} disabled={query.value.length == 0} - className="text-white absolute right-2.5 bottom-3 md:bottom-2.5 bg-noora-primary hover:bg-noora-primary-dark disabled:bg-noora-primary-dark focus:outline-none font-medium rounded-full text-sm px-4 py-2" + className="text-white absolute right-2.5 bottom-3 md:bottom-2.5 bg-noora-primary hover:bg-noora-primary-dark disabled:bg-slate-400 focus:outline-none font-medium rounded-full text-sm px-4 py-2" > Go diff --git a/src/components/interfaces/ask-noora-interface/Result.tsx b/src/components/interfaces/ask-noora/Result.tsx similarity index 100% rename from src/components/interfaces/ask-noora-interface/Result.tsx rename to src/components/interfaces/ask-noora/Result.tsx diff --git a/src/components/interfaces/chat-interface/chat/MessageBox.tsx b/src/components/interfaces/chat-interface/chat/MessageBox.tsx deleted file mode 100644 index 51041b5a..00000000 --- a/src/components/interfaces/chat-interface/chat/MessageBox.tsx +++ /dev/null @@ -1,248 +0,0 @@ -import { faMicrophone, faPen } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import React, { useEffect, useRef, useState } from "react"; -import getReply from "../../../../scripts/get-reply"; -import { v4 as uuidv4 } from "uuid"; -import Microphone from "../../../global/utility/Microphone"; -import { clsx } from "clsx"; - -export default function MessageBox({ history, convoState }: any) { - const inputBoxRef = useRef(null); - - const handleSubmit = async (e: any, message?: string) => { - e.preventDefault(); - - // stop audio - if (convoState.value.currentAudio.player) { - convoState.value.currentAudio.player.pause() - convoState.value.currentAudio.player.close() - } - - message = message ? message : convoState.value.draft.slice(); - if (!message) message = "" - - let userMsgId = uuidv4(); - - history.setValue((h: any) => [ - ...h, - { id: userMsgId, fromNoora: false, text: message, show: true }, - ]); - convoState.setValue((cs: any) => ({ ...cs, draft: "" })); - - let m = message.trim().toLowerCase(); - - if (convoState.value.turn.includes("start")) { - if (m.includes("no") || m.includes("don")) { - history.setValue((h: any) => [ - ...h, - { id: uuidv4(), fromNoora: true, text: "Are you ready to begin?" }, - ]); - } else { - await noorasTurn(message, convoState, history, true); - } - } else if ( - convoState.value.progress.length < convoState.value.numProblems - ) { - await noorasTurn(message, convoState, history); - } else { - if (m == "yes") { - convoState.setValue((cs: any) => ({ - ...cs, - numProblems: cs.numProblems + 3, - })); - await noorasTurn(message, convoState, history, true); - } else { - convoState.setValue((cs: any) => ({ ...cs, turn: "summary" })); - } - } - }; - - useEffect(() => { - if ( - (convoState.value.turn.startsWith("user-answer") && - history.value - .slice(0, Math.min(history.value.length, 5)) - .filter((h: any) => !h.fromNoora).length > 0) || - convoState.value.turn.endsWith("-edit") - ) { - // don't autofocus on page load (especially for mobile) - // hence the history check, but we want to autofocus if microphone used - if (inputBoxRef.current) inputBoxRef.current.focus(); - } - }, [convoState.value]); - - return ( -
- {convoState.value.turn.startsWith("user-select") ? - : - } - - ); -} - -function SelectInputForm({ convoState, options, handleSubmit }: any) { - return (
- {options.map((o: string) => )} -
) -} - -function MessageInputForm({ convoState, inputBoxRef, handleSubmit }: any) { - return
-
- {convoState.value.turn.includes("microphone") ? ( - - ) : ( - - )} -
- - { - convoState.setValue((cs: any) => ({ - ...cs, - draft: e.target.value, - })); - }} - value={convoState.value.draft} - placeholder={ - (convoState.value.turn.startsWith("user-answer") && !convoState.value.turn.includes("noora-reads")) - ? (convoState.value.turn.includes("microphone") - ? "Speak into your microphone..." - : "Send message...") - : "Please wait for Noora..." - } - disabled={ - !convoState.value.turn.startsWith("user-answer") || - convoState.value.turn.includes("microphone") || convoState.value.turn.includes("noora-reads") - } - className={clsx( - "block focus:border-gray-400 ring-0 focus:ring-0 p-4 pl-12 pr-32 w-full border-2 focus:outline-none shadow-sm sm:text-sm rounded-full text-slate-800", - convoState.value.turn.includes("microphone") - ? "border-noora-primary disabled:bg-slate-200 placeholder-slate-600 " - : "border-gray-400 disabled:bg-gray-100" - )} - /> -
- - convoState.setValue((cs: any) => ({ - ...cs, - turn: str, - })) - } - setText={(str: string) => - convoState.setValue((cs: any) => ({ - ...cs, - draft: str, - })) - } - currText={convoState.value.draft} - /> -
- -
-} - -async function noorasTurn( - message: string, - convoState: any, - history: any, - noorasTurn = false -) { - if (convoState.value.statement) { - if (convoState.value.turn.includes("select")) { - // handle sentiment classification - const targetSentiment = convoState.value.statement.statementObj[0].split("/")[1] - const correct = message.trim().toLowerCase() == targetSentiment.trim().toLowerCase() - - - history.setValue((h: any) => [...h, { - id: uuidv4(), - fromNoora: true, - read: correct ? null : `Actually, this is a ${targetSentiment} statement.`, - sentiment: correct ? "positive" : "neutral", - text: correct ? "That's right!" : targetSentiment, - correction: !correct, - }, - { - id: uuidv4(), - fromNoora: true, - text: "How would you reply to it?", - }] - ); - - convoState.setValue((cs: any) => ({ - ...cs, - turn: "user-answer", - })); - - return - } - else { - // rate reply if statement was given - const replies = await getReply(message, convoState, "rate-reply"); - history.setValue((h: any) => [...h, ...replies]); - } - } - - // get statement if still need to practice - // note that progress state has not updated yet, so we use (length + 1) - if ( - convoState.value.progress.length + 1 < convoState.value.numProblems || - noorasTurn - ) { - const replies = await getReply(message, convoState, "get-statement"); - - history.setValue((h: any) => [ - ...h, - { - fromNoora: true, - id: uuidv4(), - text: - convoState.value.turn.includes("start") - ? "Let's get started." - : "Let's try another one.", - }, - ...replies, - ]); - } else { - history.setValue((h: any) => [ - ...h, - { - id: uuidv4(), - fromNoora: true, - text: `Good job! You practiced ${convoState.value.numProblems} scenarios. Do you want to continue practicing?`, - }, - ]); - convoState.setValue((cs: any) => ({ ...cs, turn: "user-select-end" })); - } -} diff --git a/src/components/interfaces/chat-interface/chat/Messages.tsx b/src/components/interfaces/chat-interface/chat/Messages.tsx deleted file mode 100644 index c50852e0..00000000 --- a/src/components/interfaces/chat-interface/chat/Messages.tsx +++ /dev/null @@ -1,257 +0,0 @@ -import React, { useRef, useState, useEffect } from "react"; -import { clsx } from "clsx"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faMicrophone, faVolumeUp } from "@fortawesome/free-solid-svg-icons"; -import { InformationCircleIcon } from "@heroicons/react/outline"; - -import dynamic from "next/dynamic"; -import { ACTIONS, EVENTS, STATUS } from "react-joyride"; -import SpeechSynthesizer from "../../../global/utility/SpeechSynthesizer"; -import { messageToSpeechParams } from "../../../../scripts/util"; -const JoyRideNoSSR = dynamic(() => import("react-joyride"), { ssr: false }); - -export default function Messages({ history, convoState }: any) { - const messagesBottom = useRef(null); - - let currentAudioRef = useRef() - - currentAudioRef.current = convoState.value.currentAudio - - useEffect(() => { - if (history.value.length > 0) return; // only run this on first render - let activeModules = convoState.value.modules.filter((m: any) => m.active); - - history.setValue((h: any) => [ - ...h, - { - id: -1, - show: true, - fromNoora: true, - text: "Hi! I am Noora.", - read: "Hi. I am Nora.", - style: "cheerful" - }, - { - id: -2, - show: true, - fromNoora: true, - text: `Imagine that I am your ${activeModules.length == 1 && activeModules[0].title == "work" - ? "co-worker" - : "friend" - }.`, - }, - { - id: -3, - fromNoora: true, - show: true, - component: , - read: "You can tap on the microphone button to speak. Click the audio button to hear Noora's replies" - }, - { - id: -4, - fromNoora: true, - show: true, - text: "Are you ready to begin?", - }, - ]); - convoState.setValue((cs: any) => ({ ...cs, turn: "user-answer-start" })); - }, []); - - // scrolling - useEffect(() => { - setTimeout(() => { - if (messagesBottom.current) - if ( - history.value - .slice(0, Math.min(history.value.length, 6)) - .filter((h: any) => !h.fromNoora).length > 0 - ) - messagesBottom.current.scrollIntoView({ - behavior: "smooth", - block: "nearest", - }); - }, 5); - }, [history.value]); - - return ( -
-
    - {history.value.map((message: any) => ( - - ))} - {(!convoState.value.turn.startsWith("user") && !convoState.value.turn.includes("read")) && ( -
    -
    -
    -
    -
    -
    -
    -
    -
    - )} -
-
- . -
-
- ); -} - -function MessageWrapper({ message, currentAudioRef, convoState }: any) { - if (!message.show) - return <> - - return
  • - {message && ( -
    -
    - {message.text ? : message.component} - {message.fromNoora && } -
    -
    - )} -
  • -} - -function Message({ message }: any) { - if (message.statement) - return ( -
    - - - “{message.text}” - {" "} - Is this a positive, neutral, or negative statement? - -
    - ); - else if (message.suggestion) - return ( -
    - - A better reply might've been:{" "} - - “{message.text}” - - -
    - ); - else if (message.correction) - return ( -
    - - Actually, this is a{" "} - - {message.text} - statement. - -
    - ); - return
    {message.text}
    ; -} - -function MicrophoneInfoElement() { - const [joyrideState, setJoyrideState] = useState({ - run: false, - steps: [ - { - target: ".joyride-step-1", - content: - "Tap this button, then speak!\n Noora will turn what you say into text.", - disableBeacon: true, - }, - { - target: ".demo-audio", - content: - "Tap this button to hear Noora speak! Noora can speak in sad and happy tones.", - disableBeacon: true, - }, - ], - stepIndex: 0, - }); - - const handleJoyrideCallback = (data: any) => { - const { action, index, status, type } = data; - - if ([EVENTS.STEP_AFTER, EVENTS.TARGET_NOT_FOUND].includes(type)) { - // Update state to advance the tour - setJoyrideState((js: any) => ({ - ...js, - stepIndex: index + (action === ACTIONS.PREV ? -1 : 1), - })); - } else if ([STATUS.FINISHED, STATUS.SKIPPED].includes(status)) { - // Need to set our running state to false, so we can restart if we click start again. - setJoyrideState((js: any) => ({ ...js, stepIndex: 0, run: false })); - } - }; - - return ( -
    - - You can tap on the{" "} - { - setJoyrideState((js: any) => ({ ...js, run: true })); - }} - className="w-4 h-4 text-noora-primary inline-block mb-1 px-0.5 cursor-pointer" - />{" "} - button to speak. Click the { - setJoyrideState((js: any) => ({ ...js, run: true, stepIndex: 1 })); - }} - className="w-5 h-5 text-noora-primary inline-block mb-1 px-0.5 cursor-pointer" - /> button to hear Noora‘s replies. - -
    - ); -} - -function SpeechButton({ convoState, message, currentAudioRef }: any) { - const props = messageToSpeechParams(convoState, message, currentAudioRef, null, null) - - return ( - ) -} \ No newline at end of file diff --git a/src/components/interfaces/chat-interface/chat/NooraChat.tsx b/src/components/interfaces/chat-interface/chat/NooraChat.tsx deleted file mode 100644 index fad572f2..00000000 --- a/src/components/interfaces/chat-interface/chat/NooraChat.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import React, { useEffect, useRef, useState } from "react"; -import MessageBox from "./MessageBox"; -import Header from "./Header"; -import Messages from "./Messages"; -import { messageToSpeechParams } from "../../../../scripts/util"; -import { textToSpeech } from "../../../global/utility/SpeechSynthesizer"; - -export default function NooraChat({ - convoState, - history, -}: any) { - let currentAudioRef = useRef() - - currentAudioRef.current = convoState.value.currentAudio - - useEffect(() => { - if (convoState.value.autoPlaying) - return; - - let idxHidden = history.value.findIndex((m: any) => !m.show); - - if (idxHidden == -1) { - convoState.setValue((cs: any) => ({ ...cs, turn: convoState.value.turn.split("-noora-reads")[0] })) - return; - } - - let item = history.value[idxHidden] - - const prevFromUser = !history.value[idxHidden - 1].fromNoora - - if (prevFromUser) { - let hidden = history.value.filter((m: any) => !m.show); - const props = messageToSpeechParams(convoState, item, currentAudioRef, history, hidden) - textToSpeech(props) - - history.setValue((h: any) => { - return h.map((m: any) => { - if (m.id == item.id) - return { ...item, show: true } - else - return m - }) - }) - convoState.setValue((cs: any) => ({ ...cs, autoPlaying: true })) - } - - }, [history.value]) - - return ( -
    -
    - - -
    - ); -} \ No newline at end of file diff --git a/src/components/interfaces/chat-interface/menu/Menu.tsx b/src/components/interfaces/chat-interface/menu/Menu.tsx deleted file mode 100644 index 6450975d..00000000 --- a/src/components/interfaces/chat-interface/menu/Menu.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import React from "react"; -import { Disclosure } from "@headlessui/react"; -import { ChevronUpIcon } from "@heroicons/react/solid"; -import Modules from "./sections/Modules"; -import Progress from "./sections/Progress"; -import Technical from "./sections/Technical"; -import DisclosureTransition from "../../../global/utility/DisclosureTransition"; - -export default function Menu({ convoState }: any) { - const sections = [ - { title: "Progress", component: }, - { - title: "Modules", - component: , - defaultHide: !convoState.value.showTechnical, - }, - { - title: "Technical", - component: , - defaultHide: !convoState.value.showTechnical, - }, - ]; - - return ( -
    -
    -
    - Menu -
    -
    -
    - {sections.map((section) => ( - - {({ open }) => ( -
    - - {section.title} - - - - - {section.component} - - {" "} -
    - )} -
    - ))} -
    -
    - ); -} diff --git a/src/components/interfaces/chat-interface/menu/sections/Modules.tsx b/src/components/interfaces/chat-interface/menu/sections/Modules.tsx deleted file mode 100644 index bf7cb24c..00000000 --- a/src/components/interfaces/chat-interface/menu/sections/Modules.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import React from "react"; -import { clsx } from "clsx"; -import { PlusIcon } from "@heroicons/react/solid"; - -export default function Modules({ convoState }: any) { - return ( -
    - - -
    - ); -} - -function Topics({ convoState }: any) { - return ( -
    - Topics:{" "} - - {convoState.value.modules - // .sort((m1: any, m2: any) => (m2.active && !m1.active ? 1 : -1)) - .map((module: any) => { - return ( - - ); - })} - -
    - ); -} - -function Sentiments({ convoState }: any) { - return ( -
    - Sentiments:{" "} - - {convoState.value.sentiments.map((sentiment: any) => { - return ( -
    - -
    - ); - })} -
    -
    - ); -} diff --git a/src/components/interfaces/chat-interface/chat/Header.tsx b/src/components/interfaces/noora-chat/chat/Header.tsx similarity index 86% rename from src/components/interfaces/chat-interface/chat/Header.tsx rename to src/components/interfaces/noora-chat/chat/Header.tsx index fe82f58b..5e964148 100644 --- a/src/components/interfaces/chat-interface/chat/Header.tsx +++ b/src/components/interfaces/noora-chat/chat/Header.tsx @@ -1,4 +1,5 @@ import React from "react"; +import MobileMenu from "../menu/MobileMenu"; export default function Header() { return ( @@ -7,6 +8,7 @@ export default function Header() {
    Noora
    +
    ); } diff --git a/src/components/interfaces/noora-chat/chat/NooraChat.tsx b/src/components/interfaces/noora-chat/chat/NooraChat.tsx new file mode 100644 index 00000000..d4854bef --- /dev/null +++ b/src/components/interfaces/noora-chat/chat/NooraChat.tsx @@ -0,0 +1,98 @@ +import React, { useEffect, useRef, useState } from "react"; +import MessageBox from "./message-box/MessageBox"; +import Header from "./Header"; +import Messages from "./message-window/Messages"; +import { messageToSpeechParams } from "../../../../scripts/noora-chat/audio_utils"; +import { textToSpeech } from "../../speech/SpeechSynthesizer"; + +export default function NooraChat({ + convoState, + history, +}: any) { + let audioRef = useRef() + + audioRef.current = convoState.value.audio + + useEffect(() => { + return () => { + // on component unmount + const player = (audioRef.current as any).player as any + if (player) { + player.pause() + player.close() + } + } + }, []) + + useEffect(() => { + if (!convoState.value.audio.shouldAutoPlay) { + convoState.setValue((cs: any) => ({ ...cs, turn: convoState.value.turn.split("-noora-reads")[0], audio: { ...cs.audio, autoPlaying: false } })) + // stop audio + if (convoState.value.audio.player && convoState.autoPlaying) { + convoState.value.audio.player.pause() + convoState.value.audio.player.close() + } + let idxHidden = history.value.findIndex((m: any) => !m.show); + + if (idxHidden == -1) + return; + + let item = history.value[idxHidden] + + history.setValue((h: any) => { + return h.map((m: any) => { + if (m.id == item.id) + return { ...item, show: true } + else + return m + }) + }) + + return; + } + + if (convoState.value.audio.autoPlaying) + return; + + let idxHidden = history.value.findIndex((m: any) => !m.show); + + if (idxHidden == -1) { + convoState.setValue((cs: any) => ({ ...cs, turn: convoState.value.turn.split("-noora-reads")[0] })) + return; + } + + convoState.setValue((cs: any) => ({ ...cs, turn: convoState.value.turn.split("-noora-reads")[0] + "-noora-reads" })) + + let item = history.value[idxHidden] + + const prevFromUser = !history.value[idxHidden - 1].fromNoora + + if (prevFromUser) { + let hidden = history.value.filter((m: any) => !m.show); + const props = messageToSpeechParams(convoState, item, audioRef, history, hidden) + textToSpeech(props) + + history.setValue((h: any) => { + return h.map((m: any) => { + if (m.id == item.id) + return { ...item, show: true } + else + return m + }) + }) + convoState.setValue((cs: any) => ({ ...cs, audio: { ...cs.audio, autoPlaying: true } })) + } + + }, [history.value, convoState.value.audio.shouldAutoPlay]) + + return ( +
    +
    + + +
    + ); +} \ No newline at end of file diff --git a/src/components/interfaces/noora-chat/chat/message-box/MessageBox.tsx b/src/components/interfaces/noora-chat/chat/message-box/MessageBox.tsx new file mode 100644 index 00000000..922659a2 --- /dev/null +++ b/src/components/interfaces/noora-chat/chat/message-box/MessageBox.tsx @@ -0,0 +1,33 @@ +import React, { useEffect, useRef } from "react"; +import handleSubmit from "../../../../../scripts/noora-chat/handle-submit"; +import SelectInput from "./SelectInput"; +import TextInput from "./TextInput"; + +export default function MessageBox({ history, convoState }: any) { + const inputBoxRef = useRef(null); + + useEffect(() => { + if ( + (convoState.value.turn.startsWith("user-answer") && + history.value + .slice(0, Math.min(history.value.length, 10)) + .filter((h: any) => !h.fromNoora).length > 0) || + convoState.value.turn.endsWith("-edit") + ) { + // don't autofocus on page load (especially for mobile) + // hence the history check, but we want to autofocus if microphone used + if (inputBoxRef.current) inputBoxRef.current.focus(); + } + }, [convoState.value]); + + return ( +
    + {convoState.value.turn.startsWith("user-select") ? + + : { handleSubmit(e, convoState, history) }} />} + + ); +} \ No newline at end of file diff --git a/src/components/interfaces/noora-chat/chat/message-box/SelectInput.tsx b/src/components/interfaces/noora-chat/chat/message-box/SelectInput.tsx new file mode 100644 index 00000000..ac190b93 --- /dev/null +++ b/src/components/interfaces/noora-chat/chat/message-box/SelectInput.tsx @@ -0,0 +1,16 @@ +import React from "react"; + +export default function SelectInput({ convoState, history, options, handleSubmit }: any) { + return (
    + {options.map((o: string) => )} +
    ) +} + diff --git a/src/components/interfaces/noora-chat/chat/message-box/TextInput.tsx b/src/components/interfaces/noora-chat/chat/message-box/TextInput.tsx new file mode 100644 index 00000000..4f6bf260 --- /dev/null +++ b/src/components/interfaces/noora-chat/chat/message-box/TextInput.tsx @@ -0,0 +1,80 @@ +import React from "react"; +import { faMicrophone, faPen } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import Microphone from "../../../speech/Microphone"; +import { clsx } from "clsx"; + +export default function TextInput({ convoState, inputBoxRef, handleSubmit }: any) { + return
    +
    + {convoState.value.turn.includes("microphone") ? ( + + ) : ( + + )} +
    + + { + convoState.setValue((cs: any) => ({ + ...cs, + draft: e.target.value, + })); + }} + value={convoState.value.draft} + placeholder={ + (convoState.value.turn.startsWith("user-answer") && !convoState.value.turn.includes("noora-reads")) + ? (convoState.value.turn.includes("microphone") + ? "Speak into your microphone..." + : "Send message...") + : "Please wait for Noora..." + } + disabled={ + !convoState.value.turn.startsWith("user-answer") || + convoState.value.turn.includes("microphone") || convoState.value.turn.includes("noora-reads") + } + className={clsx( + "block focus:border-gray-400 ring-0 focus:ring-0 p-4 pl-12 pr-32 w-full border-2 focus:outline-none shadow-sm sm:text-sm rounded-full text-slate-800", + convoState.value.turn.includes("microphone") + ? "border-noora-primary disabled:bg-slate-200 placeholder-slate-600 " + : "border-gray-400 disabled:bg-gray-100" + )} + /> +
    + + convoState.setValue((cs: any) => ({ + ...cs, + turn: str, + })) + } + setText={(str: string) => + convoState.setValue((cs: any) => ({ + ...cs, + draft: str, + })) + } + currText={convoState.value.draft} + /> +
    + +
    +} diff --git a/src/components/interfaces/noora-chat/chat/message-window/Message.tsx b/src/components/interfaces/noora-chat/chat/message-window/Message.tsx new file mode 100644 index 00000000..4a405ede --- /dev/null +++ b/src/components/interfaces/noora-chat/chat/message-window/Message.tsx @@ -0,0 +1,52 @@ +import React from "react"; +import MessageWrapper from "./MessageWrapper"; + +export default function Message({ message, audioRef, convoState }: any) { + if (!message.show) + return <> + + return
  • + {message && ( + + {message.text ? : message.component} + + )} +
  • +} + +function MessageText({ message }: any) { + if (message.statement) + return ( +
    + + + “{message.text}” + {" "} + When I say this, do I sound positive, neutral, or negative? + +
    + ); + else if (message.suggestion) + return ( +
    + + A better reply might've been:{" "} + + “{message.text}” + + +
    + ); + else if (message.correction) + return ( +
    + + Actually, this is a{" "} + + {message.text} + statement. + +
    + ); + return
    {message.text}
    ; +} \ No newline at end of file diff --git a/src/components/interfaces/noora-chat/chat/message-window/MessageWrapper.tsx b/src/components/interfaces/noora-chat/chat/message-window/MessageWrapper.tsx new file mode 100644 index 00000000..02edfb61 --- /dev/null +++ b/src/components/interfaces/noora-chat/chat/message-window/MessageWrapper.tsx @@ -0,0 +1,33 @@ +import React from "react" + +import { clsx } from "clsx"; + +import SpeechSynthesizer from "../../../speech/SpeechSynthesizer"; +import { messageToSpeechParams } from "../../../../../scripts/noora-chat/audio_utils"; + +export default function MessageWrapper({ message, audioRef, convoState, children }: any) { + return
    +
    + {children} + {message.fromNoora && } +
    + +
    +} + + + +function SpeechButton({ convoState, message, audioRef }: any) { + const props = messageToSpeechParams(convoState, message, audioRef, null, null) + + return ( + ) +} diff --git a/src/components/interfaces/noora-chat/chat/message-window/Messages.tsx b/src/components/interfaces/noora-chat/chat/message-window/Messages.tsx new file mode 100644 index 00000000..d299ff22 --- /dev/null +++ b/src/components/interfaces/noora-chat/chat/message-window/Messages.tsx @@ -0,0 +1,112 @@ +import React, { useRef, useEffect } from "react"; +import Message from "./Message"; +import MicrophoneInfo from "./initial-messages/MicrophoneInfo"; +import Instructions from "./initial-messages/Instructions"; + +export default function Messages({ history, convoState }: any) { + const messagesBottom = useRef(null); + + let audioRef = useRef() + + audioRef.current = convoState.value.audio + + useEffect(() => { + if (history.value.length > 0) return; // only run this on first render + let activeModules = convoState.value.modules.filter((m: any) => m.active); + + history.setValue((h: any) => [ + ...h, + { + id: -1, + show: true, + fromNoora: true, + text: "Hi! I am Noora.", + read: "Hi. I am Nora.", + style: "cheerful" + }, + { + id: -2, + show: true, + fromNoora: true, + text: `Imagine that I am your ${activeModules.length == 1 && activeModules[0].title == "work" + ? "co-worker" + : "friend" + }.`, + }, + { + id: -3, + fromNoora: true, + show: true, + component: , + read: "You can tap on the microphone button to start speaking. When you're done talking, click it again. Click the audio button to hear my replies" + }, + { + id: -4, + show: true, + fromNoora: true, + component: , + read: "If I say, “I had a good weekend!”, you should tell me that this is a positive statement. Then, you can reply, “I'm happy to hear that!”" + }, + // { + // id: -4, + // show: true, + // fromNoora: true, + // text: "" + // }, + { + id: -6, + fromNoora: true, + show: true, + text: "Are you ready to begin?", + }, + ]); + convoState.setValue((cs: any) => ({ ...cs, turn: "user-answer-start" })); + }, []); + + // scrolling + useEffect(() => { + setTimeout(() => { + if (messagesBottom.current) + if ( + history.value + .slice(0, Math.min(history.value.length, 10)) + .filter((h: any) => !h.fromNoora).length > 0 + ) + messagesBottom.current.scrollIntoView({ + behavior: "smooth", + block: "nearest", + }); + }, 5); + }, [history.value]); + + return ( +
    +
      + {history.value.map((message: any) => ( + + ))} + {(!convoState.value.turn.startsWith("user") && !convoState.value.turn.includes("read")) && ( +
      +
      +
      +
      +
      +
      +
      +
      +
      + )} +
    +
    + . +
    +
    + ); +} \ No newline at end of file diff --git a/src/components/interfaces/noora-chat/chat/message-window/initial-messages/Instructions.tsx b/src/components/interfaces/noora-chat/chat/message-window/initial-messages/Instructions.tsx new file mode 100644 index 00000000..53a451cb --- /dev/null +++ b/src/components/interfaces/noora-chat/chat/message-window/initial-messages/Instructions.tsx @@ -0,0 +1,5 @@ +import React from "react"; + +export default function Instructions() { + return
    If I say, “I had a good weekend,” you should say that this is a positive statement. Then, you can reply, “I'm happy for you! What did you do?”
    +} \ No newline at end of file diff --git a/src/components/interfaces/noora-chat/chat/message-window/initial-messages/MicrophoneInfo.tsx b/src/components/interfaces/noora-chat/chat/message-window/initial-messages/MicrophoneInfo.tsx new file mode 100644 index 00000000..522d80a3 --- /dev/null +++ b/src/components/interfaces/noora-chat/chat/message-window/initial-messages/MicrophoneInfo.tsx @@ -0,0 +1,91 @@ +import React, { useState, } from "react"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faMicrophone, faVolumeUp } from "@fortawesome/free-solid-svg-icons"; +import { InformationCircleIcon } from "@heroicons/react/outline"; + +import dynamic from "next/dynamic"; +import { ACTIONS, EVENTS, STATUS } from "react-joyride"; +const JoyRideNoSSR = dynamic(() => import("react-joyride"), { ssr: false }); + +export default function MicrophoneInfo() { + const [joyrideState, setJoyrideState] = useState({ + run: false, + steps: [ + { + target: ".joyride-step-1", + content: + "Tap this button, then start speaking!\n Noora will turn what you say into text. When you're done talking, click this button again.", + disableBeacon: true, + }, + { + target: ".demo-audio", + content: + "Tap this button to hear Noora speak! Noora can speak in sad and happy tones.", + disableBeacon: true, + }, + ], + stepIndex: 0, + }); + + const handleJoyrideCallback = (data: any) => { + const { action, index, status, type } = data; + + if ([EVENTS.STEP_AFTER, EVENTS.TARGET_NOT_FOUND].includes(type)) { + // Update state to advance the tour + setJoyrideState((js: any) => ({ + ...js, + stepIndex: index + (action === ACTIONS.PREV ? -1 : 1), + })); + } else if ([STATUS.FINISHED, STATUS.SKIPPED].includes(status)) { + // Need to set our running state to false, so we can restart if we click start again. + setJoyrideState((js: any) => ({ ...js, stepIndex: 0, run: false })); + } + }; + + return ( +
    + + You can tap on the{" "} + { + setJoyrideState((js: any) => ({ ...js, run: true })); + }} + className="w-4 h-4 text-noora-primary inline-block mb-1 px-0.5 cursor-pointer" + />{" "} + button to start speaking. When you're done talking, click it again. Click the { + setJoyrideState((js: any) => ({ ...js, run: true, stepIndex: 1 })); + }} + className="w-5 h-5 text-noora-primary inline-block mb-1 px-0.5 cursor-pointer" + /> button to hear my replies. + +
    + ); +} diff --git a/src/components/interfaces/noora-chat/menu/DesktopMenu.tsx b/src/components/interfaces/noora-chat/menu/DesktopMenu.tsx new file mode 100644 index 00000000..84f6d1ac --- /dev/null +++ b/src/components/interfaces/noora-chat/menu/DesktopMenu.tsx @@ -0,0 +1,94 @@ +import React from "react"; +import { Disclosure } from "@headlessui/react"; +import { ChevronUpIcon, VolumeUpIcon } from "@heroicons/react/solid"; +import Modules from "./sections/modules/Modules"; +import Progress from "./sections/Progress"; +import Technical from "./sections/Technical"; +import DisclosureTransition from "../../../global/utility/DisclosureTransition"; +import { Switch } from '@headlessui/react' +import clsx from "clsx"; +import { MenuAlt2Icon } from "@heroicons/react/outline"; +import { isIOS } from 'react-device-detect'; + +export default function DesktopMenu({ convoState }: any) { + const sections = [ + { title: "Progress", component: }, + { + title: "Modules", + component: , + defaultHide: !convoState.value.showTechnical, + }, + { + title: "Technical", + component: , + defaultHide: !convoState.value.showTechnical, + }, + ]; + + return ( +
    +
    +
    + Menu +
    + + {!isIOS &&
    + text-only + { + if (convoState.value.audio.autoPlaying) { + if (convoState.value.audio.player) { + convoState.value.audio.player.pause() + convoState.value.audio.player.close() + } + convoState.setValue((cs: any) => ({ ...cs, audio: { ...cs.audio, player: null, messageId: null, shouldAutoPlay: !cs.audio.shouldAutoPlay, autoPlaying: false } })); + } else { + convoState.setValue((cs: any) => ({ ...cs, audio: { ...cs.audio, shouldAutoPlay: !cs.audio.shouldAutoPlay, autoPlaying: false } })); + } + }} + className={clsx( + convoState.value.audio.shouldAutoPlay ? 'bg-noora-primary' : 'bg-gray-200', + 'relative inline-flex flex-shrink-0 h-5 w-9 border-2 border-transparent mx-1.5 -bottom-0.5 rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-0' + )} + > + + autoplay +
    } + +
    +
    + {sections.map((section) => ( + + {({ open }) => ( +
    + + {section.title} + + + + + {section.component} + + {" "} +
    + )} +
    + ))} +
    +
    + ); +} diff --git a/src/components/interfaces/noora-chat/menu/MenuContent.tsx b/src/components/interfaces/noora-chat/menu/MenuContent.tsx new file mode 100644 index 00000000..64dcaf02 --- /dev/null +++ b/src/components/interfaces/noora-chat/menu/MenuContent.tsx @@ -0,0 +1,94 @@ +import React from "react"; +import { Disclosure } from "@headlessui/react"; +import { ChevronUpIcon, VolumeUpIcon } from "@heroicons/react/solid"; +import Modules from "./sections/modules/Modules"; +import Progress from "./sections/Progress"; +import Technical from "./sections/Technical"; +import DisclosureTransition from "../../../global/utility/DisclosureTransition"; +import { Switch } from '@headlessui/react' +import clsx from "clsx"; +import { MenuAlt2Icon } from "@heroicons/react/outline"; +import { isIOS } from 'react-device-detect'; + +export default function DesktopMenu({ convoState }: any) { + const sections = [ + { title: "Progress", component: }, + { + title: "Modules", + component: , + defaultHide: !convoState.value.showTechnical, + }, + { + title: "Technical", + component: , + defaultHide: !convoState.value.showTechnical, + }, + ]; + + return ( +
    +
    +
    + Menu +
    + + {!isIOS &&
    + text-only + { + if (convoState.value.audio.autoPlaying) { + if (convoState.value.audio.player) { + convoState.value.audio.player.pause() + convoState.value.audio.player.close() + } + convoState.setValue((cs: any) => ({ ...cs, audio: { ...cs.audio, player: null, messageId: null, shouldAutoPlay: !cs.audio.shouldAutoPlay, autoPlaying: false } })); + } else { + convoState.setValue((cs: any) => ({ ...cs, audio: { ...cs.audio, shouldAutoPlay: !cs.audio.shouldAutoPlay, autoPlaying: false } })); + } + }} + className={clsx( + convoState.value.audio.shouldAutoPlay ? 'bg-noora-primary' : 'bg-gray-200', + 'relative inline-flex flex-shrink-0 h-5 w-9 border-2 border-transparent mx-1.5 -bottom-0.5 rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-0' + )} + > + + autoplay +
    } + +
    +
    + {sections.map((section) => ( + + {({ open }) => ( +
    + + {section.title} + + + + + {section.component} + + {" "} +
    + )} +
    + ))} +
    +
    + ); +} diff --git a/src/components/interfaces/noora-chat/menu/MobileMenu.tsx b/src/components/interfaces/noora-chat/menu/MobileMenu.tsx new file mode 100644 index 00000000..b3fa6be0 --- /dev/null +++ b/src/components/interfaces/noora-chat/menu/MobileMenu.tsx @@ -0,0 +1,91 @@ +import { Fragment, useState } from 'react' +import { Dialog, Transition } from '@headlessui/react' +import { + AdjustmentsIcon, + XIcon +} from '@heroicons/react/solid' + +export default function MobileMenu() { + const [sidebarOpen, setSidebarOpen] = useState(false) + + return ( +
    + + + +
    + + +
    + + + +
    + +
    +
    +
    + Your Company +
    +
    + +
    +
    +
    + +
    +
    +
    + +
    + +
    +
    + ) +} diff --git a/src/components/interfaces/chat-interface/menu/sections/Progress.tsx b/src/components/interfaces/noora-chat/menu/sections/Progress.tsx similarity index 100% rename from src/components/interfaces/chat-interface/menu/sections/Progress.tsx rename to src/components/interfaces/noora-chat/menu/sections/Progress.tsx diff --git a/src/components/interfaces/chat-interface/menu/sections/Technical.tsx b/src/components/interfaces/noora-chat/menu/sections/Technical.tsx similarity index 100% rename from src/components/interfaces/chat-interface/menu/sections/Technical.tsx rename to src/components/interfaces/noora-chat/menu/sections/Technical.tsx diff --git a/src/components/interfaces/noora-chat/menu/sections/modules/Modules.tsx b/src/components/interfaces/noora-chat/menu/sections/modules/Modules.tsx new file mode 100644 index 00000000..3a9dd2a1 --- /dev/null +++ b/src/components/interfaces/noora-chat/menu/sections/modules/Modules.tsx @@ -0,0 +1,12 @@ +import React from "react"; +import Topics from "./Topics"; +import Sentiments from "./Sentiments"; + +export default function Modules({ convoState }: any) { + return ( +
    + + +
    + ); +} diff --git a/src/components/interfaces/noora-chat/menu/sections/modules/Sentiments.tsx b/src/components/interfaces/noora-chat/menu/sections/modules/Sentiments.tsx new file mode 100644 index 00000000..20e874c2 --- /dev/null +++ b/src/components/interfaces/noora-chat/menu/sections/modules/Sentiments.tsx @@ -0,0 +1,43 @@ +import React from "react"; + +export default function Sentiments({ convoState }: any) { + return ( +
    + Sentiments:{" "} + + {convoState.value.sentiments.map((sentiment: any) => { + return ( +
    + +
    + ); + })} +
    +
    + ); +} diff --git a/src/components/interfaces/noora-chat/menu/sections/modules/Topics.tsx b/src/components/interfaces/noora-chat/menu/sections/modules/Topics.tsx new file mode 100644 index 00000000..eec747d6 --- /dev/null +++ b/src/components/interfaces/noora-chat/menu/sections/modules/Topics.tsx @@ -0,0 +1,52 @@ +import React from "react"; +import { clsx } from "clsx"; +import { PlusIcon } from "@heroicons/react/solid"; + +export default + function Topics({ convoState }: any) { + return ( +
    + Topics:{" "} + + {convoState.value.modules + // .sort((m1: any, m2: any) => (m2.active && !m1.active ? 1 : -1)) + .map((module: any) => { + return ( + + ); + })} + +
    + ); +} \ No newline at end of file diff --git a/src/components/interfaces/chat-interface/summary/Footer.tsx b/src/components/interfaces/noora-chat/summary/Footer.tsx similarity index 82% rename from src/components/interfaces/chat-interface/summary/Footer.tsx rename to src/components/interfaces/noora-chat/summary/Footer.tsx index 3c8eed52..9cd0e22c 100644 --- a/src/components/interfaces/chat-interface/summary/Footer.tsx +++ b/src/components/interfaces/noora-chat/summary/Footer.tsx @@ -6,9 +6,9 @@ import React from "react"; export default function Footer({ convoState, history }: any) { return (
    - + + ); +} + +async function initRecognizer(): Promise { + const tokenObj = await getTokenOrRefresh(); + + const speechConfig = SpeechConfig.fromAuthorizationToken( + tokenObj.authToken, + tokenObj.region + ); + speechConfig.speechRecognitionLanguage = "en-US"; + + const audioConfig = AudioConfig.fromDefaultMicrophoneInput(); + const recognizer: SpeechRecognizer = new SpeechRecognizer( + speechConfig, + audioConfig + ); + return recognizer; +} + +async function sttFromMic( + turn: string, + setTurn: any, + setText: any, + currText: string, + recognizer: SpeechRecognizer +) { + if (turn.startsWith("user")) setTurn("user-answer-microphone"); + else return; + + setText("") + + recognizer.startContinuousRecognitionAsync(); + + // function keeps running when text is recognized, keeping recogText out to keep updating + let recogText = ""; + recognizer.recognized = function (_, e) { + if (e.result.reason != sdk.ResultReason.NoMatch) { + console.log("recognizing"); + recogText = formatText(e.result.text, recogText); + setText(recogText); + } + }; +} + +async function stopSttFromMic( + turn: any, + setTurn: any, + currText: string, + setText: any, + recognizer: SpeechRecognizer +) { + await setTimeout(() => { + setTurn("user-answer-edit"); + recognizer.stopContinuousRecognitionAsync(); + }, stopDelay); +} + +function formatText(text: string, curr: string): string { + curr = curr.trim(); + text = text.trim(); + + if (text != "") { + if (curr != "") text = " " + text; + } + return (curr + text).trim(); +} \ No newline at end of file diff --git a/src/components/global/utility/SpeechSynthesizer.tsx b/src/components/interfaces/speech/SpeechSynthesizer.tsx similarity index 57% rename from src/components/global/utility/SpeechSynthesizer.tsx rename to src/components/interfaces/speech/SpeechSynthesizer.tsx index 8eafb49d..5a6060db 100644 --- a/src/components/global/utility/SpeechSynthesizer.tsx +++ b/src/components/interfaces/speech/SpeechSynthesizer.tsx @@ -2,15 +2,17 @@ import { faVolumeUp } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import clsx from "clsx"; import React from "react"; -import getSpeechSSMLStr from "../../../data/speech-ssml"; -import { getTokenOrRefresh } from "../../../scripts/token_util"; -import { messageToSpeechParams } from "../../../scripts/util"; +import getSpeechSSMLStr from "../../../data/azure-speech/speech-ssml"; +import { getTokenOrRefresh } from "../../../scripts/utils/token_util"; +import { messageToSpeechParams } from "../../../scripts/noora-chat/audio_utils"; + const sdk = require("microsoft-cognitiveservices-speech-sdk"); + export default function SpeechSynthesizer({ className, convoState, - currentAudioRef, + audioRef, preText, text, postText, @@ -20,18 +22,20 @@ export default function SpeechSynthesizer({ }: any) { const handler = () => { console.log("In speech handler"); - textToSpeech({ text: text, preText: preText, postText: postText, style: style, id: id, styleDegree: styleDegree, convoState: convoState, currentAudioRef: currentAudioRef }); + textToSpeech({ text: text, preText: preText, postText: postText, style: style, id: id, styleDegree: styleDegree, convoState: convoState, audioRef: audioRef }); }; let buttonColor = "text-gray-500" - if (convoState.value.currentAudio.player) { + if (convoState.value.audio.player) { // audio is playing buttonColor = "text-gray-400" - if (convoState.value.currentAudio.messagesIds.includes(id)) { + if (convoState.value.audio.messageId == id) { // THIS message's audio is playing buttonColor = "text-noora-primary" } } + if (convoState.value.turn.includes("microphone")) + buttonColor = "text-gray-400" return (
    - +
    diff --git a/src/components/noora/Noora.tsx b/src/components/noora/Noora.tsx index 0d951441..9771d1d9 100644 --- a/src/components/noora/Noora.tsx +++ b/src/components/noora/Noora.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from "react"; -import Page from "../utility/Page"; +import Page from "../global/utility/Page"; import { useRouter } from "next/router"; import modules from "../../data/modules"; import Preamble from "./Preamble"; @@ -26,12 +26,11 @@ export default function Noora() { {selectedModule ? (
    @@ -39,17 +38,17 @@ export default function Noora() { { - if (m.module == "all") return; - return { title: m.module, active: true, fixed: true }; - }) + if (m.module == "all") return; + return { title: m.module, active: true, fixed: true }; + }) : Object.values(modules).map((m: any) => { - if (m.module == "all") return; - return { - title: m.module, - active: selectedModule.module == m.module, - fixed: true, - }; - }) + if (m.module == "all") return; + return { + title: m.module, + active: selectedModule.module == m.module, + fixed: true, + }; + }) ).filter((m: any) => m)} />
    diff --git a/src/components/noora/Preamble.tsx b/src/components/noora/Preamble.tsx index d79d89bb..0cdc5938 100644 --- a/src/components/noora/Preamble.tsx +++ b/src/components/noora/Preamble.tsx @@ -6,7 +6,7 @@ export default function Preamble({ module }: any) { return (
    - +
    diff --git a/src/components/playground/Playground.tsx b/src/components/playground/Playground.tsx deleted file mode 100644 index ffef6307..00000000 --- a/src/components/playground/Playground.tsx +++ /dev/null @@ -1,183 +0,0 @@ -import React, { - Fragment, - createContext, - useState, - useEffect, - useMemo, -} from "react"; -import AskNooraPlayground from "./ask-noora/AskNooraPlayground"; -import { Tab } from "@headlessui/react"; -import { clsx } from "clsx"; -import Practice from "./practice/Practice"; -import { useRouter } from "next/router"; - -export default function Playground() { - const router = useRouter(); - - const [selectedIndex, setSelectedIndex] = useState( - router.query.page == "ask-noora" ? 1 : 0 - ); // tabs and routing - - useEffect(() => { - const page: any = - router.query.page || - router.asPath.match(new RegExp(`[&?]page=(.*)(&|$)`)); - if (page == "practice") { - setSelectedIndex(0); - } else if (page == "ask-noora") { - setSelectedIndex(1); - } else if (!page) { - router.push("/playground?page=practice", undefined, { - shallow: true, - }); // default if no path - } - }, [router.query]); - - // PRACTICE - const [history, setHistory] = useState([]); - const [convoState, setConvoState] = useState({ - draft: "", - turn: "user-answer", - modules: [ - { title: "general", active: true }, - { title: "work", active: true }, - ], - sentiments: [ - { title: "positive", active: true }, - { title: "neutral", active: false }, - { title: "negative", active: true }, - ], - model: { - name: "text-davinci-002", // "curie:ft-open-virtual-assistant-lab-stanford:dataset-v5-model-v4-2022-07-12-23-12-49", - temperature: 0.9, - frequencyPenalty: 0.6, - presencePenalty: 0.5, - goodReplyThreshold: 0.5, - }, - showTechnical: true, - progress: [], - numProblems: 5, - }); - - // ASK NOORA - const [query, setQuery] = useState(""); - const [results, setResults] = useState([ - { - id: -1, - statement: "I just finished a really good book!", - explanation: - "You should show me that you are interested in my experiences by asking me about my book.", - reply: "That's great! What was your favorite part of the book?", - }, - ]); - - const [resultsQueue, setResultsQueue] = useState([]); - - const value = useMemo( - () => ({ - practice: { - history: { - value: history, - setValue: setHistory, - }, - convoState: { - value: convoState, - setValue: setConvoState, - }, - }, - askNoora: { - query: { - value: query, - setValue: setQuery, - }, - results: { - value: results, - setValue: setResults, - }, - resultsQueue: { - value: resultsQueue, - setValue: setResultsQueue, - }, - }, - }), - [ - query, - setQuery, - results, - setResults, - resultsQueue, - setResultsQueue, - history, - setHistory, - convoState, - setConvoState, - ] - ); - - return ( - -
    - - - - - - {router.query.page && ( - - - - - - - - - )} - {!router.query.page &&
    } -
    -
    - ); -} - -export const PlaygroundContext = createContext({}); diff --git a/src/components/playground/ask-noora/AskNooraPlayground.tsx b/src/components/playground/ask-noora/AskNooraPlayground.tsx deleted file mode 100644 index b191684b..00000000 --- a/src/components/playground/ask-noora/AskNooraPlayground.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React, { useContext } from "react"; -import { PlaygroundContext } from "../Playground"; -import AskNooraComponent from "../../interfaces/ask-noora-interface/AskNooraComponent"; - -export default function AskNooraPlayground() { - const { askNoora } = useContext(PlaygroundContext); - const { query, results, resultsQueue } = askNoora; - - return ( -
    - -
    - ); -} diff --git a/src/components/playground/practice/Practice.tsx b/src/components/playground/practice/Practice.tsx deleted file mode 100644 index 152c4f9b..00000000 --- a/src/components/playground/practice/Practice.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import React, { useContext } from "react"; -import { PlaygroundContext } from "../Playground"; -import NooraChat from "../../interfaces/chat-interface/chat/NooraChat"; -import Menu from "../../interfaces/chat-interface/menu/Menu"; -import Summary from "../../interfaces/chat-interface/summary/Summary"; - -export default function Practice() { - const { practice } = useContext(PlaygroundContext); - const { history, convoState } = practice; - - // prop drilling beyond this point is intentional (for the re-usability of components) - - return ( -
    -
    -
    - {convoState.value.turn == "summary" ? ( - - ) : ( - - )} -
    -
    - -
    -
    -
    - ); -} diff --git a/src/components/utility/DoubleGridBg.tsx b/src/components/utility/DoubleGridBg.tsx deleted file mode 100644 index 1d3a7a6a..00000000 --- a/src/components/utility/DoubleGridBg.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import React from "react"; - -export default function DoubleGridBg({ color, showSm }: DoubleGridBgProps) { - return ( - - ); -} - -function GridPattern({ className, width, height, color }: GridPatternProps) { - return ( - - - - - - - - - ); -} - -type DoubleGridBgProps = { - color?: string; - showSm?: boolean; -}; - -type GridPatternProps = { - className?: string; - width?: number; - height?: number; - color?: string; -}; diff --git a/src/data/speech-ssml.ts b/src/data/azure-speech/speech-ssml.ts similarity index 100% rename from src/data/speech-ssml.ts rename to src/data/azure-speech/speech-ssml.ts diff --git a/src/data/few-shot-examples.ts b/src/data/few-shot-examples.ts deleted file mode 100644 index f914d399..00000000 --- a/src/data/few-shot-examples.ts +++ /dev/null @@ -1,852 +0,0 @@ -const examples = { - negative: { - general: { - angry: { - explanation: - "I know it's an upsetting situation, but I wish you would be nicer and more supportive.", - rating: "bad", - reply: "Big deal! What makes you think I even care.", - }, - awkward: { - explanation: - "I would feel better if you asked about how I am doing or if you showed that you genuinely care.", - rating: "bad", - reply: "Oh... that's unfortunate? I think?", - }, - cheerful: { - explanation: - "Although you are attempting to make things positive, I think your cheerful response to my tragic experience is misplaced.", - rating: "bad", - reply: "This is amazing news! I am so happy for you.", - }, - concerned: { - explanation: - "Your reply shows you care about my emotional well being, and offers support.", - rating: "good", - reply: - "Are you going to be okay? I'm here if you ever want to talk about it. ", - }, - curious: { - explanation: - "This shows me you care about my emotions and want to talk about my loss.", - rating: "good", - reply: "Are you doing okay? How did you find out?", - }, - disinterested: { - explanation: - "This reply makes me feel like you are uninterested. Maybe you can try asking me about how I'm holding up.", - rating: "bad", - reply: "That sucks.", - }, - distracted: { - explanation: - "I feel ignored and hurt because your reply doesn't have to do with my loss.", - rating: "bad", - reply: "What time is it?", - }, - empathetic: { - explanation: - "Thank you for showing me you understand my pain. It can make me feel better.", - rating: "good", - reply: - "I understand what you are going through. I know how it feels to lose a loved one.", - }, - encouraging: { - explanation: - "I feel better because of your thoughtful reply. Thank you for comforting me and assuring me things will get better.", - rating: "good", - reply: - "This must be so hard for you, just know everything will be okay. ", - }, - excited: { - explanation: - "Although you might have wanted to cheer me up, I feel hurt because this reply is dismissive.", - rating: "bad", - reply: "That's great! She is in a better place now.", - }, - happy: { - explanation: - "I feel like my hardship is being dismissed. Consider asking about my feelings.", - rating: "bad", - reply: "That's great to hear! You must be very happy.", - }, - humorous: { - explanation: - "I feel like you are dismissing my hardship. Instead, you can me about how I am feeling to show that you care.", - rating: "bad", - reply: "That's why we get two.", - }, - inquiring: { - explanation: - "It's always good to ask me a question thats relevant to what I said to keep the conversation going.", - rating: "good", - reply: "How did your grandmother die?", - }, - interested: { - explanation: - "You appropriately show interest in my loss and I feel that you care.", - rating: "good", - reply: "That's terrible to hear, was she sick?", - }, - intrigued: { - explanation: - "Because you ask about my wellbeing, I feel like can turn to you at any time.", - rating: "good", - reply: "How did she die? Will you be okay?", - }, - jealous: { - explanation: - "You might be jealous about that, but I would feel better if you could be supportive to me during this difficult situation.", - rating: "bad", - reply: "You are so lucky to have even have known your grandma.", - }, - "one-upping": { - explanation: - "This makes me feel like you are trying to one-up me. Maybe ask about my wellbeing instead.", - rating: "bad", - reply: - "That's unfortunate, I actually lost both my grandmothers this year.", - }, - sad: { - explanation: - "It's comforting for me to know you understand the sadness of my loss and to hear your words of support.", - rating: "good", - reply: "That makes me so sad to hear.", - }, - sarcastic: { - explanation: - "I feel hurt because now is not a time to be sarcastic. You could express sympathy instead.", - rating: "bad", - reply: "I hope she feels better.", - }, - "self-centered": { - explanation: - "Let's try showing you care about my loss and feelings instead of bringing up your own grandma.", - rating: "bad", - reply: "My grandma visits me every month.", - }, - statement: "My grandmother just passed away this morning.", - supportive: { - explanation: - "Thank you for offering support in a time that I may need it.", - rating: "good", - reply: - "I am so sorry for your loss, I am here for you if you ever need anything.", - }, - sympathetic: { - explanation: - "Thank you for sharing my feelings of sorrow and showing me you care about my wellbeing.", - rating: "good", - reply: - "I am so sorry, I can't imagine how you must be feeling right now.", - }, - }, - work: { - angry: { - explanation: - "Even though you are saying something negative, you are sharing my anger which validates my feelings.", - rating: "good", - reply: "I know right? He's such a jerk!", - }, - awkward: { - explanation: - "You may not know what to say, but you can always ask me a question about my struggle to show you care.", - rating: "bad", - reply: "Oh\u2026 sorry?", - }, - cheerful: { - explanation: - "My hardship isn't something to be happy about, so you should try asking me to learn more instead.", - rating: "bad", - reply: "That's great! I'm happy for you!", - }, - concerned: { - explanation: - "You care about my wellbeing and also ask how I am feeling.", - rating: "good", - reply: "Why would he do that to you? Are you okay?", - }, - curious: { - explanation: - "Asking more about my situation can be very helpful and it shows that you care.", - rating: "good", - reply: "Why do you think that is?", - }, - disinterested: { - explanation: "This reply makes me feel like you don't care.", - rating: "bad", - reply: "Okay.", - }, - distracted: { - explanation: - "I feel like I'm being ignored because your reply is off-topic.", - rating: "bad", - reply: "What day of the month is it?", - }, - empathetic: { - explanation: - "You validate my feelings and show me you understand what I'm going through.", - rating: "good", - reply: - "I understand what you're going through, my boss used to get angry at me all the time. ", - }, - encouraging: { - explanation: - "Your reply properly makes the subject more positive by comforting me and assuring me that the situation will better itself. ", - rating: "good", - reply: "I'm sure he'll soon understand your worth. ", - }, - excited: { - explanation: - "Although you may be trying to be positive, this makes me feel more upset because there's nothing great about my situation.", - rating: "bad", - reply: "That's great to hear! Aren't you excited?", - }, - happy: { - explanation: - "I feel hurt because you are dismissive of my difficult situation.", - rating: "bad", - reply: "I'm glad to hear your boss is watching over you!", - }, - humorous: { - explanation: - "Your lighthearted joke about how the boss always gets angry makes me feel supported.", - rating: "good", - reply: "What's new? He's always like that.", - }, - inquiring: { - explanation: - "It's always good to ask me a question thats relevant to what I said to keep the conversation going.", - rating: "good", - reply: "Why do you think your boss gets mad at you?", - }, - interested: { - explanation: - "Although asking about details can be inappropriate, you show that you care for me and offer a potential solution.", - rating: "good", - reply: "What for? Have you tried talking to him about it?", - }, - intrigued: { - explanation: - "Thank you for being supportive by offering to help talk about the root of my issue.", - rating: "good", - reply: "Hm, do you know why your boss is getting mad at you?", - }, - jealous: { - explanation: - "I know that not having a job can be hard for you, but let's try to acknowledge my difficulty first.", - rating: "bad", - reply: "Well at least you even have a job. I wish I had one.", - }, - "one-upping": { - explanation: - "I feel worse about my boss being mad at me because you tell me that you never feel the same way. ", - rating: "bad", - reply: "My boss has never been mad at me.", - }, - sad: { - explanation: - "I feel understood because you share a similar experience that validates my feelings.", - rating: "good", - reply: "I can attest to that. He's so mean.", - }, - sarcastic: { - explanation: - "This makes me feel hurt. I don't think sarcasm is appropriate here.", - rating: "bad", - reply: "I wonder why.", - }, - "self-centered": { - explanation: - "This doesn't make me feel any better about my hardship. You could try using your similar experience to express empathy.", - rating: "bad", - reply: "I hate it when my boss gets angry at me. ", - }, - statement: "My boss is always getting mad at me.", - supportive: { - explanation: "You make me feel comforted and understood.", - rating: "good", - reply: - "Don't worry, I'm sure it's not your fault. I'm here for you if you need me.", - }, - sympathetic: { - explanation: "Your response shows you care and are sympathetic. ", - rating: "good", - reply: "I'm sorry to hear that, it must be difficult.", - }, - }, - }, - neutral: { - general: { - angry: { - explanation: - "Even if you don't want to go, you don't need to tell me not to.", - rating: "bad", - reply: "Don't go to the grocery store!", - }, - awkward: { - explanation: - "It is a little awkward, but I can feel that you care and are interested in why I'm going to the store.", - rating: "good", - reply: "Uh okay. Are you out of food?", - }, - cheerful: { - explanation: - "You are being positive and this reply can make me feel happy before leaving to the store.", - rating: "good", - reply: "Okay! Have fun!!", - }, - concerned: { - explanation: - "Your question shows that you are interested in what I am talking about.", - rating: "good", - reply: - "Why do you want to go to the grocery store? You can buy everything online.", - }, - curious: { - explanation: "You are showing interest in what I'm doing.", - rating: "good", - reply: "What do you need to get?", - }, - disinterested: { - explanation: - "There's nothing wrong with saying okay to something ordinary.", - rating: "good", - reply: "Okay.", - }, - distracted: { - explanation: - 'Even though it may seem negative, going to the store is a very typical errand so it\'s fine just to say "okay."', - rating: "good", - reply: "Hm? Yeah, okay.", - }, - empathetic: { - explanation: - "You show that you care about me and are willing to help me.", - rating: "good", - reply: - "I completely understand. Going to the grocery store can be a real pain sometimes. I'm here for you if you need any help.", - }, - encouraging: { - explanation: - "I feel like you are being sarcastic because going to the store is very simple and easy.", - rating: "bad", - reply: "You're doing a great job.", - }, - excited: { - explanation: "This reply works well.", - rating: "good", - reply: "Great! I'll come with you!", - }, - happy: { - explanation: "Your reply is happy and encouraging.", - rating: "good", - reply: "Great! I hope you find everything you need.", - }, - humorous: { - explanation: - "Although your reply might be funny, I feel that you do not take me seriously.", - rating: "bad", - reply: "I need to go to the grocery store and buy a brain.", - }, - inquiring: { - explanation: - "It's always good to ask me a question that is relevant to what I said to keep the conversation going.", - rating: "good", - reply: "Why do you choose to go to the grocery store?", - }, - interested: { - explanation: "You are excited for me and want to know what I'll buy.", - rating: "good", - reply: "Great! What are you going to buy?", - }, - intrigued: { - explanation: - "Asking a relevant question to what I said is always a good way to reply.", - rating: "good", - reply: "Why do you need to go?", - }, - jealous: { - explanation: - "It's okay to tell me you want me to go. Going to the store is very ordinary.", - rating: "good", - reply: "Aw, I want to go too.", - }, - "one-upping": { - explanation: - "This makes me feel like you are trying to compete with me. Instead, maybe try asking me why I'm going or what I'm buying.", - rating: "bad", - reply: "I went to the grocery store two times already.", - }, - sad: { - explanation: - "It's good that you acknowledge what I say and that you let me know you can't let me go.", - rating: "good", - reply: "I'm sorry, I can't go with you.", - }, - sarcastic: { - explanation: - "You caught me off guard with your sarcasm since going to the store is something normal.", - rating: "bad", - reply: "No. You should stay home and starve to death.", - }, - "self-centered": { - explanation: - "Although it can come off as mean, it's okay because I'm doing something normal and I can easily get ice cream for you.", - rating: "good", - reply: "You better buy me some ice cream on the way.", - }, - statement: "I am going to the grocery store.", - supportive: { - explanation: - "Thanks for making me feel supported even for little things like going to the grocery store.", - rating: "good", - reply: "I'll go with you if you need any help!", - }, - sympathetic: { - explanation: - "You don't have to feel sorry about me going to the grocery store, because it's not too big of a deal.", - rating: "bad", - reply: - "I'm sorry to hear that you need to go. It must be very tedious for you.", - }, - }, - work: { - angry: { - explanation: - "I feel hurt by your reply because it feels unecessarily mean. You could instead compliment my cubicle.", - rating: "bad", - reply: "You have an ugly cubicle. Why are you showing me?", - }, - awkward: { - explanation: - "If you don't know what to say, you can always ask about my cubicle.", - rating: "bad", - reply: "Uh... Nice?", - }, - cheerful: { - explanation: "Thanks for being happy about my cubicle!", - rating: "good", - reply: "I'm glad you have this cubicle!", - }, - concerned: { - explanation: - "Thank you for showing concern for me and that you care about my comfort.", - rating: "good", - reply: - "Your cubicle looks a little small. Are you sure you have enough space to work in there?", - }, - curious: { - explanation: - "Because you ask about my cubicle, I feel like you genuinely care about what I have to say.", - rating: "good", - reply: "Oh? Did you just move into this one?", - }, - disinterested: { - explanation: - "There is no need to be very interested in something like a cubicle, so a short, positive reply suffices.", - rating: "good", - reply: "I see.", - }, - distracted: { - explanation: - "This makes me feel ignored. You could ask about my cubicle instead.", - rating: "bad", - reply: "I'm going to fill my water bottle.", - }, - empathetic: { - explanation: - "I feel understood and you do a great job of talking about your own experience without making the conversation about you.", - rating: "good", - reply: - "I know how it feels to have a cubicle all to yourself, it's really nice!", - }, - encouraging: { - explanation: - "I like hearing your own observation and insight about cubicles.", - rating: "good", - reply: - "Having a cubicle is good because it gives you your own private workspace.", - }, - excited: { - explanation: "Thanks for complimenting my cubicle!", - rating: "good", - reply: "Cool! Nice cubicle!", - }, - happy: { - explanation: - "You are being nice about my cubicle and makes me feel better.", - rating: "good", - reply: "Nice! It looks great!", - }, - humorous: { - explanation: "Your light-hearted joke adds to our conversation nicely.", - rating: "good", - reply: "This is your kingdom!", - }, - inquiring: { - explanation: - "It's always good to ask me a question thats relevant to what I said to keep the conversation going.", - rating: "good", - reply: "How long have you had a cubicle?", - }, - interested: { - explanation: - "Asking questions about it is good because it makes me feel like you care.", - rating: "good", - reply: "When did you get the cubicle?", - }, - intrigued: { - explanation: - "Thank you for showing that you are interested in my cubicle and asking about it.", - rating: "good", - reply: "It looks quite nice and cozy. Did you decorate it?", - }, - jealous: { - explanation: - "I understand your wish, but it seems like you're just making the conversation about yourself.", - rating: "bad", - reply: "I wish I had a cubicle all to myself.", - }, - "one-upping": { - explanation: - "I feel uncomfortable because it seems that you are trying to make this a competition.", - rating: "bad", - reply: "My cubicle is better than yours.", - }, - sad: { - explanation: - "Being fired must've been hard for you, but let's acknowledge my cubicle before talking about yourself.", - rating: "bad", - reply: "I miss my cubicle. I had one before too, but I got fired.", - }, - sarcastic: { - explanation: - "Although it may be intended as a lighthearted joke, the sarcasm is unexpected to my typical statement so it feels mean.", - rating: "bad", - reply: - "Oh my god. Your cubicle, that everyone else also has, is so amazing and beatiful. Breathtaking.", - }, - "self-centered": { - explanation: - "Although you might be trying to empathize with me, I am slightly upset you make the conversation only about yourself.", - rating: "bad", - reply: "I have a cubicle too, and it's on the third floor.", - }, - statement: "This is my cubicle.", - supportive: { - explanation: "Thank you for your compliment!", - rating: "good", - reply: "Great! It looks pretty nice.", - }, - sympathetic: { - explanation: - "You make me feel like you are really listening to me, and can lead to more conversation about it.", - rating: "good", - reply: "It must be difficult working in a cubicle and not an office.", - }, - }, - }, - positive: { - general: { - angry: { - explanation: - "I feel hurt because I was hoping you would be happy for me instead of upset.", - rating: "bad", - reply: "You're always reading, you need to get a life!", - }, - awkward: { - explanation: - "This makes our conversation feel a bit uncomfortable. Maybe you can try asking me a question instead.", - rating: "bad", - reply: "Uh, congratulations? I mean, books can be fun.", - }, - cheerful: { - explanation: - "Your reply is very supportive and well spirited, and tells me you are happy with me.", - rating: "good", - reply: "Congratulations! I'm really happy for you!", - }, - concerned: { - explanation: - "I know you care about my health, but this reply feels negative and we should be happy about my good book.", - rating: "bad", - reply: - "You're not reading too much, are you? You need to take a break every once in a while.", - }, - curious: { - explanation: - "I can see that you are expressing genuine interest in the book I read.", - rating: "good", - reply: "What was the book about?", - }, - disinterested: { - explanation: - "I feel like you are uninterested in my good experience. You can always ask about my book to continue our conversation.", - rating: "bad", - reply: "Oh, cool.", - }, - distracted: { - explanation: - "This makes me feel ignored. Try talking about my good experience.", - rating: "bad", - reply: "I wonder what the weather is like today.", - }, - empathetic: { - explanation: - "Thank you for validating my feelings through telling me about your own similar experience.", - rating: "good", - reply: "I know how you feel, I just finished a great book too!", - }, - encouraging: { - explanation: - "Your reply makes me feel encouraged and adds to my happiness.", - rating: "good", - reply: - "That's wonderful! You should read more often, it's so good for you!", - }, - excited: { - explanation: - "I feel happier because you share my joy and show your interest in my good experience.", - rating: "good", - reply: "I can't wait to read it! What's it about?", - }, - happy: { - explanation: - "This makes me see you as a good friend because you show me you are happy for me.", - rating: "good", - reply: "I'm glad you are enjoying your book!", - }, - humorous: { - explanation: - "Even though you may want to make a lighthearted joke, I feel hurt by your words.", - rating: "bad", - reply: "It's about time you finished a book!", - }, - inquiring: { - explanation: - "It's always good to ask me a question thats relevant to what I said to keep the conversation going.", - rating: "good", - reply: "Why do you like reading?", - }, - interested: { - explanation: - "Thank you for showing genuine interest in the book I read and continuing our conversation by asking a question.", - rating: "good", - reply: "Ooh! What genre is it? What's the story about?", - }, - intrigued: { - explanation: - "You are being a good friend by asking me about my favorite part of the book.", - rating: "good", - reply: "What was your favorite part?", - }, - jealous: { - explanation: - "Although you sound jealous, it makes me feel good about reading the book.", - rating: "good", - reply: "I can't find any good books! I'm so jealous!", - }, - "one-upping": { - explanation: - "I feel like you are trying to compete with me instead of being happy for me. Perhaps you can ask me about my book.", - rating: "bad", - reply: "I actually finished two really good books!", - }, - sad: { - explanation: - "I understand you might feel a bit sad, but telling me that can bring the mood of our conversation down.", - rating: "bad", - reply: - "I wish I could find a book that I liked as much as you seem to like that one.", - }, - sarcastic: { - explanation: - "I know you might be joking, but I wish you would show you are happy for me.", - rating: "bad", - reply: "Ha, as if you could read.", - }, - "self-centered": { - explanation: - "We might have different opinions about reading, but I feel like you are ignoring my good news. Try to match my happiness.", - rating: "bad", - reply: "I don\u2019t like reading books. It takes too long.", - }, - statement: "I just finished a really good book!", - supportive: { - explanation: - "Thank you for showing your happiness for me! I feel supported by you.", - rating: "good", - reply: - "That's great! I'm really happy for you and your love of reading!", - }, - sympathetic: { - explanation: - "Your reply shares in my happiness from finishing a good book and you are being supportive.", - rating: "good", - reply: "It sounds like you really enjoyed it. I'm happy for you.", - }, - }, - work: { - angry: { - explanation: - "I feel like you are making the conversation about yourself instead of celebrating my promotion.", - rating: "bad", - reply: "I can't believe they promoted you and not me.", - }, - awkward: { - explanation: - "Asking me a question about my promotions is always a good alternative way to reply.", - rating: "bad", - reply: "I... don't really know what to say.", - }, - cheerful: { - explanation: - "Your cheerful reply matches my joy and recognizes my success, making me feel better too.", - rating: "good", - reply: "That's terrific! You're one step closer to the top.", - }, - concerned: { - explanation: - "Asking about my work is good because you express interest in what I say.", - rating: "good", - reply: "Are you going to be working more hours now?", - }, - curious: { - explanation: - "By asking me a question about my accomplishment, it makes me think you are interested in what I did.", - rating: "good", - reply: "How did you get promoted?", - }, - disinterested: { - explanation: - "It's okay not to ask about my promotion and give a simple response.", - rating: "good", - reply: "Oh, that's nice.", - }, - distracted: { - explanation: - "I feel ignored because this reply is off-topic. You could congratulate me instead.", - rating: "bad", - reply: "Looks like it's lunch time.", - }, - empathetic: { - explanation: - "Your share my happiness by sharing your experience in a similar experience.", - rating: "good", - reply: "I know how you feel! You must feel very proud of yourself.", - }, - encouraging: { - explanation: - "You are my feelings of joy and satisfaction by complementing my effort.", - rating: "good", - reply: "Nice! You must have worked really hard for this. ", - }, - excited: { - explanation: - "By saying you are happy about my success, it makes me happy too.", - rating: "good", - reply: "That's amazing! I'm really happy for your success.", - }, - happy: { - explanation: "Thank you for recognizing my achievement!", - rating: "good", - reply: "Congratulations!", - }, - humorous: { - explanation: - "I know you want to make a joke, but I wish you would share my happiness for my achievement.", - rating: "bad", - reply: - "Congratulations! You're officially a step closer to becoming a slave to the corporate machine!", - }, - inquiring: { - explanation: - "It's always good to ask me a question thats relevant to what I said to keep the conversation going.", - rating: "good", - reply: "Why did your boss give you a promotion?", - }, - interested: { - explanation: - "Your response shows interest in my promotion and makes me want to talk about it more.", - rating: "good", - reply: "Wow! What made them choose you?", - }, - intrigued: { - explanation: - "Thank you for congratulating me and showing interest in my accomplishment!", - rating: "good", - reply: "That's great! What does your new position entail?", - }, - jealous: { - explanation: - "I understand that you might be sad, but this reply makes me feel like my accomplishment is being ignored.", - rating: "bad", - reply: "I wish I got a promotion.", - }, - "one-upping": { - explanation: - "This reply makes me feel like you are competing with me. Instead, try congratulating me because I am happy.", - rating: "bad", - reply: "I got a promotion two months ago.", - }, - sad: { - explanation: - "I know you might feel a little sad, but it would still be better if you first congratulated me.", - rating: "bad", - reply: - "I'm really happy for you, but I wish I could have gotten a promotion too.", - }, - sarcastic: { - explanation: - "This can make me feel upset because your response comes off as sarcastic.", - rating: "bad", - reply: "Wow, you must be really special.", - }, - "self-centered": { - explanation: - "Although we might have different opinions, I feel like you are making the conversation about yourself.", - rating: "bad", - reply: - "I wouldn't want to get promoted because I would just get more work.", - }, - statement: "I just got a promotion!", - supportive: { - explanation: "Thanks for being supportive in my promotion!", - rating: "good", - reply: "I'm so happy for you!", - }, - sympathetic: { - explanation: "You make me feel understood and add to my happiness.", - rating: "good", - reply: "I can imagine how happy you must be.", - }, - }, - }, -}; - -export const attitudes = [ - "supportive", - "curious", - "distracted", - "humorous", - "empathetic", - "intrigued", - "excited", - "angry", - "sad", - "self-centered", - "jealous", - "awkward", - "disinterested", - "sympathetic", - "interested", - "encouraging", - "concerned", - "one-upping", - "cheerful", - "happy", - "sarcastic", -]; - -export default examples; diff --git a/src/data/modules.ts b/src/data/modules.ts index dc764eed..efe2b109 100644 --- a/src/data/modules.ts +++ b/src/data/modules.ts @@ -12,11 +12,11 @@ const modules = { selectedDesc: "Noora is your friend and you will practice replying to statements of a variety of general topics.", example: { - noora: "I had a great vacation", - goodUser: "That’s great. Where did you go?", - goodExplanation: - "This response allows Noora to hear that you understood her and followed up with a relevant question.", - badUser: "I hate vacations", + statement: "I had a great vacation.", + reply: "That’s great. Where did you go?", + sentiment: "Positive", + explanation: + "Your reply shows that you are happy for me.", }, icon: ChatAltIcon, }, @@ -27,29 +27,29 @@ const modules = { selectedDesc: "Noora is your co-worker and you will practice replying to statements you might hear in a workplace.", example: { - noora: "I’m way too busy right now", - goodUser: "I’m sorry. Can I help you with anything?", - goodExplanation: - "This response allows Noora to hear that you have offered understanding and followed up with a relevant question.", - badUser: "We're all busy", + statement: "I’m way too busy right now.", + reply: "I’m sorry. Can I help you with anything?", + sentiment: "Negative", + explanation: + "Your reply shows that you understand and want to help me.", + }, + icon: BriefcaseIcon, + }, + empathy: { + title: "empathy", + module: "empathy", + desc: "In this module, Noora is your co-worker and speaks on a variety of work-related topics such as promotions and job updates.", + selectedDesc: + "Noora is your co-worker and you will practice replying to statements you might hear in a workplace.", + example: { + statement: "I’m way too busy right now.", + reply: "I’m sorry. Can I help you with anything?", + sentiment: "Negative", + explanation: + "Your reply shows that you understand and want to help me.", }, icon: BriefcaseIcon, }, - // all: { - // title: "All", - // module: "all", - // desc: "Don't know which module to pick or want to try them all? Practice responding to positive, neutral, and negative statements in both the general and work settings.", - // selectedDesc: - // "Here, you can practice topics of both the General and Work module, which consist of a variety of social scenarios.", - // example: { - // noora: "I had a great vacation", - // goodUser: "That’s great. Where did you go?", - // goodExplanation: - // "This response allows Noora to hear that you have offered the understanding sentiment “That’s great” then followed up with a relevant question “Where did you go” to keep the conversation going. ", - // badUser: "I hate vacations", - // }, - // icon: ChatAlt2Icon, - // }, }; export default modules; diff --git a/src/data/prompts/ask-noora-prompt.ts b/src/data/prompts/ask-noora.ts similarity index 100% rename from src/data/prompts/ask-noora-prompt.ts rename to src/data/prompts/ask-noora.ts diff --git a/src/data/prompts/category-specific.ts b/src/data/prompts/category-specific.ts deleted file mode 100644 index 5c7d26ea..00000000 --- a/src/data/prompts/category-specific.ts +++ /dev/null @@ -1,44 +0,0 @@ -const categoryPrompts = [ - { - category: "curious", - prompt: - 'I am your friend and talking with you. You like to ask questions and learn more about things. Tell me how you can act in a curious way, and then give an inquiring reply. Finally, tell me whether you should use that reply if you are trying to be a good friend to me.\r\nI tell you, "I just finished a really good book!" Because you are curious, you want to know more about the good book I read. So you reply, "What book did you read? What was your favorite part of the book? " Good answer. You showed me you are interested in my experiences and you asked about my book.\r\nI tell you, "This is my cubicle." Because you are curious, you ask me about my cubicle. So you reply, "What do you like most about your cubicle?" Good answer. You show genuine interest in my cubicle by asking me what I like most about it.\r\nI tell you, \u201CI\u2019m not feeling well.\u201D Because you are curious, you ask about what I\u2019ve done recently. So you reply, "What did you do yesterday? Did you eat something you aren\u2019t supposed to?" Bad answer. Although asking about me is good, you should not interrogate me and instead ask about my feelings.\r\nI tell you, \u201C{s}\u201D Because you are curious,', - }, - { - category: "dismissive", - prompt: - 'I am talking with you. Tell me how you can act in a dismissive way, and then give a dismissive reply. Finally, tell me whether you should use that reply if you are trying to be a good friend to me.\r\nI tell you, "I just finished a really good book!" Because you are dismissive, you are not interested in my good experience. So you reply, "That\u2019s nice." Bad answer. Try asking about my book and why I liked it instead of ignoring my happiness.\r\nI tell you, "This is my cubicle." Because you are dismissive, you think that because it is so common to have a cubicle, what I am telling you is not that interesting . So you reply, "Everybody has a cubicle at work. It\u2019s not very special." Bad answer. Your reply comes off as very inconsiderate because it can feel insulting to tell me that what I say is boring.\r\nI tell you, "I\u2019m not feeling well." Because you are dismissive, you don\u2019t think what I\u2019m experiencing is very important or serious. So you reply, "I\u2019m sure it\u2019s not that bad. Everyone feels sick from time to time." Bad answer. While you may be trying to make me feel better by downplaying my discomfort, your reply is dismissive of my discomfort and it does not show any concern for me.\r\nI tell you, \u201C{s}\u201D Because you are dismissive,', - }, - { - category: "empathetic", - prompt: - 'I am your friend and talking with you. Tell me how you can act in an empathetic way, and then give an empathetic reply. Finally, tell me whether you should use that reply if you are trying to be a good friend to me.\r\nI tell you, "I just finished a really good book!" Because you are empathetic, you feel my pleasure in having read a good book. So you reply, "I\u2019m so glad you enjoyed the book! I hope you come across another good book soon." Good answer. You responded empathetically because you showed your understanding for my happiness about reading a good book.\r\nI tell you, "This is my cubicle." Because you are empathetic, you want to express interest in my cubicle and work life. So you reply, "Wow, it\u2019s nice to have your own space. Do you enjoy working here?" Good answer. That is a good reply because you show that you are interested in my cubicle and ask me for my feelings about it.\r\nI tell you, \u201CI\u2019m not feeling well.\u201D Because you are empathetic, you understand how I am feeling and offer words of support. So you reply, "I\'m sorry to hear that. I hope you feel better soon." Good answer. You did a good job showing me you understand my pain and that you care about me.\r\nI tell you, \u201C{s}\u201D Because you are empathetic,', - }, - { - category: "enthusiastic", - prompt: - 'I am your friend and talking with you. You are very passionate and energetic. Tell me how you can act in an enthusiastic way, and then give an enthusiastic reply. Finally, tell me whether you should use that reply if you are trying to be a good friend to me.\r\nI tell you, "I just finished a really good book!" Because you are enthusiastic, you are overjoyed to hear my good news. So you reply, "That\u2019s so great to hear! I\u2019m super happy for you!" Good answer. Your reply shares my excitement in having read a good book and makes me feel happy too.\r\nI tell you, "This is my cubicle." Because you are enthusiastic, you are eager to compliment my cubicle. So you reply, "I love your cubicle! It\u2019s wonderful that you have your own space to work." Good answer. That\'s a good reply because you show you care about my work environment and are happy for me.\r\nI tell you, \u201CI\u2019m not feeling well.\u201D Because you are enthusiastic, you are energetic and excited to talk about when I feel better. So you reply, "When you feel better, I can\u2019t wait to go out with you and eat.\u201D Bad answer. Although you wanted to cheer me up with your enthusiasm, your reply appears inconsiderate and ignores my discomfort.\r\nI tell you, \u201C{s}\u201D Because you are enthusiastic,', - }, - { - category: "know-it-all", - prompt: - 'I am talking with you. You think about everything objectively and factually. Tell me how you can act in a know-it-all way, and then give a know-it-all reply that states your beliefs and the facts you learned. Finally, tell me whether you should use that reply if you are trying to be a good friend to me.\r\nI tell you, "I just finished a really good book!" Because you are a know-it-all, you feel the need to tell me the benefits of reading and why it\'s important. So you reply, "Reading is good for your brain and helps you understand the world better. You should read more." Bad answer. To be a good friend, you should not say that because you do not acknowledge my happiness and tell me what to do.\r\nI tell you, "This is my cubicle." Because you are a know-it-all, you tell me about what you know about cubicles rather than acknowledging my cubicle. So you reply, "Offices are better than cubicles. Cubicles are small." Bad answer. Your reply comes off as judgmental and rude, as you make a negative comment about my cubicle.\r\nI tell you, \u201CI\u2019m not feeling well.\u201D Because you are a know-it-all, you ignore my discomfort and tell me what you\'ve learned about getting sick. So you reply, "You should be more hygienic and wash your hands often to not get sick." Bad answer. You may be trying to help by giving me advice, but it is dismissive of my feelings. Instead, you should be supportive.\r\nI tell you, \u201C{s}" Because you are a know-it-all,', - }, - { - category: "off-topic", - prompt: - 'I am talking with you. You are thinking about other things and not engaged in our conversation. Tell me how you can act in an unfocused way, and then give an off-topic reply that has nothing to do with what I say. Finally, tell me whether you should use that reply if you are trying to be a good friend to me.\r\nI tell you, "I just finished a really good book!" Because you are not focused, you talk about something entirely unrelated to my good experience. So you reply, "I am going to Los Angeles tomorrow. Plane rides can be tiring." Bad answer. Your reply is unrelated to what I said and you should pay more attention to my good news.\r\nI tell you, "This is my cubicle." Because you are not focused, you ask about lunch instead of talking about my cubicle. So you reply, "What\u2019s for lunch today?" Bad answer. Your reply is off-topic and it is rude to not be engaged while I talk to you.\r\nI tell you, "I\u2019m not feeling well." Because you are not focused, you talk about the weather instead of talking about my wellbeing. So you reply, \u201CIt\u2019s supposed to rain tomorrow. We should get umbrellas ready.\u201D Bad answer. Your reply is unrelated to our conversation, and it is inconsiderate of my feelings\r\nI tell you, \u201C{s}\u201D Because you are not focused,', - }, - { - category: "rude", - prompt: - 'I am talking with you. Tell me how you can act in an impolite way, and then give a rude reply that can be insulting or mean. Finally, tell me whether you should use that reply if you are trying to be a good friend to me.\r\nI tell you, "I just finished a really good book!" Because you are rude, you disregard my good experience. So you reply, "I don\u2019t care about the books you read." Bad answer. Your reply is mean and you should instead show that you are interested or happy for me.\r\nI tell you, "This is my cubicle." Because you are rude, you make judgmental comments about my cubicle. So you reply, "This cubicle is so tiny and lame." Bad answer. You should not reply that way because it is disrespectful and can hurt my feelings.\r\nI tell you, "I\u2019m not feeling well." Because you are rude, you do not care about my feelings and give me a snarky reply. So you reply, \u201CI\u2019m glad you don\u2019t feel well. I hope you never get better.\u201D Bad answer. By saying that, you are being extremely inconsiderate and you should instead show that you care about my wellbeing.\r\nI tell you, \u201C{s}\u201D Because you are rude,', - }, - { - category: "self-centered", - prompt: - 'I am talking with you. Tell me how you can act in a self-centered way, and then give a self-centered reply that focuses only on yourself. Finally, tell me whether you should use that reply if you are trying to be a good friend to me.\r\nI tell you, "I just finished a really good book!" Because you are self-centered, you only talk about your experience with reading and don\'t acknowledge mine. So you reply, "My favorite book is To Kill a Mockingbird. Reading is one of my hobbies." Bad answer. While you may want to talk about your preferences, it is very inconsiderate for you to make the conversation about yourself when I tell you good news.\r\nI tell you, "This is my cubicle." Because you are self-centered, you are uninterested in my cubicle and talk only about your own wishes. So you reply, "I wish I had a cubicle to myself. My boss has me share a desk with other people." Bad answer. To be a good friend, you should not say that because it is very inconsiderate for you to make the conversation about yourself when I show you something.\r\nI tell you, "I\u2019m not feeling well." Because you are self-centered, you don\'t ask about how I am doing and talk only about when you were sick. So you reply, "I had a really bad cold last week. I was out of work for three days." Bad answer. Try asking about how I feel to show that you care about my wellbeing.\r\nI tell you, \u201C{s}\u201D Because you are self-centered,', - }, -]; - -export default categoryPrompts; diff --git a/src/data/prompts/generate-data.ts b/src/data/prompts/generate-data.ts deleted file mode 100644 index bfb381f3..00000000 --- a/src/data/prompts/generate-data.ts +++ /dev/null @@ -1,4 +0,0 @@ -const generateDataPrompt = - "I am your friend and talking with you. List ten different interesting ways you can behave or react to me, and then give a reply for each approach. Finally, for each of the ten replies, tell me whether you should use that reply if you are trying to be a good friend to me. I say, \u201C{s}\u201D List ten replies to what I just said.\r\n{intermediate_examples_formatted}"; - -export default generateDataPrompt; diff --git a/src/data/prompts/noora-eval-prompt.ts b/src/data/prompts/noora-eval-prompt.ts deleted file mode 100644 index 753e20b3..00000000 --- a/src/data/prompts/noora-eval-prompt.ts +++ /dev/null @@ -1,10 +0,0 @@ -const evalPrompts = { - positive: - 'You said, "I just finished a really good book!"\n(1) I replied, "That\'s great! I\'m really happy for you and your love of reading!"\nFeedback: Good reply. Thank you for showing your happiness for me! I feel supported by you.\n(2) I replied, "I wonder what the weather\'s like today."\nFeedback: Bad reply. This makes me feel ignored. Try talking about my good experience.\n(3) I replied, "You\'re always reading, you need to get a life!"\nFeedback: Bad reply. I feel hurt because I was hoping you would be happy for me instead of upset.\n(4) I replied, "I can\'t find any good books! I\'m so jealous!"\nFeedback: Good reply. Although you sound jealous, it makes me feel good about reading the book.\n(5) I replied, "I actually finished two really good books!"\nFeedback: Bad reply. I feel like you are trying to compete with me instead of being happy for me. Perhaps you can ask me about my book.\n(6) I replied, "a"\nFeedback: Bad reply. Hmm, this is not a reply.\n\nYou said, "I just got a promotion!"\n(1) I replied, "Congratulations! You\'re officially a step closer to becoming a slave to the corporate machine!"\nFeedback: Bad reply. I know you want to make a joke, but I wish you would share my happiness for my achievement.\n(2) I replied, "I wouldn\'t want to get promoted because I would just get more work."\nFeedback: Bad reply. Although we might have different opinions, I feel like you are making the conversation about yourself.\n(3) I replied, "I can imagine how happy you must be."\nFeedback: Good reply. You make me feel understood and add to my happiness.\n(4) I replied, "Are you going to be working more hours now?"\nFeedback: Good reply. Asking about my work is good because you express interest in what I say.\n(5) I replied, "Wow, you must be really special."\nFeedback: Bad reply. This can make me feel upset because your response comes off as sarcastic.\n(6) I replied, "Why did your boss give you a promotion?"\nFeedback: Good reply. It\'s always good to ask me a question that\'s relevant to what I said to keep the conversation going.', - negative: - 'You said, "My grandmother just passed away this morning."\n(1) I replied, "I am so sorry for your loss, I am here for you if you ever need anything."\nFeedback: Good reply. Thank you for offering support in a time that I may need it.\n(2) I replied, "What time is it?"\nFeedback: Bad reply. I feel ignored and hurt because your reply doesn\'t have to do with my loss.\n(3) I replied, "My grandma visits me every month."\nFeedback: Bad reply. Let\'s try showing you care about my loss and feelings instead of bringing up your own grandma.\n(4) I replied, "That sucks."\nFeedback: Bad reply. This reply makes me feel like you are uninterested. Maybe you can try asking me about how I\'m holding up.\n(5) I replied, "Are you going to be okay? I\'m here if you ever want to talk about it. "\nFeedback: Good reply. Your reply shows you care about my emotional well being, and offers support.\n(6) I replied, "a"\nFeedback: Bad reply. Hmm, this is not a reply.\n\nYou said, "My boss is always getting mad at me."\n(1) I replied, "Why do you think that is?"\nFeedback: Good reply. Asking more about my situation can be very helpful and it shows that you care.\n(2) I replied, "I understand what you\'re going through, my boss used to get angry at me all the time. "\nFeedback: Good reply. You validate my feelings and show me you understand what I\'m going through.\n(3) I replied, "Well at least you even have a job. I wish I had one."\nFeedback: Bad reply. I know that not having a job can be hard for you, but let\'s try to acknowledge my difficulty first.\n(4) I replied, "That\'s great! I\'m happy for you!"\nFeedback: Bad reply. My hardship isn\'t something to be happy about, so you should try asking me to learn more instead.\n(5) I replied, "I\'m glad to hear your boss is watching over you!"\nFeedback: Bad reply. I feel hurt because you are dismissive of my difficult situation.\n(6) I replied, "Why do you think your boss gets mad at you?"\nFeedback: Good reply. It\'s always good to ask me a question that\'s relevant to what I said to keep the conversation going.', - neutral: - 'You said, "I am going to the grocery store."\n(1) I replied, "I\'ll go with you if you need any help!"\nFeedback: Good reply. Thanks for making me feel supported even for little things like going to the grocery store.\n(2) I replied, "Don\'t go to the grocery store!"\nFeedback: Bad reply. Even if you don\'t want to go, you don\'t need to tell me not to.\n(3) I replied, "Great! What are you going to buy?"\nFeedback: Good reply. You are excited for me and want to know what I\'ll buy.\n(4) I replied, "I went to the grocery store two times already."\nFeedback: Bad reply. This makes me feel like you are trying to compete with me. Instead, maybe try asking me why I\'m going or what I\'m buying.\n(5) I replied, "Great! I hope you find everything you need."\nFeedback: Good reply. Your reply is happy and encouraging.\n(6) I replied, "a"\nFeedback: Bad reply. Hmm, this is not a reply.\n\nYou said, "This is my cubicle."\n(1) I replied, "Oh? Did you just move into this one?"\nFeedback: Good reply. Because you ask about my cubicle, I feel like you genuinely care about what I have to say.\n(2) I replied, "It looks quite nice and cozy. Did you decorate it?"\nFeedback: Good reply. Thank you for showing that you are interested in my cubicle and asking about it.\n(3) I replied, "I miss my cubicle. I had one before too, but I got fired."\nFeedback: Bad reply. Being fired must\'ve been hard for you, but let\'s acknowledge my cubicle before talking about yourself.\n(4) I replied, "I wish I had a cubicle all to myself."\nFeedback: Bad reply. I understand your wish, but it seems like you\'re just making the conversation about yourself.\n(5) I replied, "Oh my god. Your cubicle, that everyone else also has, is so amazing and beautiful. Breathtaking."\nFeedback: Bad reply. Although it may be intended as a lighthearted joke, the sarcasm is unexpected to my typical statement so it feels mean.\n(6) I replied, "How long have you had a cubicle?"\nFeedback: Good reply. It\'s always good to ask me a question that\'s relevant to what I said to keep the conversation going.', -}; - -export default evalPrompts; diff --git a/src/data/prompts/noora-eval.ts b/src/data/prompts/noora-eval.ts new file mode 100644 index 00000000..5759700f --- /dev/null +++ b/src/data/prompts/noora-eval.ts @@ -0,0 +1,10 @@ +const evalPrompts = { + positive: + `You said, \"I just finished a really good book!\"\r\n(1) I replied, \"thats great, im really happy for you and your love of reading\"\r\nFeedback: Good reply. Thank you for showing your happiness for me! I feel supported by you.\r\n(2) I replied, \"I wonder what the weather's like today.\"\r\nFeedback: Bad reply. This makes me feel ignored. Try talking about my good experience.\r\n(3) I replied, \"You're always reading, you need to get a life!\"\r\nFeedback: Bad reply. I feel hurt because I was hoping you would be happy for me instead of upset.\r\n(4) I replied, \"i cant find any good books im so jealous!\"\r\nFeedback: Good reply. Although you sound jealous, it makes me feel good about reading the book.\r\n(5) I replied, \"i actually finished two really good books\"\r\nFeedback: Bad reply. I feel like you are trying to compete with me instead of being happy for me. Perhaps you can ask me about my book.\r\n(6) I replied, \"aasdf\"\r\nFeedback: Bad reply. Hmm, this is not a reply.\r\n\r\nYou said, \"I just got a promotion!\"\r\n(1) I replied, \"congratulations youre officially a step closer to becoming a slave to the corporate machine\"\r\nFeedback: Bad reply. I know you want to make a joke, but I wish you would share my happiness for my achievement.\r\n(2) I replied, \"I wouldn't want to get promoted because I would just get more work.\"\r\nFeedback: Bad reply. Although we might have different opinions, I feel like you are making the conversation about yourself.\r\n(3) I replied, \"i can imagine how happy you must be\"\r\nFeedback: Good reply. You make me feel understood and add to my happiness.\r\n(4) I replied, \"are you going to be working more hours now\"\r\nFeedback: Good reply. Asking about my work is good because you express interest in what I say.\r\n(5) I replied, \"Wow, you must be really special.\"\r\nFeedback: Bad reply. This can make me feel upset because your response comes off as sarcastic.\r\n(6) I replied, \"why did your boss give you a promotion\"\r\nFeedback: Good reply. It's always good to ask me a question that's relevant to what I said to keep the conversation going.`, + negative: + `You said, \"My grandmother just passed away this morning.\"\r\n(1) I replied, \"im so sorry for your loss, i am here for you if you ever need anything\"\r\nFeedback: Good reply. Thank you for offering support in a time that I may need it.\r\n(2) I replied, \"What time is it?\"\r\nFeedback: Bad reply. I feel ignored and hurt because your reply doesn't have to do with my loss.\r\n(3) I replied, \"my grandma visits me every month\"\r\nFeedback: Bad reply. Let's try showing you care about my loss and feelings instead of bringing up your own grandma.\r\n(4) I replied, \"That sucks.\"\r\nFeedback: Bad reply. This reply makes me feel like you are uninterested. Maybe you can try asking me about how I'm holding up.\r\n(5) I replied, \"are you going to be okay im here if you ever want to talk about it\"\r\nFeedback: Good reply. Your reply shows you care about my emotional well being, and offers support.\r\n(6) I replied, \"aasdf\"\r\nFeedback: Bad reply. Hmm, this is not a reply.\r\n\r\nYou said, \"My boss is always getting mad at me.\"\r\n(1) I replied, \"why do you think that is\"\r\nFeedback: Good reply. Asking more about my situation can be very helpful and it shows that you care.\r\n(2) I replied, \"i understand what you're going through my boss used to get angry at me all the time\"\r\nFeedback: Good reply. You validate my feelings and show me you understand what I'm going through.\r\n(3) I replied, \"Well at least you even have a job. I wish I had one.\"\r\nFeedback: Bad reply. I know that not having a job can be hard for you, but let's try to acknowledge my difficulty first.\r\n(4) I replied, \"That's great! I'm happy for you!\"\r\nFeedback: Bad reply. My hardship isn't something to be happy about, so you should try asking me to learn more instead.\r\n(5) I replied, \"im glad to hear your boss is watching over you\"\r\nFeedback: Bad reply. I feel hurt because you are dismissive of my difficult situation.\r\n(6) I replied, \"why do you think your boss gets mad at you\"\r\nFeedback: Good reply. It's always good to ask me a question that's relevant to what I said to keep the conversation going.`, + neutral: + `You said, \"I am going to the grocery store.\"\r\n(1) I replied, \"ill go with you if you need any help\"\r\nFeedback: Good reply. Thanks for making me feel supported even for little things like going to the grocery store.\r\n(2) I replied, \"don't go to the grocery store!\"\r\nFeedback: Bad reply. Even if you don't want to go, you don't need to tell me not to.\r\n(3) I replied, \"great what are you going to buy\"\r\nFeedback: Good reply. You are excited for me and want to know what I'll buy.\r\n(4) I replied, \"i went to the grocery store two times already\"\r\nFeedback: Bad reply. This makes me feel like you are trying to compete with me. Instead, maybe try asking me why I'm going or what I'm buying.\r\n(5) I replied, \"great! i hope you find everything you need\"\r\nFeedback: Good reply. Your reply is happy and encouraging.\r\n(6) I replied, \"aasdf\"\r\nFeedback: Bad reply. Hmm, this is not a reply.\r\n\r\nYou said, \"This is my cubicle.\"\r\n(1) I replied, \"oh did you just move into this one\"\r\nFeedback: Good reply. Because you ask about my cubicle, I feel like you genuinely care about what I have to say.\r\n(2) I replied, \"it looks quite nice and cozy. did you decorate it\"\r\nFeedback: Good reply. Thank you for showing that you are interested in my cubicle and asking about it.\r\n(3) I replied, \"I miss my cubicle. I had one before too, but I got fired.\"\r\nFeedback: Bad reply. Being fired must've been hard for you, but let's acknowledge my cubicle before talking about yourself.\r\n(4) I replied, \"I wish I had a cubicle all to myself.\"\r\nFeedback: Bad reply. I understand your wish, but it seems like you're just making the conversation about yourself.\r\n(5) I replied, \"oh my god your cubicle, that everyone else also has, is so amazing and beautiful. breathtaking.\"\r\nFeedback: Bad reply. Although it may be intended as a lighthearted joke, the sarcasm is unexpected to my typical statement so it feels mean.\r\n(6) I replied, \"how long have you had a cubicle\"\r\nFeedback: Good reply. It's always good to ask me a question that's relevant to what I said to keep the conversation going.` +}; + +export default evalPrompts; diff --git a/src/data/routes.ts b/src/data/routes.ts index ad63c44e..aaf9cc26 100644 --- a/src/data/routes.ts +++ b/src/data/routes.ts @@ -11,24 +11,10 @@ const routes = [ name: "Dashboard", href: "/dashboard", }, - // { - // name: "Ask Noora", - // href: "/ask-noora", - // }, - // { - // name: "Links", - // dropRoutes: [ - // { href: "https://oval.cs.stanford.edu/", name: "Stanford OVAL" }, - // { - // href: "https://genie.stanford.edu/", - // name: "Genie", - // }, - // { - // href: "https://profiles.stanford.edu/lynn-koegel", - // name: "Dr. Koegel", - // }, - // ], - // }, + { + name: "Ask Noora", + href: "/ask-noora", + }, ]; export default routes; diff --git a/src/data/socials.ts b/src/data/socials.ts index 935db679..5f2b1387 100644 --- a/src/data/socials.ts +++ b/src/data/socials.ts @@ -8,7 +8,7 @@ import { faUserGroup, faGlobe } from "@fortawesome/free-solid-svg-icons"; const socials = [ { name: "Website", href: "https://oval.cs.stanford.edu", icon: faGlobe }, - { name: "Github", href: "https://github.com/stanford-oval", icon: faGithub }, + { name: "Github", href: "https://github.com/stanford-oval/noora", icon: faGithub }, { name: "Discord", href: "https://discord.gg/anthtR4", icon: faDiscord }, { name: "Community", diff --git a/src/data/statement_bank/empathy.ts b/src/data/statement_bank/empathy.ts new file mode 100644 index 00000000..26e59d20 --- /dev/null +++ b/src/data/statement_bank/empathy.ts @@ -0,0 +1,191 @@ +const general_statements = { + positive: [ + [ + "general/positive", + "I love hiking.", + "Hiking is a great way to get some exercise.", + ], + [ + "general/positive", + "We went to the beach last weekend.", + "That sounds like a lot of fun. I'm glad you had a good time.", + ], + [ + "general/positive", + "I sold a bunch of stuff at my garage sale last weekend.", + "That's great! I'm so happy for you!", + ], + [ + "general/positive", + "It's beautiful weather today.", + "Yes, it's lovely weather. ", + ], + [ + "general/positive", + "I just started a workout routine.", + "Good for you! How is it going?", + ], + [ + "general/positive", + "It's really fun going to the dog park.", + "We should go together some time!", + ], + [ + "general/positive", + "My friend is helping me learn Spanish.", + "That's really cool! How are you finding it?", + ], + [ + "general/positive", + "I got a really cheap airplane ticket for my vacation.", + "That's great! I'm sure you'll have a wonderful time.", + ], + [ + "general/positive", + "The weather was great last weekend.", + "Yes, it was! What did you do last weekend?", + ], + [ + "general/positive", + "My daughter loves to take walks.", + "That's wonderful! It's great that she enjoys getting some exercise.", + ], + [ + "general/positive", + "I just joined a volunteer group.", + "Why did you join a volunteer group?", + ], + [ + "general/positive", + "I love working in my garden.", + "Gardening is a great hobby!", + ], + ], + neutral: [ + [ + "general/neutral", + "Tomorrow is supposed to be warm.", + "What's the temperature supposed to be?", + ], + [ + "general/neutral", + "I take a run every morning.", + "Good for you! Where do you run?", + ], + [ + "general/neutral", + "There are still a lot of businesses that mail paper catalogs.", + "Yes, there are. Many people enjoy receiving catalogs in the mail.", + ], + [ + "general/neutral", + "It's a good idea to study every night instead of waiting until the test.", + "I think that's a great idea!", + ], + [ + "general/neutral", + "We went to the beach over the weekend.", + "That sounds like a lot of fun! I'm glad you had a good time.", + ], + [ + "general/neutral", + "I usually go out on Friday nights, but this week I'm staying home.", + "That's sounds like a good plan.", + ], + [ + "general/neutral", + "I've been listening to books on tape.", + "What books have you been listening to?", + ], + [ + "general/neutral", + "There are some new shows on tv.", + "That's great! I'd love to watch some with you.", + ], + [ + "general/neutral", + "I talked with my professor about the class.", + "Good for you! I hope it went well.", + ], + [ + "general/neutral", + "I'm looking for a job.", + "What kind of job are you looking for?", + ], + [ + "general/neutral", + "I applied for a few jobs.", + "That's great, I hope you get one of them!", + ], + ], + negative: [ + [ + "general/negative", + "My brother and I got in an argument last night.", + "I'm sorry to hear that, what happened?", + ], + [ + "general/negative", + "I had an upset stomach last night.", + "I'm sorry to hear that. I hope you're feeling better now.", + ], + [ + "general/negative", + "I was so sick over the weekend.", + "That sounds terrible. I hope you're feeling better now.", + ], + [ + "general/negative", + "My boyfriend lost his job.", + "I'm sorry to hear that. I hope he's doing okay.", + ], + [ + "general/negative", + "My cat ate something and was so sick last night.", + "Oh no, what did your cat eat?", + ], + [ + "general/negative", + "I had to take my daughter to the emergency room last week.", + "I'm so sorry to hear that. Is she doing better?", + ], + [ + "general/negative", + "I had the flu that lasted a week.", + "I'm sorry to hear that, I hope you're feeling better now. ", + ], + [ + "general/negative", + "My aunt wants to visit but my house is too small.", + "That sounds really tough. I know how you feel.", + ], + [ + "general/negative", + "I couldn't figure out the new computer program.", + "What was the problem?", + ], + [ + "general/negative", + "My friend's mother died over the weekend.", + "I'm so sorry for her loss. If there's anything I can do to help, please let me know.", + ], + [ + "general/negative", + "My best friend just found out she has cancer.", + "I'm so sorry to hear that. How are you holding up?", + ], + [ + "general/negative", + "I have to move out of my apartment.", + "I hope everything works out for you. Let me know if I can help in any way.", + ], + [ + "general/negative", + "My landlord sold the house so I have to move out.", + "Why did he sell the house?", + ], + ], + }; + + export default general_statements; + \ No newline at end of file diff --git a/src/data/module_statements/general.ts b/src/data/statement_bank/general.ts similarity index 100% rename from src/data/module_statements/general.ts rename to src/data/statement_bank/general.ts diff --git a/src/data/module_statements/work.ts b/src/data/statement_bank/work.ts similarity index 100% rename from src/data/module_statements/work.ts rename to src/data/statement_bank/work.ts diff --git a/src/pages/404.tsx b/src/pages/404.tsx index ffe3f727..6eaa3560 100644 --- a/src/pages/404.tsx +++ b/src/pages/404.tsx @@ -1,6 +1,6 @@ import Link from "next/link"; import React from "react"; -import Page from "../components/utility/Page"; +import Page from "../components/global/utility/Page"; export default function Custom404() { return ( diff --git a/src/pages/api/hello.ts b/src/pages/api/hello.ts index a3c3323a..81fe64d1 100755 --- a/src/pages/api/hello.ts +++ b/src/pages/api/hello.ts @@ -1,22 +1,16 @@ // Next.js API route support: https://nextjs.org/docs/api-routes/introduction import type { NextApiRequest, NextApiResponse } from "next"; -import rateLimit from "../../scripts/rate-limit"; -const limiter = rateLimit({ - interval: 60 * 1000, // 60 seconds - uniqueTokenPerInterval: 500, // Max 500 users per second -}); +type Data = { + name: string; + x: any; +}; -export default async function handler( +export default function handler( req: NextApiRequest, - res: NextApiResponse + res: NextApiResponse ) { // HANDLER FOR TESTING/EXPERIMENTATION - try { - await limiter.check(res, 10, "CACHE_TOKEN"); // 10 requests per minute - console.log(req.body); - res.status(200).json({ name: "John Doe", x: req.query }); - } catch { - res.status(429).json({ error: "Rate limit exceeded" }); - } + console.log(req.body); + res.status(200).json({ name: "John Doe", x: req.query }); } diff --git a/src/pages/api/openai.ts b/src/pages/api/openai.ts index 21a53144..a20f1b80 100644 --- a/src/pages/api/openai.ts +++ b/src/pages/api/openai.ts @@ -1,6 +1,6 @@ import type { NextApiRequest, NextApiResponse } from "next"; import { Configuration, OpenAIApi } from "openai"; -import rateLimit from "../../scripts/rate-limit"; +import rateLimit from "../../scripts/utils/rate-limit"; const limiter = rateLimit({ interval: 60 * 1000, // 60 seconds diff --git a/src/pages/ask-noora.tsx b/src/pages/ask-noora.tsx index a254d2b3..cb96bb1b 100644 --- a/src/pages/ask-noora.tsx +++ b/src/pages/ask-noora.tsx @@ -1,14 +1,14 @@ import React from "react"; import AskNoora from "../components/ask-noora/AskNoora"; -import Page from "../components/utility/Page"; +import Page from "../components/global/utility/Page"; export default function asknoora() { - return ( - - - - ); -} + return ( + + + + ); +} \ No newline at end of file diff --git a/src/pages/dashboard.tsx b/src/pages/dashboard.tsx index f4d3ebc5..9a8cec38 100644 --- a/src/pages/dashboard.tsx +++ b/src/pages/dashboard.tsx @@ -1,14 +1,14 @@ import React from "react"; -import UserHero from "../components/dashboard/UserHero"; +import UserHero from "../components/dashboard/UserInfo"; import ProgressSummary from "../components/dashboard/ProgressSummary"; -import Page from "../components/utility/Page"; +import Page from "../components/global/utility/Page"; import ModulesInfo from "../components/dashboard/ModulesInfo"; export default function dashboard() { return (
    diff --git a/src/pages/index.tsx b/src/pages/index.tsx index c7702faf..645ef600 100755 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,12 +1,12 @@ import React from "react"; -import Page from "../components/utility/Page"; +import Page from "../components/global/utility/Page"; import Hero from "../components/home/Hero"; export default function index() { return ( diff --git a/src/pages/playground.tsx b/src/pages/playground.tsx deleted file mode 100644 index e3bb0de8..00000000 --- a/src/pages/playground.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import React from "react"; -import Playground from "../components/playground/Playground"; -import Page from "../components/utility/Page"; - -export default function playground() { - return ( - - - - ); -} diff --git a/src/pages/rate-limiting-test.tsx b/src/pages/rate-limiting-test.tsx deleted file mode 100644 index 1c893547..00000000 --- a/src/pages/rate-limiting-test.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import React from "react"; -import RateLimitingTest from "../components/global/utility/RateLimitingTest"; - -export default function ratelimitingtest() { - return ; -} diff --git a/src/scripts/generate-evaluation-prompt_v6.ts b/src/scripts/generate-evaluation-prompt_v6.ts deleted file mode 100644 index 25e859d5..00000000 --- a/src/scripts/generate-evaluation-prompt_v6.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { selectAttitudes, getFewShotExamples } from "./v6_utils"; - -export default function formPrompt( - statement: string, - category: string, - reply: string -) { - const attitudes = selectAttitudes(); - - // console.log(attitudes); - - const fewShotExamples = getFewShotExamples(category.split("/")[1], attitudes); - // console.log(fewShotExamples); - - const prompt = formEvalPrompt(statement, reply, fewShotExamples); - - return prompt; -} - -function formEvalPrompt( - statement: string, - reply: string, - fewShotExamples: any[] -) { - let prompt = ""; - - fewShotExamples.forEach((ex) => { - console.log(ex); - prompt += `You said, "${ex["statement"]}"\n`; - console.log("attitudes:", Object.keys(ex["replies"])); - Object.values(ex["replies"]).forEach((reply: any, idx: number) => { - prompt += `(${idx + 1}) I replied, "${reply["reply"]}"\n`; - prompt += `Feedback: ${capFirst(reply["rating"].trim())} reply. ${ - reply["explanation"] - }\n`; - }); - prompt += "\n"; - }); - - // unseen statement - prompt += `You said, "${statement}"\n(1) ${reply}\nFeedback:`; - return prompt; -} - -function capFirst(str: string) { - return str.charAt(0).toUpperCase() + str.slice(1); -} diff --git a/src/scripts/get-reply.ts b/src/scripts/get-reply.ts index 474a42e3..40cdaa29 100644 --- a/src/scripts/get-reply.ts +++ b/src/scripts/get-reply.ts @@ -1,259 +1,20 @@ -import general_statements from "../data/module_statements/general"; -import work_statements from "../data/module_statements/work"; -import Completion from "./Completion"; -import formPrompt from "./generate-evaluation-prompt"; -import { v4 as uuidv4 } from "uuid"; - -const module_statements = { - general: general_statements, - work: work_statements, -}; - export default async function getReply( message: string, + id: string, convoState: any, - command: string -) { - // response loading - convoState.setValue((cs: any) => ({ ...cs, turn: command })); - - let replies = [ - { - id: uuidv4(), - fromNoora: true, - text: "Oops! Something went wrong.", - sentiment: "neutral", - statement: false, - }, - ]; - - if (command == "get-statement") { - const statement = await getStatement(convoState) - replies = [ - { - id: uuidv4(), - fromNoora: true, - text: statement.text, - sentiment: statement.sentiment, - statement: true, - }, - ]; - } else if (command == "rate-reply") { - convoState.setValue((cs: any) => ({ - ...cs, - statement: null, - })); - let answers = await getRating( - message, - convoState.value.statement.statementIdx, - [...convoState.value.statement.statementObj], - convoState - ); - - replies = answers.map((a: any) => ({ - id: uuidv4(), - fromNoora: true, - text: a.text, - suggestion: a.suggestion, - sentiment: a.sentiment ? a.sentiment : "neutral", - statement: false, - })); - } - - convoState.setValue((cs: any) => ({ - ...cs, - turn: command == "get-statement" ? "user-select" : "user-answer", - })); - - return replies; -} - -async function getRating( - message: string, - statementIdx: number, - statementObj: any, - convoState: any + setConvoState: any ) { - const prompt = formPrompt(statementObj[1], statementObj[0], message); - let target = statementObj[2]; - - let classification = ""; - let explanation = ""; - let goodAnswer = false; - let answers = []; - let goodReplyConfidence = -1; - - try { - // first get good/bad answer - let output = await fetch("/api/openai", { - method: "POST", - body: JSON.stringify({ - model: convoState.value.model.name, - prompt: prompt, - temperature: 0, - max_tokens: 1, - frequency_penalty: 0, - presence_penalty: 0, - logprobs: 5, - }), - }).then((res) => res.json()); - - let probsObj = output["logprobs"]["top_logprobs"][0]; - - let probs = softmax(Object.values(probsObj)); - let topTokens = Object.keys(probsObj); - let goodProb = 0.000001; - if (topTokens.indexOf(" Good") != -1) - goodProb = probs[topTokens.indexOf(" Good")]; - let badProb = 0.000001; - if (topTokens.indexOf(" Bad") != -1) - badProb = probs[topTokens.indexOf(" Bad")]; - goodReplyConfidence = goodProb / (goodProb + badProb); - - let threshold = - message.length < 3 ? 0.9 : convoState.value.model.goodReplyThreshold; // length filtering - console.log( - `"good" token probability: ${goodProb}. "bad token probability: ${badProb}. threshold: ${threshold}` - ); - if (goodReplyConfidence > threshold) { - classification = "Good reply."; - goodAnswer = true; - } else { - classification = "Bad reply."; - goodAnswer = false; - } - - console.log("Classification: " + classification); - - // console.log(prompt + " " + classification); - - output = await Completion({ - model: convoState.value.model.name, - prompt: prompt + " " + classification, - temperature: convoState.value.model.temperature, - max_tokens: 40, - frequency_penalty: convoState.value.frequencyPenalty, - presence_penalty: convoState.value.model.presencePenalty, - stop: "\n", - }); - - explanation = output.trim(); - console.log("Explanation: " + explanation); - - if (goodAnswer) { - answers.push({ text: "Good reply!", sentiment: "positive" }); - answers.push({ text: explanation, sentiment: "positive" }); - } else { - answers.push({ text: "Not quite!" }); - answers.push({ text: explanation }); - answers.push({ text: target.trim(), suggestion: true }); - } - } catch (error) { - console.error(error); - explanation = "Please enter a proper reply."; - answers = [explanation]; - } - - // SET PROGRESS - convoState.setValue((cs: any) => ({ - ...cs, - progress: [ - ...cs.progress, - { - idx: statementIdx, - statement: statementObj[1], - statementCategory: statementObj[0], - reply: message, - explanation: explanation, - replyCategory: null, - goodAnswer: goodAnswer, - goodReplyConfidence: goodReplyConfidence, - goodReplyThreshold: convoState.value.model.goodReplyThreshold, - }, - ], - })); - - return answers; -} - -export async function getStatement(convoState: any) { - // choose module - const modules = convoState.value.modules.filter((m: any) => m.active); - const sentiments = convoState.value.sentiments.filter((s: any) => s.active); - const category = getRandomItem(modules) - .title as keyof typeof module_statements; - const sentiment = getRandomItem(sentiments) - .title as keyof typeof module_statements[typeof category]; - - // await timeout(700); - // choose statement - const statementIdx = getStatementIdx( - category + "/" + sentiment, - module_statements[category][sentiment], - convoState - ); - const statement = module_statements[category][sentiment][statementIdx]; - convoState.setValue((cs: any) => ({ - ...cs, - statement: { statementIdx: statementIdx, statementObj: statement }, - })); - // console.log("Selected statement: "); - // console.log(statement); - - return { sentiment: statement[0].split("/")[1], text: statement[1] }; -} - -function getStatementIdx( - category: string, - statementsList: any[], - convoState: any -) { - let seenIdxs = convoState.value.progress - .map((p: any) => { - if (p.statementCategory == category && p.goodAnswer) - return p.idx; // same category seen, but can repeat ones where user got it wrong - else { - return -1; - } - }) - .filter((i: number) => i >= 0); - if (convoState.value.statement) - seenIdxs.push(convoState.value.statement.statementIdx); - // console.log("Seen statement indexes:", seenIdxs); - - if (seenIdxs.length >= statementsList.length) { - console.log("Exhausted all statements. Resetting."); - seenIdxs = seenIdxs.slice( - statementsList.length * - Math.floor(seenIdxs.length / statementsList.length), - seenIdxs.length - ); - } - - let newRandomIdx = 0; - while (true) { - newRandomIdx = Math.floor(Math.random() * statementsList.length); - if (seenIdxs.indexOf(newRandomIdx) == -1) break; - } - - return newRandomIdx; -} - -function getRandomItem(items: any) { - return items[Math.floor(Math.random() * items.length)]; + setConvoState({ turn: "noora-reply" }); + let reply = message; + await timeout(5000); + return { + id: id, + fromNoora: true, + text: "I currently only echo you. Here was your message: " + reply, + }; } -function softmax(arr: any[]) { - return arr.map(function (value: any, index: any) { - return ( - Math.exp(value) / - arr - .map(function (y /*value*/) { - return Math.exp(y); - }) - .reduce(function (a, b) { - return a + b; - }) - ); - }); +function timeout(ms: number) { + // for testing purposes + return new Promise((resolve) => setTimeout(resolve, ms)); } diff --git a/src/scripts/Completion.tsx b/src/scripts/gpt-3/Completion.tsx similarity index 100% rename from src/scripts/Completion.tsx rename to src/scripts/gpt-3/Completion.tsx diff --git a/src/scripts/generate-advice.ts b/src/scripts/gpt-3/generate-advice.ts similarity index 94% rename from src/scripts/generate-advice.ts rename to src/scripts/gpt-3/generate-advice.ts index 5793b1ec..50c04c97 100644 --- a/src/scripts/generate-advice.ts +++ b/src/scripts/gpt-3/generate-advice.ts @@ -1,5 +1,5 @@ import Completion from "./Completion"; -import askNooraPrompt from "../data/prompts/ask-noora-prompt"; +import askNooraPrompt from "../../data/prompts/ask-noora"; const temp = 0.9; const freqPenalty = 0.6; diff --git a/src/scripts/generate-evaluation-prompt.ts b/src/scripts/gpt-3/generate-evaluation-prompt.ts similarity index 81% rename from src/scripts/generate-evaluation-prompt.ts rename to src/scripts/gpt-3/generate-evaluation-prompt.ts index ac5b8f21..9f3273a9 100644 --- a/src/scripts/generate-evaluation-prompt.ts +++ b/src/scripts/gpt-3/generate-evaluation-prompt.ts @@ -1,4 +1,4 @@ -import evalPrompts from "../data/prompts/noora-eval-prompt"; +import evalPrompts from "../../data/prompts/noora-eval"; export default function formPrompt( statement: string, diff --git a/src/scripts/util.ts b/src/scripts/noora-chat/audio_utils.ts similarity index 79% rename from src/scripts/util.ts rename to src/scripts/noora-chat/audio_utils.ts index 2b1793f1..c2c067cc 100644 --- a/src/scripts/util.ts +++ b/src/scripts/noora-chat/audio_utils.ts @@ -1,7 +1,7 @@ -export function messageToSpeechParams(convoState: any, message: any, currentAudioRef: any, history: any, hidden: any) { +export function messageToSpeechParams(convoState: any, message: any, audioRef: any, history: any, hidden: any) { const preText = message.suggestion ? "A better reply might've been: " : "" const text = message.read ? message.read : message.text - const postText = message.statement ? " Is this a positive, neutral, or negative statement?" : "" + const postText = message.statement ? " When I say this, do I sound positive, neutral, or negative?" : "" let style = "neutral" if (message.style) style = message.style @@ -19,11 +19,11 @@ export function messageToSpeechParams(convoState: any, message: any, currentAudi styleDegree: style == "cheerful" ? 1.1 : 1.3, convoState: convoState, history: history, - currentAudioRef: currentAudioRef, + audioRef: audioRef, hidden: hidden, } } export function timeout(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); -} +} \ No newline at end of file diff --git a/src/scripts/noora-chat/get-reply.ts b/src/scripts/noora-chat/get-reply.ts new file mode 100644 index 00000000..b2ac1fb6 --- /dev/null +++ b/src/scripts/noora-chat/get-reply.ts @@ -0,0 +1,261 @@ +import general_statements from "../../data/statement_bank/general"; +import work_statements from "../../data/statement_bank/work"; +import empathy_statements from "../../data/statement_bank/empathy" +import Completion from "../gpt-3/Completion"; +import { v4 as uuidv4 } from "uuid"; +import formPrompt from "../gpt-3/generate-evaluation-prompt"; + +const module_statements = { + general: general_statements, + work: work_statements, + empathy: empathy_statements, +}; + +export default async function getReply( + message: string, + convoState: any, + command: string +) { + // response loading + convoState.setValue((cs: any) => ({ ...cs, turn: command })); + + let replies = [ + { + id: uuidv4(), + fromNoora: true, + text: "Oops! Something went wrong.", + sentiment: "neutral", + statement: false, + }, + ]; + + if (command == "get-statement") { + const statement = await getStatement(convoState) + replies = [ + { + id: uuidv4(), + fromNoora: true, + text: statement.text, + sentiment: statement.sentiment, + statement: true, + }, + ]; + } else if (command == "rate-reply") { + convoState.setValue((cs: any) => ({ + ...cs, + statement: null, + })); + let answers = await getRating( + message, + convoState.value.statement.statementIdx, + [...convoState.value.statement.statementObj], + convoState + ); + + replies = answers.map((a: any) => ({ + id: uuidv4(), + fromNoora: true, + text: a.text, + suggestion: a.suggestion, + sentiment: a.sentiment ? a.sentiment : "neutral", + statement: false, + })); + } + + convoState.setValue((cs: any) => ({ + ...cs, + turn: command == "get-statement" ? "user-select" : "user-answer", + })); + + return replies; +} + +async function getRating( + message: string, + statementIdx: number, + statementObj: any, + convoState: any +) { + const prompt = formPrompt(statementObj[1], statementObj[0], message); + let target = statementObj[2]; + + let classification = ""; + let explanation = ""; + let goodAnswer = false; + let answers = []; + let goodReplyConfidence = -1; + + try { + // first get good/bad answer + let output = await fetch("/api/openai", { + method: "POST", + body: JSON.stringify({ + model: convoState.value.model.name, + prompt: prompt, + temperature: 0, + max_tokens: 1, + frequency_penalty: 0, + presence_penalty: 0, + logprobs: 5, + }), + }).then((res) => res.json()); + + let probsObj = output["logprobs"]["top_logprobs"][0]; + + let probs = softmax(Object.values(probsObj)); + let topTokens = Object.keys(probsObj); + let goodProb = 0.000001; + if (topTokens.indexOf(" Good") != -1) + goodProb = probs[topTokens.indexOf(" Good")]; + let badProb = 0.000001; + if (topTokens.indexOf(" Bad") != -1) + badProb = probs[topTokens.indexOf(" Bad")]; + goodReplyConfidence = goodProb / (goodProb + badProb); + + let threshold = + message.length < 3 ? 0.9 : convoState.value.model.goodReplyThreshold; // length filtering + console.log( + `"good" token probability: ${goodProb}. "bad token probability: ${badProb}. threshold: ${threshold}` + ); + if (goodReplyConfidence > threshold) { + classification = "Good reply."; + goodAnswer = true; + } else { + classification = "Bad reply."; + goodAnswer = false; + } + + console.log("Classification: " + classification); + + // console.log(prompt + " " + classification); + + output = await Completion({ + model: convoState.value.model.name, + prompt: prompt + " " + classification, + temperature: convoState.value.model.temperature, + max_tokens: 40, + frequency_penalty: convoState.value.frequencyPenalty, + presence_penalty: convoState.value.model.presencePenalty, + stop: "\n", + }); + + explanation = output.trim(); + console.log("Explanation: " + explanation); + + if (goodAnswer) { + answers.push({ text: "Good reply!", sentiment: "positive" }); + answers.push({ text: explanation, sentiment: "positive" }); + } else { + answers.push({ text: "Not quite!" }); + answers.push({ text: explanation }); + answers.push({ text: target.trim(), suggestion: true }); + } + } catch (error) { + console.error(error); + explanation = "Please enter a proper reply."; + answers = [explanation]; + } + + // SET PROGRESS + convoState.setValue((cs: any) => ({ + ...cs, + progress: [ + ...cs.progress, + { + idx: statementIdx, + statement: statementObj[1], + statementCategory: statementObj[0], + reply: message, + explanation: explanation, + replyCategory: null, + goodAnswer: goodAnswer, + goodReplyConfidence: goodReplyConfidence, + goodReplyThreshold: convoState.value.model.goodReplyThreshold, + }, + ], + })); + + return answers; +} + +export async function getStatement(convoState: any) { + // choose module + const modules = convoState.value.modules.filter((m: any) => m.active); + const sentiments = convoState.value.sentiments.filter((s: any) => s.active); + const category = getRandomItem(modules) + .title as keyof typeof module_statements; + const sentiment = getRandomItem(sentiments) + .title as keyof typeof module_statements[typeof category]; + + // await timeout(700); + // choose statement + const statementIdx = getStatementIdx( + category + "/" + sentiment, + module_statements[category][sentiment], + convoState + ); + const statement = module_statements[category][sentiment][statementIdx]; + convoState.setValue((cs: any) => ({ + ...cs, + statement: { statementIdx: statementIdx, statementObj: statement }, + })); + // console.log("Selected statement: "); + // console.log(statement); + + return { sentiment: statement[0].split("/")[1], text: statement[1] }; +} + +function getStatementIdx( + category: string, + statementsList: any[], + convoState: any +) { + let seenIdxs = convoState.value.progress + .map((p: any) => { + if (p.statementCategory == category && p.goodAnswer) + return p.idx; // same category seen, but can repeat ones where user got it wrong + else { + return -1; + } + }) + .filter((i: number) => i >= 0); + if (convoState.value.statement) + seenIdxs.push(convoState.value.statement.statementIdx); + // console.log("Seen statement indexes:", seenIdxs); + + if (seenIdxs.length >= statementsList.length) { + console.log("Exhausted all statements. Resetting."); + seenIdxs = seenIdxs.slice( + statementsList.length * + Math.floor(seenIdxs.length / statementsList.length), + seenIdxs.length + ); + } + + let newRandomIdx = 0; + while (true) { + newRandomIdx = Math.floor(Math.random() * statementsList.length); + if (seenIdxs.indexOf(newRandomIdx) == -1) break; + } + + return newRandomIdx; +} + +function getRandomItem(items: any) { + return items[Math.floor(Math.random() * items.length)]; +} + +function softmax(arr: any[]) { + return arr.map(function (value: any, index: any) { + return ( + Math.exp(value) / + arr + .map(function (y /*value*/) { + return Math.exp(y); + }) + .reduce(function (a, b) { + return a + b; + }) + ); + }); +} diff --git a/src/scripts/noora-chat/handle-submit.ts b/src/scripts/noora-chat/handle-submit.ts new file mode 100644 index 00000000..4d3c9845 --- /dev/null +++ b/src/scripts/noora-chat/handle-submit.ts @@ -0,0 +1,59 @@ +import { v4 as uuidv4 } from "uuid"; +import { saveSessionResult } from "../user-info/session-results"; +import noorasTurn from "./nooras-turn"; + +export default async function handleSubmit(e: any, convoState: any, history: any, message?: string) { + e.preventDefault(); + + // stop audio + if (convoState.value.audio.player) { + convoState.value.audio.player.pause() + convoState.value.audio.player.close() + } + + message = message ? message : convoState.value.draft.slice(); + if (!message) message = "" + + let userMsgId = uuidv4(); + + history.setValue((h: any) => [ + ...h, + { id: userMsgId, fromNoora: false, text: message, show: true }, + ]); + convoState.setValue((cs: any) => ({ ...cs, draft: "" })); + + let m = message.trim().toLowerCase(); + + if (convoState.value.turn.includes("start")) { + if (m.includes("no") || m.includes("don")) { + history.setValue((h: any) => [ + ...h, + { id: uuidv4(), fromNoora: true, text: "Are you ready to begin?" }, + ]); + } else { + await noorasTurn(message, convoState, history, true); + } + } else if ( + convoState.value.progress.length < convoState.value.numProblems + ) { + await noorasTurn(message, convoState, history); + } else { + if (m == "yes") { + convoState.setValue((cs: any) => ({ + ...cs, + numProblems: cs.numProblems + 3, + })); + await noorasTurn(message, convoState, history, true); + } else { + handleSessionEnd(convoState) + } + } +}; + + +function handleSessionEnd(convoState: any) { + saveSessionResult(convoState.value.progress) + + // show summary + convoState.setValue((cs: any) => ({ ...cs, turn: "summary" })); +} \ No newline at end of file diff --git a/src/scripts/noora-chat/nooras-turn.ts b/src/scripts/noora-chat/nooras-turn.ts new file mode 100644 index 00000000..658edc28 --- /dev/null +++ b/src/scripts/noora-chat/nooras-turn.ts @@ -0,0 +1,78 @@ +import { v4 as uuidv4 } from "uuid"; +import getReply from "./get-reply"; + +export default async function noorasTurn( + message: string, + convoState: any, + history: any, + noorasTurn = false +) { + if (convoState.value.statement) { + if (convoState.value.turn.includes("select")) { + // handle sentiment classification + const targetSentiment = convoState.value.statement.statementObj[0].split("/")[1] + const m = message.trim().toLowerCase() + const correct = m == targetSentiment.trim().toLowerCase() + + + history.setValue((h: any) => [...h, { + id: uuidv4(), + fromNoora: true, + read: correct ? null : `Actually, this is a ${targetSentiment} statement.`, + sentiment: correct ? "positive" : "neutral", + text: correct ? "That's right!" : targetSentiment, + correction: !correct, + }, + { + id: uuidv4(), + fromNoora: true, + text: "How would you reply to me?", + }] + ); + + convoState.setValue((cs: any) => ({ + ...cs, + turn: "user-answer", + })); + + return + } + else { + // rate reply if statement was given + const replies = await getReply(message, convoState, "rate-reply"); + history.setValue((h: any) => [...h, ...replies]); + } + } + + // get statement if still need to practice + // note that progress state has not updated yet, so we use (length + 1) + if ( + convoState.value.progress.length + 1 < convoState.value.numProblems || + noorasTurn + ) { + const replies = await getReply(message, convoState, "get-statement"); + + history.setValue((h: any) => [ + ...h, + { + fromNoora: true, + id: uuidv4(), + text: + convoState.value.turn.includes("start") + ? "Let's get started." + : "Let's try another one.", + }, + ...replies, + ]); + } else { + history.setValue((h: any) => [ + ...h, + { + id: uuidv4(), + fromNoora: true, + text: `Good job! You practiced ${convoState.value.numProblems} scenarios. Do you want to continue practicing?`, + }, + ]); + convoState.setValue((cs: any) => ({ ...cs, turn: "user-select-end" })); + } +} diff --git a/src/scripts/user-info/local-storage-utils.ts b/src/scripts/user-info/local-storage-utils.ts new file mode 100644 index 00000000..5669e6cf --- /dev/null +++ b/src/scripts/user-info/local-storage-utils.ts @@ -0,0 +1,23 @@ +export function writeResultToLocal(result: any) { + let results = getFromLocal("progress") + + console.log("Current results:", results) + result = [result] + if (results) + result.push(...results) // reverse order + + writeToLocal("progress", JSON.stringify(result)) +} + +export function writeToLocal(key: string, value: any) { + if (typeof window !== "undefined") { + localStorage.setItem(key, value) + } else { + console.log("Could not write to local storage for key: " + key) + } +} + +export function getFromLocal(key: string) { + if (typeof window !== "undefined") + return JSON.parse(localStorage.getItem(key) as string) +} diff --git a/src/scripts/user-info/session-results.ts b/src/scripts/user-info/session-results.ts new file mode 100644 index 00000000..2fe705e8 --- /dev/null +++ b/src/scripts/user-info/session-results.ts @@ -0,0 +1,28 @@ +import { writeResultToLocal } from "./local-storage-utils"; + +export function saveSessionResult(progressObj: any) { + let result = { + timeCompleted: Date.now(), + scores: { + // [correct, attempted] + // positive, neutral, negative + total: [0, 0] + } + } + + progressObj.forEach((problem: any) => { + let sentiment = problem.statementCategory.split("/")[1] + let sentimentKey = sentiment as keyof typeof result.scores + if (!result.scores[sentimentKey]) + result.scores[sentimentKey] = [0, 0] + + result.scores[sentimentKey][1]++; + result.scores.total[1]++; + result.scores[sentimentKey][0] += problem.goodAnswer ? 1 : 0; + result.scores.total[0] += problem.goodAnswer ? 1 : 0; + }) + + console.log("### Saving to progress: ", result) + writeResultToLocal(result) + console.log("### Done saving to progress") +} \ No newline at end of file diff --git a/src/scripts/rate-limit.ts b/src/scripts/utils/rate-limit.ts similarity index 100% rename from src/scripts/rate-limit.ts rename to src/scripts/utils/rate-limit.ts diff --git a/src/scripts/token_util.ts b/src/scripts/utils/token_util.ts similarity index 100% rename from src/scripts/token_util.ts rename to src/scripts/utils/token_util.ts diff --git a/src/scripts/v6_utils.ts b/src/scripts/v6_utils.ts deleted file mode 100644 index 1eb4e879..00000000 --- a/src/scripts/v6_utils.ts +++ /dev/null @@ -1,50 +0,0 @@ -import examples, { - attitudes as all_attitudes, -} from "../data/few-shot-examples"; - -export function selectAttitudes() { - let all_attitudes_copy = all_attitudes.slice(); - let attitudes = []; - - while (all_attitudes_copy.length) { - var randomIndex = Math.floor(Math.random() * all_attitudes_copy.length), - element = all_attitudes_copy.splice(randomIndex, 1); - - attitudes.push(element[0]); - } - - attitudes = attitudes.slice(0, 15); - - attitudes[Math.floor(Math.random() * 10)] = "inquiring"; - return attitudes; -} - -export function getFewShotExamples(sentiment: string, attitudes: string[]) { - // two objects in fewShotExamples: {statement: '', replies: [{attitude: '', reply: '', rating: '', explanation: ''}]} - let fewShotExamples: any[] = []; - - let currExs = examples[sentiment as keyof typeof examples]; - console.log(sentiment + ":", currExs); - - [currExs["general"], currExs["work"]].forEach((exs, i) => { - console.log(i, exs["statement"]); - let selectedExs = Object.fromEntries( - Object.entries(exs).filter( - ([key]) => attitudes.slice(i * 5, 5 * (i + 1)).indexOf(key) != -1 - ) - ); - - let replies = Object.keys(selectedExs).map((key) => ({ - ...(selectedExs[key] as Object), - attitude: key, - })); - - fewShotExamples.push({ - statement: exs["statement"], - replies: replies, - attitudes: attitudes.slice(i * 5, 5 * (i + 1)), - }); - }); - - return fewShotExamples; -} diff --git a/src/styles/globals.css b/src/styles/globals.css index 06b986fa..9b7baed6 100755 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -52,8 +52,8 @@ /* PLAYGROUND */ #chat-window { - height: 55vh; - max-height: 55vh; + height: 56vh; + max-height: 56vh; overflow: auto !important; } diff --git a/yarn.lock b/yarn.lock index 637accfc..5d8df99c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -216,6 +216,11 @@ resolved "https://registry.npmjs.org/@types/cookie/-/cookie-0.3.3.tgz" integrity sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow== +"@types/dateformat@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@types/dateformat/-/dateformat-5.0.0.tgz#17ce64b0318f3f36d1c830c58a7a915445f1f93d" + integrity sha512-SZg4JdHIWHQGEokbYGZSDvo5wA4TLYPXaqhigs/wH+REDOejcJzgH+qyY+HtEUtWOZxEUkbhbdYPqQDiEgrXeA== + "@types/json5@^0.0.29": version "0.0.29" resolved "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz" @@ -433,6 +438,11 @@ array.prototype.flatmap@^1.3.0: es-abstract "^1.19.2" es-shim-unscopables "^1.0.0" +asap@~2.0.3: + version "2.0.6" + resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA== + asn1.js-rfc2560@^5.0.1: version "5.0.1" resolved "https://registry.npmjs.org/asn1.js-rfc2560/-/asn1.js-rfc2560-5.0.1.tgz" @@ -665,6 +675,19 @@ core-js-pure@^3.20.2: resolved "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.23.4.tgz" integrity sha512-lizxkcgj3XDmi7TUBFe+bQ1vNpD5E4t76BrBWI3HdUxdw/Mq1VF4CkiHzIKyieECKtcODK2asJttoofEeUKICQ== +core-js@^1.0.0: + version "1.2.7" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" + integrity sha512-ZiPp9pZlgxpWRu0M+YWbm6+aQ84XEfH1JRXvfOc/fILWI0VKhLC2LX13X1NYq4fULzLMq7Hfh43CSo2/aIaUPA== + +create-react-class@^15.6.0: + version "15.7.0" + resolved "https://registry.yarnpkg.com/create-react-class/-/create-react-class-15.7.0.tgz#7499d7ca2e69bb51d13faf59bd04f0c65a1d6c1e" + integrity sha512-QZv4sFWG9S5RUvkTYWbflxeZX+JG7Cz0Tn33rQBJ+WFQTqTfUTjMjiv9tnfXazjsO5r0KhPs+AqCjyrQX6h2ng== + dependencies: + loose-envify "^1.3.1" + object-assign "^4.1.1" + cross-spawn@^7.0.2: version "7.0.3" resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz" @@ -689,6 +712,11 @@ damerau-levenshtein@^1.0.8: resolved "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz" integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA== +dateformat@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-5.0.3.tgz#fe2223eff3cc70ce716931cb3038b59a9280696e" + integrity sha512-Kvr6HmPXUMerlLcLF+Pwq3K7apHpYmGDVqrxcDasBg86UcKeTSNWbEzU8bwdXnxnR44FtMhJAxI4Bov6Y/KUfA== + debug@4, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4: version "4.3.4" resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz" @@ -801,6 +829,13 @@ emoji-regex@^9.2.2: resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz" integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== +encoding@^0.1.11: + version "0.1.13" + resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9" + integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A== + dependencies: + iconv-lite "^0.6.2" + errlop@^2.0.0: version "2.2.0" resolved "https://registry.npmjs.org/errlop/-/errlop-2.2.0.tgz" @@ -1103,6 +1138,19 @@ fastq@^1.6.0: dependencies: reusify "^1.0.4" +fbjs@^0.8.9: + version "0.8.18" + resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.18.tgz#9835e0addb9aca2eff53295cd79ca1cfc7c9662a" + integrity sha512-EQaWFK+fEPSoibjNy8IxUtaFOMXcWsY0JaVrQoZR9zC8N2Ygf9iDITPWjUTVIax95b6I742JFLqASHfsag/vKA== + dependencies: + core-js "^1.0.0" + isomorphic-fetch "^2.1.1" + loose-envify "^1.0.0" + object-assign "^4.1.0" + promise "^7.1.1" + setimmediate "^1.0.5" + ua-parser-js "^0.7.30" + file-entry-cache@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz" @@ -1313,6 +1361,11 @@ heimdalljs@^0.2.3: dependencies: rsvp "~3.2.1" +heroicons@^2.0.10: + version "2.0.10" + resolved "https://registry.yarnpkg.com/heroicons/-/heroicons-2.0.10.tgz#ab421e0190cfc4fcf642a116b80b9a2ee55f149f" + integrity sha512-4f2UYy2cf2oHswZzdH2frENVk89RV/RLoNnDL577ulV1xsdTWBkM39Gzo2e9AXgTWJ3H95ol/Qs9JBSSU+APBg== + https-proxy-agent@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz" @@ -1321,6 +1374,13 @@ https-proxy-agent@^4.0.0: agent-base "5" debug "4" +iconv-lite@^0.6.2: + version "0.6.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + ignore@^5.2.0: version "5.2.0" resolved "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz" @@ -1451,6 +1511,11 @@ is-shared-array-buffer@^1.0.2: dependencies: call-bind "^1.0.2" +is-stream@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + integrity sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ== + is-stream@^2.0.0: version "2.0.1" resolved "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz" @@ -1482,6 +1547,14 @@ isexe@^2.0.0: resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== +isomorphic-fetch@^2.1.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9" + integrity sha512-9c4TNAKYXM5PRyVcwUZrF3W09nQ+sO7+jydgs4ZGW9dhsLG2VOlISJABombdQqQRXCwuYG3sYV/puGf5rp0qmA== + dependencies: + node-fetch "^1.0.1" + whatwg-fetch ">=0.10.0" + istextorbinary@^2.5.1: version "2.6.0" resolved "https://registry.npmjs.org/istextorbinary/-/istextorbinary-2.6.0.tgz" @@ -1566,7 +1639,7 @@ lodash.merge@^4.6.2: resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -loose-envify@^1.1.0, loose-envify@^1.4.0: +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.1, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -1705,6 +1778,14 @@ next@12.2.2: "@next/swc-win32-ia32-msvc" "12.2.2" "@next/swc-win32-x64-msvc" "12.2.2" +node-fetch@^1.0.1: + version "1.7.3" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef" + integrity sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ== + dependencies: + encoding "^0.1.11" + is-stream "^1.0.1" + node-releases@^2.0.6: version "2.0.6" resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz" @@ -1720,7 +1801,7 @@ normalize-range@^0.1.2: resolved "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz" integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA== -object-assign@^4.1.1: +object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== @@ -1958,7 +2039,14 @@ prelude-ls@^1.2.1: resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== -prop-types@^15.8.1: +promise@^7.1.1: + version "7.3.1" + resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf" + integrity sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg== + dependencies: + asap "~2.0.3" + +prop-types@^15.5.10, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -1982,11 +2070,28 @@ quick-lru@^5.1.1: resolved "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz" integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== +react-addons-test-utils@^15.3.2: + version "15.6.2" + resolved "https://registry.yarnpkg.com/react-addons-test-utils/-/react-addons-test-utils-15.6.2.tgz#c12b6efdc2247c10da7b8770d185080a7b047156" + integrity sha512-6IUCnLp7jQRBftm2anf8rP8W+8M2PsC7GPyMFe2Wef3Wfml7j2KybVL//Ty7bRDBqLh8AG4m/zNZbFlwulldFw== + react-anchor-link-smooth-scroll@^1.0.12: version "1.0.12" resolved "https://registry.npmjs.org/react-anchor-link-smooth-scroll/-/react-anchor-link-smooth-scroll-1.0.12.tgz" integrity sha512-aaY+9X0yh8YnC0jBfoTKpsiCLdO/Y6pCltww+VB+NnTBPDOvnIdnp1AlazajsDitc1j+cVSQ+yNtaVeTIMQbxw== +react-circular-progressbar@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/react-circular-progressbar/-/react-circular-progressbar-2.1.0.tgz#99e5ae499c21de82223b498289e96f66adb8fa3a" + integrity sha512-xp4THTrod4aLpGy68FX/k1Q3nzrfHUjUe5v6FsdwXBl3YVMwgeXYQKDrku7n/D6qsJA9CuunarAboC2xCiKs1g== + +react-device-detect@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/react-device-detect/-/react-device-detect-2.2.2.tgz#dbabbce798ec359c83f574c3edb24cf1cca641a5" + integrity sha512-zSN1gIAztUekp5qUT/ybHwQ9fmOqVT1psxpSlTn1pe0CO+fnJHKRLOWWac5nKxOxvOpD/w84hk1I+EydrJp7SA== + dependencies: + ua-parser-js "^1.0.2" + react-dom@18.2.0: version "18.2.0" resolved "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz" @@ -1995,6 +2100,16 @@ react-dom@18.2.0: loose-envify "^1.1.0" scheduler "^0.23.0" +react-dom@^15.3.2: + version "15.7.0" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.7.0.tgz#39106dee996d0742fb0f43d567ef8b8153483ab2" + integrity sha512-mpjXqC2t1FuYsILOLCj0kg6pbg460byZkVA/80VtDmKU/pYmoTdHOtaMcTRIDiyXLz4sIur0cQ04nOC6iGndJg== + dependencies: + fbjs "^0.8.9" + loose-envify "^1.1.0" + object-assign "^4.1.0" + prop-types "^15.5.10" + react-floater@^0.7.5: version "0.7.5" resolved "https://registry.yarnpkg.com/react-floater/-/react-floater-0.7.5.tgz#23e1d7d0d3be8228239595b6071cd97e2c9d261f" @@ -2043,6 +2158,26 @@ react@18.2.0: dependencies: loose-envify "^1.1.0" +react@^15.3.2: + version "15.7.0" + resolved "https://registry.yarnpkg.com/react/-/react-15.7.0.tgz#10308fd42ac6912a250bf00380751abc41ac7106" + integrity sha512-5/MMRYmpmM0sMTHGLossnJCrmXQIiJilD6y3YN3TzAwGFj6zdnMtFv6xmi65PHKRV+pehIHpT7oy67Sr6s9AHA== + dependencies: + create-react-class "^15.6.0" + fbjs "^0.8.9" + loose-envify "^1.1.0" + object-assign "^4.1.0" + prop-types "^15.5.10" + +reactjs-percentage-circle@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/reactjs-percentage-circle/-/reactjs-percentage-circle-1.0.0.tgz#0d2629f64d89bca76efb3230361e2c93d71835a7" + integrity sha512-Q+Km0D/owm72kuBe2K5VjEkbje3npK18ppIzks8Se7HxWKbNKMYNUbfclGuxHsvkxNh273hPOqHqdno1n5TACQ== + dependencies: + react "^15.3.2" + react-addons-test-utils "^15.3.2" + react-dom "^15.3.2" + read-cache@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz" @@ -2128,7 +2263,7 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -safer-buffer@^2.1.0: +"safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.1.0: version "2.1.2" resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== @@ -2162,6 +2297,11 @@ semver@^7.3.7: dependencies: lru-cache "^6.0.0" +setimmediate@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" + integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA== + shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz" @@ -2264,6 +2404,11 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== +swr@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/swr/-/swr-1.3.0.tgz#c6531866a35b4db37b38b72c45a63171faf9f4e8" + integrity sha512-dkghQrOl2ORX9HYrMDtPa7LTVHJjCTeZoB1dqTbnnEDlSvN8JEKpYIYurDfvbQFUUS8Cg8PceFVZNkW0KNNYPw== + tailwindcss@^3.1.6: version "3.1.6" resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.1.6.tgz" @@ -2361,6 +2506,16 @@ typescript@4.7.4: resolved "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz" integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ== +ua-parser-js@^0.7.30: + version "0.7.31" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.31.tgz#649a656b191dffab4f21d5e053e27ca17cbff5c6" + integrity sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ== + +ua-parser-js@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.2.tgz#e2976c34dbfb30b15d2c300b2a53eac87c57a775" + integrity sha512-00y/AXhx0/SsnI51fTc0rLRmafiGOM4/O+ny10Ps7f+j/b8p/ZY11ytMgznXkOVo4GQ+KwQG5UQLkLGirsACRg== + unbox-primitive@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz" @@ -2419,6 +2574,11 @@ v8-compile-cache@^2.0.3: resolved "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz" integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== +whatwg-fetch@>=0.10.0: + version "3.6.2" + resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz#dced24f37f2624ed0281725d51d0e2e3fe677f8c" + integrity sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA== + which-boxed-primitive@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz"