nexus/.planning/phases/37-web-chat-voice-ui/37-03-PLAN.md

14 KiB

phase plan type wave depends_on files_modified autonomous requirements must_haves
37-web-chat-voice-ui 03 execute 2
37-01
ui/src/components/ChatVoicePlayer.tsx
ui/src/components/ChatVoiceBadge.tsx
ui/src/components/VoiceModeToggle.tsx
true
WCHAT-04
WCHAT-05
WCHAT-06
truths artifacts key_links
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

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>

@.planning/phases/37-web-chat-voice-ui/37-RESEARCH.md ``` POST /api/synthesize Body: { text: string, voiceId?: string } Response: audio/wav binary buffer ```
type VoiceMode = "text" | "voice_input" | "full_voice";
export function useVoiceMode(): {
  mode: VoiceMode;
  setMode: (next: VoiceMode) => Promise<void>;
  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: <concise spoken version of the response>
DETAILED: <full markdown response with code blocks etc>
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 `` 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: `