diff --git a/client/package-lock.json b/client/package-lock.json index 9c030a1..6c8602e 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -711,208 +711,266 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.22.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.0.tgz", - "integrity": "sha512-/IZQvg6ZR0tAkEi4tdXOraQoWeJy9gbQ/cx4I7k9dJaCk9qrXEcdouxRVz5kZXt5C2bQ9pILoAA+KB4C/d3pfw==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.28.1.tgz", + "integrity": "sha512-2aZp8AES04KI2dy3Ss6/MDjXbwBzj+i0GqKtWXgw2/Ma6E4jJvujryO6gJAghIRVz7Vwr9Gtl/8na3nDUKpraQ==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.22.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.0.tgz", - "integrity": "sha512-ETHi4bxrYnvOtXeM7d4V4kZWixib2jddFacJjsOjwbgYSRsyXYtZHC4ht134OsslPIcnkqT+TKV4eU8rNBKyyQ==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.28.1.tgz", + "integrity": "sha512-EbkK285O+1YMrg57xVA+Dp0tDBRB93/BZKph9XhMjezf6F4TpYjaUSuPt5J0fZXlSag0LmZAsTmdGGqPp4pQFA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.22.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.0.tgz", - "integrity": "sha512-ZWgARzhSKE+gVUX7QWaECoRQsPwaD8ZR0Oxb3aUpzdErTvlEadfQpORPXkKSdKbFci9v8MJfkTtoEHnnW9Ulng==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.28.1.tgz", + "integrity": "sha512-prduvrMKU6NzMq6nxzQw445zXgaDBbMQvmKSJaxpaZ5R1QDM8w+eGxo6Y/jhT/cLoCvnZI42oEqf9KQNYz1fqQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.22.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.0.tgz", - "integrity": "sha512-h0ZAtOfHyio8Az6cwIGS+nHUfRMWBDO5jXB8PQCARVF6Na/G6XS2SFxDl8Oem+S5ZsHQgtsI7RT4JQnI1qrlaw==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.28.1.tgz", + "integrity": "sha512-WsvbOunsUk0wccO/TV4o7IKgloJ942hVFK1CLatwv6TJspcCZb9umQkPdvB7FihmdxgaKR5JyxDjWpCOp4uZlQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.28.1.tgz", + "integrity": "sha512-HTDPdY1caUcU4qK23FeeGxCdJF64cKkqajU0iBnTVxS8F7H/7BewvYoG+va1KPSL63kQ1PGNyiwKOfReavzvNA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.28.1.tgz", + "integrity": "sha512-m/uYasxkUevcFTeRSM9TeLyPe2QDuqtjkeoTpP9SW0XxUWfcYrGDMkO/m2tTw+4NMAF9P2fU3Mw4ahNvo7QmsQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.22.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.0.tgz", - "integrity": "sha512-9pxQJSPwFsVi0ttOmqLY4JJ9pg9t1gKhK0JDbV1yUEETSx55fdyCjt39eBQ54OQCzAF0nVGO6LfEH1KnCPvelA==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.28.1.tgz", + "integrity": "sha512-QAg11ZIt6mcmzpNE6JZBpKfJaKkqTm1A9+y9O+frdZJEuhQxiugM05gnCWiANHj4RmbgeVJpTdmKRmH/a+0QbA==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.22.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.0.tgz", - "integrity": "sha512-YJ5Ku5BmNJZb58A4qSEo3JlIG4d3G2lWyBi13ABlXzO41SsdnUKi3HQHe83VpwBVG4jHFTW65jOQb8qyoR+qzg==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.28.1.tgz", + "integrity": "sha512-dRP9PEBfolq1dmMcFqbEPSd9VlRuVWEGSmbxVEfiq2cs2jlZAl0YNxFzAQS2OrQmsLBLAATDMb3Z6MFv5vOcXg==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.22.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.0.tgz", - "integrity": "sha512-U4G4u7f+QCqHlVg1Nlx+qapZy+QoG+NV6ux+upo/T7arNGwKvKP2kmGM4W5QTbdewWFgudQxi3kDNST9GT1/mg==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.28.1.tgz", + "integrity": "sha512-uGr8khxO+CKT4XU8ZUH1TTEUtlktK6Kgtv0+6bIFSeiSlnGJHG1tSFSjm41uQ9sAO/5ULx9mWOz70jYLyv1QkA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.22.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.0.tgz", - "integrity": "sha512-aQpNlKmx3amwkA3a5J6nlXSahE1ijl0L9KuIjVOUhfOh7uw2S4piR3mtpxpRtbnK809SBtyPsM9q15CPTsY7HQ==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.28.1.tgz", + "integrity": "sha512-QF54q8MYGAqMLrX2t7tNpi01nvq5RI59UBNx+3+37zoKX5KViPo/gk2QLhsuqok05sSCRluj0D00LzCwBikb0A==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.28.1.tgz", + "integrity": "sha512-vPul4uodvWvLhRco2w0GcyZcdyBfpfDRgNKU+p35AWEbJ/HPs1tOUrkSueVbBS0RQHAf/A+nNtDpvw95PeVKOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.22.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.0.tgz", - "integrity": "sha512-9fx6Zj/7vve/Fp4iexUFRKb5+RjLCff6YTRQl4CoDhdMfDoobWmhAxQWV3NfShMzQk1Q/iCnageFyGfqnsmeqQ==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.28.1.tgz", + "integrity": "sha512-pTnTdBuC2+pt1Rmm2SV7JWRqzhYpEILML4PKODqLz+C7Ou2apEV52h19CR7es+u04KlqplggmN9sqZlekg3R1A==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.22.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.0.tgz", - "integrity": "sha512-VWQiCcN7zBgZYLjndIEh5tamtnKg5TGxyZPWcN9zBtXBwfcGSZ5cHSdQZfQH/GB4uRxk0D3VYbOEe/chJhPGLQ==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.28.1.tgz", + "integrity": "sha512-vWXy1Nfg7TPBSuAncfInmAI/WZDd5vOklyLJDdIRKABcZWojNDY0NJwruY2AcnCLnRJKSaBgf/GiJfauu8cQZA==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.22.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.0.tgz", - "integrity": "sha512-EHmPnPWvyYqncObwqrosb/CpH3GOjE76vWVs0g4hWsDRUVhg61hBmlVg5TPXqF+g+PvIbqkC7i3h8wbn4Gp2Fg==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.28.1.tgz", + "integrity": "sha512-/yqC2Y53oZjb0yz8PVuGOQQNOTwxcizudunl/tFs1aLvObTclTwZ0JhXF2XcPT/zuaymemCDSuuUPXJJyqeDOg==", "cpu": [ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.22.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.0.tgz", - "integrity": "sha512-tsSWy3YQzmpjDKnQ1Vcpy3p9Z+kMFbSIesCdMNgLizDWFhrLZIoN21JSq01g+MZMDFF+Y1+4zxgrlqPjid5ohg==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.28.1.tgz", + "integrity": "sha512-fzgeABz7rrAlKYB0y2kSEiURrI0691CSL0+KXwKwhxvj92VULEDQLpBYLHpF49MSiPG4sq5CK3qHMnb9tlCjBw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.22.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.0.tgz", - "integrity": "sha512-anr1Y11uPOQrpuU8XOikY5lH4Qu94oS6j0xrulHk3NkLDq19MlX8Ng/pVipjxBJ9a2l3+F39REZYyWQFkZ4/fw==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.28.1.tgz", + "integrity": "sha512-xQTDVzSGiMlSshpJCtudbWyRfLaNiVPXt1WgdWTwWz9n0U12cI2ZVtWe/Jgwyv/6wjL7b66uu61Vg0POWVfz4g==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.22.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.0.tgz", - "integrity": "sha512-7LB+Bh+Ut7cfmO0m244/asvtIGQr5pG5Rvjz/l1Rnz1kDzM02pSX9jPaS0p+90H5I1x4d1FkCew+B7MOnoatNw==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.28.1.tgz", + "integrity": "sha512-wSXmDRVupJstFP7elGMgv+2HqXelQhuNf+IS4V+nUpNVi/GUiBgDmfwD0UGN3pcAnWsgKG3I52wMOBnk1VHr/A==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.22.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.0.tgz", - "integrity": "sha512-+3qZ4rer7t/QsC5JwMpcvCVPRcJt1cJrYS/TMJZzXIJbxWFQEVhrIc26IhB+5Z9fT9umfVc+Es2mOZgl+7jdJQ==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.28.1.tgz", + "integrity": "sha512-ZkyTJ/9vkgrE/Rk9vhMXhf8l9D+eAhbAVbsGsXKy2ohmJaWg0LPQLnIxRdRp/bKyr8tXuPlXhIoGlEB5XpJnGA==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.22.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.0.tgz", - "integrity": "sha512-YdicNOSJONVx/vuPkgPTyRoAPx3GbknBZRCOUkK84FJ/YTfs/F0vl/YsMscrB6Y177d+yDRcj+JWMPMCgshwrA==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.28.1.tgz", + "integrity": "sha512-ZvK2jBafvttJjoIdKm/Q/Bh7IJ1Ose9IBOwpOXcOvW3ikGTQGmKDgxTC6oCAzW6PynbkKP8+um1du81XJHZ0JA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -1138,10 +1196,11 @@ } }, "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==", - "dev": true + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true, + "license": "MIT" }, "node_modules/@types/node": { "version": "20.12.12", @@ -1971,10 +2030,11 @@ "dev": true }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -3947,9 +4007,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "dev": true, "funding": [ { @@ -3957,6 +4017,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -4989,12 +5050,13 @@ } }, "node_modules/rollup": { - "version": "4.22.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.0.tgz", - "integrity": "sha512-W21MUIFPZ4+O2Je/EU+GP3iz7PH4pVPUXSbEZdatQnxo29+3rsUjgrJmzuAZU24z7yRAnFN6ukxeAhZh/c7hzg==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.28.1.tgz", + "integrity": "sha512-61fXYl/qNVinKmGSTHAZ6Yy8I3YIJC/r2m9feHo6SwVAVcLT5MPwOUFe7EuURA/4m0NR8lXG4BBXuo/IZEsjMg==", "dev": true, + "license": "MIT", "dependencies": { - "@types/estree": "1.0.5" + "@types/estree": "1.0.6" }, "bin": { "rollup": "dist/bin/rollup" @@ -5004,22 +5066,25 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.22.0", - "@rollup/rollup-android-arm64": "4.22.0", - "@rollup/rollup-darwin-arm64": "4.22.0", - "@rollup/rollup-darwin-x64": "4.22.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.22.0", - "@rollup/rollup-linux-arm-musleabihf": "4.22.0", - "@rollup/rollup-linux-arm64-gnu": "4.22.0", - "@rollup/rollup-linux-arm64-musl": "4.22.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.22.0", - "@rollup/rollup-linux-riscv64-gnu": "4.22.0", - "@rollup/rollup-linux-s390x-gnu": "4.22.0", - "@rollup/rollup-linux-x64-gnu": "4.22.0", - "@rollup/rollup-linux-x64-musl": "4.22.0", - "@rollup/rollup-win32-arm64-msvc": "4.22.0", - "@rollup/rollup-win32-ia32-msvc": "4.22.0", - "@rollup/rollup-win32-x64-msvc": "4.22.0", + "@rollup/rollup-android-arm-eabi": "4.28.1", + "@rollup/rollup-android-arm64": "4.28.1", + "@rollup/rollup-darwin-arm64": "4.28.1", + "@rollup/rollup-darwin-x64": "4.28.1", + "@rollup/rollup-freebsd-arm64": "4.28.1", + "@rollup/rollup-freebsd-x64": "4.28.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.28.1", + "@rollup/rollup-linux-arm-musleabihf": "4.28.1", + "@rollup/rollup-linux-arm64-gnu": "4.28.1", + "@rollup/rollup-linux-arm64-musl": "4.28.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.28.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.28.1", + "@rollup/rollup-linux-riscv64-gnu": "4.28.1", + "@rollup/rollup-linux-s390x-gnu": "4.28.1", + "@rollup/rollup-linux-x64-gnu": "4.28.1", + "@rollup/rollup-linux-x64-musl": "4.28.1", + "@rollup/rollup-win32-arm64-msvc": "4.28.1", + "@rollup/rollup-win32-ia32-msvc": "4.28.1", + "@rollup/rollup-win32-x64-msvc": "4.28.1", "fsevents": "~2.3.2" } }, diff --git a/client/public/assets/images/demo/image1.jpg b/client/public/assets/images/demo/image1.jpg new file mode 100644 index 0000000..9b3120c Binary files /dev/null and b/client/public/assets/images/demo/image1.jpg differ diff --git a/client/public/assets/images/demo/image10.jpg b/client/public/assets/images/demo/image10.jpg new file mode 100644 index 0000000..9b3120c Binary files /dev/null and b/client/public/assets/images/demo/image10.jpg differ diff --git a/client/public/assets/images/demo/image11.jpg b/client/public/assets/images/demo/image11.jpg new file mode 100644 index 0000000..9b3120c Binary files /dev/null and b/client/public/assets/images/demo/image11.jpg differ diff --git a/client/public/assets/images/demo/image12.jpg b/client/public/assets/images/demo/image12.jpg new file mode 100644 index 0000000..9b3120c Binary files /dev/null and b/client/public/assets/images/demo/image12.jpg differ diff --git a/client/public/assets/images/demo/image13.jpg b/client/public/assets/images/demo/image13.jpg new file mode 100644 index 0000000..9b3120c Binary files /dev/null and b/client/public/assets/images/demo/image13.jpg differ diff --git a/client/public/assets/images/demo/image14.jpg b/client/public/assets/images/demo/image14.jpg new file mode 100644 index 0000000..9b3120c Binary files /dev/null and b/client/public/assets/images/demo/image14.jpg differ diff --git a/client/public/assets/images/demo/image15.jpg b/client/public/assets/images/demo/image15.jpg new file mode 100644 index 0000000..9b3120c Binary files /dev/null and b/client/public/assets/images/demo/image15.jpg differ diff --git a/client/public/assets/images/demo/image16.jpg b/client/public/assets/images/demo/image16.jpg new file mode 100644 index 0000000..9b3120c Binary files /dev/null and b/client/public/assets/images/demo/image16.jpg differ diff --git a/client/public/assets/images/demo/image17.jpg b/client/public/assets/images/demo/image17.jpg new file mode 100644 index 0000000..9b3120c Binary files /dev/null and b/client/public/assets/images/demo/image17.jpg differ diff --git a/client/public/assets/images/demo/image18.jpg b/client/public/assets/images/demo/image18.jpg new file mode 100644 index 0000000..9b3120c Binary files /dev/null and b/client/public/assets/images/demo/image18.jpg differ diff --git a/client/public/assets/images/demo/image19.jpg b/client/public/assets/images/demo/image19.jpg new file mode 100644 index 0000000..9b3120c Binary files /dev/null and b/client/public/assets/images/demo/image19.jpg differ diff --git a/client/public/assets/images/demo/image2.jpg b/client/public/assets/images/demo/image2.jpg new file mode 100644 index 0000000..9b3120c Binary files /dev/null and b/client/public/assets/images/demo/image2.jpg differ diff --git a/client/public/assets/images/demo/image20.jpg b/client/public/assets/images/demo/image20.jpg new file mode 100644 index 0000000..9b3120c Binary files /dev/null and b/client/public/assets/images/demo/image20.jpg differ diff --git a/client/public/assets/images/demo/image3.jpg b/client/public/assets/images/demo/image3.jpg new file mode 100644 index 0000000..9b3120c Binary files /dev/null and b/client/public/assets/images/demo/image3.jpg differ diff --git a/client/public/assets/images/demo/image4.jpg b/client/public/assets/images/demo/image4.jpg new file mode 100644 index 0000000..9b3120c Binary files /dev/null and b/client/public/assets/images/demo/image4.jpg differ diff --git a/client/public/assets/images/demo/image5.jpg b/client/public/assets/images/demo/image5.jpg new file mode 100644 index 0000000..9b3120c Binary files /dev/null and b/client/public/assets/images/demo/image5.jpg differ diff --git a/client/public/assets/images/demo/image6.jpg b/client/public/assets/images/demo/image6.jpg new file mode 100644 index 0000000..9b3120c Binary files /dev/null and b/client/public/assets/images/demo/image6.jpg differ diff --git a/client/public/assets/images/demo/image7.jpg b/client/public/assets/images/demo/image7.jpg new file mode 100644 index 0000000..9b3120c Binary files /dev/null and b/client/public/assets/images/demo/image7.jpg differ diff --git a/client/public/assets/images/demo/image8.jpg b/client/public/assets/images/demo/image8.jpg new file mode 100644 index 0000000..9b3120c Binary files /dev/null and b/client/public/assets/images/demo/image8.jpg differ diff --git a/client/public/assets/images/demo/image9.jpg b/client/public/assets/images/demo/image9.jpg new file mode 100644 index 0000000..9b3120c Binary files /dev/null and b/client/public/assets/images/demo/image9.jpg differ diff --git a/client/src/components/Button/Button.tsx b/client/src/components/Button/Button.tsx index b7df605..adf373c 100644 --- a/client/src/components/Button/Button.tsx +++ b/client/src/components/Button/Button.tsx @@ -1,6 +1,6 @@ import { FC } from "react"; -type ButtonProps = React.ButtonHTMLAttributes; +type ButtonProps = React.ButtonHTMLAttributes; export const Button: FC = ({ children, className, ...props }) => { return ( ); }; + + +export const SwitchButton: FC = ({ children, className, ...props }) => { + return ( + + ); +}; diff --git a/client/src/components/ImageGallery/ImageGallery.tsx b/client/src/components/ImageGallery/ImageGallery.tsx new file mode 100644 index 0000000..f52b049 --- /dev/null +++ b/client/src/components/ImageGallery/ImageGallery.tsx @@ -0,0 +1,140 @@ + +import { useState } from "react"; +import { Button } from "../Button/Button"; + +// Natural images +import img1 from "/assets/images/demo/image1.jpg"; +import img2 from "/assets/images/demo/image2.jpg"; +import img3 from "/assets/images/demo/image3.jpg"; +import img4 from "/assets/images/demo/image4.jpg"; +import img5 from "/assets/images/demo/image5.jpg"; +import img6 from "/assets/images/demo/image6.jpg"; +import img7 from "/assets/images/demo/image7.jpg"; +import img8 from "/assets/images/demo/image8.jpg"; +import img9 from "/assets/images/demo/image9.jpg"; +import img10 from "/assets/images/demo/image10.jpg"; +import img11 from "/assets/images/demo/image11.jpg"; +import img12 from "/assets/images/demo/image12.jpg"; +import img13 from "/assets/images/demo/image13.jpg"; +import img14 from "/assets/images/demo/image14.jpg"; +import img15 from "/assets/images/demo/image15.jpg"; +import img16 from "/assets/images/demo/image16.jpg"; +import img17 from "/assets/images/demo/image17.jpg"; +import img18 from "/assets/images/demo/image18.jpg"; +import img19 from "/assets/images/demo/image19.jpg"; +import img20 from "/assets/images/demo/image20.jpg"; + +const images = [ + img1, + img2, + img3, + img4, + img5, + img6, + img7, + img8, + img9, + img10, + img11, + img12, + img13, + img14, + img15, + img16, + img17, + img18, + img19, + img20, +] + +var images_order: number[] = []; +for (let i = 0; i < images.length; i++) { + images_order.push(i) +} + +type ImageGalleryProps = React.InputHTMLAttributes & { + // Properties for the ImageGallery + paramsSetter: Function; + clickAction: Function; + size: number; + numImages: number; +} + + +type ImageItemProps = React.InputHTMLAttributes & { + // Properties for a single item in the ImageGallery + // Two actions: + // paramsSetter sets the chosen image url into the model params + // clickAction then starts the conversation + paramsSetter: Function; + clickAction: Function; + size: number; + imageUrl: string; +} + + +function ImageSelect(props: ImageItemProps) { + // Represents a single image in the gallery + const [isHover, setIsHover] = useState(false); + + const handleMouseEnter = () => { + setIsHover(true); + }; + const handleMouseLeave = () => { + setIsHover(false); + }; + let bordercolor = isHover ? "#f7a319" : "black"; + let bgalpha = isHover ? 0.05 : 0.6; + let textalpha = isHover ? 1.0 : 0.0 + let label = isHover ? "Connect" : "X"; + let style = { + width: props.size, + height: props.size, + background: `url(${props.imageUrl})`, + backgroundSize: "100% 100%", + border: `3px solid ${bordercolor}`, + margin: "2px", + padding: "0px", + color: `rgba(255, 255, 255, ${textalpha})`, + boxShadow: `inset 0 0 0 1000px rgba(0,0,0,${bgalpha})`, + textShadow: `2px 2px 2px rgba(0, 0, 0, ${textalpha})` + }; + return ( + + ); +} + + +const shuffle = (array: number[]) => { + return array.sort(() => Math.random() - 0.5); +}; + +export const ImageGallery = (props: ImageGalleryProps) => { + const [ordering, SetOrdering] = useState(images_order); + + function handleShuffle() { + SetOrdering(shuffle([...ordering])); + } + + // Image Gallery widget (random subset) + const steps = []; + for (let i = 0; i < props.numImages; i++) { + steps.push(); + } + + return ( +
+
+ + +
+
{steps}
+
) + ; +}; diff --git a/client/src/index.css b/client/src/index.css index 51043bb..4587196 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -3,15 +3,20 @@ @tailwind utilities; @layer utilities { + /* Hide scrollbar for Chrome, Safari and Opera */ .no-scrollbar::-webkit-scrollbar { display: none; } + /* Hide scrollbar for IE, Edge and Firefox */ .no-scrollbar { - -ms-overflow-style: none; /* IE and Edge */ - scrollbar-width: none; /* Firefox */ + -ms-overflow-style: none; + /* IE and Edge */ + scrollbar-width: none; + /* Firefox */ } + .scrollbar::-webkit-scrollbar { width: 10px; } @@ -36,7 +41,8 @@ "controls" "player" "player-text"; - @media screen and (min-width: 768px){ + + @media screen and (min-width: 768px) { grid-template-columns: 2fr 2.5fr; grid-template-rows: min-content min-content min-content 1fr; gap: 30px 30px; @@ -55,10 +61,15 @@ max-width: 450px; } -.presentation > p { +.presentation>p { padding-top: 10px; } + +.gallery { + max-width: 450px; +} + .cute-words { color: #54e8b3; } @@ -73,7 +84,9 @@ } -.controls { grid-area: controls; } +.controls { + grid-area: controls; +} .player { grid-area: player; @@ -88,12 +101,29 @@ /* margin:auto; */ } -.server-audio { grid-area: server-audio; } +.server-audio { + grid-area: server-audio; +} -.user-audio { grid-area: user-audio; } +.user-audio { + grid-area: user-audio; +} -.player-stats { grid-area: player-stats; width: 100%; height:100%; } +.player-stats { + grid-area: player-stats; + width: 100%; + height: 100%; +} -.commands { grid-area: commands; width: 100%; height:100%;} +.commands { + grid-area: commands; + width: 100%; + height: 100%; +} -.player-text { grid-area: player-text; width: 100%; height:100%; overflow:scroll;} +.player-text { + grid-area: player-text; + width: 100%; + height: 100%; + overflow: scroll; +} \ No newline at end of file diff --git a/client/src/pages/Conversation/Conversation.tsx b/client/src/pages/Conversation/Conversation.tsx index 627e19e..1679706 100644 --- a/client/src/pages/Conversation/Conversation.tsx +++ b/client/src/pages/Conversation/Conversation.tsx @@ -49,10 +49,10 @@ const buildURL = ({ } const wsProtocol = (window.location.protocol === 'https:') ? 'wss' : 'ws'; const url = new URL(`${wsProtocol}://${workerAddr}/api/chat`); - if(workerAuthId) { + if (workerAuthId) { url.searchParams.append("worker_auth_id", workerAuthId); } - if(email) { + if (email) { url.searchParams.append("email", email); } url.searchParams.append("text_temperature", params.textTemperature.toString()); @@ -64,12 +64,17 @@ const buildURL = ({ url.searchParams.append("audio_seed", audioSeed.toString()); url.searchParams.append("repetition_penalty_context", params.repetitionPenaltyContext.toString()); url.searchParams.append("repetition_penalty", params.repetitionPenalty.toString()); + // Add image params if given + if (params.imageUrl != undefined) { + url.searchParams.append("image_url", params.imageUrl.toString()); + url.searchParams.append("image_resolution", params.imageResolution.toString()); + } console.log(url.toString()); return url.toString(); }; -export const Conversation:FC = ({ +export const Conversation: FC = ({ workerAddr, workerAuthId, audioContext, @@ -77,7 +82,7 @@ export const Conversation:FC = ({ sessionAuthId, sessionId, onConversationEnd, - isBypass=false, + isBypass = false, email, ...params }) => { @@ -95,7 +100,7 @@ export const Conversation:FC = ({ const audioStreamDestination = useRef(audioContext.current.createMediaStreamDestination()); const mediaRecorder = useRef(null); - const audioRecorder = useRef(new MediaRecorder(audioStreamDestination.current.stream, { mimeType: getMimeType("audio"), audioBitsPerSecond: 128000 })); + const audioRecorder = useRef(new MediaRecorder(audioStreamDestination.current.stream, { mimeType: getMimeType("audio"), audioBitsPerSecond: 128000 })); const [videoURL, setVideoURL] = useState(""); const [audioURL, setAudioURL] = useState(""); const [isOver, setIsOver] = useState(false); @@ -136,10 +141,10 @@ export const Conversation:FC = ({ audioRecorder.current.onstop = async () => { let blob: Blob; const mimeType = getMimeType("audio"); - if(mimeType.includes("webm")) { + if (mimeType.includes("webm")) { blob = await fixWebmDuration(new Blob(audioChunks.current, { type: mimeType })); - } else { - blob = new Blob(audioChunks.current, { type: mimeType }); + } else { + blob = new Blob(audioChunks.current, { type: mimeType }); } setAudioURL(URL.createObjectURL(blob)); audioChunks.current = []; @@ -157,30 +162,30 @@ export const Conversation:FC = ({ useEffect(() => { - if(!canvasRef) { + if (!canvasRef) { console.log("No canvas ref"); return; } - if(!logoRef) { + if (!logoRef) { console.log("No logo ref"); return; } - if(!isLogoLoaded) { + if (!isLogoLoaded) { console.log("Logo not loaded"); return; } - if(!canvasRef.current) { + if (!canvasRef.current) { console.log("No canvas"); return; } - if(!logoRef.current) { + if (!logoRef.current) { console.log("No logo"); return; } const ctx = canvasRef.current.getContext("2d"); - if(ctx) { - ctx.drawImage(logoRef.current, 20, 250 , 320, 98); + if (ctx) { + ctx.drawImage(logoRef.current, 20, 250, 320, 98); ctx.lineWidth = 1; ctx.strokeStyle = "white"; ctx.strokeRect(5, 5, 370, 370); @@ -188,12 +193,12 @@ export const Conversation:FC = ({ }, [canvasRef, logoRef, isLogoLoaded]); const startRecording = useCallback(() => { - if(isRecording.current) { + if (isRecording.current) { return; } console.log(Date.now() % 1000, "Starting recording"); console.log("Starting recording"); - if(canvasRef.current) { + if (canvasRef.current) { // Note: Attaching a track from this stream to the existing MediaRecorder // rather than creating a new MediaRecorder for the canvas stream // doesn't work on Safari as it just ends the recording immediately. @@ -201,7 +206,7 @@ export const Conversation:FC = ({ console.log("Adding canvas to stream"); const captureStream = canvasRef.current.captureStream(30); captureStream.addTrack(audioStreamDestination.current.stream.getAudioTracks()[0]); - mediaRecorder.current = new MediaRecorder(captureStream, { mimeType: getMimeType("video"), videoBitsPerSecond: 1000000}); + mediaRecorder.current = new MediaRecorder(captureStream, { mimeType: getMimeType("video"), videoBitsPerSecond: 1000000 }); mediaRecorder.current.ondataavailable = (e) => { console.log("Video data available"); videoChunks.current.push(e.data); @@ -209,7 +214,7 @@ export const Conversation:FC = ({ mediaRecorder.current.onstop = async () => { let blob: Blob; const mimeType = getMimeType("video"); - if(mimeType.includes("webm")) { + if (mimeType.includes("webm")) { blob = await fixWebmDuration(new Blob(videoChunks.current, { type: mimeType })); } else { blob = new Blob(videoChunks.current, { type: mimeType }); @@ -232,7 +237,7 @@ export const Conversation:FC = ({ const stopRecording = useCallback(() => { console.log("Stopping recording"); console.log("isRecording", isRecording) - if(!isRecording.current) { + if (!isRecording.current) { return; } worklet.current?.disconnect(audioStreamDestination.current); @@ -250,82 +255,103 @@ export const Conversation:FC = ({ socket, }} > - -
-
-
- {isOver && !isBypass && ( - - ) - } + { - audioContext.current.resume(); - isConnected ? stop() : start(); - }} - > - {!isConnected ? "Connect" : "Disconnect"} - - ) + startRecording, + stopRecording, + audioContext, + worklet, + audioStreamDestination, + micDuration, + actualAudioPlayed, } -
-
-
+ }> +
+
+
+ {isOver && !isBypass && ( + + ) + } + { + (!isOver || isBypass) && ( + + ) + } +
+
+
AudioStats) => (getAudioStats.current = callback) } /> - -
+ +
{audioURL && } {videoURL && } {videoURL && getExtension("video") === "webm" && }
+
+
+ +
+
+ +
-
- +
+ + {!workerAuthId && }
-
- -
-
-
- - {!workerAuthId && } + + { + console.log("Logo loaded"); + setIsLogoLoaded(true); + }} />
- - { - console.log("Logo loaded"); - setIsLogoLoaded(true); - }} /> -
- + ); }; diff --git a/client/src/pages/Conversation/components/AudioVisualizer/ClientVisualizer.tsx b/client/src/pages/Conversation/components/AudioVisualizer/ClientVisualizer.tsx index 9a8391c..e25e685 100644 --- a/client/src/pages/Conversation/components/AudioVisualizer/ClientVisualizer.tsx +++ b/client/src/pages/Conversation/components/AudioVisualizer/ClientVisualizer.tsx @@ -24,7 +24,7 @@ const COLORS = [ ]; export const ClientVisualizer: FC = ({ analyser, parent, copyCanvasRef }) => { - const [canvasWidth, setCanvasWidth] = useState(parent.current ? Math.min(parent.current.clientWidth, parent.current.clientHeight) : 0 ); + const [canvasWidth, setCanvasWidth] = useState(parent.current ? Math.min(parent.current.clientWidth, parent.current.clientHeight) : 0); const requestRef = useRef(null); const canvasRef = useRef(null); @@ -40,11 +40,11 @@ export const ClientVisualizer: FC = ({ analyser, parent, c ) => { const barHeight = height / 10 - gap; for (let i = 1; i <= 10; i++) { - const barY = y + height + gap + Math.min(1, width / 30)- (i * barHeight + i * gap); + const barY = y + height + gap + Math.min(1, width / 30) - (i * barHeight + i * gap); ctx.fillStyle = COLORS[i - 1]; ctx.strokeStyle = "white"; ctx.lineWidth = Math.min(1, height / 100); - if(i <= volume) { + if (i <= volume) { ctx.fillRect(x, barY, width, barHeight); } ctx.strokeRect(x, barY, width, barHeight); @@ -53,7 +53,7 @@ export const ClientVisualizer: FC = ({ analyser, parent, c [], ); - const draw = useCallback((ctx:CanvasRenderingContext2D, audioData: Uint8Array, x:number, y: number, width:number, height: number) => { + const draw = useCallback((ctx: CanvasRenderingContext2D, audioData: Uint8Array, x: number, y: number, width: number, height: number) => { const stereoGap = Math.floor(width / 30); const barGap = Math.floor(height / 30); const padding = Math.floor(width / 30); @@ -72,7 +72,7 @@ export const ClientVisualizer: FC = ({ analyser, parent, c MAX_INTENSITY, ); const volume = Math.floor((intensity * 10) / MAX_INTENSITY); - ctx.fillStyle = "#000000"; + ctx.fillStyle = "rgba(0, 0, 0, 0)"; ctx.fillRect(x, y, width, height); drawBars( ctx, @@ -96,7 +96,7 @@ export const ClientVisualizer: FC = ({ analyser, parent, c const visualizeData = useCallback(() => { const width = parent.current ? Math.min(parent.current.clientWidth, parent.current.clientHeight) : 0 - if(width !== canvasWidth) { + if (width !== canvasWidth) { console.log("Setting canvas width"); setCanvasWidth(width); } @@ -114,10 +114,10 @@ export const ClientVisualizer: FC = ({ analyser, parent, c return; } ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height); - draw(ctx, audioData, 0, 0, width, width); - if(copyCanvasRef?.current) { + draw(ctx, audioData, 0, 0, width, width); + if (copyCanvasRef?.current) { const copyCtx = copyCanvasRef.current.getContext("2d"); - if(copyCtx) { + if (copyCtx) { copyCtx.clearRect(220, 40, 140, 180); draw(copyCtx, audioData, 220, 40, 140, 180); } diff --git a/client/src/pages/Conversation/components/AudioVisualizer/ServerVisualizer.tsx b/client/src/pages/Conversation/components/AudioVisualizer/ServerVisualizer.tsx index 5b1c678..18d7390 100644 --- a/client/src/pages/Conversation/components/AudioVisualizer/ServerVisualizer.tsx +++ b/client/src/pages/Conversation/components/AudioVisualizer/ServerVisualizer.tsx @@ -5,19 +5,21 @@ import { useSocketContext } from "../../SocketContext"; type AudioVisualizerProps = { analyser: AnalyserNode | null; parent: RefObject; + imageUrl: string | undefined; copyCanvasRef?: RefObject; }; const MAX_INTENSITY = 255; -export const ServerVisualizer: FC = ({ analyser, parent, copyCanvasRef }) => { - const [canvasWidth, setCanvasWidth] = useState( parent.current ? Math.min(parent.current.clientWidth, parent.current.clientHeight) : 0 ); +export const ServerVisualizer: FC = ({ analyser, parent, imageUrl, copyCanvasRef }) => { + const [canvasWidth, setCanvasWidth] = useState(parent.current ? Math.min(parent.current.clientWidth, parent.current.clientHeight) : 0); const requestRef = useRef(null); const canvasRef = useRef(null); const { isConnected } = useSocketContext(); - const draw = useCallback((width: number, centerX:number, centerY:number,audioData: Uint8Array, ctx: CanvasRenderingContext2D) => { + + const draw = useCallback((width: number, centerX: number, centerY: number, audioData: Uint8Array, imageUrl: string | undefined, ctx: CanvasRenderingContext2D) => { const maxCircleWidth = Math.floor(width * 0.95); const averageIntensity = Math.sqrt( audioData.reduce((acc, curr) => acc + curr * curr, 0) / audioData.length, @@ -30,11 +32,21 @@ export const ServerVisualizer: FC = ({ analyser, parent, c const relIntensity = intensity / MAX_INTENSITY; const radius = ((isConnected ? 0.3 + 0.7 * relIntensity : relIntensity) * maxCircleWidth) / 2; // Draw a circle with radius based on intensity - ctx.clearRect( centerX - width /2, centerY - width/2 , width, width); - ctx.fillStyle = "#000000"; - ctx.fillRect(centerX - width / 2, centerY - width / 2, width, width); + if (imageUrl == undefined) { + ctx.clearRect(centerX - width / 2, centerY - width / 2, width, width); + ctx.fillStyle = 'rgba(0, 0, 0, 0)'; + ctx.fillRect(centerX - width / 2, centerY - width / 2, width, width); + } else { + const img = new Image() + img.src = imageUrl; + img.onload = function () { + ctx.drawImage(img, centerX - width / 2, centerY - width / 2, width, width); + }; + console.log(img.src); + } ctx.beginPath(); - ctx.fillStyle = "#39e3a7"; + //ctx.fillStyle = "#39e3a7"; + ctx.fillStyle = 'rgba(57, 227, 167, 0.5)'; ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI); ctx.fill(); ctx.closePath(); @@ -43,7 +55,8 @@ export const ServerVisualizer: FC = ({ analyser, parent, c if (isConnected) { ctx.beginPath(); ctx.arc(centerX, centerY, maxCircleWidth / 6, 0, 2 * Math.PI); - ctx.fillStyle = "#BCFCE5"; + // ctx.fillStyle = "#BCFCE5"; + ctx.fillStyle = 'rgba(188, 252, 229, 0.5)'; ctx.fill(); ctx.closePath(); } @@ -54,12 +67,14 @@ export const ServerVisualizer: FC = ({ analyser, parent, c ctx.strokeStyle = "white"; ctx.lineWidth = width / 50; ctx.stroke(); + ctx.fillStyle = 'rgba(0, 0, 0, 0.6)'; + ctx.fill() ctx.closePath(); }, [isConnected]); const visualizeData = useCallback(() => { const width = parent.current ? Math.min(parent.current.clientWidth, parent.current.clientHeight) : 0; - if(width !== canvasWidth) { + if (width !== canvasWidth) { console.log("Setting canvas width"); setCanvasWidth(width); } @@ -71,18 +86,20 @@ export const ServerVisualizer: FC = ({ analyser, parent, c const ctx = canvasRef.current.getContext("2d"); const audioData = new Uint8Array(140); analyser?.getByteFrequencyData(audioData); - if(!ctx){ + if (!ctx) { console.log("Canvas context not found"); return; } const centerX = width / 2; const centerY = width / 2; - draw(width, centerX, centerY, audioData, ctx); - if(copyCanvasRef?.current){ + // Hack: For the image, we display it using CSS background-image + // in the main image, but we display it via canvas so that + // it is in the video export + draw(width, centerX, centerY, audioData, undefined, ctx); + if (copyCanvasRef?.current) { const copyCtx = copyCanvasRef.current.getContext("2d"); - if(copyCtx){ - copyCtx.clearRect(50, 50, 150, 150); - draw(150, 125, 125, audioData, copyCtx); + if (copyCtx) { + draw(150, 125, 125, audioData, imageUrl, copyCtx); } } }, [analyser, isConnected, canvasWidth, parent, copyCanvasRef]); diff --git a/client/src/pages/Conversation/components/ModelParams/ModelParams.tsx b/client/src/pages/Conversation/components/ModelParams/ModelParams.tsx index 2a5b1d4..99eb09d 100644 --- a/client/src/pages/Conversation/components/ModelParams/ModelParams.tsx +++ b/client/src/pages/Conversation/components/ModelParams/ModelParams.tsx @@ -4,9 +4,10 @@ import { Button } from "../../../../components/Button/Button"; type ModelParamsProps = { isConnected: boolean; + isImageMode: boolean; modal?: RefObject, -} & ReturnType; -export const ModelParams:FC = ({ +} & ReturnType; +export const ModelParams: FC = ({ textTemperature, textTopk, audioTemperature, @@ -14,6 +15,7 @@ export const ModelParams:FC = ({ padMult, repetitionPenalty, repetitionPenaltyContext, + imageResolution, setTextTemperature, setTextTopk, setAudioTemperature, @@ -21,55 +23,64 @@ export const ModelParams:FC = ({ setPadMult, setRepetitionPenalty, setRepetitionPenaltyContext, + setImageResolution, resetParams, isConnected, + isImageMode, modal, }) => { return (
- - +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {isImageMode && - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Text temperature:{textTemperature} setTextTemperature(parseFloat(e.target.value))} />
Text topk:{textTopk} setTextTopk(parseInt(e.target.value))} />
Audio temperature:{audioTemperature} setAudioTemperature(parseFloat(e.target.value))} />
Audio topk:{audioTopk} setAudioTopk(parseInt(e.target.value))} />
Padding multiplier:{padMult} setPadMult(parseFloat(e.target.value))} />
Repeat penalty:{repetitionPenalty} setRepetitionPenalty(parseFloat(e.target.value))} />
Repeat penalty last N:{repetitionPenaltyContext} setRepetitionPenaltyContext(parseFloat(e.target.value))} />
Text temperature:{textTemperature} setTextTemperature(parseFloat(e.target.value))} />Image max-side (px):{imageResolution} setImageResolution(parseFloat(e.target.value))} />
Text topk:{textTopk} setTextTopk(parseInt(e.target.value))} />
Audio temperature:{audioTemperature} setAudioTemperature(parseFloat(e.target.value))} />
Audio topk:{audioTopk} setAudioTopk(parseInt(e.target.value))} />
Padding multiplier:{padMult} setPadMult(parseFloat(e.target.value))} />
Repeat penalty:{repetitionPenalty} setRepetitionPenalty(parseFloat(e.target.value))} />
Repeat penalty last N:{repetitionPenaltyContext} setRepetitionPenaltyContext(parseFloat(e.target.value))} />
-
- - -
-
+ } + + +
+ {!isConnected && } + {!isConnected && } +
+
) }; diff --git a/client/src/pages/Conversation/components/ServerAudio/ServerAudio.tsx b/client/src/pages/Conversation/components/ServerAudio/ServerAudio.tsx index 511ad3b..5bdcc0f 100644 --- a/client/src/pages/Conversation/components/ServerAudio/ServerAudio.tsx +++ b/client/src/pages/Conversation/components/ServerAudio/ServerAudio.tsx @@ -4,9 +4,10 @@ import { ServerVisualizer } from "../AudioVisualizer/ServerVisualizer"; type ServerAudioProps = { setGetAudioStats: (getAudioStats: () => AudioStats) => void; + imageUrl: string | undefined; copyCanvasRef?: React.RefObject; }; -export const ServerAudio: FC = ({ setGetAudioStats,copyCanvasRef }) => { +export const ServerAudio: FC = ({ setGetAudioStats, imageUrl, copyCanvasRef }) => { const { analyser, hasCriticalDelay, setHasCriticalDelay } = useServerAudio({ setGetAudioStats, }); @@ -27,7 +28,7 @@ export const ServerAudio: FC = ({ setGetAudioStats,copyCanvasR
)}
- +
); diff --git a/client/src/pages/Conversation/components/TextDisplay/TextDisplay.tsx b/client/src/pages/Conversation/components/TextDisplay/TextDisplay.tsx index f1e88be..c08a9eb 100644 --- a/client/src/pages/Conversation/components/TextDisplay/TextDisplay.tsx +++ b/client/src/pages/Conversation/components/TextDisplay/TextDisplay.tsx @@ -3,12 +3,28 @@ import { useServerText } from "../../hooks/useServerText"; type TextDisplayProps = { containerRef: React.RefObject; + displayColor: boolean | undefined; }; -export const TextDisplay:FC = ({ - containerRef, +// Palette 2: Purple to Green Moshi +// sns.diverging_palette(288, 145, s=90, l=72, n=11) +const textDisplayColors = [ + "#d19bf7", "#d7acf6", "#debdf5", "#e4cef4", + "#ebe0f3", "#eef2f0", "#c8ead9", "#a4e2c4", + "#80d9af", "#5bd09a", "#38c886"] + +function clamp_color(v: number) { + return v <= 0 + ? 0 + : v >= textDisplayColors.length + ? textDisplayColors.length + : v +} + +export const TextDisplay: FC = ({ + containerRef, displayColor }) => { - const { text } = useServerText(); + const { text, textColor } = useServerText(); const currentIndex = text.length - 1; const prevScrollTop = useRef(0); @@ -21,9 +37,27 @@ export const TextDisplay:FC = ({ }); } }, [text]); - - return ( -
+ if (displayColor && (textColor.length == text.length)) { + return ( +
+ {text.map((t, i) => ( + + {t} + + )) + } +
+ ); + } + else { + return ( +
{text.map((t, i) => ( = ({ {t} ))} -
- ); +
+ ); + }; }; diff --git a/client/src/pages/Conversation/components/UserAudio/UserAudio.tsx b/client/src/pages/Conversation/components/UserAudio/UserAudio.tsx index 8b71706..d0dc525 100644 --- a/client/src/pages/Conversation/components/UserAudio/UserAudio.tsx +++ b/client/src/pages/Conversation/components/UserAudio/UserAudio.tsx @@ -6,7 +6,7 @@ import { ClientVisualizer } from "../AudioVisualizer/ClientVisualizer"; type UserAudioProps = { copyCanvasRef: React.RefObject; }; -export const UserAudio: FC = ({copyCanvasRef}) => { +export const UserAudio: FC = ({ copyCanvasRef }) => { const [analyser, setAnalyser] = useState(null); const { sendMessage, isConnected } = useSocketContext(); const containerRef = useRef(null); @@ -65,7 +65,7 @@ export const UserAudio: FC = ({copyCanvasRef}) => { return (
- +
); }; diff --git a/client/src/pages/Conversation/hooks/useModelParams.ts b/client/src/pages/Conversation/hooks/useModelParams.ts index f696428..db752eb 100644 --- a/client/src/pages/Conversation/hooks/useModelParams.ts +++ b/client/src/pages/Conversation/hooks/useModelParams.ts @@ -7,6 +7,10 @@ export const DEFAULT_AUDIO_TOPK = 250; export const DEFAULT_PAD_MULT = 0; export const DEFAULT_REPETITION_PENALTY_CONTEXT = 64; export const DEFAULT_REPETITION_PENALTY = 1.0; +export const DEFAULT_IMAGE_RESOLUTION = 224; +export const DEFAULT_IMAGE_URL = undefined; +export const DEFAULT_IMAGE_MULT = 1.0; +export const DEFAULT_DISPLAY_COLOR = false; export type ModelParamsValues = { textTemperature: number; @@ -16,19 +20,25 @@ export type ModelParamsValues = { padMult: number; repetitionPenaltyContext: number, repetitionPenalty: number, + imageResolution: number, + imageUrl: string | undefined, + displayColor: boolean, }; type useModelParamsArgs = Partial; -export const useModelParams = (params?:useModelParamsArgs) => { +export const useModelParams = (params?: useModelParamsArgs) => { const [textTemperature, setTextTemperatureBase] = useState(params?.textTemperature || DEFAULT_TEXT_TEMPERATURE); - const [textTopk, setTextTopkBase]= useState(params?.textTopk || DEFAULT_TEXT_TOPK); + const [textTopk, setTextTopkBase] = useState(params?.textTopk || DEFAULT_TEXT_TOPK); const [audioTemperature, setAudioTemperatureBase] = useState(params?.audioTemperature || DEFAULT_AUDIO_TEMPERATURE); const [audioTopk, setAudioTopkBase] = useState(params?.audioTopk || DEFAULT_AUDIO_TOPK); const [padMult, setPadMultBase] = useState(params?.padMult || DEFAULT_PAD_MULT); const [repetitionPenalty, setRepetitionPenaltyBase] = useState(params?.repetitionPenalty || DEFAULT_REPETITION_PENALTY); const [repetitionPenaltyContext, setRepetitionPenaltyContextBase] = useState(params?.repetitionPenaltyContext || DEFAULT_REPETITION_PENALTY_CONTEXT); + const [imageResolution, setImageResolutionBase] = useState(params?.imageResolution || DEFAULT_IMAGE_RESOLUTION); + const [imageUrl, setImageUrlBase] = useState(params?.imageUrl || DEFAULT_IMAGE_URL); + const [displayColor, setDisplayColorBase] = useState(params?.displayColor == undefined ? DEFAULT_DISPLAY_COLOR : params?.displayColor); const resetParams = useCallback(() => { setTextTemperatureBase(DEFAULT_TEXT_TEMPERATURE); @@ -36,8 +46,11 @@ export const useModelParams = (params?:useModelParamsArgs) => { setAudioTemperatureBase(DEFAULT_AUDIO_TEMPERATURE); setAudioTopkBase(DEFAULT_AUDIO_TOPK); setPadMultBase(DEFAULT_PAD_MULT); - setRepetitionPenalty(DEFAULT_REPETITION_PENALTY); - setRepetitionPenaltyContext(DEFAULT_REPETITION_PENALTY_CONTEXT); + setRepetitionPenaltyBase(DEFAULT_REPETITION_PENALTY); + setRepetitionPenaltyContextBase(DEFAULT_REPETITION_PENALTY_CONTEXT); + setImageResolutionBase(DEFAULT_IMAGE_RESOLUTION); + setImageUrlBase(DEFAULT_IMAGE_URL); + setDisplayColorBase(DEFAULT_DISPLAY_COLOR) }, [ setTextTemperatureBase, setTextTopkBase, @@ -46,44 +59,58 @@ export const useModelParams = (params?:useModelParamsArgs) => { setPadMultBase, setRepetitionPenaltyBase, setRepetitionPenaltyContextBase, + setImageResolutionBase, + setImageUrlBase, + setDisplayColorBase ]); const setTextTemperature = useCallback((value: number) => { - if(value <= 1.2 || value >= 0.2) { + if (value <= 1.2 && value >= 0.2) { setTextTemperatureBase(value); } }, []); const setTextTopk = useCallback((value: number) => { - if(value <= 500 || value >= 10) { + if (value <= 500 && value >= 10) { setTextTopkBase(value); } }, []); const setAudioTemperature = useCallback((value: number) => { - if(value <= 1.2 || value >= 0.2) { + if (value <= 1.2 && value >= 0.2) { setAudioTemperatureBase(value); } }, []); const setAudioTopk = useCallback((value: number) => { - if(value <= 500 || value >= 10) { + if (value <= 500 && value >= 10) { setAudioTopkBase(value); } }, []); const setPadMult = useCallback((value: number) => { - if(value <= 4 || value >= -4) { + if (value <= 4 && value >= -4) { setPadMultBase(value); } }, []); const setRepetitionPenalty = useCallback((value: number) => { - if(value <= 2.0 || value >= 1.0) { + if (value <= 2.0 && value >= 1.0) { setRepetitionPenaltyBase(value); } }, []); const setRepetitionPenaltyContext = useCallback((value: number) => { - if(value <= 200|| value >= 0) { + if (value <= 200 && value >= 0) { setRepetitionPenaltyContextBase(value); } }, []); - + const setImageResolution = useCallback((value: number) => { + if (value <= 512 && value >= 64) { + setImageResolutionBase(value); + } + }, []); + const setImageUrl = useCallback((value: string | undefined) => { + // TODO(amelie): Maybe check whether path exists ? + setImageUrlBase(value); + }, []); + const setDisplayColor = useCallback((value: boolean) => { + setDisplayColorBase(value); + }, []); return { textTemperature, textTopk, @@ -92,6 +119,9 @@ export const useModelParams = (params?:useModelParamsArgs) => { padMult, repetitionPenalty, repetitionPenaltyContext, + imageResolution, + imageUrl, + displayColor, setTextTemperature, setTextTopk, setAudioTemperature, @@ -99,6 +129,9 @@ export const useModelParams = (params?:useModelParamsArgs) => { setPadMult, setRepetitionPenalty, setRepetitionPenaltyContext, + setImageUrl, + setImageResolution, + setDisplayColor, resetParams, } } diff --git a/client/src/pages/Conversation/hooks/useServerText.ts b/client/src/pages/Conversation/hooks/useServerText.ts index ee80459..3f43da5 100644 --- a/client/src/pages/Conversation/hooks/useServerText.ts +++ b/client/src/pages/Conversation/hooks/useServerText.ts @@ -4,6 +4,7 @@ import { decodeMessage } from "../../../protocol/encoder"; export const useServerText = () => { const [text, setText] = useState([]); + const [textColor, setTextColor] = useState([]); const [totalTextMessages, setTotalTextMessages] = useState(0); const { socket } = useSocketContext(); @@ -13,6 +14,10 @@ export const useServerText = () => { if (message.type === "text") { setText(text => [...text, message.data]); setTotalTextMessages(count => count + 1); + } else if (message.type === "coloredtext") { + setText(text => [...text, message.data]); + setTextColor(textColor => [...textColor, message.color]); + setTotalTextMessages(count => count + 1); } }, []); @@ -28,5 +33,5 @@ export const useServerText = () => { }; }, [socket]); - return { text, totalTextMessages }; + return { text, textColor, totalTextMessages }; }; diff --git a/client/src/pages/Queue/Queue.tsx b/client/src/pages/Queue/Queue.tsx index 7343955..5a52312 100644 --- a/client/src/pages/Queue/Queue.tsx +++ b/client/src/pages/Queue/Queue.tsx @@ -3,29 +3,52 @@ import { FC, useEffect, useState, useCallback, useRef, MutableRefObject } from " import eruda from "eruda"; import { useSearchParams } from "react-router-dom"; import { Conversation } from "../Conversation/Conversation"; -import { Button } from "../../components/Button/Button"; +import { Button, SwitchButton } from "../../components/Button/Button"; +import { ImageGallery } from "../../components/ImageGallery/ImageGallery"; import { useModelParams } from "../Conversation/hooks/useModelParams"; import { ModelParams } from "../Conversation/components/ModelParams/ModelParams"; import { env } from "../../env"; -export const Queue:FC = () => { +function getFloatFromStorage(val: string | null) { + return (val == null) ? undefined : parseFloat(val) +} + +function getIntFromStorage(val: string | null) { + return (val == null) ? undefined : parseInt(val) +} +function getBooleanFromStorage(val: string | null) { + return (val == "true") ? true : ((val == "false") ? false : undefined) +} + +export const Queue: FC = () => { const [searchParams] = useSearchParams(); const overrideWorkerAddr = searchParams.get("worker_addr"); const [hasMicrophoneAccess, setHasMicrophoneAccess] = useState(false); const [showMicrophoneAccessMessage, setShowMicrophoneAccessMessage] = useState(false); const [shouldConnect, setShouldConnect] = useState(false); - const modelParams = useModelParams(); + const startAsImage = getBooleanFromStorage(localStorage.getItem("isImageMode")); + const [isImageMode, setisImageMode] = useState(startAsImage == undefined ? false : startAsImage); + const modelParams = useModelParams({ + textTemperature: getFloatFromStorage(localStorage.getItem("textTemperature")), + textTopk: getIntFromStorage(localStorage.getItem("textTopk")), + audioTemperature: getFloatFromStorage(localStorage.getItem("audioTemperature")), + audioTopk: getIntFromStorage(localStorage.getItem("audioTopk")), + padMult: getFloatFromStorage(localStorage.getItem("padMult")), + repetitionPenalty: getFloatFromStorage(localStorage.getItem("repetitionPenalty")), + repetitionPenaltyContext: getIntFromStorage(localStorage.getItem("repetitionPenaltyContext")), + imageResolution: getIntFromStorage(localStorage.getItem("imageResolution")) + }); const modalRef = useRef(null); const audioContext = useRef(null); const worklet = useRef(null); // enable eruda in development useEffect(() => { - if(env.VITE_ENV === "development") { + if (env.VITE_ENV === "development") { eruda.init(); } () => { - if(env.VITE_ENV === "development") { + if (env.VITE_ENV === "development") { eruda.destroy(); } }; @@ -36,19 +59,19 @@ export const Queue:FC = () => { await window.navigator.mediaDevices.getUserMedia({ audio: true }); setHasMicrophoneAccess(true); return true; - } catch(e) { + } catch (e) { console.error(e); setShowMicrophoneAccessMessage(true); setHasMicrophoneAccess(false); } return false; -}, [setHasMicrophoneAccess, setShowMicrophoneAccessMessage]); + }, [setHasMicrophoneAccess, setShowMicrophoneAccessMessage]); const startProcessor = useCallback(async () => { - if(!audioContext.current) { + if (!audioContext.current) { audioContext.current = new AudioContext(); } - if(worklet.current) { + if (worklet.current) { return; } let ctx = audioContext.current; @@ -62,15 +85,15 @@ export const Queue:FC = () => { worklet.current.connect(ctx.destination); }, [audioContext, worklet]); - const onConnect = useCallback(async() => { - await startProcessor(); - const hasAccess = await getMicrophoneAccess(); - if(hasAccess) { - setShouldConnect(true); - } + const onConnect = useCallback(async () => { + await startProcessor(); + const hasAccess = await getMicrophoneAccess(); + if (hasAccess) { + setShouldConnect(true); + } }, [setShouldConnect, startProcessor, getMicrophoneAccess]); - if(hasMicrophoneAccess && audioContext.current && worklet.current) { + if (hasMicrophoneAccess && audioContext.current && worklet.current) { return ( { return (
-

Moshi

+

M{isImageMode ? "👁️" : "o"}shi

+ { setisImageMode(!isImageMode); modelParams.setImageUrl(undefined) }}> + {isImageMode ? "Back to Moshi" : "Go to Moshi Vision"} + {/* To add more space to the top add padding to the top of the following div by changing the pt-4 class to pt-8 or pt-12. (see: https://tailwindcss.com/docs/padding) - If you'd like to move this part to the bottom of the screen, change the class to pb-4 or pb-8 and move the following so it is contained by the last one in the page. + 👁️ If you'd like to move this part to the bottom of the screen, change the class to pb-4 or pb-8 and move the following so it is contained by the last one in the page. Font size can be changed by changing the text-sm class to text-lg or text-xl. (see : https://tailwindcss.com/docs/font-size) As for the links you can use the one below as an example and add more by copying it and changing the href and text. */}
-

Moshi is an experimental conversational AI.

-

Take everything it says with a grain of salt.

-

Conversations are limited to 5 min.

-

Moshi thinks and speaks at the same time.

-

Moshi can listen and talk at all time:
maximum flow between you and Moshi.

-

Ask it to do some Pirate role play, how to make Lasagna, - or what movie it watched last.

-

We strive to support all browsers, Chrome works best.

-

Baked with <3 @Kyutai.

+

Moshi is an experimental conversational AI.

+

Take everything it says with a grain of salt.

+

Conversations are limited to 5 min.

+

Moshi thinks and speaks at the same time.

+

Moshi can listen and talk at all time:
maximum flow between you and Moshi.

+

Ask it to do some Pirate role play, how to make Lasagna, + or what movie it watched last.

+

We strive to support all browsers, Chrome works best.

+

Baked with <3 @Kyutai.

+
+ {isImageMode ? + + : + } +
<> {showMicrophoneAccessMessage &&

Please enable your microphone before proceeding

} - - - -
- -
-
- -
-
+ + +
+ +
+
+ +
+
-
+
) }; diff --git a/client/src/protocol/encoder.ts b/client/src/protocol/encoder.ts index 14dcf4c..17f6cce 100644 --- a/client/src/protocol/encoder.ts +++ b/client/src/protocol/encoder.ts @@ -18,6 +18,8 @@ export const encodeMessage = (message: WSMessage): Uint8Array => { return new Uint8Array([0x01, ...message.data]); case "text": return new Uint8Array([0x02, ...new TextEncoder().encode(message.data)]); + case "coloredtext": + return new Uint8Array([0x02, 0x05, ...new TextEncoder().encode(message.data)]); case "control": return new Uint8Array([0x03, CONTROL_MESSAGES_MAP[message.action]]); case "metadata": @@ -53,6 +55,12 @@ export const decodeMessage = (data: Uint8Array): WSMessage => { type: "text", data: new TextDecoder().decode(payload), }; + case 0x07: + return { + type: "coloredtext", + color: payload[0], + data: new TextDecoder().decode(payload.slice(1)), + }; case 0x03: { const action = Object.keys(CONTROL_MESSAGES_MAP).find( key => CONTROL_MESSAGES_MAP[key as CONTROL_MESSAGE] === payload[0], diff --git a/client/src/protocol/types.ts b/client/src/protocol/types.ts index ece92da..f2d8475 100644 --- a/client/src/protocol/types.ts +++ b/client/src/protocol/types.ts @@ -2,6 +2,7 @@ export type MessageType = | "handshake" | "audio" | "text" + | "coloredtext" | "control" | "metadata"; @@ -19,32 +20,37 @@ export type MODEL = keyof typeof MODELS_MAP; export type WSMessage = | { - type: "handshake"; - version: VERSION; - model: MODEL; - } + type: "handshake"; + version: VERSION; + model: MODEL; + } | { - type: "audio"; - data: Uint8Array; - } + type: "audio"; + data: Uint8Array; + } | { - type: "text"; - data: string; - } + type: "text"; + data: string; + } | { - type: "control"; - action: CONTROL_MESSAGE; - } + type: "coloredtext"; + color: number; + data: string; + } + | { + type: "control"; + action: CONTROL_MESSAGE; + } | { - type: "metadata"; - data: unknown; - } + type: "metadata"; + data: unknown; + } | { type: "error"; data: string; } | { - type:"ping"; + type: "ping"; } export const CONTROL_MESSAGES_MAP = { diff --git a/client/tsconfig.json b/client/tsconfig.json index cea84c5..073977c 100644 --- a/client/tsconfig.json +++ b/client/tsconfig.json @@ -3,7 +3,11 @@ "target": "ES2020", "useDefineForClassFields": true, "module": "ESNext", - "lib": ["ES2020", "DOM", "DOM.Iterable"], + "lib": [ + "ES2020", + "DOM", + "DOM.Iterable" + ], "skipLibCheck": true, "outDir": "dist", /* Bundler mode */ @@ -13,13 +17,16 @@ "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", - /* Linting */ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, - "types": ["vite/client"] + "types": [ + "vite/client" + ] }, - "include": ["src"] -} + "include": [ + "src" + ] +} \ No newline at end of file diff --git a/client/vite.config.ts b/client/vite.config.ts index 92f55da..4768110 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -1,9 +1,9 @@ import { ProxyOptions, defineConfig, loadEnv } from "vite"; import topLevelAwait from "vite-plugin-top-level-await"; -export default defineConfig(({mode}) => { +export default defineConfig(({ mode }) => { const env = loadEnv(mode, process.cwd()); - const proxyConf:Record = env.VITE_QUEUE_API_URL ? { + const proxyConf: Record = env.VITE_QUEUE_API_URL ? { "/api": { target: env.VITE_QUEUE_API_URL, changeOrigin: true, @@ -16,7 +16,7 @@ export default defineConfig(({mode}) => { cert: "./cert.pem", key: "./key.pem", }, - proxy:{ + proxy: { ...proxyConf, } },