mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-05-18 20:08:19 +00:00
- 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>
92 lines
3.7 KiB
TypeScript
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>
|
|
);
|
|
};
|