FinMK / frontend /src /components /VoiceInput.jsx
Kumar
Refactor: Exclude PDF and CSV files from Git to fix HF push error
24e6f5b
import { useState, useEffect, useRef } from 'react';
import { Mic, MicOff, Loader } from 'lucide-react';
import api from '../api/axios';
const VoiceInput = ({ onTransactionAdded }) => {
const [isListening, setIsListening] = useState(false);
const [isProcessing, setIsProcessing] = useState(false);
const [error, setError] = useState(null);
const recognitionRef = useRef(null);
const silenceTimerRef = useRef(null);
const stopSilenceTimer = () => {
if (silenceTimerRef.current) {
clearTimeout(silenceTimerRef.current);
silenceTimerRef.current = null;
}
};
const startSilenceTimer = () => {
stopSilenceTimer();
silenceTimerRef.current = setTimeout(() => {
console.log("Silence timeout reached. Stopping recognition.");
if (recognitionRef.current) recognitionRef.current.stop();
}, 6000); // 2 seconds of silence
};
useEffect(() => {
if ('webkitSpeechRecognition' in window || 'SpeechRecognition' in window) {
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
recognitionRef.current = new SpeechRecognition();
recognitionRef.current.continuous = true; // Changed to true
recognitionRef.current.interimResults = true; // Changed to true for better feedback
recognitionRef.current.lang = 'en-US';
recognitionRef.current.onstart = () => {
setIsListening(true);
setError(null);
startSilenceTimer();
};
recognitionRef.current.onend = () => {
setIsListening(false);
stopSilenceTimer();
};
recognitionRef.current.onerror = (event) => {
console.error("Speech recognition error", event.error);
if (event.error !== 'no-speech') {
setError(`Voice error: ${event.error}`);
}
setIsListening(false);
stopSilenceTimer();
};
recognitionRef.current.onresult = (event) => {
startSilenceTimer(); // Reset timer on every result
let finalTranscript = '';
for (let i = event.resultIndex; i < event.results.length; ++i) {
if (event.results[i].isFinal) {
finalTranscript += event.results[i][0].transcript;
}
}
if (finalTranscript) {
console.log("Heard Final:", finalTranscript);
sendTextCheck(finalTranscript);
recognitionRef.current.stop(); // Process immediately if we have a full final result
}
};
} else {
setError("Browser does not support Web Speech API.");
}
return () => {
if (recognitionRef.current) recognitionRef.current.stop();
stopSilenceTimer();
};
}, []);
const toggleListening = () => {
if (isListening) {
recognitionRef.current.stop();
} else {
recognitionRef.current.start();
}
};
const sendTextCheck = async (text) => {
setIsProcessing(true);
try {
const response = await api.post('finance/voice/command/', { text });
console.log("Voice Response:", response.data);
if (response.data.transaction) {
if (onTransactionAdded) onTransactionAdded();
alert(`Added: ${response.data.transaction.title} - ${response.data.parsed.amount}`);
} else {
alert(`Heard: "${response.data.text}". Could not verify details.`);
}
} catch (err) {
console.error("Voice Command Error:", err);
setError(err.response?.data?.error || "Failed to process voice command");
} finally {
setIsProcessing(false);
}
};
return (
<div style={{ position: 'fixed', bottom: '2rem', right: '2rem', zIndex: 1000 }}>
{error && (
<div style={{
position: 'absolute', bottom: '100%', right: 0, marginBottom: '0.5rem',
background: 'rgba(239, 68, 68, 0.9)', color: 'white', padding: '0.5rem',
borderRadius: '0.5rem', fontSize: '0.8rem', whiteSpace: 'nowrap'
}}>
{error}
</div>
)}
<button
onClick={toggleListening}
disabled={isProcessing || !!error}
style={{
width: '60px', height: '60px',
borderRadius: '50%',
border: 'none',
background: isListening ? '#ef4444' : '#6366f1',
color: 'white',
display: 'flex', justifyContent: 'center', alignItems: 'center',
boxShadow: '0 4px 15px rgba(0,0,0,0.3)',
cursor: isProcessing ? 'wait' : 'pointer',
transition: 'all 0.3s transform',
transform: isListening ? 'scale(1.1)' : 'scale(1)',
animation: isListening ? 'pulse 1.5s infinite' : 'none',
opacity: error ? 0.5 : 1
}}
title={error || "Speak to add transaction"}
>
{isProcessing ? (
<Loader className="animate-spin" size={24} />
) : isListening ? (
<MicOff size={24} />
) : (
<Mic size={24} />
)}
</button>
{/* Voice Tip */}
<div style={{
position: 'absolute',
top: '100%',
right: '50%',
transform: 'translateX(50%)',
marginTop: '8px',
background: 'rgba(0,0,0,0.7)',
color: 'white',
padding: '4px 8px',
borderRadius: '4px',
fontSize: '10px',
whiteSpace: 'nowrap',
pointerEvents: 'none'
}}>
Add Expense/Income for Amount
</div>
<style>{`
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7); }
70% { box-shadow: 0 0 0 15px rgba(239, 68, 68, 0); }
100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); }
}
`}</style>
</div>
);
};
export default VoiceInput;