mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-05-18 20:08:19 +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/components/call-desk/live-transcript.tsx
Normal file
90
src/components/call-desk/live-transcript.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user