initial commit

This commit is contained in:
amelie 2024-12-19 14:37:56 +01:00
parent f301c163b8
commit 8ad1c6834b
38 changed files with 753 additions and 326 deletions

221
client/package-lock.json generated
View file

@ -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"
}
},

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View file

@ -1,6 +1,6 @@
import { FC } from "react";
type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement>;
type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement>;
export const Button: FC<ButtonProps> = ({ children, className, ...props }) => {
return (
<button
@ -11,3 +11,15 @@ export const Button: FC<ButtonProps> = ({ children, className, ...props }) => {
</button>
);
};
export const SwitchButton: FC<ButtonProps> = ({ children, className, ...props }) => {
return (
<button
className={`border-0 disabled:text-white-100 border-white bg-black p-2 hover:text-purple-300 active:bg-gray-700 ${className ?? ""}`}
{...props}
>
{children}
</button>
);
};

View file

@ -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<HTMLInputElement> & {
// Properties for the ImageGallery
paramsSetter: Function;
clickAction: Function;
size: number;
numImages: number;
}
type ImageItemProps = React.InputHTMLAttributes<HTMLInputElement> & {
// 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 (
<button style={style} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}
onClick={async () => { await props.paramsSetter(props.imageUrl); props.clickAction() }
} > {label}</button >
);
}
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(<ImageSelect
key={"natural_" + ordering[i]}
imageUrl={images[ordering[i]]} {...props}></ImageSelect >);
}
return (
<div>
<div className="flex justify-center items-center p-2 flex-center" style={{ marginRight: "12%", marginLeft: "12%" }}>
<span style={{ display: "flex", flex: 1 }}></span>
<Button onClick={handleShuffle} style={{ display: "flex" }}>
🔄
</Button>
</div>
<div className="imageGallery" >{steps}</div>
</div >)
;
};

View file

@ -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;
}

View file

