feat: refactor app structure and components for improved organization and functionality

- Moved components and hooks into a new directory structure under `src/app` for better organization.
- Updated the main entry point to use `index.ts` and adjusted routing accordingly.
- Introduced new components such as `ActiveProcesses`, `AgentStreamView`, and `ConnectionStatus` to enhance the app's functionality.
- Removed deprecated components and hooks to streamline the codebase.
- Updated styles to align with the new theme structure and ensure consistency across the app.

This refactor aims to improve maintainability and scalability of the application.
This commit is contained in:
Mohamed Boudra
2025-10-22 15:48:53 +02:00
parent 2d1ab5bf79
commit 89641a18e7
31 changed files with 8915 additions and 522 deletions
+228 -84
View File
@@ -6067,6 +6067,7 @@
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.81.4.tgz",
"integrity": "sha512-AMcDadefBIjD10BRqkWw+W/VdvXEomR6aEZ0fhQRAv7igrBzb4PTn4vHKYg+sUK0e3wa74kcMy2DLc/HtnGcMA==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 20.19.4"
}
@@ -6212,6 +6213,7 @@
"resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.81.4.tgz",
"integrity": "sha512-8mpnvfcLcnVh+t1ok6V9eozWo8Ut+TZhz8ylJ6gF9d6q9EGDQX6s8jenan5Yv/pzN4vQEKI4ib2pTf/FELw+SA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@react-native/dev-middleware": "0.81.4",
"debug": "^4.4.0",
@@ -6242,6 +6244,7 @@
"resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.81.4.tgz",
"integrity": "sha512-SU05w1wD0nKdQFcuNC9D6De0ITnINCi8MEnx9RsTD2e4wN83ukoC7FpXaPCYyP6+VjFt5tUKDPgP1O7iaNXCqg==",
"license": "BSD-3-Clause",
"peer": true,
"engines": {
"node": ">= 20.19.4"
}
@@ -6251,6 +6254,7 @@
"resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.81.4.tgz",
"integrity": "sha512-hu1Wu5R28FT7nHXs2wWXvQ++7W7zq5GPY83llajgPlYKznyPLAY/7bArc5rAzNB7b0kwnlaoPQKlvD/VP9LZug==",
"license": "MIT",
"peer": true,
"dependencies": {
"@isaacs/ttlcache": "^1.4.1",
"@react-native/debugger-frontend": "0.81.4",
@@ -6273,6 +6277,7 @@
"resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz",
"integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==",
"license": "MIT",
"peer": true,
"dependencies": {
"is-docker": "^2.0.0",
"is-wsl": "^2.1.1"
@@ -6289,6 +6294,7 @@
"resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz",
"integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==",
"license": "MIT",
"peer": true,
"dependencies": {
"async-limiter": "~1.0.0"
}
@@ -6354,6 +6360,7 @@
"resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.81.4.tgz",
"integrity": "sha512-T7fPcQvDDCSusZFVSg6H1oVDKb/NnVYLnsqkcHsAF2C2KGXyo3J7slH/tJAwNfj/7EOA2OgcWxfC1frgn9TQvw==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 20.19.4"
}
@@ -6363,6 +6370,7 @@
"resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.81.4.tgz",
"integrity": "sha512-sr42FaypKXJHMVHhgSbu2f/ZJfrLzgaoQ+HdpRvKEiEh2mhFf6XzZwecyLBvWqf2pMPZa+CpPfNPiejXjKEy8w==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 20.19.4"
}
@@ -6378,6 +6386,7 @@
"resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.81.4.tgz",
"integrity": "sha512-hBM+rMyL6Wm1Q4f/WpqGsaCojKSNUBqAXLABNGoWm1vabZ7cSnARMxBvA/2vo3hLcoR4v7zDK8tkKm9+O0LjVA==",
"license": "MIT",
"peer": true,
"dependencies": {
"invariant": "^2.2.4",
"nullthrows": "^1.1.1"
@@ -6880,18 +6889,6 @@
"@sinonjs/commons": "^3.0.0"
}
},
"node_modules/@siteed/expo-audio-studio": {
"version": "2.18.1",
"resolved": "https://registry.npmjs.org/@siteed/expo-audio-studio/-/expo-audio-studio-2.18.1.tgz",
"integrity": "sha512-gyi02iaj/ZEpMOnFM7jlgXWhy4I6pvu7dK7f95nQgFkuSDogmM/pA/n+lS31ui5HzDbVRle426EpQ1hShVmqDA==",
"license": "MIT",
"peerDependencies": {
"expo": "*",
"expo-modules-core": "~2.4.0",
"react": "*",
"react-native": "*"
}
},
"node_modules/@speechmatics/expo-two-way-audio": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/@speechmatics/expo-two-way-audio/-/expo-two-way-audio-0.1.2.tgz",
@@ -12220,19 +12217,6 @@
"node": ">=8.0"
}
},
"node_modules/expo-status-bar": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/expo-status-bar/-/expo-status-bar-3.0.8.tgz",
"integrity": "sha512-L248XKPhum7tvREoS1VfE0H6dPCaGtoUWzRsUv7hGKdiB4cus33Rc0sxkWkoQ77wE8stlnUlL5lvmT0oqZ3ZBw==",
"license": "MIT",
"dependencies": {
"react-native-is-edge-to-edge": "^1.2.1"
},
"peerDependencies": {
"react": "*",
"react-native": "*"
}
},
"node_modules/expo-structured-headers": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/expo-structured-headers/-/expo-structured-headers-5.0.0.tgz",
@@ -17430,6 +17414,7 @@
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.81.4.tgz",
"integrity": "sha512-bt5bz3A/+Cv46KcjV0VQa+fo7MKxs17RCcpzjftINlen4ZDUl0I6Ut+brQ2FToa5oD0IB0xvQHfmsg2EDqsZdQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@jest/create-cache-key-function": "^29.7.0",
"@react-native/assets-registry": "0.81.4",
@@ -17519,7 +17504,6 @@
"resolved": "https://registry.npmjs.org/react-native-edge-to-edge/-/react-native-edge-to-edge-1.7.0.tgz",
"integrity": "sha512-ERegbsq28yoMndn/Uq49i4h6aAhMvTEjOfkFh50yX9H/dMjjCr/Tix/es/9JcPRvC+q7VzCMWfxWDUb6Jrq1OQ==",
"license": "MIT",
"peer": true,
"peerDependencies": {
"react": "*",
"react-native": "*"
@@ -17576,12 +17560,11 @@
}
},
"node_modules/react-native-nitro-modules": {
"version": "0.31.1",
"resolved": "https://registry.npmjs.org/react-native-nitro-modules/-/react-native-nitro-modules-0.31.1.tgz",
"integrity": "sha512-ecr3bTWvLj5/Ce3BO54XRHy4uNHfFR2iVFLf9g/zDRkaD4iOg6kF9hatkIXwUPZi6apIf9P91BS+bco0lSIcVA==",
"version": "0.30.0",
"resolved": "https://registry.npmjs.org/react-native-nitro-modules/-/react-native-nitro-modules-0.30.0.tgz",
"integrity": "sha512-DHFSEbM7cwVHmpbGvmd+b5iqy67My0In9oBc8eSMvjTkLWrDkkiE5jLdt6ZJyDF7Fn3C5Tr6Lq2bUa596VSW2g==",
"hasInstallScript": true,
"license": "MIT",
"peer": true,
"peerDependencies": {
"react": "*",
"react-native": "*"
@@ -17730,55 +17713,6 @@
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
"license": "MIT"
},
"node_modules/react-native-webrtc": {
"version": "124.0.7",
"resolved": "https://registry.npmjs.org/react-native-webrtc/-/react-native-webrtc-124.0.7.tgz",
"integrity": "sha512-gnXPdbUS8IkKHq9WNaWptW/yy5s6nMyI6cNn90LXdobPVCgYSk6NA2uUGdT4c4J14BRgaFA95F+cR28tUPkMVA==",
"license": "MIT",
"dependencies": {
"base64-js": "1.5.1",
"debug": "4.3.4",
"event-target-shim": "6.0.2"
},
"peerDependencies": {
"react-native": ">=0.60.0"
}
},
"node_modules/react-native-webrtc/node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"license": "MIT",
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/react-native-webrtc/node_modules/event-target-shim": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-6.0.2.tgz",
"integrity": "sha512-8q3LsZjRezbFZ2PN+uP+Q7pnHUMmAOziU2vA2OwoFaKIXxlxl38IylhSSgUorWu/rf4er67w0ikBqjBFk/pomA==",
"license": "MIT",
"engines": {
"node": ">=10.13.0"
},
"funding": {
"url": "https://github.com/sponsors/mysticatea"
}
},
"node_modules/react-native-webrtc/node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"license": "MIT"
},
"node_modules/react-native-worklets": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.5.1.tgz",
@@ -17820,6 +17754,7 @@
"resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.81.4.tgz",
"integrity": "sha512-LWTGUTzFu+qOQnvkzBP52B90Ym3stZT8IFCzzUrppz8Iwglg83FCtDZAR4yLHI29VY/x/+pkcWAMCl3739XHdw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/core": "^7.25.2",
"@babel/parser": "^7.25.3",
@@ -17840,13 +17775,15 @@
"version": "0.81.4",
"resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.81.4.tgz",
"integrity": "sha512-9nRRHO1H+tcFqjb9gAM105Urtgcanbta2tuqCVY0NATHeFPDEAB7gPyiLxCHKMi1NbhP6TH0kxgSWXKZl1cyRg==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/react-native/node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"license": "MIT",
"peer": true,
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -17857,6 +17794,7 @@
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
"integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
}
@@ -17867,6 +17805,7 @@
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"deprecated": "Glob versions prior to v9 are no longer supported",
"license": "ISC",
"peer": true,
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
@@ -17887,6 +17826,7 @@
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"license": "ISC",
"peer": true,
"dependencies": {
"brace-expansion": "^1.1.7"
},
@@ -17899,6 +17839,7 @@
"resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz",
"integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==",
"license": "MIT",
"peer": true,
"dependencies": {
"async-limiter": "~1.0.0"
}
@@ -21175,7 +21116,6 @@
"@react-navigation/bottom-tabs": "^7.4.0",
"@react-navigation/elements": "^2.6.3",
"@react-navigation/native": "^7.1.8",
"@siteed/expo-audio-studio": "^2.18.1",
"@speechmatics/expo-two-way-audio": "^0.1.2",
"buffer": "^6.0.3",
"expo": "^54.0.18",
@@ -21192,7 +21132,6 @@
"expo-linking": "~8.0.8",
"expo-router": "~6.0.13",
"expo-splash-screen": "~31.0.10",
"expo-status-bar": "~3.0.8",
"expo-symbols": "~1.0.7",
"expo-system-ui": "~6.0.7",
"expo-updates": "~29.0.12",
@@ -21200,10 +21139,12 @@
"lucide-react-native": "^0.546.0",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-native": "0.81.4",
"react-native": "^0.81.5",
"react-native-css": "^3.0.1",
"react-native-edge-to-edge": "^1.7.0",
"react-native-gesture-handler": "~2.28.0",
"react-native-markdown-display": "^7.0.2",
"react-native-nitro-modules": "^0.30.0",
"react-native-permissions": "^5.4.2",
"react-native-reanimated": "~4.1.1",
"react-native-safe-area-context": "~5.6.0",
@@ -21211,7 +21152,6 @@
"react-native-svg": "^15.14.0",
"react-native-unistyles": "^3.0.15",
"react-native-web": "~0.21.0",
"react-native-webrtc": "^124.0.7",
"react-native-worklets": "0.5.1"
},
"devDependencies": {
@@ -21222,6 +21162,79 @@
"typescript": "~5.9.2"
}
},
"packages/app/node_modules/@react-native/assets-registry": {
"version": "0.81.5",
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.81.5.tgz",
"integrity": "sha512-705B6x/5Kxm1RKRvSv0ADYWm5JOnoiQ1ufW7h8uu2E6G9Of/eE6hP/Ivw3U5jI16ERqZxiKQwk34VJbB0niX9w==",
"license": "MIT",
"engines": {
"node": ">= 20.19.4"
}
},
"packages/app/node_modules/@react-native/community-cli-plugin": {
"version": "0.81.5",
"resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.81.5.tgz",
"integrity": "sha512-yWRlmEOtcyvSZ4+OvqPabt+NS36vg0K/WADTQLhrYrm9qdZSuXmq8PmdJWz/68wAqKQ+4KTILiq2kjRQwnyhQw==",
"license": "MIT",
"dependencies": {
"@react-native/dev-middleware": "0.81.5",
"debug": "^4.4.0",
"invariant": "^2.2.4",
"metro": "^0.83.1",
"metro-config": "^0.83.1",
"metro-core": "^0.83.1",
"semver": "^7.1.3"
},
"engines": {
"node": ">= 20.19.4"
},
"peerDependencies": {
"@react-native-community/cli": "*",
"@react-native/metro-config": "*"
},
"peerDependenciesMeta": {
"@react-native-community/cli": {
"optional": true
},
"@react-native/metro-config": {
"optional": true
}
}
},
"packages/app/node_modules/@react-native/gradle-plugin": {
"version": "0.81.5",
"resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.81.5.tgz",
"integrity": "sha512-hORRlNBj+ReNMLo9jme3yQ6JQf4GZpVEBLxmTXGGlIL78MAezDZr5/uq9dwElSbcGmLEgeiax6e174Fie6qPLg==",
"license": "MIT",
"engines": {
"node": ">= 20.19.4"
}
},
"packages/app/node_modules/@react-native/js-polyfills": {
"version": "0.81.5",
"resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.81.5.tgz",
"integrity": "sha512-fB7M1CMOCIUudTRuj7kzxIBTVw2KXnsgbQ6+4cbqSxo8NmRRhA0Ul4ZUzZj3rFd3VznTL4Brmocv1oiN0bWZ8w==",
"license": "MIT",
"engines": {
"node": ">= 20.19.4"
}
},
"packages/app/node_modules/@react-native/normalize-colors": {
"version": "0.81.5",
"resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.81.5.tgz",
"integrity": "sha512-0HuJ8YtqlTVRXGZuGeBejLE04wSQsibpTI+RGOyVqxZvgtlLLC/Ssw0UmbHhT4lYMp2fhdtvKZSs5emWB1zR/g==",
"license": "MIT"
},
"packages/app/node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"packages/app/node_modules/buffer": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
@@ -21246,6 +21259,137 @@
"ieee754": "^1.2.1"
}
},
"packages/app/node_modules/commander": {
"version": "12.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
"integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"packages/app/node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"deprecated": "Glob versions prior to v9 are no longer supported",
"license": "ISC",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"packages/app/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"packages/app/node_modules/react-native": {
"version": "0.81.5",
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.81.5.tgz",
"integrity": "sha512-1w+/oSjEXZjMqsIvmkCRsOc8UBYv163bTWKTI8+1mxztvQPhCRYGTvZ/PL1w16xXHneIj/SLGfxWg2GWN2uexw==",
"license": "MIT",
"dependencies": {
"@jest/create-cache-key-function": "^29.7.0",
"@react-native/assets-registry": "0.81.5",
"@react-native/codegen": "0.81.5",
"@react-native/community-cli-plugin": "0.81.5",
"@react-native/gradle-plugin": "0.81.5",
"@react-native/js-polyfills": "0.81.5",
"@react-native/normalize-colors": "0.81.5",
"@react-native/virtualized-lists": "0.81.5",
"abort-controller": "^3.0.0",
"anser": "^1.4.9",
"ansi-regex": "^5.0.0",
"babel-jest": "^29.7.0",
"babel-plugin-syntax-hermes-parser": "0.29.1",
"base64-js": "^1.5.1",
"commander": "^12.0.0",
"flow-enums-runtime": "^0.0.6",
"glob": "^7.1.1",
"invariant": "^2.2.4",
"jest-environment-node": "^29.7.0",
"memoize-one": "^5.0.0",
"metro-runtime": "^0.83.1",
"metro-source-map": "^0.83.1",
"nullthrows": "^1.1.1",
"pretty-format": "^29.7.0",
"promise": "^8.3.0",
"react-devtools-core": "^6.1.5",
"react-refresh": "^0.14.0",
"regenerator-runtime": "^0.13.2",
"scheduler": "0.26.0",
"semver": "^7.1.3",
"stacktrace-parser": "^0.1.10",
"whatwg-fetch": "^3.0.0",
"ws": "^6.2.3",
"yargs": "^17.6.2"
},
"bin": {
"react-native": "cli.js"
},
"engines": {
"node": ">= 20.19.4"
},
"peerDependencies": {
"@types/react": "^19.1.0",
"react": "^19.1.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"packages/app/node_modules/react-native/node_modules/@react-native/virtualized-lists": {
"version": "0.81.5",
"resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.81.5.tgz",
"integrity": "sha512-UVXgV/db25OPIvwZySeToXD/9sKKhOdkcWmmf4Jh8iBZuyfML+/5CasaZ1E7Lqg6g3uqVQq75NqIwkYmORJMPw==",
"license": "MIT",
"dependencies": {
"invariant": "^2.2.4",
"nullthrows": "^1.1.1"
},
"engines": {
"node": ">= 20.19.4"
},
"peerDependencies": {
"@types/react": "^19.1.0",
"react": "*",
"react-native": "*"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"packages/app/node_modules/ws": {
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz",
"integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==",
"license": "MIT",
"dependencies": {
"async-limiter": "~1.0.0"
}
},
"packages/server": {
"name": "@voice-dev/server",
"version": "0.1.0",
+1 -7
View File
@@ -57,12 +57,6 @@
}
],
"expo-audio",
[
"@siteed/expo-audio-studio",
{
"enableBackgroundAudio": true
}
],
[
"expo-build-properties",
{
@@ -75,7 +69,7 @@
],
"experiments": {
"typedRoutes": true,
"reactCompiler": false
"reactCompiler": true
},
"extra": {
"router": {},
+8 -1
View File
@@ -2,6 +2,13 @@ module.exports = function (api) {
api.cache(true);
return {
presets: ["babel-preset-expo"],
plugins: [["react-native-unistyles/plugin", { root: "app" }]],
plugins: [
[
"react-native-unistyles/plugin",
{
root: "src",
},
],
],
};
};
-385
View File
@@ -1,385 +0,0 @@
import { useState, useEffect, useRef } from 'react';
import { Platform } from 'react-native';
import { useAudioRecorder } from '@siteed/expo-audio-studio';
import { check, request, PERMISSIONS, RESULTS } from 'react-native-permissions';
export interface RealtimeAudioConfig {
onAudioSegment?: (audioData: string) => void;
onSpeechStart?: () => void;
onSpeechEnd?: () => void;
onError?: (error: Error) => void;
volumeThreshold?: number; // RMS threshold for speech detection (0-1)
silenceDuration?: number; // ms of silence before ending segment
speechStartDuration?: number; // ms of sustained volume before starting segment
}
export interface RealtimeAudio {
start: () => Promise<void>;
stop: () => Promise<void>;
isActive: boolean;
isSpeaking: boolean;
}
/**
* Convert raw PCM data to WAV format by adding WAV headers
* @param pcmBase64 Base64-encoded PCM data
* @param sampleRate Sample rate (default: 16000)
* @param channels Number of channels (default: 1 for mono)
* @param bitsPerSample Bits per sample (default: 16)
* @returns Base64-encoded WAV data
*/
function pcmToWav(
pcmBase64: string,
sampleRate: number = 16000,
channels: number = 1,
bitsPerSample: number = 16
): string {
// Decode base64 PCM data
const pcmBinary = atob(pcmBase64);
const pcmLength = pcmBinary.length;
// Calculate WAV parameters
const byteRate = sampleRate * channels * (bitsPerSample / 8);
const blockAlign = channels * (bitsPerSample / 8);
const dataSize = pcmLength;
const fileSize = 44 + dataSize; // WAV header is 44 bytes
// Create WAV buffer
const wavBuffer = new Uint8Array(fileSize);
const view = new DataView(wavBuffer.buffer);
// Write WAV header
let offset = 0;
// RIFF chunk descriptor
writeString(view, offset, 'RIFF'); offset += 4;
view.setUint32(offset, fileSize - 8, true); offset += 4; // File size - 8
writeString(view, offset, 'WAVE'); offset += 4;
// fmt sub-chunk
writeString(view, offset, 'fmt '); offset += 4;
view.setUint32(offset, 16, true); offset += 4; // Subchunk size (16 for PCM)
view.setUint16(offset, 1, true); offset += 2; // Audio format (1 = PCM)
view.setUint16(offset, channels, true); offset += 2;
view.setUint32(offset, sampleRate, true); offset += 4;
view.setUint32(offset, byteRate, true); offset += 4;
view.setUint16(offset, blockAlign, true); offset += 2;
view.setUint16(offset, bitsPerSample, true); offset += 2;
// data sub-chunk
writeString(view, offset, 'data'); offset += 4;
view.setUint32(offset, dataSize, true); offset += 4;
// Copy PCM data
for (let i = 0; i < pcmLength; i++) {
wavBuffer[offset + i] = pcmBinary.charCodeAt(i);
}
// Convert to base64
let binary = '';
for (let i = 0; i < wavBuffer.length; i++) {
binary += String.fromCharCode(wavBuffer[i]);
}
return btoa(binary);
}
/**
* Helper to write string to DataView
*/
function writeString(view: DataView, offset: number, str: string): void {
for (let i = 0; i < str.length; i++) {
view.setUint8(offset + i, str.charCodeAt(i));
}
}
/**
* Decode base64 string to Uint8Array
*/
function decodeBase64ToUint8Array(base64: string): Uint8Array {
const binary = atob(base64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes;
}
/**
* Encode Uint8Array to base64 string
*/
function uint8ArrayToBase64(bytes: Uint8Array): string {
let binary = '';
for (let i = 0; i < bytes.length; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
}
/**
* Concatenate multiple Uint8Arrays into one
*/
function concatenateUint8Arrays(arrays: Uint8Array[]): Uint8Array {
const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);
const result = new Uint8Array(totalLength);
let offset = 0;
for (const arr of arrays) {
result.set(arr, offset);
offset += arr.length;
}
return result;
}
/**
* Hook for realtime audio streaming with volume-based VAD
* Uses expo-audio-studio for recording with echo cancellation and noise suppression
*/
export function useRealtimeAudio(config: RealtimeAudioConfig): RealtimeAudio {
const [isActive, setIsActive] = useState(false);
const [isSpeaking, setIsSpeaking] = useState(false);
const audioRecorder = useAudioRecorder();
const audioBufferRef = useRef<Uint8Array[]>([]);
const silenceStartRef = useRef<number | null>(null);
const isSpeakingRef = useRef(false);
const speechStartRef = useRef<number | null>(null); // Track when volume first exceeded threshold
const speechConfirmedRef = useRef(false); // Track if speech has been confirmed
const lastVolumeCheckRef = useRef<number>(Date.now());
const lastVolumeLogRef = useRef<number>(Date.now());
const VOLUME_THRESHOLD = config.volumeThreshold ?? 0.2;
const SILENCE_DURATION_MS = config.silenceDuration ?? 1000;
const SPEECH_START_DURATION_MS = config.speechStartDuration ?? 1000;
const VOLUME_CHECK_INTERVAL = 200; // Check volume every 200ms
async function requestMicrophonePermission(): Promise<boolean> {
try {
const permission = Platform.OS === 'ios'
? PERMISSIONS.IOS.MICROPHONE
: PERMISSIONS.ANDROID.RECORD_AUDIO;
const status = await check(permission);
if (status === RESULTS.GRANTED) {
return true;
}
const result = await request(permission);
return result === RESULTS.GRANTED;
} catch (error) {
console.error('[RealtimeAudio] Permission error:', error);
return false;
}
}
function calculateVolume(base64Audio: string): number {
try {
// Decode base64 to binary
const binary = atob(base64Audio);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
// Calculate RMS from PCM data (16-bit signed PCM, little-endian)
let sum = 0;
const sampleCount = Math.floor(bytes.length / 2);
for (let i = 0; i < bytes.length - 1; i += 2) {
// Read 16-bit value as unsigned first
let sample = bytes[i] | (bytes[i + 1] << 8);
// Convert to signed 16-bit (two's complement)
if (sample > 32767) {
sample -= 65536;
}
// Normalize to -1.0 to 1.0 range
const normalized = sample / 32768.0;
sum += normalized * normalized;
}
const rms = Math.sqrt(sum / sampleCount);
return rms;
} catch (error) {
console.error('[RealtimeAudio] Volume calculation error:', error);
return 0;
}
}
async function start(): Promise<void> {
if (isActive) {
console.log('[RealtimeAudio] Already active');
return;
}
try {
console.log('[RealtimeAudio] Starting realtime audio...');
// Request permissions
const hasPermission = await requestMicrophonePermission();
if (!hasPermission) {
throw new Error('Microphone permission denied');
}
// Start recording with audio streaming
await audioRecorder.startRecording({
sampleRate: 16000,
channels: 1,
encoding: 'pcm_16bit',
interval: 100, // Get chunks every 100ms
onAudioStream: async (event: any) => {
if (!event.data) return;
const audioData = event.data; // base64 PCM data
// Check volume periodically
const now = Date.now();
if (now - lastVolumeCheckRef.current >= VOLUME_CHECK_INTERVAL) {
lastVolumeCheckRef.current = now;
const volume = calculateVolume(audioData);
const speechDetected = volume > VOLUME_THRESHOLD;
// Decode base64 chunk to binary for buffering
const decodedChunk = decodeBase64ToUint8Array(audioData);
// Check if we're in detection phase (volume detected but not yet confirmed)
const inDetectionPhase = speechStartRef.current !== null && !isSpeakingRef.current;
if (inDetectionPhase) {
// We're in detection phase - check timer regardless of current volume
const sustainedDuration = Date.now() - speechStartRef.current!;
if (sustainedDuration >= SPEECH_START_DURATION_MS) {
// Timer elapsed - confirm speech
console.log('[RealtimeAudio] ✅ Speech confirmed after', sustainedDuration, 'ms (volume:', volume.toFixed(4), 'threshold:', VOLUME_THRESHOLD, ')');
isSpeakingRef.current = true;
speechConfirmedRef.current = true;
setIsSpeaking(true);
config.onSpeechStart?.();
audioBufferRef.current.push(decodedChunk);
silenceStartRef.current = null;
} else if (speechDetected) {
// Still in detection phase, volume still above threshold, keep buffering
audioBufferRef.current.push(decodedChunk);
// Log progress every 500ms during detection
const logNow = Date.now();
if (logNow - lastVolumeLogRef.current >= 500) {
console.log('[RealtimeAudio] 🔍 Detecting...', sustainedDuration, '/', SPEECH_START_DURATION_MS, 'ms (volume:', volume.toFixed(4), ')');
lastVolumeLogRef.current = logNow;
}
} else {
// Volume dropped before timer elapsed - false start
console.log('[RealtimeAudio] ❌ False start (volume dropped after', sustainedDuration, 'ms, needed', SPEECH_START_DURATION_MS, 'ms)');
speechStartRef.current = null;
speechConfirmedRef.current = false;
audioBufferRef.current = [];
}
} else if (speechDetected && !isSpeakingRef.current) {
// First volume detection - start timer and buffering
console.log('[RealtimeAudio] 🎤 Volume spike detected! Starting detection... (volume:', volume.toFixed(4), 'threshold:', VOLUME_THRESHOLD, ')');
speechStartRef.current = Date.now();
speechConfirmedRef.current = false;
audioBufferRef.current = [decodedChunk];
silenceStartRef.current = null;
} else if (speechDetected && isSpeakingRef.current) {
// Continuing confirmed speech - just buffer, no logs
audioBufferRef.current.push(decodedChunk);
silenceStartRef.current = null;
} else if (!speechDetected && isSpeakingRef.current) {
// Potential speech END (after confirmed speech)
audioBufferRef.current.push(decodedChunk);
if (silenceStartRef.current === null) {
silenceStartRef.current = Date.now();
console.log('[RealtimeAudio] 🔇 Silence detected, waiting', SILENCE_DURATION_MS, 'ms to confirm end...');
} else {
const silenceDuration = Date.now() - silenceStartRef.current;
if (silenceDuration >= SILENCE_DURATION_MS) {
// Speech END confirmed
const totalChunks = audioBufferRef.current.length;
console.log('[RealtimeAudio] 🛑 Speech ended after', silenceDuration, 'ms silence (captured', totalChunks, 'chunks)');
isSpeakingRef.current = false;
speechStartRef.current = null;
speechConfirmedRef.current = false;
silenceStartRef.current = null;
setIsSpeaking(false);
config.onSpeechEnd?.();
// Send buffered audio segment
if (audioBufferRef.current.length > 0) {
// Concatenate all binary chunks
const combinedBinary = concatenateUint8Arrays(audioBufferRef.current);
const audioSizeKb = (combinedBinary.length / 1024).toFixed(2);
console.log('[RealtimeAudio] 📤 Sending audio segment:', audioSizeKb, 'KB');
// Convert to base64
const combinedPcmBase64 = uint8ArrayToBase64(combinedBinary);
// Convert PCM to WAV format
const wavAudio = pcmToWav(combinedPcmBase64, 16000, 1, 16);
config.onAudioSegment?.(wavAudio);
audioBufferRef.current = [];
}
}
}
}
} else if (isSpeakingRef.current) {
// Just buffer if we're speaking but not checking volume yet
const decodedChunk = decodeBase64ToUint8Array(audioData);
audioBufferRef.current.push(decodedChunk);
}
},
});
setIsActive(true);
console.log('[RealtimeAudio] 🎙️ Audio capture started (threshold:', VOLUME_THRESHOLD, 'duration:', SPEECH_START_DURATION_MS, 'ms)');
} catch (error) {
console.error('[RealtimeAudio] Start error:', error);
const err = error instanceof Error ? error : new Error(String(error));
config.onError?.(err);
// Cleanup on error
await stop();
throw error;
}
}
async function stop(): Promise<void> {
console.log('[RealtimeAudio] Stopping realtime audio...');
// Stop recording
if (audioRecorder.isRecording) {
try {
await audioRecorder.stopRecording();
} catch (error) {
console.error('[RealtimeAudio] Error stopping recording:', error);
}
}
// Reset state
audioBufferRef.current = [];
isSpeakingRef.current = false;
speechStartRef.current = null;
speechConfirmedRef.current = false;
silenceStartRef.current = null;
setIsActive(false);
setIsSpeaking(false);
console.log('[RealtimeAudio] Realtime audio stopped');
}
// Cleanup on unmount
useEffect(() => {
return () => {
if (isActive) {
stop();
}
};
}, []);
return {
start,
stop,
isActive,
isSpeaking,
};
}
+2
View File
@@ -0,0 +1,2 @@
import "expo-router/entry";
import "./src/styles/unistyles";
+4 -10
View File
@@ -1,6 +1,6 @@
{
"name": "@voice-dev/app",
"main": "expo-router/entry",
"main": "index.ts",
"version": "1.0.0",
"scripts": {
"start": "expo start",
@@ -16,7 +16,6 @@
"@react-navigation/bottom-tabs": "^7.4.0",
"@react-navigation/elements": "^2.6.3",
"@react-navigation/native": "^7.1.8",
"@siteed/expo-audio-studio": "^2.18.1",
"@speechmatics/expo-two-way-audio": "^0.1.2",
"buffer": "^6.0.3",
"expo": "^54.0.18",
@@ -33,7 +32,6 @@
"expo-linking": "~8.0.8",
"expo-router": "~6.0.13",
"expo-splash-screen": "~31.0.10",
"expo-status-bar": "~3.0.8",
"expo-symbols": "~1.0.7",
"expo-system-ui": "~6.0.7",
"expo-updates": "~29.0.12",
@@ -41,10 +39,12 @@
"lucide-react-native": "^0.546.0",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-native": "0.81.4",
"react-native": "^0.81.5",
"react-native-css": "^3.0.1",
"react-native-edge-to-edge": "^1.7.0",
"react-native-gesture-handler": "~2.28.0",
"react-native-markdown-display": "^7.0.2",
"react-native-nitro-modules": "^0.30.0",
"react-native-permissions": "^5.4.2",
"react-native-reanimated": "~4.1.1",
"react-native-safe-area-context": "~5.6.0",
@@ -52,7 +52,6 @@
"react-native-svg": "^15.14.0",
"react-native-unistyles": "^3.0.15",
"react-native-web": "~0.21.0",
"react-native-webrtc": "^124.0.7",
"react-native-worklets": "0.5.1"
},
"devDependencies": {
@@ -61,10 +60,5 @@
"eslint": "^9.25.0",
"eslint-config-expo": "~10.0.0",
"typescript": "~5.9.2"
},
"overrides": {
"@siteed/expo-audio-studio": {
"expo-modules-core": "3.0.22"
}
}
}
@@ -1,6 +1,4 @@
import '@/styles/unistyles';
import { Stack } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import { SafeAreaProvider } from 'react-native-safe-area-context';
export default function RootLayout() {
@@ -11,7 +9,6 @@ export default function RootLayout() {
<Stack.Screen name="settings" />
<Stack.Screen name="audio-test" />
</Stack>
<StatusBar style="light" />
</SafeAreaProvider>
);
}
@@ -13,7 +13,7 @@ import { router } from "expo-router";
import { activateKeepAwakeAsync, deactivateKeepAwake } from "expo-keep-awake";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { StyleSheet } from "react-native-unistyles";
import { theme as defaultTheme } from "@/styles/theme";
import { theme as defaultTheme } from "../styles/theme";
// Simple unique ID generator
let messageIdCounter = 0;
@@ -1126,7 +1126,10 @@ export default function VoiceAssistantScreen() {
!ws.isConnected && styles.buttonDisabled,
isRecording && styles.mainButtonRecording,
isInProgress && styles.mainButtonInProgress,
userInput.trim() && !isRecording && !isInProgress && styles.mainButtonWithText,
userInput.trim() &&
!isRecording &&
!isInProgress &&
styles.mainButtonWithText,
]}
>
{isInProgress ? (
+48
View File
@@ -0,0 +1,48 @@
import { StyleSheet } from "react-native-unistyles";
// import { UnistylesRuntime } from "react-native-unistyles";
import { lightTheme, darkTheme } from "./theme";
console.log("[Unistyles] Configuring...");
// // Configure Unistyles with adaptive themes
StyleSheet.configure({
themes: {
light: lightTheme,
dark: darkTheme,
},
breakpoints: {
xs: 0,
sm: 576,
md: 768,
lg: 992,
xl: 1200,
},
settings: {
initialTheme: "dark",
},
});
console.log("[Unistyles] Configuration complete!");
// Type augmentation for TypeScript
type AppThemes = {
light: typeof lightTheme;
dark: typeof darkTheme;
};
type AppBreakpoints = {
xs: number;
sm: number;
md: number;
lg: number;
xl: number;
};
declare module "react-native-unistyles" {
export interface UnistylesThemes extends AppThemes {}
export interface UnistylesBreakpoints extends AppBreakpoints {}
}
console.log(lightTheme.colors.background);
// UnistylesRuntime.setRootViewBackgroundColor(lightTheme.colors.background);
-23
View File
@@ -1,23 +0,0 @@
import { StyleSheet } from "react-native-unistyles";
import { lightTheme, darkTheme } from "./theme";
// Configure Unistyles with adaptive themes
StyleSheet.configure({
themes: {
light: lightTheme,
dark: darkTheme,
},
settings: {
adaptiveThemes: true,
},
});
// Type augmentation for TypeScript
type AppThemes = {
light: typeof lightTheme;
dark: typeof darkTheme;
};
declare module "react-native-unistyles" {
export interface UnistylesThemes extends AppThemes {}
}
+3 -7
View File
@@ -3,12 +3,8 @@
"compilerOptions": {
"strict": true,
"paths": {
"@/*": [
"./*"
],
"@server/*": [
"../server/src/*"
]
"@/*": ["./src/*"],
"@server/*": ["../server/src/*"]
}
},
"include": [
@@ -18,4 +14,4 @@
"expo-env.d.ts",
"nativewind-env.d.ts"
]
}
}
File diff suppressed because it is too large Load Diff