├── .eslintrc.json ├── .gitignore ├── README.md ├── components ├── content │ ├── LandingContent.jsx │ └── LandingContent.module.css └── elements │ ├── branding │ ├── Footer.jsx │ ├── Footer.module.css │ ├── Logo.jsx │ └── Logo.module.css │ ├── buttons │ ├── GetStartedButton.jsx │ ├── GetStartedButton.module.css │ ├── ResetPromptButton.jsx │ ├── ResetPromptButton.module.css │ ├── SubmitPromptButton.jsx │ └── SubmitPromptButton.module.css │ ├── dialoguebox │ ├── Dialogue.jsx │ ├── Dialogue.module.css │ ├── TypingPlaceholder.jsx │ └── TypingPlaceholder.module.css │ └── text │ ├── TextFieldExample.js │ └── TextFieldExample.module.css ├── cypress.config.js ├── cypress ├── e2e │ └── spec.cy.js ├── fixtures │ └── example.json └── support │ ├── commands.js │ └── e2e.js ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── _app.js ├── _document.js ├── api │ └── server.js └── index.jsx ├── public ├── favicon.ico ├── fonts │ ├── GT-SD-lt-it.woff2 │ ├── GT-SD-md-it.woff2 │ └── Monaco.woff2 ├── ico.svg ├── twittercardimage.png └── vercel.svg ├── styles ├── Home.module.css └── globals.css └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | #environment variables 4 | .env 5 | 6 | # dependencies 7 | /node_modules 8 | /.pnp 9 | .pnp.js 10 | 11 | # testing 12 | /coverage 13 | 14 | # next.js 15 | /.next/ 16 | /out/ 17 | 18 | # production 19 | /build 20 | 21 | # misc 22 | .DS_Store 23 | *.pem 24 | 25 | # debug 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | .pnpm-debug.log* 30 | 31 | # local env files 32 | .env*.local 33 | 34 | # vercel 35 | .vercel 36 | 37 | # typescript 38 | *.tsbuildinfo 39 | next-env.d.ts 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenResponse 2 | 3 | 🧠 💻 This project is a small prompt-completion application built with Open-AI's GPT-3 (DaVinci) and Next.js. Users can enter prompts of any complexity and expect a response in text form. This application does not collect or store any data. 4 | 5 | ## Technology 6 | 7 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). React is used as the front-end library for creating UI components. Routing and node requests + API building are handled by Next.js. 8 | 9 | ## Installation 10 | 11 | ### Getting Environment Variables for OpenAI's API 12 | 13 | OpenAI provides new users with $18.00 in free credits to be used in their first 3 months across their product lineup, including GPT-3 and Codex. [Visit their site](https://openai.com/api/) and sign up to acquire a key. 14 | 15 | This will be used to authenticate access to OpenAI's API. 16 | 17 | Change directory into the project's root and create a `.env` file. 18 | 19 | ```bash 20 | cd OpenResponse 21 | touch .env 22 | ``` 23 | 24 | Add the key to your `.env` file. 25 | 26 | ``` 27 | OPENAI_API_KEY=YOUR_API_KEY_HERE 28 | ``` 29 | 30 | In the same `.env` file go to the next line and add the environment of the GPT-3 instance here. If you got your key from OpenAI's website, you can simply put the following - 31 | 32 | ``` 33 | CURRENT_ENVIRONMENT=openai 34 | ``` 35 | 36 | 37 | While in the project's root folder, install dependencies. 38 | 39 | ```bash 40 | npm i 41 | ``` 42 | 43 | Start the development server: 44 | 45 | ```bash 46 | npm run dev 47 | # or 48 | yarn dev 49 | ``` 50 | 51 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 52 | -------------------------------------------------------------------------------- /components/content/LandingContent.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/no-unescaped-entities */ 2 | import React from "react"; 3 | import style from "./LandingContent.module.css"; 4 | import TextFieldExample from "../elements/text/TextFieldExample"; 5 | import GetStartedButton from "../elements/buttons/GetStartedButton"; 6 | import { useState, useEffect } from "react"; 7 | 8 | export default function LandingContent({ isPromptViewOpen, setIsPromptViewOpen }) { 9 | 10 | const [ isComponentUnmounting, setIsComponentUnmounting ] = useState(false); 11 | return ( 12 |
13 |
14 |
15 |

