ChatVoicePlayer renders inline audio player with play/pause controls
ChatVoicePlayer auto-plays when autoPlay setting is true
ChatVoiceBadge shows 'Voice' badge on voice messages
ChatVoiceBadge has collapsible full markdown section for voice_full messages
VoiceModeToggle renders three pills: Text / Voice In / Full Voice
VoiceModeToggle persists selection via useVoiceMode hook
Auto-play preference stored in localStorage under nexus:voice:autoplay
path
provides
exports
ui/src/components/ChatVoicePlayer.tsx
Inline audio player for synthesized voice responses
ChatVoicePlayer
path
provides
exports
ui/src/components/ChatVoiceBadge.tsx
Voice badge + collapsible markdown on agent messages
ChatVoiceBadge
path
provides
exports
ui/src/components/VoiceModeToggle.tsx
Three-state pill toggle for voice mode
VoiceModeToggle
from
to
via
pattern
ui/src/components/ChatVoicePlayer.tsx
/api/synthesize
fetch POST to get audio blob
fetch.*api/synthesize
from
to
via
pattern
ui/src/components/ChatVoiceBadge.tsx
shadcn Collapsible
Collapsible/CollapsibleContent/CollapsibleTrigger
Collapsible
from
to
via
pattern
ui/src/components/VoiceModeToggle.tsx
ui/src/hooks/useVoiceMode.ts
useVoiceMode() hook
useVoiceMode
Build the voice output and mode selection components: ChatVoicePlayer for inline audio playback, ChatVoiceBadge for voice message display, and VoiceModeToggle for switching between text/voice_input/full_voice modes.
Purpose: These components handle the output side of voice I/O (playing synthesized responses, showing voice badges on messages) and the mode selector that controls the entire voice behavior.
Output: 3 new component files — ChatVoicePlayer, ChatVoiceBadge, VoiceModeToggle
messageType: "voice_input" → user sent via voice, agent replied with text
messageType: "voice_full" → user sent via voice, agent replied with SPOKEN + DETAILED format
SPOKEN: <concise spoken version of the response>
DETAILED: <full markdown response with code blocks etc>
Task 1: Create ChatVoicePlayer and ChatVoiceBadge components
ui/src/components/ChatVoicePlayer.tsx,
ui/src/components/ChatVoiceBadge.tsx
ui/src/components/ChatMessage.tsx,
ui/src/components/ChatMarkdownMessage.tsx
1. **ui/src/components/ChatVoicePlayer.tsx** — Inline audio player for voice responses:
```typescript
interface ChatVoicePlayerProps {
text: string; // The spoken text to synthesize
autoPlay?: boolean; // Whether to auto-play on mount
}
export function ChatVoicePlayer({ text, autoPlay = false }: ChatVoicePlayerProps)
```
Implementation:
- State: `status: "idle" | "loading" | "playing" | "paused"`, `audioUrl: string | null`
- On mount (or when text changes): POST /api/synthesize with `{ text }`, credentials: "include"
- Set status to "loading"
- Get response as blob: `const blob = await res.blob()`
- Create object URL: `const url = URL.createObjectURL(blob)`
- Store url in state, set status to "idle"
- Create `` element ref. Set src to audioUrl when available.
- If autoPlay is true AND audioUrl is set, call `audioRef.current.play()`, set status to "playing"
- Audio event listeners:
- `onEnded`: set status to "idle", revoke blob URL via `URL.revokeObjectURL(audioUrl)`
- `onPause`: set status to "paused"
- `onPlay`: set status to "playing"
- Render:
- loading: `` with "Loading audio..." text
- idle/paused: `