Files
sitemente/components/SiteMenteVoiceWidget.tsx
T

182 lines
6.4 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 } from "react";
import Vapi from "@vapi-ai/web";
const VAPI_PUBLIC_KEY = "d44a0025-24bb-426d-919a-cb0a96416ed4";
const ASSISTANT_ID = "92630ca5-e165-4360-bce0-dd8730882569";
interface SiteMenteVoiceWidgetProps {
businessName?: string;
businessType?: "restaurant" | "real-estate" | "clinic" | "car-rental" | "default";
theme?: "dark" | "light";
}
export default function SiteMenteVoiceWidget({
businessName = "SiteMente",
businessType = "default",
theme = "dark"
}: SiteMenteVoiceWidgetProps) {
const [isActive, setIsActive] = useState(false);
const [status, setStatus] = useState<"idle" | "connecting" | "active" | "error">("idle");
const [transcript, setTranscript] = useState("");
const [errorMsg, setErrorMsg] = useState<string>("");
const vapiRef = useRef<any>(null);
const startCall = async () => {
try {
console.log("Starting call - initializing Vapi inside click handler...");
setErrorMsg("");
setStatus("connecting");
// Step 1: Verify mic exists
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());
} catch (micErr) {
console.log("❌ Mic error:", micErr);
}
// 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");
setErrorMsg(msg);
setStatus("error");
setIsActive(false);
});
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);
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);
}
});
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");
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);
}
};
const buttonColor = theme === "dark" ? "bg-brand-pink" : "bg-blue-600";
return (
<div className="fixed bottom-6 right-6 z-50">
{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">
{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>
</div>
<div className="h-32 overflow-y-auto text-sm text-white/70 bg-white/5 rounded-lg p-2">
{transcript || "Escuchando..."}
</div>
</div>
)}
<button
onClick={isActive ? endCall : startCall}
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" : ""
}`}
title={isActive ? "Colgar" : "Hablar con IA"}
>
{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>
)}
</button>
{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...
</div>
)}
</div>
);
}