diff --git a/client/gatsby-browser.js b/client/gatsby-browser.js index eeac7160..6aac13b4 100644 --- a/client/gatsby-browser.js +++ b/client/gatsby-browser.js @@ -10,6 +10,8 @@ The full terms of this copyright and license should always be found in the root * See: https://www.gatsbyjs.org/docs/browser-apis/ */ +import "regenerator-runtime/runtime"; + import wrapWithProvider from "./wrap-with-provider"; import { loadSentry } from "./src/utils"; diff --git a/client/gatsby-ssr.js b/client/gatsby-ssr.js index 98f35a28..6aac13b4 100644 --- a/client/gatsby-ssr.js +++ b/client/gatsby-ssr.js @@ -5,11 +5,20 @@ Permission to use, copy, modify, and distribute this software and its documentat The full terms of this copyright and license should always be found in the root directory of this software deliverable as "license.txt" and if these terms are not found with this software, please contact the USC Stevens Center for the full license. */ /** - * Implement Gatsby's SSR (Server Side Rendering) APIs in this file. + * Implement Gatsby's Browser APIs in this file. * - * See: https://www.gatsbyjs.org/docs/ssr-apis/ + * See: https://www.gatsbyjs.org/docs/browser-apis/ */ +import "regenerator-runtime/runtime"; + import wrapWithProvider from "./wrap-with-provider"; +import { loadSentry } from "./src/utils"; + +if (process.env.GATSBY_IS_SENTRY_ENABLED === "true") { + console.log("Loading sentry"); + loadSentry(); +} + export const wrapRootElement = wrapWithProvider; diff --git a/client/package-lock.json b/client/package-lock.json index 74357978..441a5d69 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -34,6 +34,7 @@ "react-redux": "^7.2.9", "react-resize-detector": "^7.1.2", "react-scroll": "^1.8.9", + "react-speech-recognition": "^3.10.0", "redux": "^4.1.1", "redux-logger": "^3.0.6", "redux-thunk": "^2.4.2", @@ -50,6 +51,7 @@ "@types/react-dom": "^17.0.18", "@types/react-redux": "^7.1.25", "@types/react-scroll": "^1.8.6", + "@types/react-speech-recognition": "^3.9.5", "@types/smoothscroll-polyfill": "^0.3.1", "@types/uuid": "^8.3.4", "@typescript-eslint/eslint-plugin": "^4.29.1", @@ -5068,6 +5070,12 @@ "resolved": "https://registry.npmjs.org/@types/debug/-/debug-0.0.30.tgz", "integrity": "sha512-orGL5LXERPYsLov6CWs3Fh6203+dXzJkR7OnddIr2514Hsecwc8xRpzCapshBbKFImCsvS/mk6+FWiN5LyZJAQ==" }, + "node_modules/@types/dom-speech-recognition": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@types/dom-speech-recognition/-/dom-speech-recognition-0.0.4.tgz", + "integrity": "sha512-zf2GwV/G6TdaLwpLDcGTIkHnXf8JEf/viMux+khqKQKDa8/8BAUtXXZS563GnvJ4Fg0PBLGAaFf2GekEVSZ6GQ==", + "dev": true + }, "node_modules/@types/eslint": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.29.0.tgz", @@ -5260,6 +5268,15 @@ "@types/react": "*" } }, + "node_modules/@types/react-speech-recognition": { + "version": "3.9.5", + "resolved": "https://registry.npmjs.org/@types/react-speech-recognition/-/react-speech-recognition-3.9.5.tgz", + "integrity": "sha512-m3Sg3Xtj/YcEUu+nLPGwI6oq1wcSblsuyAmXgBfW6Nprfmtl+A+kH4ruPzzFKnFkq6WmmRxdsLvt0nLRAAJtBw==", + "dev": true, + "dependencies": { + "@types/dom-speech-recognition": "*" + } + }, "node_modules/@types/react-transition-group": { "version": "4.4.6", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.6.tgz", @@ -15300,6 +15317,14 @@ "node": ">=0.4.0" } }, + "node_modules/react-speech-recognition": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/react-speech-recognition/-/react-speech-recognition-3.10.0.tgz", + "integrity": "sha512-EVSr4Ik8l9urwdPiK2r0+ADrLyDDrjB0qBRdUWO+w2MfwEBrj6NuRmy1GD3x7BU/V6/hab0pl8Lupen0zwlJyw==", + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", diff --git a/client/package.json b/client/package.json index 17b67aab..c6eb3b1c 100644 --- a/client/package.json +++ b/client/package.json @@ -29,6 +29,7 @@ "react-redux": "^7.2.9", "react-resize-detector": "^7.1.2", "react-scroll": "^1.8.9", + "react-speech-recognition": "^3.10.0", "redux": "^4.1.1", "redux-logger": "^3.0.6", "redux-thunk": "^2.4.2", @@ -45,6 +46,7 @@ "@types/react-dom": "^17.0.18", "@types/react-redux": "^7.1.25", "@types/react-scroll": "^1.8.6", + "@types/react-speech-recognition": "^3.9.5", "@types/smoothscroll-polyfill": "^0.3.1", "@types/uuid": "^8.3.4", "@typescript-eslint/eslint-plugin": "^4.29.1", diff --git a/client/src/components/input.tsx b/client/src/components/input.tsx index 3f5c88d2..f732fa66 100644 --- a/client/src/components/input.tsx +++ b/client/src/components/input.tsx @@ -6,13 +6,24 @@ The full terms of this copyright and license should always be found in the root */ import React, { useEffect, useState } from "react"; import { useSelector, useDispatch } from "react-redux"; -import { Button, Divider, Paper, InputBase } from "@mui/material"; +import SpeechRecognition, { + useSpeechRecognition, +} from "react-speech-recognition"; +import { + Button, + Divider, + Paper, + InputBase, + InputAdornment, + IconButton, +} from "@mui/material"; +import { Mic, MicOutlined } from "@mui/icons-material"; +import SendRoundedIcon from "@mui/icons-material/SendRounded"; import { makeStyles } from "tss-react/mui"; import { sendQuestion, userInputChanged } from "store/actions"; import { Config, MentorQuestionSource, QuestionInput, State } from "types"; import { isMobile } from "react-device-detect"; -import SendRoundedIcon from "@mui/icons-material/SendRounded"; import "styles/layout.css"; import { useWithScreenOrientation } from "use-with-orientation"; @@ -65,6 +76,13 @@ function Input(): JSX.Element { (s) => s.questionInput ); const { displayFormat } = useWithScreenOrientation(); + const { + transcript, + listening, + browserSupportsSpeechRecognition, + resetTranscript, + } = useSpeechRecognition(); + const [stt, setSTT] = React.useState(""); const [animatingInputField, setAnimatingInputField] = useState(false); @@ -75,6 +93,13 @@ function Input(): JSX.Element { : setAnimatingInputField(true); }, [questionInput]); + useEffect(() => { + onQuestionInputChanged( + questionInput.question + transcript.substr(stt.length) + ); + setSTT(transcript); + }, [transcript]); + function handleQuestionChanged( question: string, source: MentorQuestionSource @@ -114,6 +139,18 @@ function Input(): JSX.Element { onQuestionInputSend(); } + function toggleSTT() { + if (listening) { + SpeechRecognition.stopListening(); + resetTranscript(); + setSTT(""); + } else { + resetTranscript(); + setSTT(""); + SpeechRecognition.startListening(); + } + } + // Input field keyboard was lowered const onBlur = () => { if (isMobile) { @@ -140,6 +177,10 @@ function Input(): JSX.Element { classes={{ input: classes.input, }} + disabled={listening} + style={{ + backgroundColor: listening ? "rgba(26, 107, 155, 0.1)" : "white", + }} placeholder={curQuestion || "Ask a question"} onChange={(e) => { onQuestionInputChanged(e.target.value); @@ -149,6 +190,17 @@ function Input(): JSX.Element { onKeyPress={onKeyPress} /> + {browserSupportsSpeechRecognition ? ( + + + {listening ? ( + + ) : ( + + )} + + + ) : undefined}