From 8bc904523a9b8207ebd349c2cd7b39bb5aa4f4e5 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 24 Feb 2026 15:13:54 +0000 Subject: [PATCH] Replace Vapi with Synthflow widget --- components/SiteMenteVoiceWidget.tsx | 263 ++++++++-------------------- 1 file changed, 77 insertions(+), 186 deletions(-) diff --git a/components/SiteMenteVoiceWidget.tsx b/components/SiteMenteVoiceWidget.tsx index 12a4806..8ddedac 100644 --- a/components/SiteMenteVoiceWidget.tsx +++ b/components/SiteMenteVoiceWidget.tsx @@ -1,10 +1,6 @@ "use client"; import { useState, useRef, useEffect } from "react"; -import Vapi from "@vapi-ai/web"; - -const VAPI_PUBLIC_KEY = "ee086729-fc5c-447e-9a40-840f44bc006b"; -const ASSISTANT_ID = "92630ca5-e165-4360-bce0-dd8730882569"; interface SiteMenteVoiceWidgetProps { businessName?: string; @@ -13,104 +9,38 @@ interface SiteMenteVoiceWidgetProps { initialLang?: string; } -type Mode = "text" | "voice"; +type Mode = "synthflow" | "text" | "off"; + +const SYNTHFLOW_WIDGET_ID = "0ee1b79c-43c2-41e0-aa6a-d2a560e0ca6a"; export default function SiteMenteVoiceWidget({ businessName = "SiteMente", businessType = "default", theme = "dark", - initialLang = "es" + initialLang = "en" }: SiteMenteVoiceWidgetProps) { - const [mode, setMode] = useState("text"); - const [isActive, setIsActive] = useState(false); - const [status, setStatus] = useState<"idle" | "connecting" | "active" | "error">("idle"); - const [transcript, setTranscript] = useState(""); - const [errorMsg, setErrorMsg] = useState(""); + const [mode, setMode] = useState("off"); + const [showChat, setShowChat] = useState(false); + const [language, setLanguage] = useState<'en' | 'es'>(initialLang as 'en' | 'es'); // Text chat state const [messages, setMessages] = useState<{role: "user" | "assistant", content: string}[]>([]); const [input, setInput] = useState(""); const [isSending, setIsSending] = useState(false); - const vapiRef = useRef(null); const messagesEndRef = useRef(null); - // Auto-hide chat when switching to voice mode - useEffect(() => { - if (mode === "voice") { - setShowChat(false); - } - }, [mode]); + // Scroll to bottom of messages useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages]); - // Voice mode functions - const startCall = async () => { - try { - console.log("Starting voice call..."); - setErrorMsg(""); - setStatus("connecting"); - - // Verify mic - don't block if mic check fails, let Vapi handle it - try { - const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); - console.log("✅ Mic stream created"); - // Stop the stream after checking - we don't need it, Vapi will create its own - stream.getTracks().forEach(track => track.stop()); - } catch (micErr) { - console.log("⚠️ Mic check failed, continuing anyway:", micErr); - // Don't return - let Vapi try to start the call - } - - const vapi = new Vapi(VAPI_PUBLIC_KEY); - vapiRef.current = vapi; - - vapi.on("error", (error: any) => { - console.log("Vapi error:", error); - const msg = String(error?.message || error?.error?.message || "Voice call failed"); - setErrorMsg(msg); - setStatus("error"); - setIsActive(false); - }); - - vapi.on("call-start", () => { - console.log("✅ Call started!"); - setStatus("active"); - }); - - vapi.on("call-end", () => { - console.log("Call ended"); - setStatus("idle"); - setIsActive(false); - }); - - vapi.on("transcript", (transcript: any) => { - const text = typeof transcript === "string" ? transcript : transcript?.text || ""; - setTranscript(text); - }); - - await vapi.start(ASSISTANT_ID); - setIsActive(true); - } catch (error: any) { - console.log("Start error:", error); - const msg = String(error?.message || "Failed to start call"); - setErrorMsg(msg); - setStatus("error"); - } - }; - - const endCall = async () => { - try { - if (vapiRef.current) { - await vapiRef.current.stop(); - } - setIsActive(false); - setStatus("idle"); - setTranscript(""); - } catch (error) { - console.error("End call error:", error); - } + // Toggle modes + const cycleMode = () => { + const modes: Mode[] = ["off", "text", "synthflow"]; + const currentIndex = modes.indexOf(mode); + const nextIndex = (currentIndex + 1) % modes.length; + setMode(modes[nextIndex]); }; // Text mode functions @@ -121,15 +51,23 @@ export default function SiteMenteVoiceWidget({ setInput(""); setIsSending(true); - // Add user message setMessages(prev => [...prev, { role: "user", content: userMessage }]); try { - // TODO: Replace with actual AI API call - // For now, simulate response await new Promise(resolve => setTimeout(resolve, 1000)); - // Spanish responses + const englishResponses: Record = { + "hello": `Hello! 👋 I'm the AI assistant for ${businessName}. How can I help you today?`, + "hours": `We're open Monday to Sunday. Do you have a specific question?`, + "book": "I can help you book right now. What service are you interested in?", + "contact": `You can call us at +34 XXX XXX XXX or message us here.`, + "price": "We offer packages starting from €299/month. Would you like me to tell you more?", + "menu": "We serve delicious food daily. Would you like to see our menu or make a reservation?", + "reservation": "I can help you make a reservation! What date and time works for you?", + "thanks": "You're welcome! Is there anything else I can help you with?", + "thank you": "You're welcome! Is there anything else I can help you with?", + }; + const spanishResponses: Record = { "hola": `¡Hola! 👋 Soy el asistente de ${businessName}. ¿En qué puedo ayudarte hoy?`, "horario": `Nuestros horarios de atención son de lunes a domingo. ¿Tienes alguna pregunta específica?`, @@ -137,38 +75,16 @@ export default function SiteMenteVoiceWidget({ "contacto": `Puedes llamarnos al +34 XXX XXX XXX o escribirnos aquí.`, }; - // English responses - const englishResponses: Record = { - "hello": `Hello! 👋 I'm the AI assistant for ${businessName}. How can I help you today?`, - "hours": `We're open Monday to Sunday. Do you have a specific question?`, - "book": "I can help you book right now. What service are you interested in?", - "contact": `You can call us at +34 XXX XXX XXX or message us here.`, - "price": "We offer packages starting from €299/month. Would you like me to tell you more?", - "cost": "We offer packages starting from €299/month. Would you like me to tell you more?", - "menu": "We serve delicious food daily. Would you like to see our menu or make a reservation?", - "reservation": "I can help you make a reservation! What date and time works for you?", - "thanks": "You're welcome! Is there anything else I can help you with?", - "thank you": "You're welcome! Is there anything else I can help you with?", - }; - - // Default responses - const spanishDefault = "Gracias por tu mensaje. Un miembro de nuestro equipo te responderá pronto. ¿Hay algo específico en lo que pueda ayudarte?"; - const englishDefault = "Thanks for your message! A team member will get back to you shortly. Is there anything specific I can help you with?"; - const lowerInput = userMessage.toLowerCase(); + const spanishKeywords = ['hola', 'gracias', 'si', 'no', 'por favor', 'quiero', 'necesito', 'reserva', 'horario', 'precio', 'contacto']; + const isSpanish = spanishKeywords.some(keyword => lowerInput.includes(keyword)); - // Detect language - use manual toggle first, then auto-detect - const spanishKeywords = ['hola', 'gracias', 'si', 'no', 'por favor', 'quiero', 'necesito', 'reserva', 'horario', 'precio', 'contacto', 'dónde', 'cuándo']; - const isSpanishInput = spanishKeywords.some(keyword => lowerInput.includes(keyword)); - const isEnglishInput = /^(hello|hi|hey|thanks|yes|no|please|i want|i need|book|hours|price|contact|where|when)/i.test(lowerInput); - - // Use language toggle, or fallback to detection - const useEnglish = language === 'en' || (!isSpanishInput && isEnglishInput); - const responses = useEnglish ? englishResponses : spanishResponses; - const defaultResponse = useEnglish ? englishDefault : spanishDefault; + const responses = (language === 'es' || isSpanish) ? spanishResponses : englishResponses; + const defaultResponse = language === 'es' + ? "Gracias por tu mensaje. Un miembro de nuestro equipo te responderá pronto." + : "Thanks for your message! A team member will get back to you shortly."; let response = defaultResponse; - for (const [key, value] of Object.entries(responses)) { if (lowerInput.includes(key)) { response = value; @@ -179,7 +95,6 @@ export default function SiteMenteVoiceWidget({ setMessages(prev => [...prev, { role: "assistant", content: response }]); } catch (error) { console.error("Send error:", error); - setMessages(prev => [...prev, { role: "assistant", content: "Lo siento, hubo un error. Inténtalo de nuevo." }]); } finally { setIsSending(false); } @@ -192,23 +107,24 @@ export default function SiteMenteVoiceWidget({ } }; - const toggleMode = () => { - // End any active call when switching - if (isActive) { - endCall(); - } - setShowChat(mode === "voice"); // Show chat when switching to text - setMode(prev => prev === "text" ? "voice" : "text"); - }; - - // Add state for chat panel visibility - const [language, setLanguage] = useState<'en' | 'es'>('en') - 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"; + // Get mode icon + const getModeIcon = () => { + if (mode === "synthflow") return "🎙️"; + if (mode === "text") return "💬"; + return "⚪"; + }; + + const getModeLabel = () => { + if (mode === "synthflow") return "AI Voice"; + if (mode === "text") return "Chat"; + return "Off"; + }; + return (
{/* Language Toggle */} @@ -222,36 +138,32 @@ export default function SiteMenteVoiceWidget({ {/* Mode Toggle Button */} - {/* Error Message */} - {status === "error" && errorMsg && ( -
- ⚠️ {errorMsg} + {/* Synthflow Widget */} + {mode === "synthflow" && ( +
+