Skip to content

feat(ai-components): add VoiceGlow and VoiceWave voice-agent visualizations - #3895

Merged
oliverlaz merged 2 commits into
developfrom
feat/ai-voice-visualizations
Oct 28, 2026
Merged

oliverlaz merged 2 commits into
developfrom
feat/ai-voice-visualizations

Conversation

@oliverlaz

Copy link
Copy Markdown
Member

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 (and stream-chat-expo/ai-components) with the same motion, so the platforms look and move alike, and shows them in a new voice mode in ExpoChatGPTSample.

🛠 Implementation details

Public API (from the ai-components entry 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 Reanimated SharedValue<number | null>.
  • Types VoiceAgentState, VoiceVisualizationProps, VoiceWaveProps, VoiceGlowProps, DictationLevel; useDictationState and useDictationTranscript are now exported too.
  • An optional voiceVisualization theme key: colors (default palette, the accent color) and backdropColor (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) and audioLevel (a SharedValue, read once a frame on the UI thread; null animates speech on its own). Other View props go to the root.

How it's built:

  • One motion driver, ported from Android's VoiceVisualizationDriver (per-state motion table, frame-rate-independent easing, reduced motion at 0.07x, tap bounce, HSV palette, and the Steady/Smooth level responses for glow and wave). It's plain objects and worklets, advanced in a Reanimated useFrameCallback, so the motion runs on the UI thread and keeps going while JS is busy streaming a reply.
  • VoiceGlow is a Skia RuntimeEffect: 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 no rendering prop.
  • VoiceWave is 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).
  • Drawn at most 2x for the glow and 3x for the wave, as on iOS.
  • Touches are only observed on a hit area the size of the shape, so a parent Pressable still fires and touches beside the shape pass through. Hover ripples with a mouse or pen.
  • Accessibility follows native: "Voice agent" as the label and the state as the value; Reduce Motion is honoured.
  • Where native and web differ, this follows native (e.g. no web-only wave smoothing).

Native dictation:

  • New AIDictationOnLevel event (no codegen spec change; it goes through the existing NativeEventEmitter). Android maps the recognizer's onRmsChanged; iOS measures the audio tap in dBFS, throttled to ~30 Hz, tuned on an iPhone for .measurement mode (-60 to -35 dBFS).
  • Fix: iOS dictation now remembers the AVAudioSession configuration it found and restores it when dictation ends. It used to leave the session in the .record category, which muted all playback in the app after the first dictation and kept other apps' audio ducked.

Sample: ExpoChatGPTSample gets a full-screen voice mode (VoiceGlow for the agent, VoiceWave on the microphone level). It listens with dictation, sends the transcript, follows the agent's AI state, reads the reply aloud with expo-speech and listens again. The send path moved into a shared useSendAIMessage, and the demo backend is now called with platform: 'openai', as in the React example (its Anthropic key currently fails /summarize).

Entry isolation: everything lives under src/ai-components; validate-entry-isolation passes.

🎨 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

  • Unit tests: Android's 22 driver tests and the bounce tests ported; palette, input clamping, simplex noise against GPU readback values, hit areas, wave geometry, rendering, accessibility label and value, theme fallbacks, reduced motion, and the dictation level hook. yarn test:unit, yarn lint, yarn typecheck and yarn build pass.
  • The SkSL shader compiles and renders under CanvasKit, matching Android's Paparazzi snapshot.
  • iOS simulator: both visualizations render and animate, and the voice-mode loop runs (listening, thinking, speaking, back to listening). Speech recognition itself fails on iOS 27 simulators ("Failed to initialize recognizer"), so transcription was faked there.
  • Physical iPhone: dictation, the mic level driving the wave, the spoken reply and the full voice loop work.
  • Not yet tested: Android on a device (the Android level mapping is untuned), SampleApp.

☑️ Checklist

  • I have signed the Stream CLA (required)
  • PR targets the develop branch
  • Documentation is updated (AI components README; the React Native v9 AI integration docs are in a separate getstream.io PR)
  • New code is tested in main example apps, including all possible scenarios
    • SampleApp iOS and Android
    • Expo iOS and Android (iOS done, Android pending)

…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.
@oliverlaz
oliverlaz requested a review from isekovanic October 27, 2026 13:11
- 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.
@Stream-SDK-Bot

Copy link
Copy Markdown
Contributor

SDK Size

title develop branch diff status
js_bundle_size 2036 KB 2036 KB 0 B 🟢
ai_components_size 130 KB 163 KB +33403 B 🔴

Comment thread package/src/ai-components/voice-visualization/useReduceMotion.ts
@oliverlaz
oliverlaz merged commit 304618e into develop Oct 28, 2026
9 checks passed
@oliverlaz
oliverlaz deleted the feat/ai-voice-visualizations branch October 28, 2026 08:09
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants