diff --git a/src/api.ts b/src/api.ts index a69133b..9093e4e 100644 --- a/src/api.ts +++ b/src/api.ts @@ -171,3 +171,36 @@ export async function fetchCallMessages( if (Array.isArray(res)) return res return res?.messages ?? [] } + +export interface CallFacts { + voice_session_id: number + recording_url: string + end_reason: string + duration_seconds: number + answered: boolean + started_at?: string + ended_at?: string | null +} + +/** + * The call behind a transferred session — recording included. + * + * The audio lives on the VOICE session, which is owned by the telephony leg and + * not by any person, so it is unreachable through the ordinary session API. This + * endpoint bridges that: owning the co-pilot session is the statement "you were + * the colleague on this call", and it returns only that call's own facts. + * + * 404 is normal — a session that isn't a handover has no call behind it, and a + * recording is written at call END, so a live call has facts but no audio yet. + */ +export async function fetchCallFacts( + client: ZinoClient, + sessionId: number, +): Promise { + try { + return await client.request('GET', `/api/agent-sessions/${sessionId}/call`) + } catch (err: any) { + if (err?.status === 404) return null + throw err + } +} diff --git a/src/pages/MyCalls.tsx b/src/pages/MyCalls.tsx index 3cf93ea..b07af15 100644 --- a/src/pages/MyCalls.tsx +++ b/src/pages/MyCalls.tsx @@ -1,7 +1,7 @@ import { useState } from 'react' -import { Headphones, Inbox, PhoneForwarded, RefreshCw, Sparkles, User } from 'lucide-react' +import { Clock, Headphones, Inbox, PhoneForwarded, RefreshCw, Sparkles, User } from 'lucide-react' import { useQuery, useZino } from '../zino-sdk' -import { fetchCallMessages, fetchMyCalls, type CopilotSession } from '../api' +import { fetchCallFacts, fetchCallMessages, fetchMyCalls, type CopilotSession } from '../api' import { LIVE_CALL_PREFIX } from '../config' const POLL_MS = 3000 @@ -73,7 +73,12 @@ export default function MyCalls() { /> ))} - {activeId && } + {activeId && ( +
+ + +
+ )} )} @@ -114,6 +119,62 @@ function CallRow({ ) } +/** + * The recording, and the call's own facts. + * + * This is the evidence the transcript above is a summary OF. On a demo whose + * pitch is that the AI captured the conversation faithfully, "did it?" should be + * answerable by pressing play rather than by trusting the text. + */ +function Recording({ sessionId }: { sessionId: number }) { + const { client } = useZino() + const { data, isLoading } = useQuery({ + queryKey: ['call-facts', sessionId], + queryFn: () => fetchCallFacts(client, sessionId), + // Written at call end, so it arrives late on a live call. Keep asking, but + // stop once there is audio. + refetchInterval: (q: any) => (q?.state?.data?.recording_url ? false : POLL_MS * 3), + }) + + if (isLoading || !data) return null + + return ( +
+
+

+ Call recording +

+ + + {dur(data.duration_seconds)} + + {data.end_reason && ( + + {data.end_reason.replace(/-/g, ' ')} + + )} +
+ + {data.recording_url ? ( + // preload="none": the audio is a WAV of a whole phone call. Fetching it + // for every call in the list, before anyone asks to hear one, would pull + // megabytes nobody wanted. +
+ ) +} + +function dur(s?: number): string { + const n = Number(s ?? 0) + if (!n) return '—' + return n >= 60 ? `${Math.floor(n / 60)}m ${n % 60}s` : `${n}s` +} + function Thread({ sessionId }: { sessionId: number }) { const { client } = useZino() const { data: messages = [], isLoading, error } = useQuery({