feat: call recording analysis with Deepgram diarization + AI insights

- Deepgram pre-recorded API: transcription with diarization, sentiment, topics, summary
- OpenAI structured insights: call outcome, patient satisfaction, coaching notes, action items, compliance flags
- Slideout panel UI with audio player, speaker-labeled transcript, sentiment badge
- AI pill button in recordings table between Caller and Type columns
- Redis caching (7-day TTL) to avoid re-analyzing the same recording

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-25 09:19:52 +05:30
parent 488f524f84
commit 70e0f6fc3e
2 changed files with 342 additions and 1 deletions

View File

@@ -1,5 +1,5 @@
import { useEffect, useMemo, useState, useRef } from 'react';
import { faMagnifyingGlass, faPlay, faPause } from '@fortawesome/pro-duotone-svg-icons';
import { faMagnifyingGlass, faPlay, faPause, faSparkles } from '@fortawesome/pro-duotone-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faIcon } from '@/lib/icon-wrapper';
@@ -9,6 +9,7 @@ import { Input } from '@/components/base/input/input';
import { Table } from '@/components/application/table/table';
import { TopBar } from '@/components/layout/top-bar';
import { PhoneActionCell } from '@/components/call-desk/phone-action-cell';
import { RecordingAnalysisSlideout } from '@/components/call-desk/recording-analysis';
import { apiClient } from '@/lib/api-client';
import { formatPhone, formatDateOnly } from '@/lib/format';
@@ -63,6 +64,7 @@ export const CallRecordingsPage = () => {
const [calls, setCalls] = useState<RecordingRecord[]>([]);
const [loading, setLoading] = useState(true);
const [search, setSearch] = useState('');
const [slideoutCallId, setSlideoutCallId] = useState<string | null>(null);
useEffect(() => {
apiClient.graphql<{ calls: { edges: Array<{ node: RecordingRecord }> } }>(QUERY, undefined, { silent: true })
@@ -110,6 +112,7 @@ export const CallRecordingsPage = () => {
<Table.Header>
<Table.Head label="Agent" isRowHeader />
<Table.Head label="Caller" />
<Table.Head label="AI" className="w-14" />
<Table.Head label="Type" className="w-20" />
<Table.Head label="Date" className="w-28" />
<Table.Head label="Duration" className="w-20" />
@@ -132,6 +135,21 @@ export const CallRecordingsPage = () => {
<PhoneActionCell phoneNumber={phone} displayNumber={formatPhone({ number: phone, callingCode: '+91' })} />
) : <span className="text-xs text-quaternary"></span>}
</Table.Cell>
<Table.Cell>
<span
role="button"
tabIndex={0}
onPointerDown={(e) => {
e.stopPropagation();
setSlideoutCallId(call.id);
}}
className="inline-flex items-center gap-1 rounded-full bg-brand-primary px-2.5 py-1 text-xs font-semibold text-brand-secondary hover:bg-brand-secondary hover:text-white cursor-pointer transition duration-100 ease-linear"
title="AI Analysis"
>
<FontAwesomeIcon icon={faSparkles} className="size-3" />
AI
</span>
</Table.Cell>
<Table.Cell>
<Badge size="sm" color={dirColor} type="pill-color">{dirLabel}</Badge>
</Table.Cell>
@@ -159,7 +177,28 @@ export const CallRecordingsPage = () => {
</Table.Body>
</Table>
)}
</div>
{/* Analysis slideout */}
{(() => {
const call = slideoutCallId ? filtered.find(c => c.id === slideoutCallId) : null;
if (!call?.recording?.primaryLinkUrl) return null;
return (
<RecordingAnalysisSlideout
isOpen={true}
onOpenChange={(open) => { if (!open) setSlideoutCallId(null); }}
recordingUrl={call.recording.primaryLinkUrl}
callId={call.id}
agentName={call.agentName}
callerNumber={call.callerNumber?.primaryPhoneNumber ?? null}
direction={call.direction}
startedAt={call.startedAt}
durationSec={call.durationSec}
disposition={call.disposition}
/>
);
})()}
</div>
</>
);