zurich_kotak/src/components/CallTranscript.jsx

109 lines
3.9 KiB
JavaScript

import { useEffect, useRef } from 'react'
import { createPortal } from 'react-dom'
import './Conversation.css'
/**
* The call, turn by turn.
*
* The voice agent hands the workflow a full transcript when a call ends, and
* until now Engage read it, wrote three sentences of summary, and the words
* themselves were dropped. So a customer said "I think it's 12,000 rupees" and
* "lower premium", and the file held one AI's paraphrase of that.
*
* It matters more here than it would elsewhere: `consent_artefact =
* verbal_call` is recorded from what the agent HEARD, and it is the artefact
* justifying every later contact. A colleague reviewing that months later
* should see the customer's own words.
*
* Rendered like the WhatsApp thread on purpose — agent right, caller left —
* because it is the same kind of thing and should not need learning twice.
*/
function turnsFrom(text) {
if (!text || typeof text !== 'string') return []
const out = []
// "Agent: ..." / "Caller: ..." with a turn continuing until the next label.
// Split on the labels rather than on newlines: a single turn wraps, and
// splitting per line would shatter one sentence into four bubbles.
const re = /^(Agent|Caller|Customer|User|Assistant):\s*/i
for (const raw of text.split('\n')) {
const line = raw.replace(/\s+$/, '')
if (!line.trim()) continue
const m = line.match(re)
if (m) {
const who = m[1].toLowerCase()
out.push({
side: (who === 'agent' || who === 'assistant') ? 'us' : 'them',
text: line.slice(m[0].length).trim(),
})
} else if (out.length) {
out[out.length - 1].text += (out[out.length - 1].text ? '\n' : '') + line.trim()
}
}
return out.filter((t) => t.text)
}
export default function CallTranscript({ text, name, onClose }) {
const ref = useRef(null)
const restore = useRef(null)
const turns = turnsFrom(text)
useEffect(() => {
restore.current = document.activeElement
ref.current?.focus()
const onKey = (e) => { if (e.key === 'Escape') onClose() }
document.addEventListener('keydown', onKey)
const prev = document.body.style.overflow
document.body.style.overflow = 'hidden'
return () => {
document.removeEventListener('keydown', onKey)
document.body.style.overflow = prev
if (restore.current instanceof HTMLElement) restore.current.focus()
}
}, [onClose])
return createPortal(
<div className="prose__scrim" onClick={onClose} role="presentation">
<div
className="prose__dlg conv"
role="dialog"
aria-modal="true"
aria-label="Call transcript"
tabIndex={-1}
ref={ref}
onClick={(e) => e.stopPropagation()}
>
<div className="prose__head">
<h3>The call with {name || 'the customer'}</h3>
<button type="button" onClick={onClose} aria-label="Close">
<svg viewBox="0 0 14 14" aria-hidden="true">
<path d="M3.5 3.5l7 7M10.5 3.5l-7 7" fill="none" stroke="currentColor"
strokeWidth="1.6" strokeLinecap="round" />
</svg>
</button>
</div>
<div className="conv__body">
{turns.length ? turns.map((t, i) => (
<div key={i} className={'bub bub--' + t.side}>
<p>{t.text}</p>
<span>{t.side === 'us' ? 'Meera' : (name || 'the customer')}</span>
</div>
)) : (
/* A transcript that will not parse is still evidence, so it is
shown raw rather than replaced with "nothing to display". */
<pre className="conv__raw">{text}</pre>
)}
</div>
<p className="conv__note">
Recorded by the voice agent as the call happened. This is what was said
the call notes in the trail are Engage&apos;s reading of it, which is a
different thing.
</p>
</div>
</div>
,
document.body,
)
}