feat: play the recording on the Live monitor too

The operator watches the Live monitor and never sees My calls, so the recording
was reachable only by the colleague who took the call. Now both audiences get it,
by the route each already uses:

  operator  -> call_recording_url on the lead, via the record view
  colleague -> GET /api/agent-sessions/:id/call, authorised by owning the session

Two paths because they are looking at two different things — a lead record and a
conversation — not because the data differs.

Placed ABOVE the summary in the drawer: the summary is a claim about the call and
this is the call, so on a demo about whether the AI captured things fairly the
evidence should not sit below the fold.

preload="none" on both. The drawer opens on every row click, and a WAV of a whole
phone call is not something to fetch speculatively.

tsc -b and vite build clean.
This commit is contained in:
Yashas 2026-08-27 12:31:11 +05:30
parent 0612626491
commit e442347b6e
2 changed files with 22 additions and 0 deletions

View File

@ -39,6 +39,8 @@ export const FIELDS = {
CALL_DURATION: 'call_duration_seconds', CALL_DURATION: 'call_duration_seconds',
CALL_SUMMARY: 'call_summary', CALL_SUMMARY: 'call_summary',
CONTACT_NOTES: 'contact_notes', CONTACT_NOTES: 'contact_notes',
/** Dual-channel WAV of the call, written when it ends. */
CALL_RECORDING: 'call_recording_url',
} as const } as const
/** /**

View File

@ -169,6 +169,7 @@ function DetailDrawer({ row, onClose }: { row: LeadRow; onClose: () => void }) {
const summary = str(row[FIELDS.CALL_SUMMARY]) const summary = str(row[FIELDS.CALL_SUMMARY])
const notes = str(row[FIELDS.CONTACT_NOTES]) const notes = str(row[FIELDS.CONTACT_NOTES])
const transferred = str(row[FIELDS.CALL_END_REASON]) === END_REASON_HANDOVER const transferred = str(row[FIELDS.CALL_END_REASON]) === END_REASON_HANDOVER
const recording = str(row[FIELDS.CALL_RECORDING])
return ( return (
<div className="fixed inset-0 z-20 flex justify-end bg-slate-900/20" onClick={onClose}> <div className="fixed inset-0 z-20 flex justify-end bg-slate-900/20" onClick={onClose}>
@ -205,6 +206,25 @@ function DetailDrawer({ row, onClose }: { row: LeadRow; onClose: () => void }) {
</div> </div>
)} )}
{/* The recording sits ABOVE the summary on purpose: the summary is a
claim about the conversation, and this is the conversation. On a demo
about whether the AI captured things fairly, the evidence should not
be below the fold. */}
<div className="mt-5">
<h3 className="text-xs font-semibold uppercase tracking-wide text-slate-500">Recording</h3>
{recording ? (
// preload="none": a WAV of a whole call, and the drawer opens on
// every row click. Fetch it when someone actually presses play.
<audio controls preload="none" src={recording} className="mt-2 h-9 w-full" />
) : (
<p className="mt-1.5 text-sm text-slate-400">
{transferred || str(row[FIELDS.CALL_END_REASON])
? 'No recording for this call.'
: 'Available once the call ends.'}
</p>
)}
</div>
<Section title="Call summary" body={summary} empty="No summary yet — it is written when the call ends." /> <Section title="Call summary" body={summary} empty="No summary yet — it is written when the call ends." />
<Section title="Notes" body={notes} /> <Section title="Notes" body={notes} />