8.4 KiB
| phase | slug | status | shadcn_initialized | preset | created |
|---|---|---|---|---|---|
| 37 | web-chat-voice-ui | draft | true | new-york / neutral / tailwind-v4 | 2026-04-03 |
Phase 37 — UI Design Contract
Visual and interaction contract for frontend phases. Generated by gsd-ui-researcher, verified by gsd-ui-checker.
Design System
| Property | Value |
|---|---|
| Tool | shadcn (new-york style) |
| Preset | new-york, baseColor: neutral, cssVariables: true |
| Component library | Radix UI (via shadcn) |
| Icon library | lucide-react |
| Font | System default (inherit from body) |
Source: components.json detected + npx shadcn info -c ui confirmed.
Spacing Scale
Declared values (must be multiples of 4):
| Token | Value | Usage |
|---|---|---|
| xs | 4px | Icon gaps, waveform bar gaps, badge inner padding |
| sm | 8px | Button icon padding, compact element spacing |
| md | 16px | Chat message padding, input field padding |
| lg | 24px | Panel section spacing |
| xl | 32px | Layout gaps between major sections |
| 2xl | 48px | Major section breaks |
| 3xl | 64px | Page-level spacing |
Exceptions:
- Mic button and voice mode toggle touch target: 32px (h-8 w-8) to match existing VoiceRecordButton/TtsButton sizing pattern
- Waveform canvas height: 32px fixed (h-8) to stay within input row
Typography
| Role | Size | Weight | Line Height |
|---|---|---|---|
| Body | 14px (text-sm) | 400 (regular) | 1.5 |
| Label | 12px (text-xs) | 400 (regular) | 1.4 |
| Heading | 16px (text-base) | 600 (semibold) | 1.2 |
| Display | 20px (text-xl) | 600 (semibold) | 1.2 |
Source: Existing chat components use text-sm for message content, text-xs for file labels and badges. Matches ChatMessage.tsx and ChatInput.tsx patterns.
Color
| Role | Value | Usage |
|---|---|---|
| Dominant (60%) | --background (#eff1f5 light / #1e1e2e dark) |
Chat surface, page background |
| Secondary (30%) | --secondary (#ccd0da light / #313244 dark) |
User message bubbles, sidebar |
| Accent (10%) | --primary (#1e66f5 light / #89b4fa dark) |
Active recording state, voice mode active indicator, audio player progress bar |
| Destructive | --destructive (#d20f39 light / #f38ba8 dark) |
Stop-recording button state (Square icon when recording), delete confirmations |
Accent reserved for:
- VoiceMicButton border/ring when in active recording state
- Voice mode toggle active-state highlight (currently selected mode pill)
- Audio player progress fill while audio is playing
- Waveform bars while recording is live
Source: src/index.css CSS variable definitions. Existing VoiceRecordButton uses text-destructive for stop state — contract preserves this pattern.
Component Inventory
New components to build for this phase:
| Component | Location | Description |
|---|---|---|
VoiceMicButton |
ui/src/components/VoiceMicButton.tsx |
Replaces VoiceRecordButton — adds VAD auto-stop, waveform visualization, three visual states (idle / recording+waveform / processing) |
VoiceWaveform |
ui/src/components/VoiceWaveform.tsx |
Canvas-based amplitude bars, 30-50 data points, 32px tall, animated during recording only |
VoiceModeToggle |
ui/src/components/VoiceModeToggle.tsx |
Three-state pill toggle: "Text" / "Voice In" / "Full Voice" — persists via nexus-settings |
ChatVoicePlayer |
ui/src/components/ChatVoicePlayer.tsx |
Inline <audio> player with play/pause/stop controls using native element + URL.createObjectURL() |
ChatVoiceBadge |
ui/src/components/ChatVoiceBadge.tsx |
Small badge on agent messages with messageType voice_input or voice_full; includes expand/collapse for full markdown |
useVoiceMode |
ui/src/hooks/useVoiceMode.ts |
Hook reading/writing voiceMode from nexus-settings; returns current mode + setter |
useVadRecorder |
ui/src/hooks/useVadRecorder.ts |
Wraps @ricky0123/vad-react, exposes recording state + Float32Array callback on speech end |
Reused/extended components:
| Component | Change |
|---|---|
ChatInput.tsx |
Replace VoiceRecordButton with VoiceMicButton; add VoiceModeToggle in toolbar row |
ChatMessage.tsx |
Add ChatVoiceBadge + ChatVoicePlayer branch for messageType voice_input / voice_full |
server/src/app.ts |
Add COOP/COEP headers to static middleware for SharedArrayBuffer support |
Interaction States
VoiceMicButton
| State | Visual |
|---|---|
| idle | Mic icon (lucide), ghost variant, h-8 w-8 |
| recording | VoiceWaveform replaces icon, primary ring/border, stop action on click |
| processing (transcribing) | Loader2 animate-spin, disabled |
| VAD speech-end | Automatically transitions to processing — no manual stop needed |
VoiceModeToggle
| State | Visual |
|---|---|
| text | "Text" pill, default/muted background |
| voice_input | "Voice In" pill, primary background |
| full_voice | "Full Voice" pill, primary background |
Three pills rendered side-by-side. Active pill uses bg-primary text-primary-foreground. Inactive pills use bg-muted text-muted-foreground.
ChatVoiceBadge (on agent message)
| messageType | Badge label | Expandable section |
|---|---|---|
| voice_input | "Voice" | None — transcript shown in bubble |
| voice_full | "Voice" | Collapsed by default; expand reveals full markdown with code blocks |
Expand/collapse uses shadcn Collapsible component. Badge is rendered with shadcn Badge variant outline.
ChatVoicePlayer
| State | Visual |
|---|---|
| auto-play (on) | Plays immediately on mount; Volume2 icon shows; click to pause |
| auto-play (off) | Shows play button; user-initiated only |
| playing | Progress bar fills with --primary; Pause icon |
| paused | Play icon |
| ended | Returns to play button |
Copywriting Contract
| Element | Copy |
|---|---|
| Primary CTA (start recording) | "Start voice input" (aria-label on VoiceMicButton idle) |
| Recording active label | "Recording — speak now" (aria-label on VoiceMicButton recording state) |
| Processing label | "Transcribing..." (aria-label on VoiceMicButton processing state) |
| Voice mode toggle label | "Voice mode" (tooltip/title on toggle group) |
| Mode option: text only | "Text" |
| Mode option: voice input | "Voice In" |
| Mode option: full voice | "Full Voice" |
| Voice badge label | "Voice" |
| Expand full response | "Show full response" |
| Collapse full response | "Hide full response" |
| Auto-play setting label | "Auto-play voice responses" |
| Empty audio state | Not applicable — audio only appears when agent sends voice_full response |
| Error: mic permission denied | "Microphone access denied. Allow microphone access in your browser settings." |
| Error: transcription failed | "Transcription failed. Please try again." |
| Error: synthesis failed | "Voice synthesis failed. The text response is still available above." |
| Destructive confirmation | None — no destructive actions in this phase |
Accessibility
- All interactive voice controls have explicit
aria-labelvalues (not icon-only without label) - VoiceMicButton states must be announced via
aria-live="polite"region with status text - Audio player must have
aria-label="Voice response"on the<audio>element - VoiceModeToggle uses
role="group"witharia-label="Voice mode"on the wrapper - COOP/COEP headers are required on the Express server — without them, SharedArrayBuffer is unavailable and VAD silently fails
- AudioContext unlock must happen inside a user gesture handler (click/tap) to satisfy browser autoplay policy
Registry Safety
| Registry | Blocks Used | Safety Gate |
|---|---|---|
| shadcn official | button, badge, collapsible, tooltip, select | not required |
Third-party packages (not shadcn registry — installed via npm, not npx shadcn add):
| Package | Purpose | Safety note |
|---|---|---|
@ricky0123/vad-react ^0.0.36 |
VAD silence detection — delivers Float32Array at 16kHz on speech end | npm package, not registry block — standard npm install, no vetting gate required |
No third-party shadcn registry blocks declared. Registry vetting gate: not applicable.
Checker Sign-Off
- Dimension 1 Copywriting: PASS
- Dimension 2 Visuals: PASS
- Dimension 3 Color: PASS
- Dimension 4 Typography: PASS
- Dimension 5 Spacing: PASS
- Dimension 6 Registry Safety: PASS
Approval: pending