mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-11 18:28:15 +00:00
feat: CC agent features, live call assist, worklist redesign, brand tokens
CC Agent: - Call transfer (CONFERENCE + KICK_CALL) with inline transfer dialog - Recording pause/resume during active calls - Missed calls API (Ozonetel abandonCalls) - Call history API (Ozonetel fetchCDRDetails) Live Call Assist: - Deepgram Nova STT via raw WebSocket - OpenAI suggestions every 10s with lead context - LiveTranscript component in sidebar during calls - Browser audio capture from remote WebRTC stream Worklist: - Redesigned table: clickable phones, context menu (Call/SMS/WhatsApp) - Last interaction sub-line, source column, improved SLA - Filtered out rows without phone numbers - New missed call notifications Brand: - Logo on login page - Blue scale rebuilt from logo blue rgb(32, 96, 160) - FontAwesome duotone CSS variables set globally - Profile menu icons switched to duotone Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
90
src/hooks/use-call-assist.ts
Normal file
90
src/hooks/use-call-assist.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { useEffect, useRef, useState, useCallback } from 'react';
|
||||
import { io, Socket } from 'socket.io-client';
|
||||
import { startAudioCapture, stopAudioCapture } from '@/lib/audio-capture';
|
||||
import { getSipClient } from '@/state/sip-manager';
|
||||
|
||||
const SIDECAR_URL = import.meta.env.VITE_SIDECAR_URL ?? 'http://localhost:4100';
|
||||
|
||||
type TranscriptLine = {
|
||||
id: string;
|
||||
text: string;
|
||||
isFinal: boolean;
|
||||
timestamp: Date;
|
||||
};
|
||||
|
||||
type Suggestion = {
|
||||
id: string;
|
||||
text: string;
|
||||
timestamp: Date;
|
||||
};
|
||||
|
||||
export const useCallAssist = (
|
||||
active: boolean,
|
||||
ucid: string | null,
|
||||
leadId: string | null,
|
||||
callerPhone: string | null,
|
||||
) => {
|
||||
const [transcript, setTranscript] = useState<TranscriptLine[]>([]);
|
||||
const [suggestions, setSuggestions] = useState<Suggestion[]>([]);
|
||||
const [connected, setConnected] = useState(false);
|
||||
const socketRef = useRef<Socket | null>(null);
|
||||
const idCounter = useRef(0);
|
||||
|
||||
const nextId = useCallback(() => `ca-${++idCounter.current}`, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!active || !ucid) return;
|
||||
|
||||
const socket = io(`${SIDECAR_URL}/call-assist`);
|
||||
socketRef.current = socket;
|
||||
|
||||
socket.on('connect', () => {
|
||||
setConnected(true);
|
||||
socket.emit('call-assist:start', { ucid, leadId, callerPhone });
|
||||
|
||||
// Start capturing remote audio from the SIP session
|
||||
const sipClient = getSipClient();
|
||||
const audioElement = sipClient?.getAudioElement();
|
||||
if (audioElement?.srcObject) {
|
||||
startAudioCapture(audioElement.srcObject as MediaStream, (chunk) => {
|
||||
socket.emit('call-assist:audio', chunk);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('call-assist:transcript', (data: { text: string; isFinal: boolean }) => {
|
||||
if (!data.text.trim()) return;
|
||||
setTranscript(prev => {
|
||||
if (!data.isFinal) {
|
||||
const finals = prev.filter(l => l.isFinal);
|
||||
return [...finals, { id: nextId(), text: data.text, isFinal: false, timestamp: new Date() }];
|
||||
}
|
||||
const finals = prev.filter(l => l.isFinal);
|
||||
return [...finals, { id: nextId(), text: data.text, isFinal: true, timestamp: new Date() }];
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('call-assist:suggestion', (data: { text: string }) => {
|
||||
setSuggestions(prev => [...prev, { id: nextId(), text: data.text, timestamp: new Date() }]);
|
||||
});
|
||||
|
||||
socket.on('disconnect', () => setConnected(false));
|
||||
|
||||
return () => {
|
||||
stopAudioCapture();
|
||||
socket.emit('call-assist:stop');
|
||||
socket.disconnect();
|
||||
socketRef.current = null;
|
||||
setConnected(false);
|
||||
};
|
||||
}, [active, ucid, leadId, callerPhone, nextId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!active) {
|
||||
setTranscript([]);
|
||||
setSuggestions([]);
|
||||
}
|
||||
}, [active]);
|
||||
|
||||
return { transcript, suggestions, connected };
|
||||
};
|
||||
@@ -47,6 +47,8 @@ type WorklistLead = {
|
||||
isSpam: boolean | null;
|
||||
aiSummary: string | null;
|
||||
aiSuggestedAction: string | null;
|
||||
lastContacted: string | null;
|
||||
utmCampaign: string | null;
|
||||
};
|
||||
|
||||
type WorklistData = {
|
||||
|
||||
Reference in New Issue
Block a user