import { useState, useRef, useEffect } from 'react'; const BACKEND_URL = import.meta.env.VITE_BACKEND_URL || (import.meta.env.DEV ? 'http://localhost:8000' : ''); export default function VoiceControl({ onReply, chatMessages, visible }) { const [isListening, setIsListening] = useState(false); const [transcript, setTranscript] = useState(''); const [inputText, setInputText] = useState(''); const [isThinking, setIsThinking] = useState(false); const [isSpeaking, setIsSpeaking] = useState(false); const recognitionRef = useRef(null); const audioRef = useRef(null); const chatEndRef = useRef(null); const inputRef = useRef(null); const hasSpeechAPI = !!(window.SpeechRecognition || window.webkitSpeechRecognition); // Scroll to bottom when new messages arrive useEffect(() => { chatEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [chatMessages, isThinking]); // Focus input when panel becomes visible useEffect(() => { if (visible) { setTimeout(() => inputRef.current?.focus(), 300); } }, [visible]); // Setup speech recognition useEffect(() => { const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; if (!SpeechRecognition) return; const recognition = new SpeechRecognition(); recognition.lang = 'fr-FR'; recognition.continuous = false; recognition.interimResults = true; recognition.onresult = (event) => { let interim = ''; let final = ''; for (let i = 0; i < event.results.length; i++) { if (event.results[i].isFinal) { final += event.results[i][0].transcript; } else { interim += event.results[i][0].transcript; } } const text = final || interim; setTranscript(text); setInputText(text); }; recognition.onend = () => { setIsListening(false); }; recognition.onerror = (event) => { console.error('STT error:', event.error); setIsListening(false); }; recognitionRef.current = recognition; }, []); // Auto-send when recognition ends with a final transcript const pendingSendRef = useRef(false); useEffect(() => { if (!isListening && pendingSendRef.current && transcript.trim() && !isThinking) { pendingSendRef.current = false; sendMessage(transcript.trim()); } }, [isListening]); const toggleMic = () => { if (isListening) { pendingSendRef.current = true; recognitionRef.current?.stop(); return; } setTranscript(''); setInputText(''); pendingSendRef.current = true; recognitionRef.current?.start(); setIsListening(true); }; const handleSubmit = (e) => { e.preventDefault(); const text = inputText.trim(); if (!text || isThinking) return; sendMessage(text); }; const sendMessage = async (text) => { setIsThinking(true); setInputText(''); setTranscript(''); onReply?.({ role: 'user', text }); try { const res = await fetch(`${BACKEND_URL}/api/chat`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: text }), }); // Read SSE stream for chat reply const reader = res.body.getReader(); const decoder = new TextDecoder(); let replyText = ''; let buffer = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop() || ''; for (const line of lines) { if (line.startsWith('data:') || line.startsWith('data: ')) { try { const jsonStr = line.replace(/^data:\s*/, ''); const data = JSON.parse(jsonStr); if (data.type === 'chat_reply') { replyText = data.text; } } catch { /* skip non-JSON lines */ } } } } if (replyText) { onReply?.({ role: 'assistant', text: replyText }); await speakReply(replyText); } } catch (err) { console.error('Chat error:', err); onReply?.({ role: 'assistant', text: 'Erreur de connexion. Reessaie.' }); } finally { setIsThinking(false); } }; const speakReply = async (text) => { try { setIsSpeaking(true); const res = await fetch(`${BACKEND_URL}/api/tts`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text }), }); if (!res.ok) { setIsSpeaking(false); return; } const blob = await res.blob(); const url = URL.createObjectURL(blob); const audio = new Audio(url); audioRef.current = audio; audio.onended = () => { setIsSpeaking(false); URL.revokeObjectURL(url); }; await audio.play(); } catch (err) { console.error('TTS error:', err); setIsSpeaking(false); } }; if (!visible) return null; return (
Dis-moi par quoi tu veux commencer, ou pose-moi une question sur tes projets.
)} {chatMessages && chatMessages.map((msg, i) => (