Repository navigation
feat(ai-components): add VoiceGlow and VoiceWave voice-agent visualizations - #3895
Merged
Merged
Conversation
…ations Port the voice visualizations from the Android, iOS and web AI components, so the platforms look and move alike. - VoiceGlow (a Skia shader, ported from Android's AGSL glow) and VoiceWave (a Skia path), both driven by one motion driver that runs as worklets on the UI thread. They take `state`, `colors`, `expressivity` and `audioLevel` (a SharedValue), name themselves "Voice agent" with the state as the value, honour Reduce Motion, and react to taps and hover. - New `voiceVisualization` theme keys (colors, backdropColor). - Dictation emits the microphone level (`AIDictationOnLevel`): from the recognizer's RMS on Android and from the audio tap, in dBFS, on iOS. `useDictationAudioLevel()` exposes it. - iOS dictation now restores the audio session it found once it ends; its `.record` category used to mute all playback in the app after the first dictation. - Export `useDictationState` and `useDictationTranscript`. - ExpoChatGPTSample: a hands-free voice mode (dictation in, expo-speech out), the send path shared through `useSendAIMessage`, and the demo backend called with the `openai` platform.
- Dictation records who started it (`useDictation(options, { source })`, the dictation store's
`source`), and `ComposerView` only fills its input with its own button's dictation, so a voice
mode no longer erases or overwrites the composer's text.
- Keep the shape's touch area as a real view (`collapsable={false}`) so taps and hover land.
- Fade in once, with a flag, instead of re-arming while the opacity still reads 0.
- Add `stateLabels` to translate the state values screen readers read.
- Throttle Android's microphone level events to about 30 a second, as on iOS.
- Drop a dead initial audio-level read.
- ExpoChatGPTSample: find the agent's reply by message identity rather than by clock time, give
voice mode its own dictation source, ignore stale dictation errors, and use a static icon for
the voice-mode button instead of an always-animating glow.
- Tests: the shader compiles once on a fresh module, the touch area isn't collapsable, translated
state labels, and outside dictation leaving the composer's text alone.
Contributor
SDK Size
|
1 of 6 tasks
isekovanic
reviewed
Oct 27, 2026
isekovanic
approved these changes
Oct 27, 2026
oliverlaz
added a commit
that referenced
this pull request
Oct 28, 2026
…ations (V10) (#3896) Ticket: https://linear.app/stream/issue/RN-468 ## 🎯 Goal Port of #3895 (the `develop` counterpart) to `V10`: the `VoiceWave` and `VoiceGlow` voice-agent visualizations in `stream-chat-react-native/ai-components` (and `stream-chat-expo/ai-components`), the `useDictationAudioLevel` hook, and the voice mode in `ExpoChatGPTSample`. Android, iOS and web already ship these visualizations in their AI components. ## 🛠 Implementation details Everything under `package/` is identical to #3895 (both of its commits, `0f07f0366` and `38a224a65`, squashed into one), and applied to V10 without changes. See #3895 for the full description. In short: - `VoiceWave` and `VoiceGlow`: one motion driver ported from Android, run as Reanimated worklets on the UI thread; the glow is Android's AGSL shader ported to Skia SkSL, the wave a Skia path. Props: `state`, `colors`, `expressivity`, `audioLevel` (`SharedValue<number | null>`) and `stateLabels`, plus `View` props. - `useDictationAudioLevel()`, backed by a new `AIDictationOnLevel` event in `package/shared-native/{android,ios}`; `useDictationState` and `useDictationTranscript` are now exported. - An optional `voiceVisualization` theme key (`colors`, `backdropColor`). - A `source` option on `useDictation`, so `ComposerView` only fills its input from its own mic button. - Fix: iOS dictation restores the `AVAudioSession` it found when dictation ends. **What differs from #3895 on V10** (all in `examples/ExpoChatGPTSample`): - `screens/ChatContent.tsx`: V10 already moved channel creation and `startAI`/`summarize` from `preSendMessageRequest` into the client's `sendMessageRequest` handler in `app/_layout.tsx`, so the port only adds `VoiceMode` and `useSendAIMessage` there and drops the imports it no longer needs. - `components/VoiceMode/useVoiceConversation.ts`: stream-chat v10 has no `channel.state.latestMessages`, so the voice mode reads `channel.messagePaginator.items`; and custom data lives under `custom`, so it checks `message.custom?.ai_generated`. - `hooks/useSendAIMessage.ts`: the doc comment names the `sendMessageRequest` handler instead of `preSendMessageRequest`. Docs: the v10-rc AI integration pages are updated in GetStream/getstream.io#810, together with v9. ## 🎨 UI Changes Same UI as #3895; see its recordings. ## 🧪 Testing - `yarn lint` (including `validate-entry-isolation`), `cd package && yarn test:typecheck`, `yarn typecheck`, `yarn build` and `yarn test:unit` (246 suites, 2302 tests) pass on this branch. `shared-native:sync` was run. - The unit tests are the same as on #3895 (driver, bounce, palette, geometry, rendering, accessibility, theme fallbacks, reduced motion, dictation level). - Not yet tested on a device on V10: the voice mode's switch to `messagePaginator.items` is type-checked only. Dictation needs a physical device (iOS 27 simulators can't initialize the speech recognizer). ## ☑️ Checklist - [ ] I have signed the [Stream CLA](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform) (required) - [ ] PR targets the `develop` branch (targets `V10`; the `develop` counterpart is #3895) - [x] Documentation is updated (AI components README; v10-rc docs in GetStream/getstream.io#810) - [ ] New code is tested in main example apps, including all possible scenarios - [ ] SampleApp iOS and Android - [ ] Expo iOS and Android
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Ticket: https://linear.app/stream/issue/RN-468
🎯 Goal
Android (GetStream/stream-chat-android#6848, GetStream/stream-chat-android#6849, GetStream/stream-chat-android#6851), iOS (GetStream/stream-chat-swift#4338) and web (GetStream/stream-chat-react#3339) added two voice-agent visualizations to their AI components. This brings them to
stream-chat-react-native/ai-components(andstream-chat-expo/ai-components) with the same motion, so the platforms look and move alike, and shows them in a new voice mode inExpoChatGPTSample.🛠 Implementation details
Public API (from the
ai-componentsentry point):VoiceWave: a single string carrying a wave packet that erupts with speech, its colors flowing along it.VoiceGlow: a soft globe whose color masses orbit inside it and swell with speech.useDictationAudioLevel(): the microphone's live level while dictation listens, as a ReanimatedSharedValue<number | null>.VoiceAgentState,VoiceVisualizationProps,VoiceWaveProps,VoiceGlowProps,DictationLevel;useDictationStateanduseDictationTranscriptare now exported too.voiceVisualizationtheme key:colors(default palette, the accent color) andbackdropColor(light or dark look; the system color scheme when unset).Both components take
state(idle | connecting | listening | thinking | speaking),colors(one to five colors),expressivity(0 to 2) andaudioLevel(aSharedValue, read once a frame on the UI thread;nullanimates speech on its own). OtherViewprops go to the root.How it's built:
VoiceVisualizationDriver(per-state motion table, frame-rate-independent easing, reduced motion at 0.07x, tap bounce, HSV palette, and theSteady/Smoothlevel responses for glow and wave). It's plain objects and worklets, advanced in a ReanimateduseFrameCallback, so the motion runs on the UI thread and keeps going while JS is busy streaming a reply.VoiceGlowis a SkiaRuntimeEffect: Android's AGSL shader is SkSL, so it's ported nearly verbatim and runs on every Android version. Every glow shares one compiled effect. There is no Canvas/CSS fallback and norenderingprop.VoiceWaveis a Skia path with a 13-stop gradient, as Android draws it; its speech envelope uses the shader's float32 simplex noise (as on iOS and the web).Pressablestill fires and touches beside the shape pass through. Hover ripples with a mouse or pen.Native dictation:
AIDictationOnLevelevent (no codegen spec change; it goes through the existingNativeEventEmitter). Android maps the recognizer'sonRmsChanged; iOS measures the audio tap in dBFS, throttled to ~30 Hz, tuned on an iPhone for.measurementmode (-60 to -35 dBFS).AVAudioSessionconfiguration it found and restores it when dictation ends. It used to leave the session in the.recordcategory, which muted all playback in the app after the first dictation and kept other apps' audio ducked.Sample:
ExpoChatGPTSamplegets a full-screen voice mode (VoiceGlowfor the agent,VoiceWaveon the microphone level). It listens with dictation, sends the transcript, follows the agent's AI state, reads the reply aloud withexpo-speechand listens again. The send path moved into a shareduseSendAIMessage, and the demo backend is now called withplatform: 'openai', as in the React example (its Anthropic key currently fails/summarize).Entry isolation: everything lives under
src/ai-components;validate-entry-isolationpasses.🎨 UI Changes
Voice mode in
ExpoChatGPTSample, on a physical iPhone 15 Pro Max and the iOS simulator. Screenshots and a recording to be added.🧪 Testing
yarn test:unit,yarn lint,yarn typecheckandyarn buildpass.☑️ Checklist
developbranch