Files
sitemente/components/SiteMenteVoiceWidget.tsx
T
2026-02-24 15:17:48 +00:00

268 lines
11 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState, useRef, useEffect } from "react";
interface SiteMenteVoiceWidgetProps {
businessName?: string;
businessType?: "restaurant" | "real-estate" | "clinic" | "car-rental" | "default";
theme?: "dark" | "light";
initialLang?: string;
}
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 = "en"
}: SiteMenteVoiceWidgetProps) {
const [mode, setMode] = useState<Mode>("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 messagesEndRef = useRef<HTMLDivElement>(null);
// Scroll to bottom of messages
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages]);
// Toggle modes
const cycleMode = () => {
const modes: Mode[] = ["off", "text", "synthflow"];
const currentIndex = modes.indexOf(mode);
const nextIndex = (currentIndex + 1) % modes.length;
setModeInternal(modes[nextIndex]);
};
const setModeInternal = (newMode: Mode) => {
setMode(newMode);
};
// Text mode functions
const sendMessage = async () => {
if (!input.trim() || isSending) return;
const userMessage = input.trim();
setInput("");
setIsSending(true);
setMessages(prev => [...prev, { role: "user", content: userMessage }]);
try {
await new Promise(resolve => setTimeout(resolve, 1000));
const englishResponses: Record<string, string> = {
"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<string, string> = {
"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?`,
"reservar": "Para hacer una reserva, puedo ayudarte ahora mismo. ¿Qué servicio te interesa?",
"contacto": `Puedes llamarnos al +34 XXX XXX XXX o escribirnos aquí.`,
};
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));
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;
break;
}
}
setMessages(prev => [...prev, { role: "assistant", content: response }]);
} catch (error) {
console.error("Send error:", error);
} finally {
setIsSending(false);
}
};
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
};
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 (
<div className="fixed bottom-6 right-6 z-50">
{/* Language Toggle */}
<button
onClick={() => setLanguage(lang => lang === 'en' ? 'es' : 'en')}
className={`absolute bottom-28 right-0 ${buttonColor} px-3 py-1.5 rounded-full text-xs text-white shadow-lg mb-2 flex items-center gap-1.5 hover:scale-105 transition-transform`}
title="Toggle Language"
>
{language === 'en' ? '🇪🇸 ES' : '🇬🇧 EN'}
</button>
{/* Mode Toggle Buttons - Direct Selection */}
<div className="absolute bottom-16 right-0 flex flex-col gap-1 mb-2">
<button
onClick={() => setModeInternal("text")}
className={`${mode === "text" ? buttonColor : "bg-white/20"} px-3 py-1.5 rounded-full text-xs text-white shadow-lg flex items-center gap-1.5 hover:scale-105 transition-transform`}
title="Text Chat"
>
💬 Chat
</button>
<button
onClick={() => setModeInternal("synthflow")}
className={`${mode === "synthflow" ? "bg-green-500" : "bg-white/20"} px-3 py-1.5 rounded-full text-xs text-white shadow-lg flex items-center gap-1.5 hover:scale-105 transition-transform`}
title="AI Voice"
>
🎙 Voice
</button>
</div>
{/* Synthflow Widget */}
{mode === "synthflow" && (
<div className="absolute bottom-16 right-0 w-[400px] h-[550px] mb-2 rounded-xl overflow-hidden shadow-2xl">
<iframe
id="audio_iframe"
src={`https://widget.synthflow.ai/widget/v2/${SYNTHFLOW_WIDGET_ID}/1771945296284x399137457562280600`}
allow="microphone"
width="400px"
height="550px"
pointerEvents="auto"
scrolling="no"
style={{ background: "transparent", border: "none", zIndex: 999 }}
title="Synthflow AI Voice"
/>
</div>
)}
{/* Text Chat Panel */}
{mode === "text" && (
<div className={`absolute bottom-16 right-0 w-80 ${bgColor} border border-white/20 rounded-xl shadow-2xl mb-2 overflow-hidden`}>
{/* Header */}
<div className={`flex items-center justify-between p-3 border-b ${theme === "dark" ? "border-white/10" : "border-gray-200"}`}>
<div className="flex items-center gap-2">
<span className="text-sm font-medium ${textColor}">💬 {businessName}</span>
</div>
<span className="w-2 h-2 rounded-full bg-green-500"></span>
</div>
{/* Messages */}
<div className="h-64 overflow-y-auto p-3 space-y-3">
{messages.length === 0 && (
<div className="text-center text-sm text-gray-500 py-4">
👋 ¡Hola! Escríbeme para ayudarte
</div>
)}
{messages.map((msg, i) => (
<div key={i} className={`flex ${msg.role === "user" ? "justify-end" : "justify-start"}`}>
<div className={`max-w-[80%] rounded-lg px-3 py-2 text-sm ${
msg.role === "user"
? "bg-brand-pink text-white"
: theme === "dark" ? "bg-white/10 text-white" : "bg-gray-100 text-gray-900"
}`}>
{msg.content}
</div>
</div>
))}
{isSending && (
<div className="flex justify-start">
<div className={`max-w-[80%] rounded-lg px-3 py-2 text-sm ${theme === "dark" ? "bg-white/10 text-white/70" : "bg-gray-100 text-gray-500"}`}>
Escribiendo...
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
{/* Input */}
<div className={`p-3 border-t ${theme === "dark" ? "border-white/10" : "border-gray-200"}`}>
<div className="flex gap-2">
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyPress={handleKeyPress}
placeholder={language === 'es' ? "Escribe tu mensaje..." : "Type your message..."}
className={`flex-1 px-3 py-2 rounded-lg text-sm ${inputBg} ${textColor} placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-brand-pink`}
disabled={isSending}
/>
<button
onClick={sendMessage}
disabled={!input.trim() || isSending}
className={`${buttonColor} p-2 rounded-lg text-white disabled:opacity-50 disabled:cursor-not-allowed`}
>
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />
</svg>
</button>
</div>
</div>
</div>
)}
{/* Main Button - Toggle chat visibility when in text mode */}
<button
onClick={() => setShowChat(!showChat)}
className={`${buttonColor} w-14 h-14 rounded-full shadow-lg flex items-center justify-center transition-all hover:scale-110 ${
mode === "synthflow" ? "animate-pulse ring-4 ring-green-500/50" : ""
}`}
title={mode === "text" ? (showChat ? "Close" : "Open Chat") : "Click to enable"}
>
{mode === "synthflow" ? (
<svg className="w-6 h-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z" />
</svg>
) : showChat ? (
<svg className="w-6 h-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
) : (
<svg className="w-6 h-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
</svg>
)}
</button>
</div>
);
}