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 } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faSparkles, faMicrophone } from '@fortawesome/pro-duotone-svg-icons';
import { cx } from '@/utils/cx';
type TranscriptLine = {
id: string;
text: string;
isFinal: boolean;
timestamp: Date;
};
type Suggestion = {
id: string;
text: string;
timestamp: Date;
};
type LiveTranscriptProps = {
transcript: TranscriptLine[];
suggestions: Suggestion[];
connected: boolean;
};
export const LiveTranscript = ({ transcript, suggestions, connected }: LiveTranscriptProps) => {
const scrollRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}
}, [transcript.length, suggestions.length]);
// Merge transcript and suggestions by timestamp
const items = [
...transcript.map(t => ({ ...t, kind: 'transcript' as const })),
...suggestions.map(s => ({ ...s, kind: 'suggestion' as const, isFinal: true })),
].sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
return (
<div className="flex flex-1 flex-col overflow-hidden">
{/* Header */}
<div className="flex items-center gap-2 px-4 py-3 border-b border-secondary">
<FontAwesomeIcon icon={faSparkles} className="size-3.5 text-fg-brand-primary" />
<span className="text-xs font-bold uppercase tracking-wider text-brand-secondary">Live Assist</span>
<div className={cx(
"ml-auto size-2 rounded-full",
connected ? "bg-success-solid" : "bg-disabled",
)} />
</div>
{/* Transcript body */}
<div ref={scrollRef} className="flex-1 overflow-y-auto px-4 py-3 space-y-2">
{items.length === 0 && (
<div className="flex flex-col items-center justify-center py-8 text-center">
<FontAwesomeIcon icon={faMicrophone} className="size-6 text-fg-quaternary mb-2" />
<p className="text-xs text-quaternary">Listening to customer...</p>
<p className="text-xs text-quaternary">Transcript will appear here</p>
</div>
)}
{items.map(item => {
if (item.kind === 'suggestion') {
return (
<div key={item.id} className="rounded-lg bg-brand-primary p-3 border border-brand">
<div className="flex items-center gap-1.5 mb-1">
<FontAwesomeIcon icon={faSparkles} className="size-3 text-fg-brand-primary" />
<span className="text-xs font-semibold text-brand-secondary">AI Suggestion</span>
</div>
<p className="text-sm text-primary">{item.text}</p>
</div>
);
}
return (
<div key={item.id} className={cx(
"text-sm",
item.isFinal ? "text-primary" : "text-tertiary italic",
)}>
<span className="text-xs text-quaternary mr-2">
{item.timestamp.toLocaleTimeString('en-IN', { hour: '2-digit', minute: '2-digit', second: '2-digit' })}
</span>
{item.text}
</div>
);
})}
</div>
</div>
);
};