import { useEffect, useRef, useState } from 'react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faWaveformLines, faSpinner, faPlay, faPause } from '@fortawesome/pro-duotone-svg-icons'; import { Badge } from '@/components/base/badges/badges'; import { Button } from '@/components/base/buttons/button'; import { SlideoutMenu } from '@/components/application/slideout-menus/slideout-menu'; import { apiClient } from '@/lib/api-client'; import { formatPhone, formatDateOnly } from '@/lib/format'; import { cx } from '@/utils/cx'; type Utterance = { speaker: number; start: number; end: number; text: string; }; type Insights = { keyTopics: string[]; actionItems: string[]; coachingNotes: string[]; complianceFlags: string[]; patientSatisfaction: string; callOutcome: string; }; type Analysis = { transcript: Utterance[]; summary: string | null; sentiment: 'positive' | 'neutral' | 'negative' | 'mixed'; sentimentScore: number; insights: Insights; durationSec: number; }; const sentimentConfig = { positive: { label: 'Positive', color: 'success' as const }, neutral: { label: 'Neutral', color: 'gray' as const }, negative: { label: 'Negative', color: 'error' as const }, mixed: { label: 'Mixed', color: 'warning' as const }, }; const formatTimestamp = (sec: number): string => { const m = Math.floor(sec / 60); const s = Math.floor(sec % 60); return `${m}:${s.toString().padStart(2, '0')}`; }; const formatDuration = (sec: number | null): string => { if (!sec) return ''; const m = Math.floor(sec / 60); const s = sec % 60; return `${m}:${s.toString().padStart(2, '0')}`; }; // Inline audio player for the slideout header const SlideoutPlayer = ({ url }: { url: string }) => { const audioRef = useRef(null); const [playing, setPlaying] = useState(false); const toggle = () => { if (!audioRef.current) return; if (playing) { audioRef.current.pause(); } else { audioRef.current.play(); } setPlaying(!playing); }; return (
{playing ? 'Playing...' : 'Play recording'}
); }; // Insights section rendered after analysis completes const InsightsSection = ({ label, children }: { label: string; children: React.ReactNode }) => (
{label}
{children}
); type RecordingAnalysisSlideoutProps = { isOpen: boolean; onOpenChange: (open: boolean) => void; recordingUrl: string; callId: string; agentName: string | null; callerNumber: string | null; direction: string | null; startedAt: string | null; durationSec: number | null; disposition: string | null; }; export const RecordingAnalysisSlideout = ({ isOpen, onOpenChange, recordingUrl, callId, agentName, callerNumber, direction, startedAt, durationSec, disposition, }: RecordingAnalysisSlideoutProps) => { const [analysis, setAnalysis] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const hasTriggered = useRef(false); // Auto-trigger analysis when the slideout opens useEffect(() => { if (!isOpen || hasTriggered.current) return; hasTriggered.current = true; setLoading(true); setError(null); apiClient.post('/api/recordings/analyze', { recordingUrl, callId }) .then((result) => setAnalysis(result)) .catch((err: any) => setError(err.message ?? 'Analysis failed')) .finally(() => setLoading(false)); }, [isOpen, recordingUrl, callId]); const dirLabel = direction === 'INBOUND' ? 'Inbound' : 'Outbound'; const dirColor = direction === 'INBOUND' ? 'blue' : 'brand'; const formattedPhone = callerNumber ? formatPhone({ number: callerNumber, callingCode: '+91' }) : null; return ( {({ close }) => ( <>

Call Analysis

{dirLabel} {agentName && {agentName}} {formattedPhone && ( <> - {formattedPhone} )}
{startedAt && {formatDateOnly(startedAt)}} {durationSec != null && durationSec > 0 && {formatDuration(durationSec)}} {disposition && ( {disposition.replace(/_/g, ' ').toLowerCase().replace(/\b\w/g, c => c.toUpperCase())} )}
{loading && (

Analyzing recording...

Transcribing and generating insights

)} {error && !loading && (

Transcription is temporarily unavailable. Please try again.

)} {analysis && !loading && ( )}
)}
); }; // Separated analysis results display for readability const AnalysisResults = ({ analysis }: { analysis: Analysis }) => { const sentCfg = sentimentConfig[analysis.sentiment]; return (
{/* Sentiment + topics */}
{sentCfg.label} {analysis.insights.keyTopics.slice(0, 4).map((topic) => ( {topic} ))}
{/* Summary */} {analysis.summary && (
Summary

{analysis.summary}

)} {/* Call outcome */}
Call Outcome

{analysis.insights.callOutcome}

{/* Insights grid */}

{analysis.insights.patientSatisfaction}

{analysis.insights.actionItems.length > 0 && (
    {analysis.insights.actionItems.map((item, i) => (
  • - {item}
  • ))}
)} {analysis.insights.coachingNotes.length > 0 && (
    {analysis.insights.coachingNotes.map((note, i) => (
  • - {note}
  • ))}
)} {analysis.insights.complianceFlags.length > 0 && (
Compliance Flags
    {analysis.insights.complianceFlags.map((flag, i) => (
  • - {flag}
  • ))}
)}
{/* Transcript */} {analysis.transcript.length > 0 && (
Transcript
{analysis.transcript.map((u, i) => { const isAgent = u.speaker === 0; return (
{formatTimestamp(u.start)} {isAgent ? 'Agent' : 'Customer'} {u.text}
); })}
)}
); };