16 | I'm an AI built with OpenAI and NextJS. I'll read and 17 | respond to any prompt that you type out. 18 |

19 |

20 | An example prompt would look like this 21 |

22 | 26 |

27 | I might say something like this in response 28 |

29 | 33 |
34 |
35 |
36 |

37 | Prompts can be extremely open-ended or as specific as you’d 38 | like. 39 |

40 | {!isPromptViewOpen && ( 41 | 45 | )} 46 |
47 |
48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /components/content/LandingContent.module.css: -------------------------------------------------------------------------------- 1 | /* LandingContent.css */ 2 | 3 | #unmounting { 4 | opacity: 0; 5 | transform: rotateX(-0.5deg) scale(0.95); 6 | transition: ease; 7 | transition-duration: 0.25s; 8 | } 9 | 10 | .left-column { 11 | display: flex; 12 | flex-direction: column; 13 | transform-style: preserve-3d; 14 | transform-origin: top; 15 | row-gap: 68px; 16 | } 17 | 18 | .intro-text-description-wrapper { 19 | display: flex; 20 | max-width: 1280px; 21 | } 22 | 23 | .intro-text-description { 24 | max-width: 867px; 25 | } 26 | 27 | .subtitle, .description { 28 | font-family: Monaco; 29 | font-size: 24px; 30 | font-weight: 200; 31 | -webkit-font-smoothing: antialiased; 32 | } 33 | 34 | .subtitle { 35 | margin: 0; 36 | margin-top: 30px; 37 | } 38 | 39 | .description { 40 | margin-top: 68px; 41 | line-height: 1.5; 42 | font-size: 1.5rem; 43 | max-width: 876px; 44 | text-align: left; 45 | } 46 | 47 | .description-call-to-action { 48 | font-family: Monaco; 49 | font-size: 24px; 50 | font-weight: 200; 51 | -webkit-font-smoothing: antialiased; 52 | line-height: 1.5; 53 | font-size: 1.5rem; 54 | max-width: 876px; 55 | text-align: left; 56 | } 57 | 58 | .call-to-action-row { 59 | display: flex; 60 | flex-direction: row; 61 | width: 1280px; 62 | justify-content: space-between; 63 | } 64 | 65 | @media (max-width: 1500px) { 66 | .call-to-action-row { 67 | width: 100%; 68 | } 69 | 70 | .left-column { 71 | width: 97%; 72 | } 73 | } 74 | @media (max-width: 1430px) { 75 | .call-to-action-row { 76 | flex-direction: column; 77 | width: 856px; 78 | } 79 | } -------------------------------------------------------------------------------- /components/elements/branding/Footer.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/no-unescaped-entities */ 2 | import React from "react"; 3 | import style from "./Footer.module.css"; 4 | 5 | export default function Footer() { 6 | return ( 7 | 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /components/elements/branding/Footer.module.css: -------------------------------------------------------------------------------- 1 | /* Footer.css */ 2 | 3 | .footer-main { 4 | display: flex; 5 | justify-content: flex-start; 6 | align-self: center; 7 | width: 1280px; 8 | background-color: #1c1c1c; 9 | margin-bottom: 98px; 10 | } 11 | 12 | .footer-wrapper { 13 | display: flex; 14 | flex-direction: column; 15 | justify-content: flex-end; 16 | /* padding: 0 30px; */ 17 | } 18 | 19 | .footer-text-column-wrapper { 20 | display: flex; 21 | flex-direction: row; 22 | column-gap: 10px; 23 | } 24 | 25 | .footer-logo-column { 26 | display: flex; 27 | flex-direction: column; 28 | width: fit-content; 29 | } 30 | 31 | .footer-text-column { 32 | display: flex; 33 | width: 377px; 34 | } 35 | 36 | #side { 37 | margin: 5px 0; 38 | } 39 | 40 | .divider { 41 | width: 1000px; 42 | height: 1px; 43 | /* border-left: 1px solid rgb(169,169,169); */ 44 | background: rgb(169,169,169); 45 | background: linear-gradient(-90deg, rgba(169,169,169,0.01) 15%, rgba(169,169,169,1) 100%); 46 | } 47 | 48 | .footer-text { 49 | width: 100%; 50 | display: flex; 51 | flex-direction: row; 52 | column-gap: 3px; 53 | margin: 5px 0; 54 | margin-bottom: 15px; 55 | } 56 | 57 | .footer-logo-wrapper { 58 | display: flex; 59 | flex-direction: row; 60 | align-items: center; 61 | column-gap: 3px; 62 | } 63 | 64 | .footer-logo-font { 65 | font-family: GT-DS; 66 | font-weight: 400; 67 | font-size: 20px; 68 | font-style: italic; 69 | color: #E7E7E7; 70 | -webkit-font-smoothing: antialiased; 71 | margin-top: 4px; 72 | } 73 | 74 | .footer-notes { 75 | font-family: Monaco; 76 | font-size: 16px; 77 | font-style: normal; 78 | color: #E7E7E7; 79 | -webkit-font-smoothing: antialiased; 80 | margin: 0; 81 | } 82 | 83 | #footer-notes-first { 84 | margin-left: 9px; 85 | } 86 | 87 | .footer-link { 88 | text-decoration: underline; 89 | } 90 | 91 | @media (max-width: 1500px) { 92 | .footer-main { 93 | align-items: flex-start; 94 | padding: 0 110px; 95 | align-self: unset; 96 | width: 100%; 97 | margin-bottom: 60px; 98 | } 99 | .divider { 100 | width: 100%; 101 | } 102 | .footer-text-column { 103 | width: 40%; 104 | } 105 | } 106 | 107 | @media (max-width: 1000px) { 108 | .footer-text-column-wrapper { 109 | justify-content: space-between; 110 | } 111 | } 112 | 113 | @media (max-width: 900px) { 114 | .footer-main { 115 | padding: 0 50px; 116 | margin-bottom: 60px; 117 | } 118 | } 119 | 120 | @media (max-width: 800px) { 121 | .footer-text-column-wrapper { 122 | justify-content: space-between; 123 | } 124 | .footer-text-column { 125 | width: 45%; 126 | } 127 | } 128 | 129 | @media (max-width: 700px) { 130 | .footer-text-column-wrapper { 131 | flex-direction: column; 132 | row-gap: 10px; 133 | } 134 | 135 | .footer-text-column { 136 | width: 90%; 137 | } 138 | } 139 | 140 | @media (max-width: 600px) { 141 | .footer-main { 142 | padding: 0 16px; 143 | margin-bottom: 50px; 144 | } 145 | 146 | .footer-text { 147 | column-gap: 0; 148 | } 149 | 150 | .footer-text-column { 151 | width: 100%; 152 | } 153 | 154 | .footer-logo-column { 155 | margin-top: 1px; 156 | } 157 | 158 | .footer-logo { 159 | width: 24px; 160 | height: 24px; 161 | } 162 | 163 | .footer-logo-font { 164 | font-size: 16px; 165 | } 166 | 167 | #footer-notes-first { 168 | font-size: 13px; 169 | } 170 | 171 | .footer-notes { 172 | font-size: 13px; 173 | } 174 | } -------------------------------------------------------------------------------- /components/elements/branding/Logo.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styles from './Logo.module.css'; 3 | 4 | export default function Logo () { 5 | return ( 6 |
7 | 8 | 9 | 10 |

