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:
2026-03-21 10:36:10 +05:30
parent 99bca1e008
commit 3064eeb444
21 changed files with 2583 additions and 85 deletions

View 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 };
};

View File

@@ -47,6 +47,8 @@ type WorklistLead = {
isSpam: boolean | null;
aiSummary: string | null;
aiSuggestedAction: string | null;
lastContacted: string | null;
utmCampaign: string | null;
};
type WorklistData = {