--- phase: 37-web-chat-voice-ui plan: 03 type: execute wave: 2 depends_on: ["37-01"] files_modified: - ui/src/components/ChatVoicePlayer.tsx - ui/src/components/ChatVoiceBadge.tsx - ui/src/components/VoiceModeToggle.tsx autonomous: true requirements: - WCHAT-04 - WCHAT-05 - WCHAT-06 must_haves: truths: - "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" artifacts: - path: "ui/src/components/ChatVoicePlayer.tsx" provides: "Inline audio player for synthesized voice responses" exports: ["ChatVoicePlayer"] - path: "ui/src/components/ChatVoiceBadge.tsx" provides: "Voice badge + collapsible markdown on agent messages" exports: ["ChatVoiceBadge"] - path: "ui/src/components/VoiceModeToggle.tsx" provides: "Three-state pill toggle for voice mode" exports: ["VoiceModeToggle"] key_links: - from: "ui/src/components/ChatVoicePlayer.tsx" to: "/api/synthesize" via: "fetch POST to get audio blob" pattern: "fetch.*api/synthesize" - from: "ui/src/components/ChatVoiceBadge.tsx" to: "shadcn Collapsible" via: "Collapsible/CollapsibleContent/CollapsibleTrigger" pattern: "Collapsible" - from: "ui/src/components/VoiceModeToggle.tsx" to: "ui/src/hooks/useVoiceMode.ts" via: "useVoiceMode() hook" pattern: "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 @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.planning/phases/37-web-chat-voice-ui/37-RESEARCH.md ``` POST /api/synthesize Body: { text: string, voiceId?: string } Response: audio/wav binary buffer ``` ```typescript type VoiceMode = "text" | "voice_input" | "full_voice"; export function useVoiceMode(): { mode: VoiceMode; setMode: (next: VoiceMode) => Promise; isLoading: boolean; } ``` ``` 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: DETAILED: ``` ```typescript import { Badge } from "@/components/ui/badge"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { Button } from "@/components/ui/button"; ``` 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 `