Replace Vapi with Synthflow widget
This commit is contained in:
@@ -1,10 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useRef, useEffect } from "react";
|
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 {
|
interface SiteMenteVoiceWidgetProps {
|
||||||
businessName?: string;
|
businessName?: string;
|
||||||
@@ -13,104 +9,38 @@ interface SiteMenteVoiceWidgetProps {
|
|||||||
initialLang?: string;
|
initialLang?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Mode = "text" | "voice";
|
type Mode = "synthflow" | "text" | "off";
|
||||||
|
|
||||||
|
const SYNTHFLOW_WIDGET_ID = "0ee1b79c-43c2-41e0-aa6a-d2a560e0ca6a";
|
||||||
|
|
||||||
export default function SiteMenteVoiceWidget({
|
export default function SiteMenteVoiceWidget({
|
||||||
businessName = "SiteMente",
|
businessName = "SiteMente",
|
||||||
businessType = "default",
|
businessType = "default",
|
||||||
theme = "dark",
|
theme = "dark",
|
||||||
initialLang = "es"
|
initialLang = "en"
|
||||||
}: SiteMenteVoiceWidgetProps) {
|
}: SiteMenteVoiceWidgetProps) {
|
||||||
const [mode, setMode] = useState<Mode>("text");
|
const [mode, setMode] = useState<Mode>("off");
|
||||||
const [isActive, setIsActive] = useState(false);
|
const [showChat, setShowChat] = useState(false);
|
||||||
const [status, setStatus] = useState<"idle" | "connecting" | "active" | "error">("idle");
|
const [language, setLanguage] = useState<'en' | 'es'>(initialLang as 'en' | 'es');
|
||||||
const [transcript, setTranscript] = useState("");
|
|
||||||
const [errorMsg, setErrorMsg] = useState<string>("");
|
|
||||||
|
|
||||||
// Text chat state
|
// Text chat state
|
||||||
const [messages, setMessages] = useState<{role: "user" | "assistant", content: string}[]>([]);
|
const [messages, setMessages] = useState<{role: "user" | "assistant", content: string}[]>([]);
|
||||||
const [input, setInput] = useState("");
|
const [input, setInput] = useState("");
|
||||||
const [isSending, setIsSending] = useState(false);
|
const [isSending, setIsSending] = useState(false);
|
||||||
|
|
||||||
const vapiRef = useRef<any>(null);
|
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// Auto-hide chat when switching to voice mode
|
// Scroll to bottom of messages
|
||||||
useEffect(() => {
|
|
||||||
if (mode === "voice") {
|
|
||||||
setShowChat(false);
|
|
||||||
}
|
|
||||||
}, [mode]);
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||||
}, [messages]);
|
}, [messages]);
|
||||||
|
|
||||||
// Voice mode functions
|
// Toggle modes
|
||||||
const startCall = async () => {
|
const cycleMode = () => {
|
||||||
try {
|
const modes: Mode[] = ["off", "text", "synthflow"];
|
||||||
console.log("Starting voice call...");
|
const currentIndex = modes.indexOf(mode);
|
||||||
setErrorMsg("");
|
const nextIndex = (currentIndex + 1) % modes.length;
|
||||||
setStatus("connecting");
|
setMode(modes[nextIndex]);
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Text mode functions
|
// Text mode functions
|
||||||
@@ -121,15 +51,23 @@ export default function SiteMenteVoiceWidget({
|
|||||||
setInput("");
|
setInput("");
|
||||||
setIsSending(true);
|
setIsSending(true);
|
||||||
|
|
||||||
// Add user message
|
|
||||||
setMessages(prev => [...prev, { role: "user", content: userMessage }]);
|
setMessages(prev => [...prev, { role: "user", content: userMessage }]);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// TODO: Replace with actual AI API call
|
|
||||||
// For now, simulate response
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
// Spanish responses
|
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> = {
|
const spanishResponses: Record<string, string> = {
|
||||||
"hola": `¡Hola! 👋 Soy el asistente de ${businessName}. ¿En qué puedo ayudarte hoy?`,
|
"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?`,
|
"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í.`,
|
"contacto": `Puedes llamarnos al +34 XXX XXX XXX o escribirnos aquí.`,
|
||||||
};
|
};
|
||||||
|
|
||||||
// English responses
|
|
||||||
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?",
|
|
||||||
"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 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 responses = (language === 'es' || isSpanish) ? spanishResponses : englishResponses;
|
||||||
const spanishKeywords = ['hola', 'gracias', 'si', 'no', 'por favor', 'quiero', 'necesito', 'reserva', 'horario', 'precio', 'contacto', 'dónde', 'cuándo'];
|
const defaultResponse = language === 'es'
|
||||||
const isSpanishInput = spanishKeywords.some(keyword => lowerInput.includes(keyword));
|
? "Gracias por tu mensaje. Un miembro de nuestro equipo te responderá pronto."
|
||||||
const isEnglishInput = /^(hello|hi|hey|thanks|yes|no|please|i want|i need|book|hours|price|contact|where|when)/i.test(lowerInput);
|
: "Thanks for your message! A team member will get back to you shortly.";
|
||||||
|
|
||||||
// Use language toggle, or fallback to detection
|
|
||||||
const useEnglish = language === 'en' || (!isSpanishInput && isEnglishInput);
|
|
||||||
const responses = useEnglish ? englishResponses : spanishResponses;
|
|
||||||
const defaultResponse = useEnglish ? englishDefault : spanishDefault;
|
|
||||||
|
|
||||||
let response = defaultResponse;
|
let response = defaultResponse;
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(responses)) {
|
for (const [key, value] of Object.entries(responses)) {
|
||||||
if (lowerInput.includes(key)) {
|
if (lowerInput.includes(key)) {
|
||||||
response = value;
|
response = value;
|
||||||
@@ -179,7 +95,6 @@ export default function SiteMenteVoiceWidget({
|
|||||||
setMessages(prev => [...prev, { role: "assistant", content: response }]);
|
setMessages(prev => [...prev, { role: "assistant", content: response }]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Send error:", error);
|
console.error("Send error:", error);
|
||||||
setMessages(prev => [...prev, { role: "assistant", content: "Lo siento, hubo un error. Inténtalo de nuevo." }]);
|
|
||||||
} finally {
|
} finally {
|
||||||
setIsSending(false);
|
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 buttonColor = theme === "dark" ? "bg-brand-pink" : "bg-blue-600";
|
||||||
const bgColor = theme === "dark" ? "bg-[#1a1625]" : "bg-white";
|
const bgColor = theme === "dark" ? "bg-[#1a1625]" : "bg-white";
|
||||||
const textColor = theme === "dark" ? "text-white" : "text-gray-900";
|
const textColor = theme === "dark" ? "text-white" : "text-gray-900";
|
||||||
const inputBg = theme === "dark" ? "bg-white/10" : "bg-gray-100";
|
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 (
|
return (
|
||||||
<div className="fixed bottom-6 right-6 z-50">
|
<div className="fixed bottom-6 right-6 z-50">
|
||||||
{/* Language Toggle */}
|
{/* Language Toggle */}
|
||||||
@@ -222,36 +138,32 @@ export default function SiteMenteVoiceWidget({
|
|||||||
|
|
||||||
{/* Mode Toggle Button */}
|
{/* Mode Toggle Button */}
|
||||||
<button
|
<button
|
||||||
onClick={toggleMode}
|
onClick={cycleMode}
|
||||||
className={`absolute bottom-16 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`}
|
className={`absolute bottom-16 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={mode === "text" ? "Switch to Voice" : "Switch to Text"}
|
title={mode === "off" ? "Enable Chat" : "Switch Mode"}
|
||||||
>
|
>
|
||||||
{mode === "text" ? (
|
{getModeIcon()} {getModeLabel()}
|
||||||
<>
|
|
||||||
<svg className="w-3.5 h-3.5" 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>
|
|
||||||
Voice
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<svg className="w-3.5 h-3.5" 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>
|
|
||||||
Text
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Error Message */}
|
{/* Synthflow Widget */}
|
||||||
{status === "error" && errorMsg && (
|
{mode === "synthflow" && (
|
||||||
<div className="absolute bottom-28 right-0 w-64 bg-red-600 text-white text-xs p-2 rounded-lg mb-2">
|
<div className="absolute bottom-16 right-0 w-[400px] h-[550px] mb-2 rounded-xl overflow-hidden shadow-2xl">
|
||||||
⚠️ {errorMsg}
|
<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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Chat Panel - Text Mode */}
|
{/* Text Chat Panel */}
|
||||||
{mode === "text" && showChat && (
|
{mode === "text" && (
|
||||||
<div className={`absolute bottom-16 right-0 w-80 ${bgColor} border border-white/20 rounded-xl shadow-2xl mb-2 overflow-hidden`}>
|
<div className={`absolute bottom-16 right-0 w-80 ${bgColor} border border-white/20 rounded-xl shadow-2xl mb-2 overflow-hidden`}>
|
||||||
{/* Header */}
|
{/* 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 justify-between p-3 border-b ${theme === "dark" ? "border-white/10" : "border-gray-200"}`}>
|
||||||
@@ -297,7 +209,7 @@ export default function SiteMenteVoiceWidget({
|
|||||||
value={input}
|
value={input}
|
||||||
onChange={(e) => setInput(e.target.value)}
|
onChange={(e) => setInput(e.target.value)}
|
||||||
onKeyPress={handleKeyPress}
|
onKeyPress={handleKeyPress}
|
||||||
placeholder="Escribe tu mensaje..."
|
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`}
|
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}
|
disabled={isSending}
|
||||||
/>
|
/>
|
||||||
@@ -315,49 +227,28 @@ export default function SiteMenteVoiceWidget({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Voice Mode Panel */}
|
|
||||||
{mode === "voice" && isActive && (
|
|
||||||
<div className={`absolute bottom-16 right-0 w-80 ${bgColor} border border-white/20 rounded-xl p-4 shadow-2xl mb-2`}>
|
|
||||||
<div className="flex items-center justify-between mb-2">
|
|
||||||
<span className="text-sm font-medium ${textColor}">🎙️ Voice Chat</span>
|
|
||||||
<span className={`w-2 h-2 rounded-full ${status === "active" ? "bg-green-500 animate-pulse" : "bg-yellow-500"}`}></span>
|
|
||||||
</div>
|
|
||||||
<div className={`h-32 overflow-y-auto text-sm ${theme === "dark" ? "text-white/70" : "text-gray-600"} bg-white/5 rounded-lg p-2`}>
|
|
||||||
{transcript || "Listening..."}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Main Button */}
|
{/* Main Button */}
|
||||||
<button
|
<button
|
||||||
onClick={mode === "voice" ? (isActive ? endCall : startCall) : () => setShowChat(!showChat)}
|
onClick={cycleMode}
|
||||||
className={`${buttonColor} w-14 h-14 rounded-full shadow-lg flex items-center justify-center transition-all hover:scale-110 ${
|
className={`${buttonColor} w-14 h-14 rounded-full shadow-lg flex items-center justify-center transition-all hover:scale-110 ${
|
||||||
mode === "voice" && isActive ? "animate-pulse ring-4 ring-red-500/50" : ""
|
mode === "synthflow" ? "animate-pulse ring-4 ring-green-500/50" : ""
|
||||||
}`}
|
}`}
|
||||||
title={mode === "voice" ? (isActive ? "End Call" : "Start Voice Chat") : (showChat ? "Close Chat" : "Open Chat")}
|
title={getModeLabel()}
|
||||||
>
|
>
|
||||||
{mode === "voice" ? (
|
{mode === "synthflow" ? (
|
||||||
isActive ? (
|
|
||||||
<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="M16 8l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2M5 3a2 2 0 00-2 2v1c0 8.284 6.716 15 15 15h1a2 2 0 002-2v-3.28a1 1 0 00-.684-.948l-4.493-1.498a1 1 0 00-1.21.502l-1.13 2.257a11.042 11.042 0 01-5.516-5.517l2.257-1.128a1 1 0 00.502-1.21L9.228 3.683A1 1 0 008.279 3H5z" />
|
|
||||||
</svg>
|
|
||||||
) : (
|
|
||||||
<svg className="w-6 h-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<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" />
|
<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>
|
</svg>
|
||||||
)
|
) : mode === "text" ? (
|
||||||
) : (
|
|
||||||
<svg className="w-6 h-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<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" />
|
<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>
|
</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="M12 4v16m8-8H4" />
|
||||||
|
</svg>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{mode === "voice" && status === "connecting" && (
|
|
||||||
<div className="absolute -top-8 right-0 bg-white/10 backdrop-blur px-3 py-1 rounded-full text-xs text-white">
|
|
||||||
Connecting...
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user