import Link from "next/link"; import { useState, useEffect, useRef, useCallback } from "react"; import type { UseFormReturn } from "react-hook-form"; import type { RetellWebClient } from "retell-client-js-sdk"; import { Dialog } from "@calcom/features/components/controlled-dialog"; import { getEventTypeIdForCalAiTest } from "@calcom/features/ee/workflows/lib/actionHelperFunctions"; import type { FormValues } from "@calcom/features/ee/workflows/lib/types"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { trpc } from "@calcom/trpc/react"; import { Alert } from "@calcom/ui/components/alert"; import { Button } from "@calcom/ui/components/button"; import { DialogContent, DialogFooter } from "@calcom/ui/components/dialog"; import { Icon } from "@calcom/ui/components/icon"; import { showToast } from "@calcom/ui/components/toast"; import { Tooltip } from "@calcom/ui/components/tooltip"; interface WebCallDialogProps { open: boolean; onOpenChange: (open: boolean) => void; agentId: string; teamId?: number; isOrganization?: boolean; form: UseFormReturn; eventTypeIds?: number[]; outboundEventTypeId?: number | null; } interface TranscriptEntry { speaker: "agent" | "user"; text: string; timestamp: Date; } type CallStatus = "idle" | "connecting" | "active" | "ended" | "error"; export function WebCallDialog({ open, onOpenChange, agentId, teamId, isOrganization = false, form, eventTypeIds = [], outboundEventTypeId, }: WebCallDialogProps) { const { t } = useLocale(); const [callStatus, setCallStatus] = useState("idle"); const [transcript, setTranscript] = useState([]); const [isMuted, setIsMuted] = useState(false); const [callDuration, setCallDuration] = useState(0); const [error, setError] = useState(null); const retellWebClientRef = useRef(null); const callStartTimeRef = useRef(null); const durationIntervalRef = useRef(null); const transcriptEndRef = useRef(null); const callStatusRef = useRef("idle"); const { data: hasCredits, isLoading: creditsLoading } = trpc.viewer.credits.hasAvailableCredits.useQuery( { teamId }, { enabled: open } ); const getBillingPath = () => { if (!teamId) return "/settings/billing"; return isOrganization ? "/settings/organizations/billing" : `/settings/teams/${teamId}/billing`; }; const createWebCallMutation = trpc.viewer.aiVoiceAgent.createWebCall.useMutation({ onSuccess: async (data) => { try { await startWebCall(data.accessToken); } catch (error) { console.error("Failed to start web call:", error); setCallStatus("error"); setError(t("failed_to_start_web_call_try_again")); } }, onError: (error: { message: string }) => { setCallStatus("error"); setError(error.message); showToast(error.message, "error"); }, }); const startWebCall = async (accessToken: string) => { try { const { RetellWebClient } = await import("retell-client-js-sdk"); const retellWebClient = new RetellWebClient(); retellWebClientRef.current = retellWebClient; retellWebClient.on("call_started", () => { callStartTimeRef.current = new Date(); callStatusRef.current = "active"; setCallStatus("active"); startDurationTimer(); }); retellWebClient.on("call_ended", () => { setCallStatus("ended"); stopDurationTimer(); }); retellWebClient.on("agent_start_talking", () => { console.log("Agent started talking"); }); retellWebClient.on("agent_stop_talking", () => { console.log("Agent stopped talking"); }); retellWebClient.on("update", (update: { transcript?: Array<{ role: string; content: string }> }) => { console.log("📝 Received update event:", update); if (update.transcript && Array.isArray(update.transcript)) { console.log("📜 Transcript array received:", update.transcript); try { const newEntries: TranscriptEntry[] = update.transcript .map((entry) => { if (!entry.role || !entry.content) { console.warn("⚠️ Invalid transcript entry:", entry); return null; } const mappedEntry = { speaker: entry.role === "agent" ? "agent" : "user", text: entry.content, timestamp: new Date(), }; console.log("✅ Mapped transcript entry:", mappedEntry); return mappedEntry; }) .filter(Boolean) as TranscriptEntry[]; console.log("🔄 Setting transcript with entries:", newEntries.length, "entries"); console.log("📋 Current transcript state has:", transcript.length, "entries"); setTranscript(newEntries); } catch (error) { console.error("❌ Error processing transcript update:", error); } } else { console.log( "🚫 No transcript data in update or transcript is not an array:", typeof update.transcript ); } }); retellWebClient.on("error", (error: Error | { message?: string }) => { console.error("Web call error:", error); setCallStatus("error"); setError(t("call_encountered_error_try_again")); stopDurationTimer(); }); setCallStatus("connecting"); await retellWebClient.startCall({ accessToken: accessToken, sampleRate: 24000, emitRawAudioSamples: false, }); } catch (error) { console.error("Error starting web call:", error); setCallStatus("error"); setError(t("failed_initialize_web_call_microphone_permissions")); } }; const startDurationTimer = useCallback(() => { if (durationIntervalRef.current) { clearInterval(durationIntervalRef.current); durationIntervalRef.current = null; } if (!callStartTimeRef.current) { return; } durationIntervalRef.current = setInterval(() => { if (callStartTimeRef.current) { const now = Date.now(); const startTime = callStartTimeRef.current.getTime(); const duration = Math.floor((now - startTime) / 1000); if (duration >= 0) { setCallDuration(duration); } } }, 1000); }, []); const stopDurationTimer = () => { if (durationIntervalRef.current) { clearInterval(durationIntervalRef.current); durationIntervalRef.current = null; } }; const handleStartCall = () => { const eventTypeValidation = getEventTypeIdForCalAiTest({ trigger: form.getValues("trigger"), outboundEventTypeId, eventTypeIds, activeOnEventTypeId: form.getValues("activeOn")?.[0]?.value, t, }); if (eventTypeValidation.error || !eventTypeValidation.eventTypeId) { showToast(eventTypeValidation.error || t("no_event_type_selected"), "error"); return; } if (agentId) { setError(null); setTranscript([]); setCallDuration(0); createWebCallMutation.mutate({ agentId: agentId, teamId: teamId, eventTypeId: eventTypeValidation.eventTypeId, }); } }; const handleEndCall = async () => { if (retellWebClientRef.current && callStatus === "active") { try { await retellWebClientRef.current.stopCall(); setCallStatus("ended"); stopDurationTimer(); } catch (error) { console.error("Error ending call:", error); } } }; const handleToggleMute = async () => { if (retellWebClientRef.current && callStatus === "active") { try { if (isMuted) { await retellWebClientRef.current.unmute(); } else { await retellWebClientRef.current.mute(); } setIsMuted(!isMuted); } catch (error) { console.error("Error toggling mute:", error); } } }; const formatDuration = (seconds: number) => { const mins = Math.floor(seconds / 60); const secs = seconds % 60; return `${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`; }; const getStatusIcon = () => { switch (callStatus) { case "connecting": return ; case "active": return ; case "ended": return ; case "error": return ; default: return ; } }; const getStatusText = () => { switch (callStatus) { case "connecting": return t("connecting_to_agent"); case "active": return `${t("call_active")} - ${formatDuration(callDuration)}`; case "ended": return `${t("call_ended")} - ${formatDuration(callDuration)}`; case "error": return t("call_error"); default: return t("ready_to_start_call"); } }; useEffect(() => { if (transcriptEndRef.current) { transcriptEndRef.current.scrollIntoView({ behavior: "smooth" }); } }, [transcript]); useEffect(() => { return () => { stopDurationTimer(); if (retellWebClientRef.current && callStatus === "active") { try { retellWebClientRef.current.stopCall(); } catch (error) { console.error("Error stopping call during cleanup:", error); } } }; }, [callStatus]); useEffect(() => { callStatusRef.current = callStatus; if (callStatus === "active" && callStartTimeRef.current && !durationIntervalRef.current) { startDurationTimer(); } }, [callStatus, startDurationTimer]); useEffect(() => { return () => { stopDurationTimer(); }; }, []); const resetDialogState = () => { setCallStatus("idle"); callStatusRef.current = "idle"; setTranscript([]); setIsMuted(false); setCallDuration(0); setError(null); callStartTimeRef.current = null; stopDurationTimer(); retellWebClientRef.current = null; }; return ( { if (!open) { if (retellWebClientRef.current) { try { retellWebClientRef.current.stopCall(); } catch (e) { console.error("Error stopping call on close:", e); } } resetDialogState(); } onOpenChange(open); }}> {!creditsLoading && ( {t("web_call_no_credits")}{" "} {t("purchase_credits")} ) } /> )}
{getStatusIcon()} {getStatusText()}
{callStatus === "active" && (
{t("live")}
)}

{t("transcript")}

{transcript.length > 0 && ( )}
{transcript.length === 0 ? (

{callStatus === "idle" ? t("start_call_to_see_conversation") : t("waiting_for_conversation")}

) : ( transcript.map((entry, index) => (
{entry.speaker === "agent" ? t("ai_agent") : t("you")}

{entry.text}

)) )}
{error && callStatus === "error" && (
{error}
)}
{callStatus === "active" && ( )} {callStatus === "idle" || callStatus === "error" || callStatus === "ended" || callStatus === "connecting" ? ( ) : ( )}
); }