268 lines
11 KiB
TypeScript
268 lines
11 KiB
TypeScript
"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>
|
||
);
|
||
}
|