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/.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 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 718d6fe..0000000 Binary files a/src/app/favicon.ico and /dev/null differ 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"] }