feat: add Socket.IO client and useCallEvents hook for live CTI mode

This commit is contained in:
2026-03-17 09:14:25 +05:30
parent 45bae9c1c0
commit 4f9bdc7312
5 changed files with 381 additions and 30 deletions

View File

@@ -0,0 +1,133 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { disconnectSocket, getSocket } from '@/lib/socket';
import type { CallDisposition } from '@/types/entities';
type CallState = 'idle' | 'ringing' | 'active' | 'completed';
type EnrichedLead = {
id: string;
firstName: string;
lastName: string;
phone: string;
email?: string;
source?: string;
status?: string;
campaign?: string;
interestedService?: string;
age: number;
aiSummary?: string;
aiSuggestedAction?: string;
recentActivities: { activityType: string; summary: string; occurredAt: string; performedBy: string }[];
};
type IncomingCallEvent = {
callSid: string;
eventType: 'ringing' | 'answered' | 'ended';
lead: EnrichedLead | null;
callerPhone: string;
agentName: string;
timestamp: string;
};
export const useCallEvents = (agentName: string) => {
const [callState, setCallState] = useState<CallState>('idle');
const [activeLead, setActiveLead] = useState<EnrichedLead | null>(null);
const [activeCallSid, setActiveCallSid] = useState<string | null>(null);
const [callStartTime, setCallStartTime] = useState<string | null>(null);
const [isConnected, setIsConnected] = useState(false);
const completedTimerRef = useRef<number | null>(null);
// Connect to WebSocket and register agent
useEffect(() => {
const socket = getSocket();
socket.on('connect', () => {
setIsConnected(true);
socket.emit('agent:register', agentName);
});
socket.on('disconnect', () => {
setIsConnected(false);
});
socket.on('agent:registered', (data: { agentName: string }) => {
console.log(`Registered as agent: ${data.agentName}`);
});
socket.on('call:incoming', (event: IncomingCallEvent) => {
if (event.eventType === 'ringing' || event.eventType === 'answered') {
setActiveCallSid(event.callSid);
setActiveLead(event.lead);
setCallStartTime(event.timestamp);
if (event.eventType === 'ringing') {
setCallState('ringing');
// Auto-transition to active after 1.5s (call is answered)
setTimeout(() => setCallState('active'), 1500);
} else {
setCallState('active');
}
} else if (event.eventType === 'ended') {
// Call ended from Exotel side (e.g. customer hung up)
setCallState('completed');
completedTimerRef.current = window.setTimeout(() => {
setCallState('idle');
setActiveLead(null);
setActiveCallSid(null);
setCallStartTime(null);
}, 3000);
}
});
socket.on('call:disposition:ack', () => {
// Disposition saved on server
});
socket.connect();
return () => {
if (completedTimerRef.current) {
clearTimeout(completedTimerRef.current);
}
disconnectSocket();
};
}, [agentName]);
// Send disposition to server
const sendDisposition = useCallback(
(disposition: CallDisposition, notes: string) => {
const socket = getSocket();
const duration = callStartTime
? Math.floor((Date.now() - new Date(callStartTime).getTime()) / 1000)
: 0;
socket.emit('call:disposition', {
callSid: activeCallSid,
leadId: activeLead?.id ?? null,
disposition,
notes,
agentName,
callerPhone: activeLead?.phone ?? '',
startedAt: callStartTime,
duration,
});
setCallState('completed');
completedTimerRef.current = window.setTimeout(() => {
setCallState('idle');
setActiveLead(null);
setActiveCallSid(null);
setCallStartTime(null);
}, 3000);
},
[activeCallSid, activeLead, agentName, callStartTime],
);
return {
callState,
activeLead,
activeCallSid,
isConnected,
sendDisposition,
};
};