import React, { useState, useEffect, useRef } from "react"; import { FiSend, FiUser } from "react-icons/fi"; import { FaMicrophone, FaStop } from "react-icons/fa"; import { motion, AnimatePresence } from "framer-motion"; import { v4 as uuidv4 } from "uuid"; import { API_LLM, API_TTS, WS_URL } from "../config/api"; export default function AssistantChat({ onAudioGenerated, audioRef }) { const [messages, setMessages] = useState([]); const [inputText, setInputText] = useState(""); const [isLoading, setIsLoading] = useState(false); const [recording, setRecording] = useState(false); const [transcript, setTranscript] = useState(""); const [responseText, setResponseText] = useState(""); const [isProcessing, setIsProcessing] = useState(false); const [history, setHistory] = useState([]); const [userId] = useState(uuidv4()); const audioContextRef = useRef(null); const mediaStreamRef = useRef(null); const wsRef = useRef(null); const messagesEndRef = useRef(null); const isStoppingRef = useRef(false); const workletNodeRef = useRef(null); useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); return () => stopRecording(); }, [messages]); const sendMessage = async () => { const userMsg = inputText.trim(); if (!userMsg || isLoading) return; setIsLoading(true); setMessages((prev) => [...prev, { sender: "user", text: userMsg, timestamp: new Date() }]); setInputText(""); try { const updatedHistory = [...history, { role: "user", content: userMsg }]; const llmResp = await fetch(API_LLM, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text: userMsg, user_id: userId, history: updatedHistory }), }); const llmData = await llmResp.json(); if (!llmResp.ok) { throw new Error(llmData.detail || "Erreur serveur LLM"); } const assistantText = llmData?.response || "Je n'ai pas compris."; const lang = llmData?.lang || "fr"; const newHistory = llmData?.history || updatedHistory; console.log("Requête texte envoyée:", { text: userMsg, user_id: userId, history: updatedHistory }); console.log("Réponse LLM reçue:", llmData); setMessages((prev) => [...prev, { sender: "assistant", text: assistantText, timestamp: new Date() }]); setHistory(newHistory); const ttsResp = await fetch(API_TTS, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text: assistantText, lang }), }); const ttsData = await ttsResp.json(); const audioUrl = ttsData?.url; if (audioUrl) { onAudioGenerated?.(audioUrl); if (audioRef?.current) { audioRef.current.src = audioUrl; audioRef.current.crossOrigin = "anonymous"; await audioRef.current.play().catch((err) => { console.warn("Erreur lecture audio:", err); }); } } } catch (err) { console.warn("Erreur LLM/TTS:", err); setMessages((prev) => [...prev, { sender: "assistant", text: "Erreur serveur.", timestamp: new Date() }]); } finally { setIsLoading(false); } }; const handleKeyDown = (e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); sendMessage(); } }; const startRecording = async () => { if (recording || isProcessing) return; isStoppingRef.current = false; try { setTranscript(""); setResponseText(""); setIsProcessing(true); const audioContext = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 16000, }); audioContextRef.current = audioContext; const stream = await navigator.mediaDevices.getUserMedia({ audio: { channelCount: 1, echoCancellation: true, noiseSuppression: true, autoGainControl: true }, }); mediaStreamRef.current = stream; const source = audioContext.createMediaStreamSource(stream); await audioContext.audioWorklet.addModule("/pcm-worklet.js").catch((err) => { throw new Error("Échec chargement pcm-worklet.js: " + err.message); }); const recorderNode = new AudioWorkletNode(audioContext, "pcm-processor", { processorOptions: { sampleRate: 16000, inputSampleRate: audioContext.sampleRate, chunkSize: 1024, silenceThreshold: 0.01, silenceWindow: 3, }, }); workletNodeRef.current = recorderNode; source.connect(recorderNode); const ws = new WebSocket(`${WS_URL}/live_stream/${userId}?room_id=room1`); ws.binaryType = "arraybuffer"; wsRef.current = ws; ws.onopen = () => { ws.send(JSON.stringify({ type: "start", sampleRate: 16000, format: "pcm16", channels: 1 })); setRecording(true); setIsProcessing(false); }; ws.onmessage = async (event) => { const msg = event.data; console.log("WebSocket message:", msg); if (typeof msg === "string") { const langRegex = new RegExp(`${userId}\\((fr|en|ar)\\): (.+)`); const match = msg.match(langRegex); if (match) { const trans = match[2].trim(); if (trans) { setTranscript((prev) => prev + trans + " "); setMessages((prev) => [...prev, { sender: "user", text: trans, timestamp: new Date() }]); setHistory((prev) => [...prev, { role: "user", content: trans }]); } } else if (msg.startsWith("Système: ")) { const resp = msg.replace("Système: ", "").trim(); if (resp) { setResponseText((prev) => prev + resp + " "); setMessages((prev) => [...prev, { sender: "assistant", text: resp, timestamp: new Date() }]); setHistory((prev) => [...prev, { role: "assistant", content: resp }]); } } else { try { const data = JSON.parse(msg); if (data.type === "pong") return; if (data.type === "stopped") { safeStopRecording(); return; } if (data.type === "error" && data.message?.trim()) { console.error("Erreur WebSocket:", data.message); // Log l'erreur dans la console setMessages((prev) => [...prev, { sender: "assistant", text: data.message.trim(), timestamp: new Date() }]); setHistory((prev) => [...prev, { role: "assistant", content: data.message.trim() }]); } else if (data.message?.trim()) { setResponseText((prev) => prev + data.message + " "); setMessages((prev) => [...prev, { sender: "assistant", text: data.message.trim(), timestamp: new Date() }]); setHistory((prev) => [...prev, { role: "assistant", content: data.message.trim() }]); } } catch {} } } else if (msg instanceof ArrayBuffer || msg instanceof Blob) { const blob = msg instanceof Blob ? msg : new Blob([msg], { type: "audio/wav" }); const url = URL.createObjectURL(blob); if (audioRef?.current) { audioRef.current.src = url; await audioRef.current.play().catch((err) => { console.warn("Erreur lecture audio:", err); }); } } }; ws.onclose = (e) => { if (!isStoppingRef.current && e.code !== 1000) { setTimeout(startRecording, 1000); } safeStopRecording(); }; ws.onerror = (err) => { console.warn("Erreur WebSocket:", err); safeStopRecording(); }; recorderNode.port.onmessage = (e) => { const { buffer } = e.data || {}; if (buffer && ws.readyState === WebSocket.OPEN && !isStoppingRef.current) { ws.send(buffer); } }; wsRef.current.pingInterval = setInterval(() => { if (ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: "ping" })); } }, 20000); } catch (err) { console.warn("Erreur enregistrement:", err); console.error("Micro inaccessible:", err?.message || "Erreur inconnue"); safeStopRecording(); } }; const safeStopRecording = () => { if (isStoppingRef.current) return; isStoppingRef.current = true; try { workletNodeRef.current?.port?.postMessage({ type: "flush" }); } catch {} try { if (wsRef.current?.readyState === WebSocket.OPEN) { wsRef.current.send(JSON.stringify({ type: "stop" })); } } catch {} if (wsRef.current?.pingInterval) clearInterval(wsRef.current.pingInterval); try { mediaStreamRef.current?.getTracks()?.forEach((t) => t.stop()); } catch {} mediaStreamRef.current = null; try { audioContextRef.current?.close(); } catch {} audioContextRef.current = null; if (transcript.trim() && !messages.some((msg) => msg.sender === "user" && msg.text === transcript.trim())) { setMessages((prev) => [...prev, { sender: "user", text: transcript.trim(), timestamp: new Date() }]); setHistory((prev) => [...prev, { role: "user", content: transcript.trim() }]); } setRecording(false); setIsProcessing(false); setTranscript(""); setResponseText(""); workletNodeRef.current = null; wsRef.current = null; }; const stopRecording = () => { if (wsRef.current?.readyState === WebSocket.OPEN) { wsRef.current.send(JSON.stringify({ type: "stop" })); } }; const formatTime = (date) => date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); return (