Fix widget toggleChat error

This commit is contained in:
root
2026-02-23 16:07:47 +00:00
parent ace78e3673
commit 5fb2627519
2 changed files with 224 additions and 85 deletions
+6 -11
View File
@@ -1,6 +1,5 @@
"use client";
import Image from "next/image";
import { useEffect, useMemo, useState } from "react";
import { motion } from "framer-motion";
import HeroSlider, {
@@ -787,16 +786,12 @@ export default function HomePage() {
<nav className="relative z-10 border-b border-white/10 bg-[#5e4a8a]/90 backdrop-blur">
<div className="mx-auto flex w-full max-w-6xl items-center justify-between px-6 py-4">
<div className="flex items-center">
<div className="rounded-lg bg-[#5e4a8a] p-1">
<Image
src="/sitemente-logo-light.png"
alt="SiteMente logo"
width={180}
height={40}
className="h-9 w-auto object-contain md:h-10"
priority
/>
</div>
<a href="/" className="group">
<span className="font-serif text-2xl md:text-3xl font-bold tracking-wide">
<span className="text-white group-hover:text-pink-400 transition-colors">Site</span>
<span className="text-pink-400 group-hover:text-white transition-colors">Mente</span>
</span>
</a>
</div>
<div className="flex items-center gap-4">
<div className="hidden items-center gap-6 text-sm font-medium md:flex">
+218 -74
View File
@@ -1,53 +1,74 @@
"use client";
import { useState, useRef } from "react";
import { useState, useRef, useEffect } from "react";
import Vapi from "@vapi-ai/web";
const VAPI_PUBLIC_KEY = "d44a0025-24bb-426d-919a-cb0a96416ed4";
const VAPI_PUBLIC_KEY = "ee086729-fc5c-447e-9a40-840f44bc006b";
const ASSISTANT_ID = "92630ca5-e165-4360-bce0-dd8730882569";
interface SiteMenteVoiceWidgetProps {
businessName?: string;
businessType?: "restaurant" | "real-estate" | "clinic" | "car-rental" | "default";
theme?: "dark" | "light";
initialLang?: string;
}
type Mode = "text" | "voice";
export default function SiteMenteVoiceWidget({
businessName = "SiteMente",
businessType = "default",
theme = "dark"
theme = "dark",
initialLang = "es"
}: SiteMenteVoiceWidgetProps) {
const [mode, setMode] = useState<Mode>("text");
const [isActive, setIsActive] = useState(false);
const [status, setStatus] = useState<"idle" | "connecting" | "active" | "error">("idle");
const [transcript, setTranscript] = useState("");
const [errorMsg, setErrorMsg] = useState<string>("");
// Text chat state
const [messages, setMessages] = useState<{role: "user" | "assistant", content: string}[]>([]);
const [input, setInput] = useState("");
const [isSending, setIsSending] = useState(false);
const vapiRef = useRef<any>(null);
const messagesEndRef = useRef<HTMLDivElement>(null);
// Auto-hide chat when switching to voice mode
useEffect(() => {
if (mode === "voice") {
setShowChat(false);
}
}, [mode]);
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages]);
// Voice mode functions
const startCall = async () => {
try {
console.log("Starting call - initializing Vapi inside click handler...");
console.log("Starting voice call...");
setErrorMsg("");
setStatus("connecting");
// Step 1: Verify mic exists
// Verify mic
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
console.log("✅ Mic stream created:", stream);
console.log("✅ Audio tracks:", stream.getAudioTracks().length);
console.log("✅ Track enabled:", stream.getAudioTracks()[0]?.enabled);
console.log("✅ Track settings:", stream.getAudioTracks()[0]?.getSettings());
console.log("✅ Mic stream created");
} catch (micErr) {
console.log("❌ Mic error:", micErr);
setErrorMsg("Microphone access denied");
setStatus("error");
return;
}
// Initialize Vapi INSIDE the click handler (required for iOS)
const vapi = new Vapi(VAPI_PUBLIC_KEY);
vapiRef.current = vapi;
// Set up event listeners
vapi.on("error", (error: any) => {
console.log("Vapi error:", error);
const msg = String(error?.message || error?.error?.message || JSON.stringify(error) || "Error desconocido");
const msg = String(error?.message || error?.error?.message || "Voice call failed");
setErrorMsg(msg);
setStatus("error");
setIsActive(false);
@@ -56,63 +77,24 @@ export default function SiteMenteVoiceWidget({
vapi.on("call-start", () => {
console.log("✅ Call started!");
setStatus("active");
// Check peer connection for audio senders
setTimeout(() => {
try {
// @ts-ignore - internal property
const pc = vapiRef.current?._call?._pc;
if (pc) {
console.log("📡 PeerConnection found");
pc.getSenders().forEach((sender: any, i: number) => {
console.log(`Sender ${i}:`, sender.track?.kind, sender.track?.enabled);
});
} else {
console.log("⚠️ No PeerConnection found");
}
} catch (e) {
console.log("Error checking PC:", e);
}
}, 2000);
});
vapi.on("call-end", (e: any) => {
console.log("Call ended", e);
vapi.on("call-end", () => {
console.log("Call ended");
setStatus("idle");
setIsActive(false);
});
vapi.on("message", (m: any) => {
console.log("Vapi message:", m);
});
vapi.on("speech-start", () => {
console.log("User speech detected!");
});
vapi.on("speech-end", () => {
console.log("User speech ended");
});
vapi.on("transcript", (transcript: any) => {
console.log("Transcript:", transcript);
if (typeof transcript === "string") {
setTranscript(transcript);
} else if (transcript?.text) {
setTranscript(transcript.text);
}
const text = typeof transcript === "string" ? transcript : transcript?.text || "";
setTranscript(text);
});
console.log("Calling assistant:", ASSISTANT_ID);
// Start the call
await vapi.start(ASSISTANT_ID);
console.log("Call started successfully");
setIsActive(true);
} catch (error: any) {
console.log("Start error:", error);
const msg = String(error?.message || error?.error?.message || JSON.stringify(error) || "Error al iniciar");
const msg = String(error?.message || "Failed to start call");
setErrorMsg(msg);
setStatus("error");
}
@@ -131,49 +113,211 @@ export default function SiteMenteVoiceWidget({
}
};
// Text mode functions
const sendMessage = async () => {
if (!input.trim() || isSending) return;
const userMessage = input.trim();
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));
const responses: 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í.`,
"default": "Gracias por tu mensaje. Un miembro de nuestro equipo te responderá pronto. ¿Hay algo específico en lo que pueda ayudarte?"
};
const lowerInput = userMessage.toLowerCase();
let response = responses.default;
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);
setMessages(prev => [...prev, { role: "assistant", content: "Lo siento, hubo un error. Inténtalo de nuevo." }]);
} finally {
setIsSending(false);
}
};
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
};
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 [showChat, setShowChat] = useState(true);
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";
return (
<div className="fixed bottom-6 right-6 z-50">
{/* Mode Toggle Button */}
<button
onClick={toggleMode}
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"}
>
{mode === "text" ? (
<>
<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>
{/* Error Message */}
{status === "error" && errorMsg && (
<div className="absolute bottom-16 right-0 w-64 bg-red-600 text-white text-xs p-2 rounded-lg mb-2">
<div className="absolute bottom-28 right-0 w-64 bg-red-600 text-white text-xs p-2 rounded-lg mb-2">
{errorMsg}
</div>
)}
{isActive && (
<div className="absolute bottom-16 right-0 w-80 bg-[#1a1625] 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 text-white">🤖 AI</span>
<span className={`w-2 h-2 rounded-full ${status === "active" ? "bg-green-500 animate-pulse" : "bg-yellow-500"}`}></span>
{/* Chat Panel - Text Mode */}
{mode === "text" && showChat && (
<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>
<div className="h-32 overflow-y-auto text-sm text-white/70 bg-white/5 rounded-lg p-2">
{transcript || "Escuchando..."}
{/* 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="Escribe tu mensaje..."
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>
)}
{/* 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 */}
<button
onClick={isActive ? endCall : startCall}
onClick={mode === "voice" ? (isActive ? endCall : startCall) : () => setShowChat(!showChat)}
className={`${buttonColor} w-14 h-14 rounded-full shadow-lg flex items-center justify-center transition-all hover:scale-110 ${
isActive ? "animate-pulse ring-4 ring-red-500/50" : ""
mode === "voice" && isActive ? "animate-pulse ring-4 ring-red-500/50" : ""
}`}
title={isActive ? "Colgar" : "Hablar con IA"}
title={mode === "voice" ? (isActive ? "End Call" : "Start Voice Chat") : (showChat ? "Close Chat" : "Open Chat")}
>
{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>
{mode === "voice" ? (
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">
<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 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="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>
{status === "connecting" && (
{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">
Conectando...
Connecting...
</div>
)}
</div>