182 lines
6.4 KiB
TypeScript
182 lines
6.4 KiB
TypeScript
"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>
|
||
);
|
||
}
|