"use client"; import { useState, useRef, useEffect, useCallback } from "react"; interface MiniMaxVoiceWidgetProps { businessName?: string; businessType?: "restaurant" | "real-estate" | "clinic" | "car-rental" | "default"; theme?: "dark" | "light"; apiUrl?: string; enabled?: boolean; // Toggle on/off for testing } type Lang = "es" | "en"; // Exact greeting as per spec const SPANISH_GREETING = "Hola, soy el asistente de SiteMente. ¿En qué puedo ayudarte hoy?"; const ENGLISH_GREETING = "I can also speak English. How can I help you today?"; const SPANISH_MISUNDERSTAND = "No he entendido del todo, ¿podrías repetirlo o escribirlo, por favor?"; const ENGLISH_MISUNDERSTAND = "I didn't quite catch that. Could you repeat or type it, please?"; export default function MiniMaxVoiceWidget({ businessName = "SiteMente", businessType = "restaurant", theme = "dark", apiUrl = "/api/ai/voice-chat-v2", enabled = true }: MiniMaxVoiceWidgetProps) { const [isListening, setIsListening] = useState(false); const [isSpeaking, setIsSpeaking] = useState(false); const [messages, setMessages] = useState<{role: "user" | "assistant", content: string}[]>([]); const [language, setLanguage] = useState("es"); const [showChat, setShowChat] = useState(false); const [error, setError] = useState(null); const [isInitialized, setIsInitialized] = useState(false); const recognitionRef = useRef(null); const synthRef = useRef(null); const messagesEndRef = useRef(null); const inputRef = useRef(null); // Initialize speech APIs useEffect(() => { if (typeof window === "undefined" || !enabled) return; // Speech Recognition const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; if (SpeechRecognition) { recognitionRef.current = new SpeechRecognition(); recognitionRef.current.continuous = false; recognitionRef.current.interimResults = true; recognitionRef.current.lang = "es-ES"; recognitionRef.current.onresult = (event) => { const transcript = Array.from(event.results) .map(result => result[0].transcript) .join(""); if (event.results[0].isFinal && transcript.trim()) { handleUserInput(transcript); } }; recognitionRef.current.onerror = (event) => { console.error("Speech error:", event.error); setIsListening(false); if (event.error === "not-allowed") { setError("Microphone access denied. Please allow microphone access."); } else if (event.error !== "no-speech") { setError(`Speech error: ${event.error}`); } }; recognitionRef.current.onend = () => setIsListening(false); } // Speech Synthesis synthRef.current = window.speechSynthesis; return () => { recognitionRef.current?.stop(); synthRef.current?.cancel(); }; }, [enabled]); // Initialize with greeting useEffect(() => { if (enabled && !isInitialized) { setIsInitialized(true); setMessages([{ role: "assistant", content: SPANISH_GREETING }]); speak(SPANISH_GREETING); } }, [enabled]); // Scroll to bottom useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages]); // Focus input when chat opens useEffect(() => { if (showChat && inputRef.current) { inputRef.current.focus(); } }, [showChat]); // Speak function with exact greeting behavior const speak = useCallback((text: string) => { if (!synthRef.current || !enabled) return; synthRef.current.cancel(); const utterance = new SpeechSynthesisUtterance(text); utterance.lang = language === "es" ? "es-ES" : "en-US"; utterance.rate = 0.9; utterance.pitch = 1; utterance.volume = 1; utterance.onstart = () => setIsSpeaking(true); utterance.onend = () => setIsSpeaking(false); utterance.onerror = () => setIsSpeaking(false); synthRef.current.speak(utterance); }, [language, enabled]); // Handle user input const handleUserInput = async (text: string) => { if (!text.trim() || !enabled) return; const userText = text.trim(); setMessages(prev => [...prev, { role: "user", content: userText }]); setIsSpeaking(true); // Detect language const spanishWords = ["hola", "gracias", "por favor", "quiero", "necesito", "reserva", "precio", "dónde", "cuándo", "cómo", "cuánto", "tengo", "quiero", "busco", "necesito"]; const englishWords = ["hello", "thanks", "please", "want", "need", "book", "price", "where", "how", "much", "have", "looking"]; const isSpanish = spanishWords.some(w => userText.toLowerCase().includes(w)); const isEnglish = englishWords.some(w => userText.toLowerCase().includes(w)); let detectedLang: Lang = language; if (isSpanish && !isEnglish) detectedLang = "es"; else if (isEnglish && !isSpanish) detectedLang = "en"; else if (isSpanish && isEnglish && language === "es") detectedLang = "es"; if (detectedLang !== language) { setLanguage(detectedLang); } try { const response = await fetch(apiUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ message: userText, language: detectedLang, businessType, businessName, history: messages.slice(-4) }) }); if (!response.ok) throw new Error("API failed"); const data = await response.json(); const aiResponse = data.response; setMessages(prev => [...prev, { role: "assistant", content: aiResponse }]); speak(aiResponse); } catch (err) { console.error("API error:", err); const fallback = language === "es" ? SPANISH_MISUNDERSTAND : ENGLISH_MISUNDERSTAND; setMessages(prev => [...prev, { role: "assistant", content: fallback }]); speak(fallback); } }; // Toggle microphone const toggleListening = () => { if (!recognitionRef.current) { setError("Speech recognition not supported. Try Chrome."); return; } if (isListening) { recognitionRef.current.stop(); } else { setError(null); recognitionRef.current.lang = language === "es" ? "es-ES" : "en-US"; recognitionRef.current.start(); setIsListening(true); } }; // Handle text input const handleTextSubmit = (e: React.FormEvent) => { e.preventDefault(); const text = inputRef.current?.value; if (text?.trim()) { handleUserInput(text.trim()); if (inputRef.current) inputRef.current.value = ""; } }; // Theme const buttonColor = theme === "dark" ? "bg-brand-pink" : "bg-blue-600"; const bgColor = theme === "dark" ? "bg-[#1a1625]" : "bg-white"; const textColor = theme === "dark" ? "text-white" : "text-gray-900"; const inputBg = theme === "dark" ? "bg-white/10" : "bg-gray-100"; if (!enabled) return null; return (
{/* Main Button */} {/* Chat Panel */} {showChat && (
{/* Header */}
🤖 Asistente SiteMente
{language === "es" ? "ES" : "EN"}
{/* Messages */}
{messages.map((msg, i) => (
{msg.content}
))} {isListening && (
🎤 Listening...
)}
{/* Input */}
{/* Mic Button */}
{/* Error */} {error && (
{error}
)}
)}
); } declare global { interface Window { SpeechRecognition: typeof SpeechRecognition; webkitSpeechRecognition: typeof SpeechRecognition; } }