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:
Generated
+228
-84
@@ -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",
|
||||
|
||||
@@ -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": {},
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
],
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
import "expo-router/entry";
|
||||
import "./src/styles/unistyles";
|
||||
@@ -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 ? (
|
||||
@@ -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);
|
||||
@@ -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,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
Reference in New Issue
Block a user