--- phase: 42-wallpapers-social-format-conversion-voice plan: 04 type: execute wave: 2 depends_on: [42-01] files_modified: - ui/src/components/ChatInput.tsx - ui/src/hooks/useSystemProviders.ts autonomous: true requirements: [VOICE-01, VOICE-02, VOICE-03] must_haves: truths: - "VoiceMicButton renders in ChatInput when voice mode is voice_input or full_voice" - "Offline badge shows next to mic button when whisperAvailable is true" - "Voice mode toggle allows switching between text-only, voice-input, and full-voice" - "Voice input works with local Whisper model" artifacts: - path: "ui/src/hooks/useSystemProviders.ts" provides: "Hook that fetches /api/system/providers and returns whisperAvailable" exports: ["useSystemProviders"] - path: "ui/src/components/ChatInput.tsx" provides: "Offline badge next to VoiceMicButton" contains: "Offline" key_links: - from: "ui/src/components/ChatInput.tsx" to: "ui/src/hooks/useSystemProviders.ts" via: "useSystemProviders hook for whisperAvailable" pattern: "useSystemProviders" - from: "ui/src/hooks/useSystemProviders.ts" to: "/api/system/providers" via: "fetch on mount" pattern: "system/providers" --- Wire the voice offline badge in ChatInput and create a useSystemProviders hook to surface Whisper availability. Verify existing VoiceMicButton/VoiceModeToggle integration is correct. Purpose: Fulfills VOICE-01..03 by ensuring the existing voice pipeline components are properly wired and the offline capability is surfaced in the UI. Output: New useSystemProviders hook, updated ChatInput with offline badge. @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/phases/42-wallpapers-social-format-conversion-voice/42-RESEARCH.md @.planning/phases/42-wallpapers-social-format-conversion-voice/42-UI-SPEC.md @ui/src/components/ChatInput.tsx @ui/src/components/VoiceMicButton.tsx @ui/src/components/VoiceModeToggle.tsx @server/src/routes/hardware.ts From server/src/routes/hardware.ts: ```typescript router.get("/system/providers", async (_req, res) => { // Returns { whisperAvailable: boolean, piperAvailable: boolean, ... } }); ``` From ui/src/components/ChatInput.tsx: ```typescript // Props include enableVoiceInput?: boolean // VoiceMicButton already renders when enableVoiceInput=true // VoiceModeToggle already renders when enableVoiceInput=true ``` Task 1: Create useSystemProviders hook ui/src/hooks/useSystemProviders.ts ui/src/api/hardware.ts, ui/src/hooks/useContentJob.ts, server/src/routes/hardware.ts Create ui/src/hooks/useSystemProviders.ts: 1. Export interface SystemProviders { whisperAvailable: boolean; piperAvailable: boolean } (match server response shape) 2. Export function useSystemProviders(): - Use useState for providers (SystemProviders | null, initially null) - Use useEffect to fetch GET /api/system/providers on mount (one-time fetch) - Use the same fetch/API client pattern used in existing hooks (check useContentJob or api/hardware.ts for the project's fetch pattern — likely uses the api client from ui/src/api/client.ts) - Return { providers, loading: providers === null } - On error: return null providers, do not throw (graceful degradation — badge simply won't show) This is a simple data-fetching hook. Do NOT over-engineer with caching or SWR. cd /opt/nexus/ui && npx tsc --noEmit 2>&1 | head -20 - grep "useSystemProviders" ui/src/hooks/useSystemProviders.ts - grep "whisperAvailable" ui/src/hooks/useSystemProviders.ts - grep "system/providers" ui/src/hooks/useSystemProviders.ts useSystemProviders hook fetches /api/system/providers and exposes whisperAvailable boolean. Task 2: Add offline badge next to VoiceMicButton in ChatInput ui/src/components/ChatInput.tsx ui/src/components/ChatInput.tsx, ui/src/components/VoiceMicButton.tsx Update ChatInput.tsx to show an "Offline" badge when Whisper is locally available: 1. Import useSystemProviders from "../hooks/useSystemProviders" 2. Import WifiOff from lucide-react (icon library per UI spec) 3. Inside the ChatInput component, call useSystemProviders() to get { providers } 4. Next to the VoiceMicButton (in the same flex container where it renders when enableVoiceInput=true), add the offline badge: ```tsx {enableVoiceInput && providers?.whisperAvailable && ( Offline )} ``` 5. The badge renders ONLY when: - enableVoiceInput is true (voice mode is active) - providers?.whisperAvailable is true (local Whisper binary detected) 6. Verify that VoiceMicButton and VoiceModeToggle are already rendering correctly: - VoiceMicButton shows when enableVoiceInput=true - VoiceModeToggle shows when enableVoiceInput=true - If either is NOT rendering, wire them following the existing pattern Do NOT change VoiceMicButton or VoiceModeToggle components — they are already correct from Phase 37. Per Pitfall 7 from research: badge shows when whisperAvailable===true (binary detected), NOT based on an env var. cd /opt/nexus/ui && npx tsc --noEmit 2>&1 | head -20 - grep "useSystemProviders" ui/src/components/ChatInput.tsx - grep "WifiOff" ui/src/components/ChatInput.tsx - grep "Offline" ui/src/components/ChatInput.tsx - grep "whisperAvailable" ui/src/components/ChatInput.tsx - grep "aria-label" ui/src/components/ChatInput.tsx Offline badge shows next to VoiceMicButton when local Whisper is detected. Existing voice components verified working. - `cd /opt/nexus/ui && npx tsc --noEmit` passes - Offline badge only renders when whisperAvailable is true - Voice mic button and mode toggle render when enableVoiceInput=true - useSystemProviders hook fetches whisperAvailable from /api/system/providers - Offline badge renders with WifiOff icon and correct aria-label - Existing VoiceMicButton/VoiceModeToggle integration unchanged and working - tsc compiles cleanly After completion, create `.planning/phases/42-wallpapers-social-format-conversion-voice/42-04-SUMMARY.md`