172 lines
6.8 KiB
Markdown
172 lines
6.8 KiB
Markdown
---
|
|
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"
|
|
---
|
|
|
|
<objective>
|
|
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.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
|
@$HOME/.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.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
|
|
</context>
|
|
|
|
<interfaces>
|
|
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
|
|
```
|
|
</interfaces>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Create useSystemProviders hook</name>
|
|
<files>ui/src/hooks/useSystemProviders.ts</files>
|
|
<read_first>ui/src/api/hardware.ts, ui/src/hooks/useContentJob.ts, server/src/routes/hardware.ts</read_first>
|
|
<action>
|
|
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.
|
|
</action>
|
|
<verify>
|
|
<automated>cd /opt/nexus/ui && npx tsc --noEmit 2>&1 | head -20</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- grep "useSystemProviders" ui/src/hooks/useSystemProviders.ts
|
|
- grep "whisperAvailable" ui/src/hooks/useSystemProviders.ts
|
|
- grep "system/providers" ui/src/hooks/useSystemProviders.ts
|
|
</acceptance_criteria>
|
|
<done>useSystemProviders hook fetches /api/system/providers and exposes whisperAvailable boolean.</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Add offline badge next to VoiceMicButton in ChatInput</name>
|
|
<files>ui/src/components/ChatInput.tsx</files>
|
|
<read_first>ui/src/components/ChatInput.tsx, ui/src/components/VoiceMicButton.tsx</read_first>
|
|
<action>
|
|
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 && (
|
|
<span
|
|
className="text-xs text-muted-foreground flex items-center gap-1"
|
|
aria-label="Voice input is offline (local model)"
|
|
>
|
|
<WifiOff className="h-3 w-3" />
|
|
Offline
|
|
</span>
|
|
)}
|
|
```
|
|
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.
|
|
</action>
|
|
<verify>
|
|
<automated>cd /opt/nexus/ui && npx tsc --noEmit 2>&1 | head -20</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- 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
|
|
</acceptance_criteria>
|
|
<done>Offline badge shows next to VoiceMicButton when local Whisper is detected. Existing voice components verified working.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
- `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
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- 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
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/42-wallpapers-social-format-conversion-voice/42-04-SUMMARY.md`
|
|
</output>
|