2:I[7012,["4765","static/chunks/4765-f5afdf8061f456f3.js","9856","static/chunks/9856-3b185291364d9bef.js","6687","static/chunks/app/docs/%5B...slug%5D/page-e07536548216bee4.js"],"MarkdownRenderer"] 4:I[9856,["4765","static/chunks/4765-f5afdf8061f456f3.js","9856","static/chunks/9856-3b185291364d9bef.js","6687","static/chunks/app/docs/%5B...slug%5D/page-e07536548216bee4.js"],""] 5:I[4126,[],""] 7:I[9630,[],""] 8:I[4278,["9856","static/chunks/9856-3b185291364d9bef.js","8172","static/chunks/8172-b3a2d6fe4ae10d40.js","3185","static/chunks/app/layout-2814fa5d15b84fe4.js"],"HeadingProvider"] 9:I[1476,["9856","static/chunks/9856-3b185291364d9bef.js","8172","static/chunks/8172-b3a2d6fe4ae10d40.js","3185","static/chunks/app/layout-2814fa5d15b84fe4.js"],"Header"] a:I[3167,["9856","static/chunks/9856-3b185291364d9bef.js","8172","static/chunks/8172-b3a2d6fe4ae10d40.js","3185","static/chunks/app/layout-2814fa5d15b84fe4.js"],"Sidebar"] b:I[7409,["9856","static/chunks/9856-3b185291364d9bef.js","8172","static/chunks/8172-b3a2d6fe4ae10d40.js","3185","static/chunks/app/layout-2814fa5d15b84fe4.js"],"PageFrame"] 3:T33ab, # Unified Chat/Voice UI Implementation **Last Updated**: 2025-12-04 **Status**: Complete (Phases 1-4) **Feature Flag**: `ui.unified_chat_voice` --- ## Overview The Unified Chat/Voice UI merges the previously separate Chat and Voice Mode interfaces into a single, cohesive experience. Users can seamlessly switch between text and voice input without leaving the conversation. ### Key Benefits - **Seamless Mode Switching**: Toggle between text and voice without page navigation - **Persistent Context**: Conversation state maintained across input mode changes - **Mobile-First Design**: Responsive layout with collapsible panels - **Improved Accessibility**: Full keyboard navigation and ARIA support - **Better Performance**: Lazy-loaded components and optimized state management --- ## Architecture ### Component Structure ``` src/components/unified-chat/ ├── UnifiedChatContainer.tsx # Main container with 3-panel layout ├── UnifiedHeader.tsx # Header with title editing, actions ├── UnifiedInputArea.tsx # Combined text/voice input ├── CollapsibleSidebar.tsx # Left panel: conversation list ├── CollapsibleContextPane.tsx # Right panel: citations, clinical, branches └── __tests__/ # Component test suites ├── UnifiedHeader.test.tsx ├── UnifiedInputArea.test.tsx ├── CollapsibleSidebar.test.tsx └── CollapsibleContextPane.test.tsx ``` ### Supporting Hooks ``` src/hooks/ ├── useIsMobile.ts # Mobile viewport detection ├── useVoiceModeStateMachine.ts # Voice mode state management ├── useConnectionManager.ts # WebSocket connection handling ├── useInputModeDetection.ts # Auto-detect input type └── useAudioPlayback.ts # Audio output management ``` ### State Management ``` src/stores/ ├── unifiedConversationStore.ts # Unified conversation state (Zustand) └── voiceSettingsStore.ts # Voice preferences (persisted) ``` --- ## Three-Panel Layout ``` ┌─────────────────────────────────────────────────────────────────┐ │ UnifiedHeader │ │ [≡] Conversation Title (editable) [Export][Share][⚙️] │ ├────────────┬────────────────────────────────────┬───────────────┤ │ │ │ │ │ Collapsible│ Main Chat Area │ Collapsible │ │ Sidebar │ │ Context │ │ │ ┌──────────────────────┐ │ Pane │ │ • Pinned │ │ Message List │ │ │ │ • Recent │ │ (scrollable) │ │ • Citations │ │ • Search │ └──────────────────────┘ │ • Clinical │ │ │ │ • Branches │ │ │ ┌──────────────────────┐ │ │ │ │ │ UnifiedInputArea │ │ │ │ │ │ [Mode][Input][Send] │ │ │ │ │ └──────────────────────┘ │ │ │ │ │ │ ├────────────┴────────────────────────────────────┴───────────────┤ │ (Mobile: panels slide in as overlays) │ └─────────────────────────────────────────────────────────────────┘ ``` ### Desktop Behavior - **Sidebar**: 64px collapsed, 256px expanded - **Context Pane**: 48px collapsed, 320px expanded - **Main Area**: Fills remaining space (flex-1) ### Mobile Behavior - **Sidebar**: Full-screen overlay sliding from left - **Context Pane**: Full-screen overlay sliding from right - **Header**: Hamburger menu for sidebar, info button for context - **Transitions**: 200ms duration with backdrop fade --- ## Input Mode Toggle The UnifiedInputArea provides seamless switching between text and voice modes: ### Text Mode - Auto-resizing textarea - Enter to send, Shift+Enter for newline - Character count display - Attachment button (future) ### Voice Mode - **Always-On**: Continuous listening with silence detection - **Push-to-Talk**: Hold spacebar to speak - Visual feedback for voice states: - `idle`: Microphone icon - `listening`: Pulsing microphone (blue) - `processing`: Spinner - `responding`: Speaker icon (green) - `error`: Muted microphone (red) ### State Machine ``` ┌─────────┐ activate ┌────────────┐ │ idle │ ───────────────> │ connecting │ └─────────┘ └─────┬──────┘ ▲ │ connected │ ▼ │ ┌────────────┐ │ deactivate │ listening │ <───┐ │ <─────────────────────┴─────┬──────┘ │ │ │ speech │ │ ▼ │ │ ┌────────────┐ │ │ │ processing │ │ │ └─────┬──────┘ │ │ │ complete │ │ ▼ │ │ ┌────────────┐ │ │ │ responding │ ────┘ │ └────────────┘ │ │ │ error │ error └─────────────────────────────┘ ``` --- ## Feature Flag The unified interface is gated behind the `ui.unified_chat_voice` feature flag: ```typescript // src/lib/featureFlags.ts import { featureGate } from "@voiceassist/shared"; export async function isUnifiedChatVoiceUIEnabled(): Promise { // Use the new category.feature_name pattern return await featureGate("ui.unified_chat_voice"); } // Legacy fallback (deprecated - will be removed) export function isUnifiedChatVoiceUIEnabledSync(): boolean { return localStorage.getItem("ui.unified_chat_voice") === "true" || import.meta.env.VITE_UNIFIED_UI === "true"; } ``` ### Enabling the Feature 1. **Environment Variable**: Set `VITE_UNIFIED_UI=true` in `.env` 2. **Feature Flag Service**: Enable `ui.unified_chat_voice` in Admin Panel 3. **Local Storage** (dev only): Set `ui.unified_chat_voice` to `"true"` 4. **URL Parameter**: `?unified=true` (for testing) > **Note**: The old `ff_unified_chat_voice_ui` pattern is deprecated. Use the new `ui.unified_chat_voice` naming convention. See [Naming Conventions](./admin-guide/feature-flags/naming-conventions.md). --- ## Performance Optimizations ### Lazy Loading Dialogs are lazy-loaded to reduce initial bundle size: ```typescript const KeyboardShortcutsDialog = lazy(() => import('../KeyboardShortcutsDialog').then(m => ({ default: m.KeyboardShortcutsDialog })) ); const ExportDialog = lazy(() => import('../export/ExportDialog').then(m => ({ default: m.ExportDialog })) ); const ShareDialog = lazy(() => import('../sharing/ShareDialog').then(m => ({ default: m.ShareDialog })) ); // Wrapped in Suspense with conditional rendering {isShortcutsDialogOpen && } {isExportDialogOpen && } {isShareDialogOpen && } ``` ### Memoization - `mappedMessages` uses `useMemo` to avoid re-mapping on every render - Event handlers use `useCallback` to maintain referential equality - Component props are carefully structured to minimize re-renders ### State Management The `unifiedConversationStore` uses Zustand with: - Selective subscriptions via selectors - Persist middleware for settings - DevTools integration in development --- ## Accessibility ### Keyboard Navigation | Shortcut | Action | | -------- | ------------------------- | | `Ctrl+K` | Open command palette | | `Ctrl+/` | Toggle keyboard shortcuts | | `Ctrl+N` | New conversation | | `Ctrl+E` | Export conversation | | `Ctrl+B` | Toggle sidebar | | `Ctrl+I` | Toggle context pane | | `Space` | Push-to-talk (voice mode) | | `Escape` | Close overlays | ### ARIA Support - Sidebar: `