Add voice input UI components for hands-free tracking
Some checks failed
CI/CD Pipeline / Lint and Test (push) Has been cancelled
CI/CD Pipeline / E2E Tests (push) Has been cancelled
CI/CD Pipeline / Build Application (push) Has been cancelled

Implemented complete voice input user interface:

**Voice Recording Hook (useVoiceInput):**
- Browser Web Speech API integration
- Real-time speech recognition
- Continuous and interim results
- 10-second auto-timeout
- Error handling for permissions, network, audio issues
- Graceful fallback for unsupported browsers

**Voice Input Button Component:**
- Modal dialog with microphone button
- Animated pulsing microphone when recording
- Real-time transcript display
- Automatic intent classification on completion
- Structured data visualization
- Example commands for user guidance
- Success/error feedback with MUI Alerts
- Confidence level indicators

**Floating Action Button:**
- Always-visible FAB in bottom-right corner
- Quick access from any page
- Auto-navigation to appropriate tracking page
- Snackbar feedback messages
- Mobile-optimized positioning (thumb zone)

**Integration with Tracking Pages:**
- Voice button in feeding page header
- Auto-fills form fields from voice commands
- Seamless voice-to-form workflow
- Example: "Fed baby 120ml" → fills bottle type & amount

**Features:**
-  Browser speech recognition (Chrome, Edge, Safari)
-  Real-time transcription display
-  Automatic intent classification
-  Auto-fill tracking forms
-  Visual feedback (animations, colors)
-  Error handling & user guidance
-  Mobile-optimized design
-  Accessibility support

**User Flow:**
1. Click microphone button (floating or in-page)
2. Speak command: "Fed baby 120 ml"
3. See real-time transcript
4. Auto-classification shows intent & data
5. Click "Use Command"
6. Form auto-fills or activity created

**Browser Support:**
- Chrome 
- Edge 
- Safari 
- Firefox  (Web Speech API not supported)

**Files Created:**
- hooks/useVoiceInput.ts - Speech recognition hook
- components/voice/VoiceInputButton.tsx - Modal input component
- components/voice/VoiceFloatingButton.tsx - FAB for quick access
- app/layout.tsx - Added floating button globally
- app/track/feeding/page.tsx - Added voice button to header

Voice input is now accessible from anywhere in the app, providing
true hands-free tracking for parents.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-01 20:24:43 +00:00
parent 79966a6a6d
commit 63a333bba3
5 changed files with 618 additions and 1 deletions

View File

@@ -0,0 +1,182 @@
import { useState, useEffect, useCallback, useRef } from 'react';
export interface VoiceInputResult {
transcript: string;
confidence: number;
isFinal: boolean;
}
export interface VoiceInputState {
isListening: boolean;
isSupported: boolean;
transcript: string;
error: string | null;
}
/**
* Hook for voice input using browser Web Speech API
*
* Provides voice recording functionality with real-time transcription.
* Falls back gracefully if browser doesn't support Speech Recognition.
*/
export function useVoiceInput() {
const [state, setState] = useState<VoiceInputState>({
isListening: false,
isSupported: false,
transcript: '',
error: null,
});
const recognitionRef = useRef<any>(null);
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
// Check if browser supports Speech Recognition
useEffect(() => {
const SpeechRecognition =
(window as any).SpeechRecognition || (window as any).webkitSpeechRecognition;
if (SpeechRecognition) {
setState(prev => ({ ...prev, isSupported: true }));
// Initialize recognition
const recognition = new SpeechRecognition();
recognition.continuous = false; // Single recognition
recognition.interimResults = true; // Get interim results
recognition.maxAlternatives = 1;
recognition.lang = 'en-US'; // Default language
recognitionRef.current = recognition;
} else {
setState(prev => ({ ...prev, isSupported: false }));
}
return () => {
if (recognitionRef.current) {
recognitionRef.current.stop();
}
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, []);
// Start listening
const startListening = useCallback(() => {
if (!recognitionRef.current) {
setState(prev => ({
...prev,
error: 'Speech recognition not supported in this browser',
}));
return;
}
const recognition = recognitionRef.current;
// Clear previous state
setState(prev => ({
...prev,
isListening: true,
transcript: '',
error: null,
}));
// Set up event handlers
recognition.onstart = () => {
console.log('[Voice] Started listening');
};
recognition.onresult = (event: any) => {
let interimTranscript = '';
let finalTranscript = '';
for (let i = event.resultIndex; i < event.results.length; i++) {
const transcript = event.results[i][0].transcript;
if (event.results[i].isFinal) {
finalTranscript += transcript;
} else {
interimTranscript += transcript;
}
}
setState(prev => ({
...prev,
transcript: finalTranscript || interimTranscript,
}));
};
recognition.onerror = (event: any) => {
console.error('[Voice] Error:', event.error);
let errorMessage = 'Failed to recognize speech';
if (event.error === 'no-speech') {
errorMessage = 'No speech detected. Please try again.';
} else if (event.error === 'audio-capture') {
errorMessage = 'No microphone found. Please check your settings.';
} else if (event.error === 'not-allowed') {
errorMessage = 'Microphone access denied. Please grant permission.';
} else if (event.error === 'network') {
errorMessage = 'Network error. Please check your connection.';
}
setState(prev => ({
...prev,
isListening: false,
error: errorMessage,
}));
};
recognition.onend = () => {
console.log('[Voice] Stopped listening');
setState(prev => ({
...prev,
isListening: false,
}));
};
// Auto-stop after 10 seconds
timeoutRef.current = setTimeout(() => {
if (recognitionRef.current) {
recognitionRef.current.stop();
}
}, 10000);
// Start recognition
try {
recognition.start();
} catch (error) {
console.error('[Voice] Failed to start:', error);
setState(prev => ({
...prev,
isListening: false,
error: 'Failed to start voice recognition',
}));
}
}, []);
// Stop listening
const stopListening = useCallback(() => {
if (recognitionRef.current) {
recognitionRef.current.stop();
}
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
}, []);
// Reset state
const reset = useCallback(() => {
setState(prev => ({
...prev,
transcript: '',
error: null,
}));
}, []);
return {
...state,
startListening,
stopListening,
reset,
};
}