Files
helix-engage/src/components/call-desk/live-transcript.tsx
saridsa2 488f524f84 feat: SSE agent state, UCID fix, maint module, QA bug fixes
- Fix outbound disposition: store UCID from dial API response (root cause of silent disposition failure)
- SSE agent state: real-time Ozonetel state drives status toggle (ready/break/calling/in-call/acw)
- Maint module with OTP-protected endpoints (force-ready, unlock-agent, backfill, fix-timestamps)
- Maint OTP modal with PinInput component, keyboard shortcuts (Ctrl+Shift+R/U/B/T)
- Force-logout via SSE: admin unlock pushes force-logout to connected browsers
- Silence JsSIP debug flood, add structured lifecycle logging ([SIP], [DIAL], [DISPOSE], [AGENT-STATE])
- Centralize date formatting with IST-aware formatters across 11 files
- Fix call history: non-overlapping aggregates (completed/missed), correct timestamp display
- Auto-dismiss CallWidget ended/failed state after 3 seconds
- Remove floating "Helix Phone" idle badge from all pages
- Fix dead code in agent-state endpoint (auto-assign was unreachable after return)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 14:44:48 +05:30

92 lines
3.7 KiB
TypeScript

import { useEffect, useRef } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faSparkles, faMicrophone } from '@fortawesome/pro-duotone-svg-icons';
import { formatTimeFull } from '@/lib/format';
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">
{formatTimeFull(item.timestamp.toISOString())}
</span>
{item.text}
</div>
);
})}
</div>
</div>
);
};