Add voice input UI components for hands-free tracking
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:
182
maternal-web/hooks/useVoiceInput.ts
Normal file
182
maternal-web/hooks/useVoiceInput.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user