---
phase: 37
slug: web-chat-voice-ui
status: draft
shadcn_initialized: true
preset: new-york / neutral / tailwind-v4
created: 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:
1. VoiceMicButton border/ring when in active recording state
2. Voice mode toggle active-state highlight (currently selected mode pill)
3. Audio player progress fill while audio is playing
4. 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-label` values (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"` with `aria-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
