From 6bba4ffe351366d047eb04ba6b7749c668b7cb0d Mon Sep 17 00:00:00 2001 From: abemscac Date: Thu, 19 Dec 2024 08:01:32 +0800 Subject: [PATCH 1/2] refactor: enhance app security and maintainability --- .env.example | 7 +- package-lock.json | 706 ++++++++++++++++-- package.json | 9 +- postcss.config.js | 2 +- .../completion/createCompletionRequest.ts | 3 + .../completion/createCompletionResponse.ts | 3 + .../speechToText/speechToTextRequest.ts | 4 + .../speechToText/speechToTextResponse.ts | 3 + .../textToSpeech/textToSpeechRequest.ts | 3 + .../textToSpeech/textToSpeechResponse.ts | 1 + src/app/api/chat/route.ts | 20 - src/app/api/completion/route.ts | 43 ++ src/app/api/speechToText/route.ts | 64 +- src/app/api/textToSpeech/route.ts | 46 ++ src/app/button.css | 24 +- src/app/favicon.ico | Bin 25931 -> 0 bytes src/app/layout.tsx | 19 +- src/app/page.tsx | 6 +- .../AssistantButton/AssistantButton.tsx | 337 ++------- .../AssistantButton/useMediaRecorder.ts | 144 ++++ src/utils/client/api/axios-client.ts | 8 + src/utils/client/api/completion.ts | 26 + src/utils/client/api/speechToText.ts | 38 + src/utils/client/api/textToSpeech.ts | 22 + src/utils/client/mediaRecorder.ts | 62 ++ src/utils/client/toast.ts | 13 + src/utils/server/response.ts | 26 + tailwind.config.ts | 6 +- tsconfig.json | 1 - 29 files changed, 1253 insertions(+), 393 deletions(-) create mode 100644 src/apiTypes/completion/createCompletionRequest.ts create mode 100644 src/apiTypes/completion/createCompletionResponse.ts create mode 100644 src/apiTypes/speechToText/speechToTextRequest.ts create mode 100644 src/apiTypes/speechToText/speechToTextResponse.ts create mode 100644 src/apiTypes/textToSpeech/textToSpeechRequest.ts create mode 100644 src/apiTypes/textToSpeech/textToSpeechResponse.ts delete mode 100644 src/app/api/chat/route.ts create mode 100644 src/app/api/completion/route.ts create mode 100644 src/app/api/textToSpeech/route.ts delete mode 100644 src/app/favicon.ico create mode 100644 src/components/AssistantButton/useMediaRecorder.ts create mode 100644 src/utils/client/api/axios-client.ts create mode 100644 src/utils/client/api/completion.ts create mode 100644 src/utils/client/api/speechToText.ts create mode 100644 src/utils/client/api/textToSpeech.ts create mode 100644 src/utils/client/mediaRecorder.ts create mode 100644 src/utils/client/toast.ts create mode 100644 src/utils/server/response.ts diff --git a/.env.example b/.env.example index dc35398..120453a 100644 --- a/.env.example +++ b/.env.example @@ -2,7 +2,6 @@ OPENAI_API_KEY="YOUR OPENAI API KEY" # Override openai api request base url. (Optional) # Default: https://api.openai.com/v1 # Examples: http://your-openai-proxy.com/v1 -OPENAI_BASE_URL= -NEXT_PUBLIC_ELEVENLABS_API_KEY="YOUR ELEVENLABS API KEY" - -NEXT_PUBLIC_ELEVENLABS_VOICE_ID="nWM88eUzTWbyiJW1K8NX" +OPENAI_BASE_URL=https://api.openai.com/v1 +ELEVENLABS_API_KEY="YOUR ELEVENLABS API KEY" +ELEVENLABS_VOICE_ID="cgSgspJ2msm6clMCkdW9" diff --git a/package-lock.json b/package-lock.json index 436f17e..3ec1314 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,9 +15,11 @@ "autoprefixer": "10.4.14", "axios": "^1.6.0", "debug": "^4.3.4", + "elevenlabs": "^1.50.2", "framer-motion": "^10.16.4", "langchain": "^0.0.182", "next": "13.4.13", + "openai": "^4.20.1", "postcss": "8.4.27", "react": "18.2.0", "react-dom": "18.2.0", @@ -31,7 +33,8 @@ }, "devDependencies": { "@types/recorder-js": "^1.0.3", - "encoding": "^0.1.13" + "encoding": "^0.1.13", + "prettier": "^3.4.2" } }, "node_modules/@alloc/quick-lru": { @@ -333,9 +336,10 @@ } }, "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "license": "MIT", "peer": true }, "node_modules/@types/node": { @@ -566,9 +570,10 @@ } }, "node_modules/acorn": { - "version": "8.11.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", - "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "license": "MIT", "peer": true, "bin": { "acorn": "bin/acorn" @@ -664,12 +669,13 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "node_modules/aria-query": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "license": "Apache-2.0", "peer": true, - "dependencies": { - "dequal": "^2.0.3" + "engines": { + "node": ">= 0.4" } }, "node_modules/asynckit": { @@ -821,6 +827,30 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -832,6 +862,35 @@ "node": ">=10.16.0" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", + "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", + "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/camelcase": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", @@ -944,6 +1003,12 @@ "node": ">= 0.8" } }, + "node_modules/command-exists": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz", + "integrity": "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==", + "license": "MIT" + }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -957,6 +1022,20 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/crypt": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", @@ -1058,11 +1137,60 @@ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/electron-to-chromium": { "version": "1.4.488", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.488.tgz", "integrity": "sha512-Dv4sTjiW7t/UWGL+H8ZkgIjtUAVZDgb/PwGWvMsCT7jipzUV/u5skbLXPFKb6iV0tiddVi/bcS2/kUrczeWgIQ==" }, + "node_modules/elevenlabs": { + "version": "1.50.2", + "resolved": "https://registry.npmjs.org/elevenlabs/-/elevenlabs-1.50.2.tgz", + "integrity": "sha512-Zq5fFACjctRB1Ix1k5dLnx+GLPy3Z3fKrilmXGDAdEA7WOT6GL4SA6aH7/v13Tw5CSKu/z/XzBz/7T3JeyK8LA==", + "license": "MIT", + "dependencies": { + "command-exists": "^1.2.9", + "execa": "^5.1.1", + "form-data": "^4.0.0", + "form-data-encoder": "^4.0.2", + "formdata-node": "^6.0.3", + "node-fetch": "2.7.0", + "qs": "6.11.2", + "readable-stream": "^4.5.2", + "url-join": "4.0.1" + } + }, + "node_modules/elevenlabs/node_modules/form-data-encoder": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-4.0.2.tgz", + "integrity": "sha512-KQVhvhK8ZkWzxKxOr56CPulAhH3dobtuQ4+hNQ+HekH/Wp5gSOafqRAeTphQUJAIk0GBvHZgJ2ZGRWd5kphMuw==", + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, + "node_modules/elevenlabs/node_modules/formdata-node": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-6.0.3.tgz", + "integrity": "sha512-8e1++BCiTzUno9v5IZ2J6bv4RU+3UKDmqWUQD0MIMVCd9AdhWkO1gw57oo1mNEX1dMq2EGI+FbWz4B92pscSQg==", + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, "node_modules/encoding": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", @@ -1072,6 +1200,36 @@ "iconv-lite": "^0.6.2" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", + "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -1102,6 +1260,15 @@ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/eventsource-parser": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-1.0.0.tgz", @@ -1110,6 +1277,29 @@ "node": ">=14.18" } }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, "node_modules/expr-eval": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/expr-eval/-/expr-eval-2.0.2.tgz", @@ -1279,9 +1469,49 @@ } }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.6.tgz", + "integrity": "sha512-qxsEs+9A+u85HhllWJJFicJfPDhRmjzoYdl64aMWW9yRIJmSyxdn8IEkuIM530/7T+lv0TIHd8L6Q/ra0tEoeA==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "dunder-proto": "^1.0.0", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "function-bind": "^1.1.2", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/glob": { "version": "7.1.6", @@ -1326,22 +1556,23 @@ "csstype": "^3.0.10" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -1350,6 +1581,39 @@ "node": ">=8" } }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, "node_modules/humanize-ms": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", @@ -1370,6 +1634,37 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 4" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -1411,11 +1706,15 @@ "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" }, "node_modules/is-core-module": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", - "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.0.tgz", + "integrity": "sha512-urTSINYfAYgcbLb0yDQ6egFm6h3Mo1DcF9EkyXSRjjzdHbsulg01qhwWuXdOoUBuTkbQ80KDboXa0vFJ+BDH+g==", + "license": "MIT", "dependencies": { - "has": "^1.0.3" + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -1457,6 +1756,24 @@ "@types/estree": "*" } }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, "node_modules/jiti": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.19.1.tgz", @@ -1920,14 +2237,6 @@ } } }, - "node_modules/langchain/node_modules/zod": { - "version": "3.22.4", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", - "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, "node_modules/langchainhub": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/langchainhub/-/langchainhub-0.0.6.tgz", @@ -1998,6 +2307,15 @@ "node": ">=12" } }, + "node_modules/math-intrinsics": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.0.0.tgz", + "integrity": "sha512-4MqMiKP90ybymYvsut0CH2g4XWbfLtmlCkXmtmdcDCxNB+mQcu1w/1+L/VD7vi/PSv7X2JYV7SCcR+jiPXnQtA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/md5": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", @@ -2014,6 +2332,12 @@ "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", "peer": true }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "license": "MIT" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -2053,6 +2377,15 @@ "node": ">= 0.6" } }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -2205,6 +2538,15 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/next/node_modules/zod": { + "version": "3.21.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz", + "integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/node-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", @@ -2263,6 +2605,18 @@ "node": ">=0.10.0" } }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/num-sort": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/num-sort/-/num-sort-2.1.0.tgz", @@ -2290,6 +2644,18 @@ "node": ">= 6" } }, + "node_modules/object-inspect": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", + "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -2298,23 +2664,45 @@ "wrappy": "1" } }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/openai": { - "version": "4.16.1", - "resolved": "https://registry.npmjs.org/openai/-/openai-4.16.1.tgz", - "integrity": "sha512-Gr+uqUN1ICSk6VhrX64E+zL7skjI1TgPr/XUN+ZQuNLLOvx15+XZulx/lSW4wFEAQzgjBDlMBbBeikguGIjiMg==", + "version": "4.76.3", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.76.3.tgz", + "integrity": "sha512-BISkI90m8zT7BAMljK0j00TzOoLvmc7AulPxv6EARa++3+hhIK5G6z4xkITurEaA9bvDhQ09kSNKA3DL+rDMwA==", + "license": "Apache-2.0", "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", - "digest-fetch": "^1.3.0", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", - "node-fetch": "^2.6.7", - "web-streams-polyfill": "^3.2.1" + "node-fetch": "^2.6.7" }, "bin": { "openai": "bin/cli" + }, + "peerDependencies": { + "zod": "^3.23.8" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } } }, "node_modules/openai/node_modules/@types/node": { @@ -2384,6 +2772,15 @@ "node": ">=0.10.0" } }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", @@ -2556,11 +2953,51 @@ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" }, + "node_modules/prettier": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", + "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, + "node_modules/qs": { + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", + "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -2631,6 +3068,22 @@ "pify": "^2.3.0" } }, + "node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -2710,6 +3163,26 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -2733,6 +3206,105 @@ "node": ">=10" } }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, "node_modules/solid-js": { "version": "1.8.5", "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.8.5.tgz", @@ -2782,6 +3354,24 @@ "node": ">=10.0.0" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/styled-jsx": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", @@ -3036,6 +3626,12 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/url-join": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", + "license": "MIT" + }, "node_modules/use-sync-external-store": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", @@ -3116,6 +3712,21 @@ "webidl-conversions": "^3.0.0" } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/whisper-speech-to-text": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/whisper-speech-to-text/-/whisper-speech-to-text-1.0.3.tgz", @@ -3139,9 +3750,10 @@ } }, "node_modules/zod": { - "version": "3.21.4", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz", - "integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==", + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", + "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 30c99ee..061b7e1 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,10 @@ "dev": "next dev", "build": "next build", "start": "next start", - "lint": "next lint" + "lint": "next lint", + "tsc": "tsc", + "format": "prettier --check \"{**/*,*}.{js,ts,tsx,css,md}\"", + "format:fix": "prettier --write \"{**/*,*}.{js,ts,tsx,css,md}\"" }, "dependencies": { "@types/node": "20.4.9", @@ -16,6 +19,7 @@ "autoprefixer": "10.4.14", "axios": "^1.6.0", "debug": "^4.3.4", + "elevenlabs": "^1.50.2", "framer-motion": "^10.16.4", "langchain": "^0.0.182", "next": "13.4.13", @@ -33,6 +37,7 @@ }, "devDependencies": { "@types/recorder-js": "^1.0.3", - "encoding": "^0.1.13" + "encoding": "^0.1.13", + "prettier": "^3.4.2" } } diff --git a/postcss.config.js b/postcss.config.js index 33ad091..12a703d 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -3,4 +3,4 @@ module.exports = { tailwindcss: {}, autoprefixer: {}, }, -} +}; diff --git a/src/apiTypes/completion/createCompletionRequest.ts b/src/apiTypes/completion/createCompletionRequest.ts new file mode 100644 index 0000000..ba7bd58 --- /dev/null +++ b/src/apiTypes/completion/createCompletionRequest.ts @@ -0,0 +1,3 @@ +export type CreateCompletionRequest = { + message: string; +}; diff --git a/src/apiTypes/completion/createCompletionResponse.ts b/src/apiTypes/completion/createCompletionResponse.ts new file mode 100644 index 0000000..c11e168 --- /dev/null +++ b/src/apiTypes/completion/createCompletionResponse.ts @@ -0,0 +1,3 @@ +export type CreateCompletionResponse = { + completion: string; +}; diff --git a/src/apiTypes/speechToText/speechToTextRequest.ts b/src/apiTypes/speechToText/speechToTextRequest.ts new file mode 100644 index 0000000..4b88deb --- /dev/null +++ b/src/apiTypes/speechToText/speechToTextRequest.ts @@ -0,0 +1,4 @@ +export type SpeechToTextRequest = { + // Form data + audio: Blob; +}; diff --git a/src/apiTypes/speechToText/speechToTextResponse.ts b/src/apiTypes/speechToText/speechToTextResponse.ts new file mode 100644 index 0000000..f66236d --- /dev/null +++ b/src/apiTypes/speechToText/speechToTextResponse.ts @@ -0,0 +1,3 @@ +export type SpeechToTextResponse = { + text: string; +}; diff --git a/src/apiTypes/textToSpeech/textToSpeechRequest.ts b/src/apiTypes/textToSpeech/textToSpeechRequest.ts new file mode 100644 index 0000000..2c22217 --- /dev/null +++ b/src/apiTypes/textToSpeech/textToSpeechRequest.ts @@ -0,0 +1,3 @@ +export type TextToSpeechRequest = { + text: string; +}; diff --git a/src/apiTypes/textToSpeech/textToSpeechResponse.ts b/src/apiTypes/textToSpeech/textToSpeechResponse.ts new file mode 100644 index 0000000..e3fc412 --- /dev/null +++ b/src/apiTypes/textToSpeech/textToSpeechResponse.ts @@ -0,0 +1 @@ +export type TextToSpeechResponse = Blob; diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts deleted file mode 100644 index 77976be..0000000 --- a/src/app/api/chat/route.ts +++ /dev/null @@ -1,20 +0,0 @@ -import OpenAI from "openai"; -import { NextRequest, NextResponse } from "next/server"; - -const openai = new OpenAI({ - apiKey: process.env.OPENAI_API_KEY, - ...(process.env.OPENAI_BASE_URL && { baseURL: process.env.OPENAI_BASE_URL }), -}); - -export const runtime = "edge"; - -export async function POST(req: NextRequest) { - const { messages } = await req.json(); - const response = await openai.chat.completions.create({ - model: "gpt-4o", - stream: false, - messages, - }); - - return NextResponse.json(response.choices[0].message.content); -} diff --git a/src/app/api/completion/route.ts b/src/app/api/completion/route.ts new file mode 100644 index 0000000..f122a4f --- /dev/null +++ b/src/app/api/completion/route.ts @@ -0,0 +1,43 @@ +import { CreateCompletionRequest } from '@/apiTypes/completion/createCompletionRequest'; +import { CreateCompletionResponse } from '@/apiTypes/completion/createCompletionResponse'; +import { BadRequest, InternalServerError, Ok } from '@/utils/server/response'; +import { NextRequest, NextResponse } from 'next/server'; +import OpenAI from 'openai'; + +const openai = new OpenAI({ + apiKey: process.env.OPENAI_API_KEY, + baseURL: process.env.OPENAI_BASE_URL, +}); + +export async function POST(req: NextRequest): Promise { + const { message } = (await req.json()) as CreateCompletionRequest; + + if (!message) { + return BadRequest('message cannot be empty'); + } + + try { + const completion = await createCompletion(message); + return Ok({ + completion, + }); + } catch (error) { + console.error('OpenAI create completion error: ', error); + return InternalServerError(); + } +} + +const createCompletion = async (message: string): Promise => { + const response = await openai.chat.completions.create({ + model: 'gpt-3.5-turbo-0125', + stream: false, + messages: [ + { + role: 'user', + content: `${message}. Reply in the same language as the input. Keep it concise and friendly, like you're chatting with a friend.`, + }, + ], + }); + + return response.choices[0].message.content ?? ''; +}; diff --git a/src/app/api/speechToText/route.ts b/src/app/api/speechToText/route.ts index ed7a82c..7fdccbd 100644 --- a/src/app/api/speechToText/route.ts +++ b/src/app/api/speechToText/route.ts @@ -1,57 +1,35 @@ -import { OpenAI } from "openai"; -import { NextResponse, NextRequest } from "next/server"; -import fs from "fs"; +import { SpeechToTextResponse } from '@/apiTypes/speechToText/speechToTextResponse'; +import { BadRequest, InternalServerError, Ok } from '@/utils/server/response'; +import { NextRequest, NextResponse } from 'next/server'; +import { OpenAI } from 'openai'; const openai = new OpenAI({ - apiKey: process.env.OPENAI_API_KEY, - ...(process.env.OPENAI_BASE_URL && { baseURL: process.env.OPENAI_BASE_URL }), - + baseURL: process.env.OPENAI_BASE_URL, }); -interface RequestBody { - audio: string; -} - export async function POST(request: NextRequest): Promise { - try { - const req = await request.json(); - const base64Audio = req.audio; - const audio = Buffer.from(base64Audio, "base64"); - - const text = await convertAudioToText(audio); + const formData = await request.formData(); + const audioWebm = formData.get('audio') as File | null; - return NextResponse.json({ result: text }, { status: 200 }); - } catch (error) { - return handleErrorResponse(error); + if (!audioWebm) { + return BadRequest('audio is required in form data'); } -} -async function convertAudioToText(audioData: Buffer) { - const outputPath = "/tmp/input.webm"; - fs.writeFileSync(outputPath, audioData); + const audioFile = new File([audioWebm], 'audio.webm', { + type: audioWebm.type, + }); try { - const response = await openai.audio.transcriptions.create({ - file: fs.createReadStream(outputPath), - model: "whisper-1", + const { text } = await openai.audio.transcriptions.create({ + file: audioFile, + model: 'whisper-1', }); - - return response.text; - } finally { - fs.unlinkSync(outputPath); - } -} - -function handleErrorResponse(error: any): NextResponse { - if (error.response) { - console.error(error.response.status, error.response.data); - return NextResponse.json({ error: error.response.data }, { status: 500 }); - } else { - console.error(`Error with OpenAI API request: ${error.message}`); - return NextResponse.json( - { error: "An error occurred during your request." }, - { status: 500 } - ); + return Ok({ + text, + }); + } catch (error) { + console.error('OpenAI create audio transcription error: ', error); + return InternalServerError(); } } diff --git a/src/app/api/textToSpeech/route.ts b/src/app/api/textToSpeech/route.ts new file mode 100644 index 0000000..006ce37 --- /dev/null +++ b/src/app/api/textToSpeech/route.ts @@ -0,0 +1,46 @@ +import { BadRequest, InternalServerError } from '@/utils/server/response'; +import { ElevenLabsClient } from 'elevenlabs'; +import { TextToSpeechRequest } from 'elevenlabs/api'; +import { NextRequest, NextResponse } from 'next/server'; + +const elevenLabsClient = new ElevenLabsClient({ + apiKey: process.env.ELEVENLABS_API_KEY, +}); + +export async function POST(req: NextRequest): Promise { + const { text } = (await req.json()) as TextToSpeechRequest; + + if (!text) { + return BadRequest('text cannot be empty'); + } + + try { + const audio = await textToSpeech(text); + const response = new NextResponse(audio as any, { + headers: { + 'Content-Type': 'audio/mpeg', + 'Content-Disposition': 'attachment; filename="audio.mp3"', + }, + }); + return response; + } catch (error) { + console.error('ElevenLabs text to speech error: ', error); + return InternalServerError(); + } +} + +const textToSpeech = async (text: string) => { + const readable = await elevenLabsClient.textToSpeech.convert( + process.env.ELEVENLABS_VOICE_ID as string, + { + output_format: 'mp3_44100_128', + text, + model_id: 'eleven_multilingual_v2', + voice_settings: { + stability: 0.5, + similarity_boost: 0.5, + }, + }, + ); + return readable; +}; diff --git a/src/app/button.css b/src/app/button.css index 7ede9fc..b730ca9 100644 --- a/src/app/button.css +++ b/src/app/button.css @@ -19,7 +19,9 @@ body { border: 0.1px solid rgba(255, 255, 255, 0.4); -webkit-animation: rainbow 3s infinite linear; border-radius: 0.5em; - box-shadow: 0 0 0.3em 0.05em #2c116e, inset 0.03em 0 0.1em 0.02em #de66e4; + box-shadow: + 0 0 0.3em 0.05em #2c116e, + inset 0.03em 0 0.1em 0.02em #de66e4; transform-style: preserve-3d; perspective: 1em; /* background-color: rgba(0, 0, 0, 1); */ @@ -27,23 +29,33 @@ body { @keyframes rainbow { 0% { transform: rotate(0deg) translateZ(0); - box-shadow: 0 0 0.3em 0.05em #2c116e, inset 0.03em 0 0.1em 0.02em #de66e4; + box-shadow: + 0 0 0.3em 0.05em #2c116e, + inset 0.03em 0 0.1em 0.02em #de66e4; } 25% { transform: rotate(90deg) translateZ(0); - box-shadow: 0 0 0.3em 0.05em #28126a, inset 0.03em 0 0.1em 0.02em #34ceaa; + box-shadow: + 0 0 0.3em 0.05em #28126a, + inset 0.03em 0 0.1em 0.02em #34ceaa; } 50% { transform: rotate(180deg) translateZ(0); - box-shadow: 0 0 0.3em 0.05em #28126a, inset 0.03em 0 0.1em 0.02em #19b3f5; + box-shadow: + 0 0 0.3em 0.05em #28126a, + inset 0.03em 0 0.1em 0.02em #19b3f5; } 75% { transform: rotate(270deg) translateZ(0); - box-shadow: 0 0 0.3em 0.05em #28126a, inset 0.03em 0 0.1em 0.02em #3d52ac; + box-shadow: + 0 0 0.3em 0.05em #28126a, + inset 0.03em 0 0.1em 0.02em #3d52ac; } 100% { transform: rotate(360deg) translateZ(0); - box-shadow: 0 0 0.3em 0.05em #28126a, inset 0.03em 0 0.1em 0.02em #de66e4; + box-shadow: + 0 0 0.3em 0.05em #28126a, + inset 0.03em 0 0.1em 0.02em #de66e4; } } .rainbow-container { diff --git a/src/app/favicon.ico b/src/app/favicon.ico deleted file mode 100644 index 718d6fea4835ec2d246af9800eddb7ffb276240c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25931 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 LrJugUA?W`A8`#=m diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 2757027..8891b81 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,14 +1,15 @@ -import "./globals.css"; -import "./button.css"; -import type { Metadata } from "next"; -import { Inter } from "next/font/google"; -import { Toaster } from "react-hot-toast"; +import './globals.css'; +import './button.css'; +import type { Metadata } from 'next'; +import { Inter } from 'next/font/google'; +import toast, { Toaster } from 'react-hot-toast'; +import { DEFAULT_TOAST_OPTIONS } from '@/utils/client/toast'; -const inter = Inter({ subsets: ["latin"] }); +const inter = Inter({ subsets: ['latin'] }); export const metadata: Metadata = { - title: "Voice Assistant", - description: "Siri, but for the web", + title: 'Voice Assistant', + description: 'Siri, but for the web', }; export default function RootLayout({ @@ -20,7 +21,7 @@ export default function RootLayout({ {children} - + ); diff --git a/src/app/page.tsx b/src/app/page.tsx index 467ff57..28a7a0c 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,6 +1,6 @@ -import React from "react"; -import AssistantButton from "@/components/AssistantButton/AssistantButton"; -import Image from "next/image"; +import React from 'react'; +import AssistantButton from '@/components/AssistantButton/AssistantButton'; +import Image from 'next/image'; export default function page() { return ( diff --git a/src/components/AssistantButton/AssistantButton.tsx b/src/components/AssistantButton/AssistantButton.tsx index 374f939..abba1bf 100644 --- a/src/components/AssistantButton/AssistantButton.tsx +++ b/src/components/AssistantButton/AssistantButton.tsx @@ -1,279 +1,108 @@ -"use client"; -import React, { useState, useEffect, useRef, ChangeEvent } from "react"; -import axios from "axios"; -import { motion } from "framer-motion"; -import toast from "react-hot-toast"; - -interface VoiceSettings { - stability: number; - similarity_boost: number; -} - -interface TextToSpeechData { - text: string; - model_id: string; - voice_settings: VoiceSettings; -} +'use client'; +import { motion } from 'framer-motion'; +import React, { useState } from 'react'; +import toast, { Toaster } from 'react-hot-toast'; +import { useMediaRecorder } from './useMediaRecorder'; +import { useSpeechToTextApi } from '@/utils/client/api/speechToText'; +import { useCompletionApi } from '@/utils/client/api/completion'; +import { useTextToSpeechApi } from '@/utils/client/api/textToSpeech'; const AssistantButton: React.FC = () => { - const [mediaRecorderInitialized, setMediaRecorderInitialized] = - useState(false); - const [audioPlaying, setAudioPlaying] = useState(false); - const inputRef = useRef(null); - const [inputValue, setInputValue] = useState(""); - const [recording, setRecording] = useState(false); - const [mediaRecorder, setMediaRecorder] = useState( - null - ); - let chunks: BlobPart[] = []; const [thinking, setThinking] = useState(false); - useEffect(() => { - if (mediaRecorder && mediaRecorderInitialized) { - // Additional setup if needed - } - }, [mediaRecorder, mediaRecorderInitialized]); - - const playAudio = async (input: string): Promise => { - const CHUNK_SIZE = 1024; - - const url = `https://api.elevenlabs.io/v1/text-to-speech/${process.env.NEXT_PUBLIC_ELEVENLABS_VOICE_ID}/stream`; - const headers = { - - Accept: "audio/mpeg", - "Content-Type": "application/json", - "xi-api-key": process.env.NEXT_PUBLIC_ELEVENLABS_API_KEY || "", - }; - const data: TextToSpeechData = { - text: input, - model_id: "eleven_multilingual_v2", - voice_settings: { - stability: 0.5, - similarity_boost: 0.5, - }, - }; - - try { - const response = await fetch(url, { - method: "POST", - headers, - body: JSON.stringify(data), - }); - - if (!response.ok) { - throw new Error("Network response was not ok."); + const { speechToText } = useSpeechToTextApi(); + const { createCompletion } = useCompletionApi(); + const { textToSpeech } = useTextToSpeechApi(); + + const { + recording, + initMediaRecorder, + mediaRecorderRef, + startRecording, + stopRecording, + clearChunk, + } = useMediaRecorder(); + + const onClick = async () => { + if (thinking) { + toast('Please wait for the assistant to finish.'); + } else if (!recording) { + await initRecorderAndStartRecording(); + } else { + const audio = await stopRecording(); + if (audio) { + await generateAssistantResponse(audio); } - - const audioContext = new (window.AudioContext || - (window as any).webkitAudioContext)(); - const source = audioContext.createBufferSource(); - - const audioBuffer = await response.arrayBuffer(); - const audioBufferDuration = audioBuffer.byteLength / CHUNK_SIZE; - - audioContext.decodeAudioData(audioBuffer, (buffer) => { - source.buffer = buffer; - source.connect(audioContext.destination); - source.start(); - }); - - setTimeout(() => { - source.stop(); - audioContext.close(); - setAudioPlaying(false); - }, audioBufferDuration * 1000); - } catch (error) { - console.error("Error:", error); - setAudioPlaying(false); } }; - const handlePlayButtonClick = (input: string): void => { - setAudioPlaying(true); - playAudio(input); - }; + const initRecorderAndStartRecording = async () => { + let mediaRecorder = mediaRecorderRef.current; + if (!mediaRecorder) { + mediaRecorder = await initMediaRecorder(); + } - // Function to start recording - const startRecording = () => { - if (mediaRecorder && mediaRecorderInitialized) { - mediaRecorder.start(); - setRecording(true); + if (mediaRecorder) { + startRecording(); } }; - // Function to stop recording - const stopRecording = () => { + const generateAssistantResponse = async (userAudio: Blob) => { setThinking(true); - toast("Thinking", { - duration: 5000, - icon: "💭", - style: { - borderRadius: "10px", - background: "#1E1E1E", - color: "#F9F9F9", - border: "0.5px solid #3B3C3F", - fontSize: "14px", - }, - position: "top-right", + toast('Thinking...', { + icon: '💭', }); - if (mediaRecorder) { - mediaRecorder.stop(); - setRecording(false); + + try { + const { text: userInput } = await speechToText(userAudio); + const { completion } = await createCompletion(userInput); + const assistantAudio = await textToSpeech(completion); + await playAudio(assistantAudio); + } catch (error) { + toast.error( + 'Uh oh, something went wrong with Aura. Please try again later.', + ); + console.error('Generate assistant response error: ', error); + } finally { + clearChunk(); + setThinking(false); } }; - return ( -
- { - - // If assistant is thinking, don't do anything - if (thinking) { - toast("Please wait for the assistant to finish.", { - duration: 5000, - icon: "🙌", - style: { - borderRadius: "10px", - background: "#1E1E1E", - color: "#F9F9F9", - border: "0.5px solid #3B3C3F", - fontSize: "14px", - }, - position: "top-right", - }); - //Timer to reset thinking state - setTimeout(() => { - setThinking(false); - }, 1500); - return; - } - if (typeof window !== "undefined" && !mediaRecorderInitialized) { - setMediaRecorderInitialized(true); - - navigator.mediaDevices - .getUserMedia({ audio: true }) - .then((stream) => { - const newMediaRecorder = new MediaRecorder(stream); - - newMediaRecorder.onstart = () => { - chunks = []; - }; - - newMediaRecorder.ondataavailable = (e) => { - chunks.push(e.data); - }; - - newMediaRecorder.onstop = async () => { - console.time("Entire function"); - - const audioBlob = new Blob(chunks, { type: "audio/webm" }); - const audioUrl = URL.createObjectURL(audioBlob); - const audio = new Audio(audioUrl); - - audio.onerror = function (err) { - console.error("Error playing audio:", err); - }; - - try { - const reader = new FileReader(); - reader.readAsDataURL(audioBlob); - - reader.onloadend = async function () { - const base64Audio = (reader.result as string).split( - "," - )[1]; // Ensure result is not null or undefined - - if (base64Audio) { - const response = await fetch("/api/speechToText", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ audio: base64Audio }), - }); - - const data = await response.json(); - - if (response.status !== 200) { - throw ( - data.error || - new Error( - `Request failed with status ${response.status}` - ) - ); - } + const playAudio = async (audio: Blob) => { + let objectUrl: string | undefined = undefined; - console.timeEnd("Speech to Text"); - - const completion = await axios.post("/api/chat", { - messages: [ - { - role: "user", - content: `${data.result} Your answer has to be as consise as possible.`, - }, - ], - }); - - handlePlayButtonClick(completion.data); - } - }; - } catch (error) { - console.log(error); - } - }; - - setMediaRecorder(newMediaRecorder); - }) - .catch((err) => - console.error("Error accessing microphone:", err) - ); - } - - if (!mediaRecorderInitialized) { - toast( - "Please grant access to your microphone. Click the button again to speak.", - { - duration: 5000, - icon: "🙌", - style: { - borderRadius: "10px", - background: "#1E1E1E", - color: "#F9F9F9", - border: "0.5px solid #3B3C3F", - fontSize: "14px", - }, - position: "top-right", - } - ); - return; - } + const cleanup = () => { + if (objectUrl) { + URL.revokeObjectURL(objectUrl); + } + }; - recording - ? null - : toast("Listening - Click again to send", { - icon: "🟢", - style: { - borderRadius: "10px", - background: "#1E1E1E", - color: "#F9F9F9", - border: "0.5px solid #3B3C3F", - fontSize: "14px", - }, - position: "top-right", - }); + try { + objectUrl = URL.createObjectURL(audio); + const audioElement = new Audio(objectUrl); + audioElement.addEventListener('ended', () => { + cleanup(); + }); + audioElement.play(); + } catch (error) { + cleanup(); + console.error('Error playing audio:', error); + } + }; - recording ? stopRecording() : startRecording(); - }} - className="hover:scale-105 ease-in-out duration-500 hover:cursor-pointer text-[70px]" - > -
-
-
-
-
-
+ return ( + +
+
+
+
+
); }; diff --git a/src/components/AssistantButton/useMediaRecorder.ts b/src/components/AssistantButton/useMediaRecorder.ts new file mode 100644 index 0000000..90f1dca --- /dev/null +++ b/src/components/AssistantButton/useMediaRecorder.ts @@ -0,0 +1,144 @@ +import { + createMediaRecorder, + createMediaRecorderErrorIcons, + createMediaRecorderErrorMessages, + parseCreateMediaErrorCode, +} from '@/utils/client/mediaRecorder'; +import { RefObject, useEffect, useRef, useState } from 'react'; +import toast from 'react-hot-toast'; + +export type UseMediaRecorder = { + recording: boolean; + initMediaRecorder: () => Promise; + mediaRecorderRef: RefObject; + startRecording: () => void; + /** + * @returns The recorded audio blob. + */ + stopRecording: () => Promise; + clearChunk: () => void; +}; + +export const useMediaRecorder = (): UseMediaRecorder => { + const [recording, setRecording] = useState(false); + + const mediaRecorderRef = useRef(); + const chunkRef = useRef(); + + const stopRecordingIntervalIdRef = useRef(); + + useEffect(() => { + return () => { + const intervalId = stopRecordingIntervalIdRef.current; + if (intervalId !== undefined) { + clearInterval(intervalId); + } + }; + }, []); + + const initMediaRecorder = async (): Promise => { + if (mediaRecorderRef.current) { + console.warn('media recorder is already initialized'); + return; + } + try { + const mediaRecorder = await createMediaRecorder(); + mediaRecorderRef.current = mediaRecorder; + mediaRecorder.onstart = () => { + chunkRef.current = undefined; + }; + mediaRecorder.ondataavailable = (e) => { + chunkRef.current = e.data; + }; + return mediaRecorder; + } catch (error) { + const errorCode = parseCreateMediaErrorCode(error); + if (errorCode) { + toast(createMediaRecorderErrorMessages[errorCode], { + icon: createMediaRecorderErrorIcons[errorCode], + }); + } else { + toast.error('Something went wrong when setting up media recorder'); + } + console.error('Init media recorder: ', error); + return undefined; + } + }; + + const startRecording = (): void => { + const mediaRecorder = mediaRecorderRef.current; + + if (!mediaRecorder) { + console.warn( + 'startRecording can only be called after media recorder is initializaed', + ); + return; + } + + mediaRecorder.start(); + setRecording(true); + toast('Listening - Click again to send', { + icon: '🟢', + }); + }; + + const stopRecording = async (): Promise => { + const mediaRecorder = mediaRecorderRef.current; + + if (mediaRecorder?.state !== 'recording') { + console.warn( + 'stopRecording can only be called when media recorder is in recording state', + ); + return; + } + + mediaRecorder.stop(); + + // The `ondataavailable` event of MediaRecorder is triggered after `stop()` + // is called. To simplify the code and improve maintainability, we return + // the recorded audio directly from this function, which requires accessing + // a value that becomes available only after `stop()` executes. + // To achieve this, we use a Promise combined with an interval. + return new Promise((resolve, reject) => { + let timer = 0; + const interval = 100; + const timeout = 3000; + stopRecordingIntervalIdRef.current = setInterval(() => { + if (chunkRef.current) { + const audioBlob = chunkRef.current; + + clearInterval(stopRecordingIntervalIdRef.current); + setRecording(false); + + if (audioBlob.size > 0) { + return resolve(audioBlob); + } else { + // No audio recorded (e.g., the user remained silent during recording) + return resolve(undefined); + } + } else if (timer >= timeout) { + clearInterval(stopRecordingIntervalIdRef.current); + setRecording(false); + return reject( + 'Timed out waiting for recorder data. This should not be happening', + ); + } else { + timer += interval; + } + }, interval); + }); + }; + + const clearChunk = (): void => { + chunkRef.current = undefined; + }; + + return { + recording, + initMediaRecorder, + mediaRecorderRef, + startRecording, + stopRecording, + clearChunk, + }; +}; diff --git a/src/utils/client/api/axios-client.ts b/src/utils/client/api/axios-client.ts new file mode 100644 index 0000000..3a70652 --- /dev/null +++ b/src/utils/client/api/axios-client.ts @@ -0,0 +1,8 @@ +import axios from 'axios'; + +export const client = axios.create({ + baseURL: '/api', + headers: { + 'Content-Type': 'application/json', + }, +}); diff --git a/src/utils/client/api/completion.ts b/src/utils/client/api/completion.ts new file mode 100644 index 0000000..4a0a2de --- /dev/null +++ b/src/utils/client/api/completion.ts @@ -0,0 +1,26 @@ +import { CreateCompletionRequest } from '@/apiTypes/completion/createCompletionRequest'; +import { CreateCompletionResponse } from '@/apiTypes/completion/createCompletionResponse'; +import { client } from './axios-client'; + +export type UseCompletionApi = { + createCompletion: (message: string) => Promise; +}; + +export const useCompletionApi = (): UseCompletionApi => { + const createCompletion = async ( + message: string, + ): Promise => { + const body: CreateCompletionRequest = { + message, + }; + const response = await client.post( + '/completion', + body, + ); + return response.data; + }; + + return { + createCompletion, + }; +}; diff --git a/src/utils/client/api/speechToText.ts b/src/utils/client/api/speechToText.ts new file mode 100644 index 0000000..9e3150c --- /dev/null +++ b/src/utils/client/api/speechToText.ts @@ -0,0 +1,38 @@ +import { SpeechToTextRequest } from '@/apiTypes/speechToText/speechToTextRequest'; +import { SpeechToTextResponse } from '@/apiTypes/speechToText/speechToTextResponse'; +import { client } from './axios-client'; + +export type UseSpeechToTextApi = { + speechToText: (audio: Blob) => Promise; +}; + +export const useSpeechToTextApi = (): UseSpeechToTextApi => { + const speechToText = async (audio: Blob): Promise => { + // Create an additional object to ensure request body aligns with + // the TS API interface. + const request: SpeechToTextRequest = { + audio, + }; + + const formData = new FormData(); + Object.entries(request).forEach(([key, value]) => { + formData.append(key, value); + }); + + const response = await client.post( + '/speechToText', + formData, + { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }, + ); + + return response.data; + }; + + return { + speechToText, + }; +}; diff --git a/src/utils/client/api/textToSpeech.ts b/src/utils/client/api/textToSpeech.ts new file mode 100644 index 0000000..18541a2 --- /dev/null +++ b/src/utils/client/api/textToSpeech.ts @@ -0,0 +1,22 @@ +import { TextToSpeechRequest } from '@/apiTypes/textToSpeech/textToSpeechRequest'; +import { client } from './axios-client'; + +export type UseTextToSpeechApi = { + textToSpeech: (text: string) => Promise; +}; + +export const useTextToSpeechApi = (): UseTextToSpeechApi => { + const textToSpeech = async (text: string): Promise => { + const body: TextToSpeechRequest = { + text, + }; + const response = await client.post('/textToSpeech', body, { + responseType: 'blob', + }); + return response.data; + }; + + return { + textToSpeech, + }; +}; diff --git a/src/utils/client/mediaRecorder.ts b/src/utils/client/mediaRecorder.ts new file mode 100644 index 0000000..e5f4e1b --- /dev/null +++ b/src/utils/client/mediaRecorder.ts @@ -0,0 +1,62 @@ +export enum CreateMediaRecorderErrorCode { + NotSupported = 'notSupported', + PermissionDenied = 'permissionDenied', +} + +export const createMediaRecorderErrorMessages: Record< + CreateMediaRecorderErrorCode, + string +> = { + [CreateMediaRecorderErrorCode.NotSupported]: + 'Audio recording is not supported in your browser', + [CreateMediaRecorderErrorCode.PermissionDenied]: + 'Please grant access to your microphone. Click the button again to speak.', +}; + +export const createMediaRecorderErrorIcons: Record< + CreateMediaRecorderErrorCode, + string +> = { + [CreateMediaRecorderErrorCode.NotSupported]: '😢', + [CreateMediaRecorderErrorCode.PermissionDenied]: '🙌', +}; + +export class CreateMediaRecorderError extends Error { + private readonly _code: CreateMediaRecorderErrorCode; + + constructor(code: CreateMediaRecorderErrorCode) { + super(); + this._code = code; + } + + get code(): CreateMediaRecorderErrorCode { + return this._code; + } +} + +export const createMediaRecorder = async (): Promise => { + if (navigator.mediaDevices && !!navigator.mediaDevices.getUserMedia) { + try { + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); + return new MediaRecorder(stream); + } catch { + throw new CreateMediaRecorderError( + CreateMediaRecorderErrorCode.PermissionDenied, + ); + } + } else { + throw new CreateMediaRecorderError( + CreateMediaRecorderErrorCode.NotSupported, + ); + } +}; + +export const parseCreateMediaErrorCode = ( + error: unknown, +): CreateMediaRecorderErrorCode | undefined => { + if (error instanceof CreateMediaRecorderError) { + return error.code; + } else { + return undefined; + } +}; diff --git a/src/utils/client/toast.ts b/src/utils/client/toast.ts new file mode 100644 index 0000000..3c8e36b --- /dev/null +++ b/src/utils/client/toast.ts @@ -0,0 +1,13 @@ +import { DefaultToastOptions } from 'react-hot-toast'; + +export const DEFAULT_TOAST_OPTIONS: DefaultToastOptions = { + position: 'top-right', + duration: 5000, + style: { + borderRadius: '10px', + background: '#1E1E1E', + color: '#F9F9F9', + border: '0.5px solid #3B3C3F', + fontSize: '14px', + }, +}; diff --git a/src/utils/server/response.ts b/src/utils/server/response.ts new file mode 100644 index 0000000..04739fe --- /dev/null +++ b/src/utils/server/response.ts @@ -0,0 +1,26 @@ +import { NextResponse } from 'next/server'; + +export const Ok = (body?: T, init?: ResponseInit): NextResponse => { + return NextResponse.json(body, { + ...init, + status: 200, + }); +}; + +export const BadRequest = (message: string): NextResponse => { + return NextResponse.json( + { message }, + { + status: 400, + }, + ); +}; + +export const InternalServerError = (error?: T): NextResponse => { + return NextResponse.json( + { error }, + { + status: 500, + }, + ); +}; diff --git a/tailwind.config.ts b/tailwind.config.ts index 1af3b8f..13d21fa 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -1,4 +1,4 @@ -import type { Config } from 'tailwindcss' +import type { Config } from 'tailwindcss'; const config: Config = { content: [ @@ -16,5 +16,5 @@ const config: Config = { }, }, plugins: [], -} -export default config +}; +export default config; diff --git a/tsconfig.json b/tsconfig.json index 87efb78..6551afa 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -28,7 +28,6 @@ "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", - "src/app/api/chat" ], "exclude": ["node_modules"] } From 048001b76edc086b689129e7cfb6475f62124c3c Mon Sep 17 00:00:00 2001 From: abemscac Date: Thu, 19 Dec 2024 08:10:46 +0800 Subject: [PATCH 2/2] chore: add github actions workflow for prettier and tsc --- .github/workflows/ci.yaml | 40 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 .github/workflows/ci.yaml diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..9b76888 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,40 @@ +name: CI +on: + pull_request: + branches: [main] +env: + NODE_VERSION: 20.18.1 +jobs: + prettier: + name: Prettier Format + runs-on: ubuntu-latest + steps: + - name: Checkout out repository code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: ${{ env.NODE_VERSION }} + - name: Install packages + run: npm ci + - name: Check code format + run: npm run format + + tsc: + name: TypeScript Compiler + runs-on: ubuntu-latest + steps: + - name: Checkout out repository code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: ${{ env.NODE_VERSION }} + - name: Install packages + run: npm ci + - name: Run tsc + run: npm run tsc