@ -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<ConversationProps> = ({
export const Conversation: FC<ConversationProps> = ({
workerAddr,
workerAuthId,
audioContext,
@ -77,7 +82,7 @@ export const Conversation:FC<ConversationProps> = ({
sessionAuthId,
sessionId,
onConversationEnd,
isBypass=false,
isBypass = false,
email,
...params
}) => {
@ -95,7 +100,7 @@ export const Conversation:FC<ConversationProps> = ({
const audioStreamDestination = useRef<MediaStreamAudioDestinationNode>(audioContext.current.createMediaStreamDestination());
const mediaRecorder = useRef<MediaRecorder | null>(null);
const audioRecorder = useRef<MediaRecorder>(new MediaRecorder(audioStreamDestination.current.stream, { mimeType: getMimeType("audio"), audioBitsPerSecond: 128000 }));
const audioRecorder = useRef<MediaRecorder>(new MediaRecorder(audioStreamDestination.current.stream, { mimeType: getMimeType("audio"), audioBitsPerSecond: 128000 }));
const [videoURL, setVideoURL] = useState<string>("");
const [audioURL, setAudioURL] = useState<string>("");
const [isOver, setIsOver] = useState(false);
@ -136,10 +141,10 @@ export const Conversation:FC<ConversationProps> = ({
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<ConversationProps> = ({
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<ConversationProps> = ({
}, [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<ConversationProps> = ({
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<ConversationProps> = ({
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<ConversationProps> = ({
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<ConversationProps> = ({
socket,
}}
>
<MediaContext.Provider value={
{
startRecording,
stopRecording,
audioContext,
worklet,
audioStreamDestination,
micDuration,
actualAudioPlayed,
}
}>
<div>
<div className="main-grid h-screen max-h-screen w-screen p-4 max-w-96 md:max-w-screen-lg m-auto">
<div className="controls text-center flex justify-center items-center gap-2">
{isOver && !isBypass && (
<Button
onClick={() => {
// Reload the page to reset the conversation on iOS
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent)
if(onConversationEnd && !isIOS) {
onConversationEnd();
return;
}
document.location.reload();
}}
>
Start Over
</Button>
)
}
<MediaContext.Provider value={
{
(!isOver || isBypass) && (
<Button
onClick={() => {
audioContext.current.resume();
isConnected ? stop() : start();
}}
>
{!isConnected ? "Connect" : "Disconnect"}
</Button>
)
startRecording,
stopRecording,
audioContext,
worklet,
audioStreamDestination,
micDuration,
actualAudioPlayed,
}
<div className={`h-4 w-4 rounded-full ${isConnected ? 'bg-green-700': 'bg-red-700'}`} />
</div>
<div className="relative player h-full max-h-full w-full justify-between gap-3 border-2 border-white md:p-12">
}>
<div>
<div className="main-grid h-screen max-h-screen w-screen p-4 max-w-96 md:max-w-screen-lg m-auto">
<div className="controls text-center flex justify-center items-center gap-2">
{isOver && !isBypass && (
<Button
onClick={() => {
// Reload the page to reset the conversation on iOS
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent)
if (onConversationEnd && !isIOS) {
onConversationEnd();
return;
}
localStorage.setItem("textTemperature", modelParams.textTemperature.toString());
localStorage.setItem("textTopk", modelParams.textTopk.toString());
localStorage.setItem("audioTemperature", modelParams.audioTemperature.toString());
localStorage.setItem("audioTopk", modelParams.audioTopk.toString());
localStorage.setItem("padMult", modelParams.padMult.toString());
localStorage.setItem("repetitionPenalty", modelParams.repetitionPenalty.toString());
localStorage.setItem("repetitionPenaltyContext", modelParams.repetitionPenaltyContext.toString());
localStorage.setItem("imageResolution", modelParams.imageResolution.toString());
localStorage.setItem("isImageMode", (modelParams.imageUrl != undefined).toString());
document.location.reload();
}}
>
Start Over
</Button>
)
}
{
(!isOver || isBypass) && (
<Button
onClick={() => {
audioContext.current.resume();
isConnected ? stop() : start();
}}
>
{!isConnected ? "Connect" : "Disconnect"}
</Button>
)
}
<div className={`h-4 w-4 rounded-full ${isConnected ? 'bg-green-700' : 'bg-red-700'}`} />
</div>
<div className="relative player h-full max-h-full w-full justify-between gap-3 border-2 border-white md:p-12"
style={{
backgroundImage: `url(${params.imageUrl})`,
backgroundSize: '70%',
backgroundRepeat: 'no-repeat',
backgroundPosition: 'center 10%',
}} >
<ServerAudio
imageUrl={params.imageUrl}
copyCanvasRef={canvasRef}
setGetAudioStats={(callback: () => AudioStats) =>
(getAudioStats.current = callback)
}
/>
<UserAudio copyCanvasRef={canvasRef}/>
<div className="pt-8 text-sm flex justify-center items-center flex-col download-links">
<UserAudio copyCanvasRef={canvasRef} />
<div className="pt-8 text-sm flex justify-center items-center flex-col download-links"
style={{
minHeight: 80,
margin: -10,
padding: 0,
}}>
{audioURL && <div><a href={audioURL} download={`moshi audio.${getExtension("audio")}`} className="pt-2 text-center block">Download audio</a></div>}
{videoURL && <div><a href={videoURL} download={`moshi video.${getExtension("video")}`} className="pt-2 text-center">Download video</a></div>}
{videoURL && getExtension("video") === "webm" && <div><a href="https://restream.io/tools/webm-to-mp4-converter" target="_blank" rel="noreferrer" className="explain-links pt-2 text-center italic block">How to convert to mp4</a></div>}
</div>
</div>
<div className="scrollbar player-text border-2 border-white " ref={textContainerRef}>
<TextDisplay containerRef={textContainerRef} displayColor={params.displayColor} />
</div>
<div className="player-stats hidden md:block">
<ServerAudioStats getAudioStats={getAudioStats} />
</div>
</div>
<div className="scrollbar player-text border-2 border-white " ref={textContainerRef}>
<TextDisplay containerRef={textContainerRef}/>
</div>
<div className="player-stats hidden md:block">
<ServerAudioStats getAudioStats={getAudioStats} />
<div className="max-w-96 md:max-w-screen-lg p-4 m-auto text-center">
<ServerInfo />
{!workerAuthId && <ModelParams {...modelParams} isConnected={isConnected} isImageMode={params.imageUrl != undefined} />}
</div>
<canvas height={380} width={380} className="hidden" ref={canvasRef} />
<img src={canvasLogo} ref={logoRef} className="hidden" onLoad={() => {
console.log("Logo loaded");
setIsLogoLoaded(true);
}} />
</div>
<div className="max-w-96 md:max-w-screen-lg p-4 m-auto text-center">
<ServerInfo/>
{!workerAuthId && <ModelParams {...modelParams} isConnected={isConnected} /> }
</div>
<canvas height={380} width={380} className="hidden" ref={canvasRef} />
<img src={canvasLogo} ref={logoRef} className="hidden" onLoad={() => {
console.log("Logo loaded");
setIsLogoLoaded(true);
}} />
</div>
</MediaContext.Provider>
</SocketContext.Provider>
</SocketContext.Provider >
);
};

View file

@ -24,7 +24,7 @@ const COLORS = [
];
export const ClientVisualizer: FC<AudioVisualizerProps> = ({ 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<number | null>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
@ -40,11 +40,11 @@ export const ClientVisualizer: FC<AudioVisualizerProps> = ({ 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<AudioVisualizerProps> = ({ 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<AudioVisualizerProps> = ({ 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<AudioVisualizerProps> = ({ 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<AudioVisualizerProps> = ({ 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);
}

View file

@ -5,19 +5,21 @@ import { useSocketContext } from "../../SocketContext";
type AudioVisualizerProps = {
analyser: AnalyserNode | null;
parent: RefObject<HTMLElement>;
imageUrl: string | undefined;
copyCanvasRef?: RefObject<HTMLCanvasElement>;
};
const MAX_INTENSITY = 255;
export const ServerVisualizer: FC<AudioVisualizerProps> = ({ analyser, parent, copyCanvasRef }) => {
const [canvasWidth, setCanvasWidth] = useState( parent.current ? Math.min(parent.current.clientWidth, parent.current.clientHeight) : 0 );
export const ServerVisualizer: FC<AudioVisualizerProps> = ({ analyser, parent, imageUrl, copyCanvasRef }) => {
const [canvasWidth, setCanvasWidth] = useState(parent.current ? Math.min(parent.current.clientWidth, parent.current.clientHeight) : 0);
const requestRef = useRef<number | null>(null);
const canvasRef = useRef<HTMLCanvasElement>(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<AudioVisualizerProps> = ({ 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<AudioVisualizerProps> = ({ 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<AudioVisualizerProps> = ({ 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<AudioVisualizerProps> = ({ 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]);

View file

@ -4,9 +4,10 @@ import { Button } from "../../../../components/Button/Button";
type ModelParamsProps = {
isConnected: boolean;
isImageMode: boolean;
modal?: RefObject<HTMLDialogElement>,
} & ReturnType<typeof useModelParams>;
export const ModelParams:FC<ModelParamsProps> = ({
} & ReturnType<typeof useModelParams>;
export const ModelParams: FC<ModelParamsProps> = ({
textTemperature,
textTopk,
audioTemperature,
@ -14,6 +15,7 @@ export const ModelParams:FC<ModelParamsProps> = ({
padMult,
repetitionPenalty,
repetitionPenaltyContext,
imageResolution,
setTextTemperature,
setTextTopk,
setAudioTemperature,
@ -21,55 +23,64 @@ export const ModelParams:FC<ModelParamsProps> = ({
setPadMult,
setRepetitionPenalty,
setRepetitionPenaltyContext,
setImageResolution,
resetParams,
isConnected,
isImageMode,
modal,
}) => {
return (
<div className=" p-2 mt-6 self-center flex flex-col text-white items-center text-center">
<table>
<tbody>
<table>
<tbody>
<tr>
<td>Text temperature:</td>
<td className="w-12 text-center">{textTemperature}</td>
<td className="p-2"><input className="range align-middle" disabled={isConnected} type="range" id="text-temperature" name="text-temperature" step="0.01" min="0.2" max="1.2" value={textTemperature} onChange={e => setTextTemperature(parseFloat(e.target.value))} /></td>
</tr>
<tr>
<td>Text topk:</td>
<td className="w-12 text-center">{textTopk}</td>
<td className="p-2"><input className="range align-middle" disabled={isConnected} type="range" id="text-topk" name="text-topk" step="1" min="10" max="500" value={textTopk} onChange={e => setTextTopk(parseInt(e.target.value))} /></td>
</tr>
<tr>
<td>Audio temperature:</td>
<td className="w-12 text-center">{audioTemperature}</td>
<td className="p-2"><input className="range align-middle" disabled={isConnected} type="range" id="audio-temperature" name="audio-temperature" step="0.01" min="0.2" max="1.2" value={audioTemperature} onChange={e => setAudioTemperature(parseFloat(e.target.value))} /></td>
</tr>
<tr>
<td>Audio topk:</td>
<td className="w-12 text-center">{audioTopk}</td>
<td className="p-2"><input className="range align-middle" disabled={isConnected} type="range" id="audio-topk" name="audio-topk" step="1" min="10" max="500" value={audioTopk} onChange={e => setAudioTopk(parseInt(e.target.value))} /></td>
</tr>
<tr>
<td>Padding multiplier:</td>
<td className="w-12 text-center">{padMult}</td>
<td className="p-2"><input className="range align-middle" disabled={isConnected} type="range" id="audio-pad-mult" name="audio-pad-mult" step="0.05" min="-4" max="4" value={padMult} onChange={e => setPadMult(parseFloat(e.target.value))} /></td>
</tr>
<tr>
<td>Repeat penalty:</td>
<td className="w-12 text-center">{repetitionPenalty}</td>
<td className="p-2"><input className="range align-middle" disabled={isConnected} type="range" id="repetition-penalty" name="repetition-penalty" step="0.01" min="1" max="2" value={repetitionPenalty} onChange={e => setRepetitionPenalty(parseFloat(e.target.value))} /></td>
</tr>
<tr>
<td>Repeat penalty last N:</td>
<td className="w-12 text-center">{repetitionPenaltyContext}</td>
<td className="p-2"><input className="range align-middle" disabled={isConnected} type="range" id="repetition-penalty-context" name="repetition-penalty-context" step="1" min="0" max="200" value={repetitionPenaltyContext} onChange={e => setRepetitionPenaltyContext(parseFloat(e.target.value))} /></td>
</tr>
{isImageMode &&
<tr>
<td>Text temperature:</td>
<td className="w-12 text-center">{textTemperature}</td>
<td className="p-2"><input className="range align-middle" disabled={isConnected} type="range" id="text-temperature" name="text-temperature" step="0.01" min="0.2" max="1.2" value={textTemperature} onChange={e => setTextTemperature(parseFloat(e.target.value))} /></td>
<td>Image max-side (px):</td>
<td className="w-12 text-center">{imageResolution}</td>
<td className="p-2"><input className="range align-middle" disabled={isConnected} type="range" id="image-resolution" name="image-resolution" step="16" min="64" max="512" value={imageResolution} onChange={e => setImageResolution(parseFloat(e.target.value))} /></td>
</tr>
<tr>
<td>Text topk:</td>
<td className="w-12 text-center">{textTopk}</td>
<td className="p-2"><input className="range align-middle" disabled={isConnected} type="range" id="text-topk" name="text-topk" step="1" min="10" max="500" value={textTopk} onChange={e => setTextTopk(parseInt(e.target.value))} /></td>
</tr>
<tr>
<td>Audio temperature:</td>
<td className="w-12 text-center">{audioTemperature}</td>
<td className="p-2"><input className="range align-middle" disabled={isConnected} type="range" id="audio-temperature" name="audio-temperature" step="0.01" min="0.2" max="1.2" value={audioTemperature} onChange={e => setAudioTemperature(parseFloat(e.target.value))} /></td>
</tr>
<tr>
<td>Audio topk:</td>
<td className="w-12 text-center">{audioTopk}</td>
<td className="p-2"><input className="range align-middle" disabled={isConnected} type="range" id="audio-topk" name="audio-topk" step="1" min="10" max="500" value={audioTopk} onChange={e => setAudioTopk(parseInt(e.target.value))} /></td>
</tr>
<tr>
<td>Padding multiplier:</td>
<td className="w-12 text-center">{padMult}</td>
<td className="p-2"><input className="range align-middle" disabled={isConnected} type="range" id="audio-pad-mult" name="audio-pad-mult" step="0.05" min="-4" max="4" value={padMult} onChange={e => setPadMult(parseFloat(e.target.value))} /></td>
</tr>
<tr>
<td>Repeat penalty:</td>
<td className="w-12 text-center">{repetitionPenalty}</td>
<td className="p-2"><input className="range align-middle" disabled={isConnected} type="range" id="repetition-penalty" name="repetition-penalty" step="0.01" min="1" max="2" value={repetitionPenalty} onChange={e => setRepetitionPenalty(parseFloat(e.target.value))} /></td>
</tr>
<tr>
<td>Repeat penalty last N:</td>
<td className="w-12 text-center">{repetitionPenaltyContext}</td>
<td className="p-2"><input className="range align-middle" disabled={isConnected} type="range" id="repetition-penalty-context" name="repetition-penalty-context" step="1" min="0" max="200" value={repetitionPenaltyContext} onChange={e => setRepetitionPenaltyContext(parseFloat(e.target.value))} /></td>
</tr>
</tbody>
</table>
<div>
<Button onClick={resetParams} className="m-2">Reset</Button>
<Button onClick={() => modal?.current?.close()} className="m-2">Validate</Button>
</div>
</div>
}
</tbody>
</table>
<div>
{!isConnected && <Button onClick={resetParams} className="m-2">Reset</Button>}
{!isConnected && <Button onClick={() => modal?.current?.close()} className="m-2">Validate</Button>}
</div>
</div >
)
};

View file

@ -4,9 +4,10 @@ import { ServerVisualizer } from "../AudioVisualizer/ServerVisualizer";
type ServerAudioProps = {
setGetAudioStats: (getAudioStats: () => AudioStats) => void;
imageUrl: string | undefined;
copyCanvasRef?: React.RefObject<HTMLCanvasElement>;
};
export const ServerAudio: FC<ServerAudioProps> = ({ setGetAudioStats,copyCanvasRef }) => {
export const ServerAudio: FC<ServerAudioProps> = ({ setGetAudioStats, imageUrl, copyCanvasRef }) => {
const { analyser, hasCriticalDelay, setHasCriticalDelay } = useServerAudio({
setGetAudioStats,
});
@ -27,7 +28,7 @@ export const ServerAudio: FC<ServerAudioProps> = ({ setGetAudioStats,copyCanvasR
</div>
)}
<div className="server-audio h-4/6 aspect-square" ref={containerRef}>
<ServerVisualizer analyser={analyser.current} parent={containerRef} copyCanvasRef={copyCanvasRef}/>
<ServerVisualizer analyser={analyser.current} parent={containerRef} imageUrl={imageUrl} copyCanvasRef={copyCanvasRef} />
</div>
</>
);

View file

@ -3,12 +3,28 @@ import { useServerText } from "../../hooks/useServerText";
type TextDisplayProps = {
containerRef: React.RefObject<HTMLDivElement>;
displayColor: boolean | undefined;
};
export const TextDisplay:FC<TextDisplayProps> = ({
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<TextDisplayProps> = ({
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<TextDisplayProps> = ({
});
}
}, [text]);
return (
<div className="h-full w-full max-w-full max-h-full p-2 text-white">
if (displayColor && (textColor.length == text.length)) {
return (
<div className="h-full w-full max-w-full max-h-full p-2 text-white">
{text.map((t, i) => (
<span
key={i}
className={`${i === currentIndex ? "font-bold" : "font-normal"}`}
style={{
color: `${textDisplayColors[clamp_color(textColor[i])]}`
}}
>
{t}
</span>
))
}
</div >
);
}
else {
return (
<div className="h-full w-full max-w-full max-h-full p-2 text-white">
{text.map((t, i) => (
<span
key={i}
@ -32,6 +66,7 @@ export const TextDisplay:FC<TextDisplayProps> = ({
{t}
</span>
))}
</div>
);
</div>
);
};
};

View file

@ -6,7 +6,7 @@ import { ClientVisualizer } from "../AudioVisualizer/ClientVisualizer";
type UserAudioProps = {
copyCanvasRef: React.RefObject<HTMLCanvasElement>;
};
export const UserAudio: FC<UserAudioProps> = ({copyCanvasRef}) => {
export const UserAudio: FC<UserAudioProps> = ({ copyCanvasRef }) => {
const [analyser, setAnalyser] = useState<AnalyserNode | null>(null);
const { sendMessage, isConnected } = useSocketContext();
const containerRef = useRef<HTMLDivElement>(null);
@ -65,7 +65,7 @@ export const UserAudio: FC<UserAudioProps> = ({copyCanvasRef}) => {
return (
<div className="user-audio h-5/6 aspect-square" ref={containerRef}>
<ClientVisualizer analyser={analyser} parent={containerRef} copyCanvasRef={copyCanvasRef}/>
<ClientVisualizer analyser={analyser} parent={containerRef} copyCanvasRef={copyCanvasRef} />
</div>
);
};

View file

@ -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<ModelParamsValues>;
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<boolean>(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,
}
}

View file

@ -4,6 +4,7 @@ import { decodeMessage } from "../../../protocol/encoder";
export const useServerText = () => {
const [text, setText] = useState<string[]>([]);
const [textColor, setTextColor] = useState<number[]>([]);
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 };
};

View file

@ -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<boolean>(false);
const [showMicrophoneAccessMessage, setShowMicrophoneAccessMessage] = useState<boolean>(false);
const [shouldConnect, setShouldConnect] = useState<boolean>(false);
const modelParams = useModelParams();
const startAsImage = getBooleanFromStorage(localStorage.getItem("isImageMode"));
const [isImageMode, setisImageMode] = useState<boolean>(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<HTMLDialogElement>(null);
const audioContext = useRef<AudioContext | null>(null);
const worklet = useRef<AudioWorkletNode | null>(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 (
<Conversation
workerAddr={overrideWorkerAddr ?? ""}
@ -84,49 +107,57 @@ export const Queue:FC = () => {
return (
<div className="text-white text-center h-screen w-screen p-4 flex flex-col items-center ">
<div>
<h1 className="text-4xl">Moshi</h1>
<h1 className="text-4xl" style={{ letterSpacing: isImageMode ? "2px" : "5px" }}>M{isImageMode ? "👁️" : "o"}shi</h1>
<SwitchButton onClick={() => { setisImageMode(!isImageMode); modelParams.setImageUrl(undefined) }}>
{isImageMode ? "Back to Moshi" : "Go to Moshi Vision"}
</SwitchButton>
{/*
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.
*/}
<div className="pt-8 text-sm flex justify-center items-center flex-col ">
<div className="presentation text-left">
<p><span className='cute-words'>Moshi</span> is an experimental conversational AI. </p>
<p>Take everything it says with a grain of <span className='cute-words'>salt</span>.</p>
<p>Conversations are limited to <span className='cute-words'>5 min</span>.</p>
<p>Moshi <span className='cute-words'>thinks</span> and <span className='cute-words'>speaks</span> at the same time.</p>
<p>Moshi can <span className='cute-words'>listen</span> and <span className='cute-words'>talk</span> at all time: <br/>maximum flow between you and <span className='cute-words'>Moshi</span>.</p>
<p>Ask it to do some <span className='cute-words'>Pirate</span> role play, how to make <span className='cute-words'>Lasagna</span>,
or what <span className='cute-words'>movie</span> it watched last.</p>
<p>We strive to support all browsers, Chrome works best.</p>
<p>Baked with &lt;3 @<a href="https://kyutai.org/" className='cute-words underline'>Kyutai</a>.</p>
<p><span className='cute-words'>Moshi</span> is an experimental conversational AI. </p>
<p>Take everything it says with a grain of <span className='cute-words'>salt</span>.</p>
<p>Conversations are limited to <span className='cute-words'>5 min</span>.</p>
<p>Moshi <span className='cute-words'>thinks</span> and <span className='cute-words'>speaks</span> at the same time.</p>
<p>Moshi can <span className='cute-words'>listen</span> and <span className='cute-words'>talk</span> at all time: <br />maximum flow between you and <span className='cute-words'>Moshi</span>.</p>
<p>Ask it to do some <span className='cute-words'>Pirate</span> role play, how to make <span className='cute-words'>Lasagna</span>,
or what <span className='cute-words'>movie</span> it watched last.</p>
<p>We strive to support all browsers, Chrome works best.</p>
<p>Baked with &lt;3 @<a href="https://kyutai.org/" className='cute-words underline'>Kyutai</a>.</p>
</div>
</div>
</div>
<div className="flex flex-grow justify-center items-center flex-col presentation">
{isImageMode ?
<ImageGallery numImages={9} size={110} paramsSetter={modelParams.setImageUrl} clickAction={onConnect}></ImageGallery>
:
<Button onClick={async () => await onConnect()}>Connect</Button>}
</div>
<div className="flex flex-grow justify-center items-center flex-col">
<>
{showMicrophoneAccessMessage &&
<p className="text-center">Please enable your microphone before proceeding</p>
}
<Button onClick={async () => await onConnect()}>Connect</Button>
<Button className="absolute top-4 right-4" onClick={()=> modalRef.current?.showModal()}>Settings</Button>
<dialog ref={modalRef} className="modal">
<div className="modal-box border-2 border-white rounded-none flex justify-center bg-black">
<ModelParams {...modelParams} isConnected={shouldConnect} modal={modalRef}/>
</div>
<form method="dialog" className="modal-backdrop">
<button>Close</button>
</form>
</dialog>
<Button className="absolute top-4 right-4" onClick={() => modalRef.current?.showModal()}>Settings</Button>
<dialog ref={modalRef} className="modal">
<div className="modal-box border-2 border-white rounded-none flex justify-center bg-black">
<ModelParams {...modelParams} isConnected={shouldConnect} isImageMode={isImageMode} modal={modalRef} />
</div>
<form method="dialog" className="modal-backdrop">
<button>Close</button>
</form>
</dialog>
</>
</div>
<div className="text-center flex justify-end items-center flex-col">
<a target="_blank" href="https://kyutai.org/moshi-terms.pdf" className="text-center">Terms of Use</a>
<a target="_blank" href="https://kyutai.org/moshi-privacy.pdf" className="text-center">Privacy Policy</a>
</div>
</div>
</div >
)
};

View file

@ -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],

View file

@ -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: "metadata";
data: unknown;
}
type: "control";
action: CONTROL_MESSAGE;
}
| {
type: "metadata";
data: unknown;
}
| {
type: "error";
data: string;
}
| {
type:"ping";
type: "ping";
}
export const CONTROL_MESSAGES_MAP = {

View file

@ -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"
]
}

View file

@ -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<string, string | ProxyOptions> = env.VITE_QUEUE_API_URL ? {
const proxyConf: Record<string, string | ProxyOptions> = 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,
}
},