"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 - improved logic // If user writes ANY English word, switch to English const spanishPatterns = /^(hola|gracias|por| favor|quiero|necesito|reserva|precio|dónde|cuándo|cómo|cuánto|tengo|busco|qué|es|son|hay|me|puedo)\s/i; const englishPatterns = /^(hello|thanks|please|want|need|book|price|where|how|much|have|can|i|my|your|the|a|an|is|are|do|does|what|who|when|where|why|how)\s/i; let detectedLang: Lang = language; // Check if it's clearly English (more inclusive) const isEnglish = englishPatterns.test(userText.toLowerCase()); const isSpanish = spanishPatterns.test(userText.toLowerCase()); // If user writes in English (more than 50% English letters or explicit English words) if (!isSpanish && (isEnglish || /[a-z]{3,}/i.test(userText))) { // More English characters or explicit English = English const englishChars = (userText.match(/[a-zA-Z]/g) || []).length; const spanishChars = (userText.match(/[áéíóúñü]/gi) || []).length; if (englishChars > spanishChars * 2 || isEnglish) { detectedLang = "en"; } } else if (isSpanish) { detectedLang = "es"; } // Update language if detected differently 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" ? "Lo siento, tengo problemas técnicos. ¿Podrías escribir tu mensaje?" : "Sorry, I'm having technical issues. Could you type your message?"; setMessages(prev => [...prev, { role: "assistant", content: fallback }]); setError("API error - check API key"); 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
{/* 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; } }