console: play the call recording alongside the transcript

The voice agent uploads a dual-channel WAV of each call to blob storage; until
now the console showed only the transcript's conclusions, not the audio. Add a
player wherever the call is read:

- THE CALL panel on the lead gets an inline "Call recording" player.
- The transcript dialog gets the player above the turns, so a reviewer can
  listen while reading — the words and the audio are the same evidence.

Both read row.call_recording_url (the platform's own field for this, surfaced
on the lead by backend 107) and use preload="none", so a multi-MB WAV is only
fetched when someone actually presses play. The player simply does not render
when a lead has no recording.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Yashas 2026-09-09 16:19:26 +05:30
parent bd4ced41ac
commit e0d51159f6
4 changed files with 51 additions and 2 deletions

View File

@ -42,7 +42,7 @@ function turnsFrom(text) {
return out.filter((t) => t.text) return out.filter((t) => t.text)
} }
export default function CallTranscript({ text, name, onClose }) { export default function CallTranscript({ text, audio, name, onClose }) {
const ref = useRef(null) const ref = useRef(null)
const restore = useRef(null) const restore = useRef(null)
const turns = turnsFrom(text) const turns = turnsFrom(text)
@ -82,6 +82,16 @@ export default function CallTranscript({ text, name, onClose }) {
</button> </button>
</div> </div>
{audio ? (
<div className="conv__audio">
{/* Listen along with the transcript. preload=none so opening the
dialog does not pull a multi-MB WAV until the user hits play. */}
<audio controls preload="none" src={audio}>
Your browser cannot play this recording.
</audio>
</div>
) : null}
<div className="conv__body"> <div className="conv__body">
{turns.length ? turns.map((t, i) => ( {turns.length ? turns.map((t, i) => (
<div key={i} className={'bub bub--' + t.side}> <div key={i} className={'bub bub--' + t.side}>

View File

@ -76,3 +76,13 @@
background: var(--zk-tint); background: var(--zk-tint);
border-radius: var(--r-md); border-radius: var(--r-md);
} }
/* The call recording, played from inside the transcript dialog. */
.conv__audio {
padding: 12px 20px 4px;
border-bottom: 1px solid var(--zk-line-soft);
}
.conv__audio audio {
width: 100%;
height: 38px;
}

View File

@ -838,6 +838,17 @@ blocked ? (
Read what was actually said Read what was actually said
</button> </button>
) : null} ) : null}
{title === 'The call' && row.call_recording_url ? (
/* The recording itself, to play alongside the transcript.
preload=none keeps the multi-MB WAV off the wire until
a person actually presses play. */
<div className="glance__rec">
<span className="glance__rec-l">Call recording</span>
<audio controls preload="none" src={row.call_recording_url}>
Your browser cannot play this recording.
</audio>
</div>
) : null}
</section> </section>
))} ))}
</div> </div>
@ -864,7 +875,7 @@ blocked ? (
<AgentCard agentKey={openAgent} onClose={() => setOpenAgent(null)} /> <AgentCard agentKey={openAgent} onClose={() => setOpenAgent(null)} />
) : null} ) : null}
{showCall ? ( {showCall ? (
<CallTranscript text={row.call_transcript} name={row.customer_name} onClose={() => setShowCall(false)} /> <CallTranscript text={row.call_transcript} audio={row.call_recording_url} name={row.customer_name} onClose={() => setShowCall(false)} />
) : null} ) : null}
{showFile ? ( {showFile ? (

View File

@ -1727,6 +1727,24 @@
/* An action that belongs to one card, not to the panel. Quiet until hovered: /* An action that belongs to one card, not to the panel. Quiet until hovered:
the notes above it are the answer most of the time, and the transcript is the notes above it are the answer most of the time, and the transcript is
the follow-up question. */ the follow-up question. */
.glance__rec {
margin-top: 12px;
display: flex;
flex-direction: column;
gap: 6px;
}
.glance__rec-l {
font-size: var(--fs-3xs);
font-weight: 500;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--zk-grey);
}
.glance__rec audio {
width: 100%;
height: 36px;
}
.glance__act { .glance__act {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;