11 | OpenResponse 12 |

13 |
14 | ); 15 | } -------------------------------------------------------------------------------- /components/elements/branding/Logo.module.css: -------------------------------------------------------------------------------- 1 | /* Logo.css */ 2 | 3 | .logo-openresponse-wrapper { 4 | display: flex; 5 | flex-direction: row; 6 | column-gap: 5px; 7 | justify-self: flex-start; 8 | width: 1280px; 9 | position: sticky; 10 | top: 0; 11 | z-index: 1; 12 | background-color: rgba(28, 28, 28, 0.5); 13 | padding: 20px 0; 14 | backdrop-filter: blur(5px); 15 | align-items: center; 16 | } 17 | 18 | .open-response-logo { 19 | width: 72px; 20 | height: 72px; 21 | } 22 | 23 | .title { 24 | margin: 0; 25 | font-family: GT-DS; 26 | font-weight: 300; 27 | font-style: italic; 28 | font-size: 64px; 29 | color: #E7E7E7; 30 | } 31 | 32 | @media (max-width: 1500px) { 33 | .logo-openresponse-wrapper { 34 | width: 100%; 35 | } 36 | } 37 | 38 | @media (max-width: 900px) { 39 | .open-response-logo { 40 | width: 56px; 41 | height: 56px; 42 | } 43 | 44 | .title { 45 | font-size: 56px; 46 | } 47 | } 48 | 49 | @media (max-width: 600px) { 50 | .logo-openresponse-wrapper { 51 | margin-top: 0px; 52 | padding-top: 16px; 53 | } 54 | 55 | .title { 56 | font-size: 40px; 57 | } 58 | } -------------------------------------------------------------------------------- /components/elements/buttons/GetStartedButton.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styles from './GetStartedButton.module.css'; 3 | 4 | export default function GetStartedButton ({ setIsPromptViewOpen, setIsComponentUnmounting }) { 5 | 6 | function togglePromptView () { 7 | setIsComponentUnmounting(true); 8 | setTimeout(() => setIsPromptViewOpen(true), 250); 9 | } 10 | 11 | return ( 12 | 20 | ); 21 | } -------------------------------------------------------------------------------- /components/elements/buttons/GetStartedButton.module.css: -------------------------------------------------------------------------------- 1 | /* GetStartedButton.css */ 2 | 3 | @font-face { 4 | font-family: Monaco; 5 | font-style: normal; 6 | font-weight: 400; 7 | src: url('/fonts/Monaco.woff2'); 8 | } 9 | 10 | .get-started-button-wrapper { 11 | width: 271px; 12 | height: 60px; 13 | background-color: #FFC226; 14 | border-radius: 23px; 15 | display: flex; 16 | flex-direction: row; 17 | justify-content: center; 18 | align-items: center; 19 | column-gap: 15px; 20 | margin-top: 1.5em; 21 | border: none; 22 | transition: cubic-bezier(0.075, 0.82, 0.165, 1); 23 | transition-duration: 1s; 24 | } 25 | 26 | .get-started-button-label { 27 | font-family: Monaco; 28 | font-size: 24px; 29 | color: #2E2E2E; 30 | -webkit-font-smoothing: antialiased; 31 | } 32 | 33 | .open-response-logo { 34 | transition: cubic-bezier(0.075, 0.82, 0.165, 1); 35 | transition-duration: 1s; 36 | } 37 | 38 | .get-started-button-wrapper:hover .open-response-logo { 39 | transition: cubic-bezier(0.075, 0.82, 0.165, 1); 40 | transition-duration: 1s; 41 | transform: scale(1.2); 42 | } 43 | 44 | .get-started-button-wrapper:hover { 45 | background-color: #FFD568; 46 | cursor: pointer; 47 | } 48 | 49 | @media (max-width: 1430px) { 50 | .get-started-button-wrapper { 51 | align-self: flex-end; 52 | } 53 | } -------------------------------------------------------------------------------- /components/elements/buttons/ResetPromptButton.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import style from './ResetPromptButton.module.css'; 3 | 4 | export default function ResetPromptButton({ setUserInput, setResponse }) { 5 | return ( 6 |
7 | 20 |
21 | Reset 22 |
23 |
24 | ) 25 | } -------------------------------------------------------------------------------- /components/elements/buttons/ResetPromptButton.module.css: -------------------------------------------------------------------------------- 1 | /* ResetPromptButton */ 2 | 3 | .reset-button-wrapper { 4 | position: relative; 5 | perspective: 100px; 6 | } 7 | 8 | .reset-button { 9 | width: 30px; 10 | height: 30px; 11 | border: none; 12 | background-color: unset; 13 | color: unset; 14 | outline: none; 15 | display: flex; 16 | justify-content: center; 17 | align-items: center; 18 | transition: cubic-bezier(0.075, 0.82, 0.165, 1); 19 | transition-duration: 0.5s; 20 | position: relative; 21 | } 22 | 23 | .reset-button-hover-suggestion { 24 | width: fit-content; 25 | height: 30px; 26 | background-color: #656565; 27 | padding: 0 6px; 28 | position: absolute; 29 | top: -41px; 30 | left: -15px; 31 | display: flex; 32 | justify-content: center; 33 | align-items: center; 34 | border-radius: 4px; 35 | transform: rotateX(15deg) scale(0.95); 36 | transform-style: preserve-3d; 37 | transform-origin: bottom; 38 | opacity: 0; 39 | transition: ease; 40 | transition-duration: 0.25s; 41 | } 42 | 43 | .reset-button:hover + .reset-button-hover-suggestion{ 44 | opacity: 1; 45 | transform: rotateX(0deg) scale(1); 46 | transform-style: preserve-3d; 47 | transform-origin: bottom; 48 | transition: ease; 49 | transition-duration: 0.25s; 50 | } 51 | 52 | .reset-button-hover-suggestion-text { 53 | font-family: Monaco; 54 | font-weight: 300; 55 | font-size: 16px; 56 | color: #E6E6E6; 57 | -webkit-font-smoothing: antialiased; 58 | } 59 | 60 | .reset-button-icon { 61 | transition: cubic-bezier(0.075, 0.82, 0.165, 1); 62 | transition-duration: 0.5s; 63 | width: 25px; 64 | height: 25px; 65 | transform: scale(1.3); 66 | } 67 | 68 | .reset-button:hover { 69 | cursor: pointer; 70 | /* transform: rotate(45deg); */ 71 | transition: cubic-bezier(0.075, 0.82, 0.165, 1); 72 | transition-duration: 0.5s; 73 | } 74 | 75 | .reset-button:hover > .reset-button-icon { 76 | transform: rotate(45deg) scale(1.5); 77 | transition: cubic-bezier(0.075, 0.82, 0.165, 1); 78 | transition-duration: 0.5s; 79 | } 80 | 81 | @media (max-width: 600px) { 82 | .reset-button { 83 | padding: 6px; 84 | } 85 | 86 | .reset-button-hover-suggestion { 87 | display: none; 88 | } 89 | 90 | .reset-button:hover > .reset-button-icon { 91 | transform: rotate(0deg) scale(1.3); 92 | transition-duration: 0s; 93 | } 94 | } 95 | 96 | -------------------------------------------------------------------------------- /components/elements/buttons/SubmitPromptButton.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import style from "./SubmitPromptButton.module.css"; 3 | 4 | export default function SubmitPromptButton ({ isEnabled, inputSubmissionHandler }) { 5 | return ( 6 |
7 | 18 |
19 | Submit 20 |
21 |
22 | ) 23 | } -------------------------------------------------------------------------------- /components/elements/buttons/SubmitPromptButton.module.css: -------------------------------------------------------------------------------- 1 | /* SubmitPromptButton.css */ 2 | 3 | .submit-prompt-button-wrapper { 4 | position: relative; 5 | perspective: 100px; 6 | } 7 | 8 | .submit-prompt-button { 9 | width: 47px; 10 | height: 32px; 11 | background-color: #FFC226; 12 | border-radius: 5px; 13 | outline: none; 14 | border: none; 15 | display: flex; 16 | justify-content: center; 17 | align-items: center; 18 | transition: cubic-bezier(0.075, 0.82, 0.165, 1); 19 | transition-duration: 0.5s; 20 | position: relative; 21 | } 22 | 23 | .submit-prompt-button-hover-suggestion { 24 | width: fit-content; 25 | height: 30px; 26 | background-color: #656565; 27 | padding: 0 6px; 28 | position: absolute; 29 | top: -41px; 30 | left: -11px; 31 | display: flex; 32 | justify-content: center; 33 | align-items: center; 34 | border-radius: 4px; 35 | transform: rotateX(15deg) scale(0.95); 36 | transform-style: preserve-3d; 37 | transform-origin: bottom; 38 | opacity: 0; 39 | transition: ease; 40 | transition-duration: 0.25s; 41 | } 42 | 43 | .submit-prompt-button:hover + .submit-prompt-button-hover-suggestion { 44 | opacity: 1; 45 | transform: rotateX(0deg) scale(1); 46 | transform-style: preserve-3d; 47 | transform-origin: bottom; 48 | transition: ease; 49 | transition-duration: 0.25s; 50 | } 51 | 52 | .submit-prompt-button-hover-suggestion-text { 53 | font-family: Monaco; 54 | font-weight: 300; 55 | font-size: 16px; 56 | color: #E6E6E6; 57 | -webkit-font-smoothing: antialiased; 58 | } 59 | 60 | .submit-prompt-button:hover { 61 | cursor: pointer; 62 | } 63 | 64 | .submit-prompt-button:disabled { 65 | background-color: #535353; 66 | /* change color */ 67 | cursor: not-allowed; 68 | transition: cubic-bezier(0.075, 0.82, 0.165, 1); 69 | transition-duration: 0.5s; 70 | } 71 | 72 | .submit-prompt-button-arrow { 73 | transition: cubic-bezier(0.075, 0.82, 0.165, 1); 74 | transition-duration: 0.5s; 75 | fill: #2E2E2E; 76 | } 77 | 78 | @media (max-width: 600px) { 79 | .submit-prompt-button-hover-suggestion { 80 | display: none; 81 | } 82 | } -------------------------------------------------------------------------------- /components/elements/dialoguebox/Dialogue.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useEffect } from "react"; 3 | import { useState } from "react"; 4 | import style from './Dialogue.module.css'; 5 | 6 | import SubmitPromptButton from "../buttons/SubmitPromptButton"; 7 | import ResetPromptButton from "../buttons/ResetPromptButton"; 8 | import TypingPlaceholder from "./TypingPlaceholder"; 9 | 10 | export default function Dialogue () { 11 | 12 | const [ userInput, setUserInput ] = useState(''); 13 | const [ response, setResponse ] = useState(''); 14 | const [ isFetchingResponse, setIsFetchingResponse ] = useState(false); 15 | const [ isUserTyping, setIsUserTyping ] = useState(false); 16 | const [ isLoaded, setIsLoaded ] = useState(false); 17 | 18 | let dialogueLoadedTimeoutOffset = 300; 19 | 20 | // dialogue history is a feature planned for future release - conversation-like UI is intended 21 | // const [ dialogueHistory, setDialogueHistory ] = useState([]); 22 | const [ isAutoTyperFinished, setIsAutoTyperFinished ] = useState(false); 23 | 24 | // handling the returned data 25 | // all responses appear to start with a \n\n header, so split to a regex and ignore the first two instances 26 | const responseParsed = response.split(/\r?\n/).map((line, index) => { 27 | if (index > 1) return ( 28 |

30 | {line} 31 |

32 | ) 33 | }) 34 | 35 | // activates an active state for OpenResponse Logo if the user has stopped tying for 6 seconds. 36 | // useEffect captures the state of the input and debounces the timer when the user enters a key within 6 seconds of the last one 37 | // return statement destroys the setTimeout function 38 | useEffect(() => { 39 | if (!!userInput) { 40 | setIsUserTyping(true); 41 | const delayedUnshowLogoSuggestion = setTimeout(() => { 42 | setIsUserTyping(false); 43 | }, 6000); 44 | 45 | return () => {clearTimeout(delayedUnshowLogoSuggestion)} 46 | } else { 47 | setIsUserTyping(false); 48 | } 49 | }, [userInput]); 50 | 51 | // simulate first typing event - no explanation needed? 52 | // first, initialize the search prompt we want answered. 53 | // in order to loop through each character of the string, we need to manually iterate through the string using an index. 54 | // If the index is less than or equal to the length of the prompt, then we setUserInput to a substring - from index 0 to indexChar. 55 | // after that, increment indexChar. 56 | // If indexChar exceeds the length of initialPrompt (meaning it would be accessing garbage data), we need to clear the interval to prevent infinite function calls. 57 | useEffect(() => { 58 | let initialPrompt = "What does it mean to be conscious? Are you conscious?" 59 | let indexChar = 0; 60 | const typingInterval = setInterval(setInitialUserInputTextPerChar, 70); 61 | 62 | function setInitialUserInputTextPerChar() { 63 | if (indexChar <= initialPrompt.length) { 64 | setUserInput(initialPrompt.slice(0, indexChar)); 65 | indexChar++; 66 | } else { 67 | clearInterval(typingInterval); 68 | } 69 | } 70 | }, []); 71 | 72 | // animation mount. once component mounts, change the transform rotateX value to 0 degress. 73 | // achievable by setting a custom id target, #loaded 74 | useEffect(() => { 75 | setTimeout(() => { 76 | setIsLoaded(true); 77 | }, dialogueLoadedTimeoutOffset); 78 | }, []); 79 | 80 | function userInputChangeHandler(e) { 81 | setUserInput(e.target.value); 82 | } 83 | 84 | // since