import { useState } from 'react' import { Headphones, Inbox, PhoneForwarded, RefreshCw, Sparkles, User } from 'lucide-react' import { useQuery, useZino } from '../zino-sdk' import { fetchCallMessages, fetchMyCalls, type CopilotSession } from '../api' import { LIVE_CALL_PREFIX } from '../config' const POLL_MS = 3000 /** * What the human colleague sees after a call was transferred to them. * * Scoped to the signed-in user by the server, not by this component: each call * is a co-pilot session OWNED BY the human it was routed to, and every read * authorizes on `session.user_id == token.sub`. Someone else's call is a 403. */ export default function MyCalls() { const { client } = useZino() const [selectedId, setSelectedId] = useState(null) const { data: calls = [], isLoading, isFetching, error, refetch } = useQuery({ queryKey: ['my-calls'], queryFn: () => fetchMyCalls(client), refetchInterval: POLL_MS, }) const activeId = selectedId ?? calls[0]?.id ?? null return (

My calls

Calls Meera transferred to you. The conversation streams in live while you talk, with quiet suggestions you can relay.

{error && (

{(error as any)?.message || 'Could not load your calls'}

)} {!isLoading && calls.length === 0 && (

No calls transferred to you yet

When Meera hands a call to your desk, it appears here — with the brief, the live conversation, and the AI's suggestions.

)} {calls.length > 0 && (
    {calls.map((c) => ( setSelectedId(c.id)} /> ))}
{activeId && }
)}
) } function CallRow({ call, active, onClick, }: { call: CopilotSession active: boolean onClick: () => void }) { const cd = (call.collected_data ?? {}) as Record const who = String(cd.name ?? cd.lead_name ?? 'Caller') const reason = String(cd.handover_reason ?? '') return (
  • ) } function Thread({ sessionId }: { sessionId: number }) { const { client } = useZino() const { data: messages = [], isLoading, error } = useQuery({ queryKey: ['call-thread', sessionId], queryFn: () => fetchCallMessages(client, sessionId), // Polls while the call is live — this is the screen the human works from. refetchInterval: POLL_MS, }) if (error) { return (
    {(error as any)?.status === 403 ? 'This call was transferred to someone else.' : (error as any)?.message || 'Could not load the conversation'}
    ) } return (
    {isLoading &&

    Loading…

    } {messages.map((m) => { const overheard = m.content.startsWith(LIVE_CALL_PREFIX) if (overheard) { return ( } label="Overheard on the call" tone="slate" text={m.content.slice(LIVE_CALL_PREFIX.length).trim()} /> ) } if (m.content.startsWith('HANDOVER BRIEF')) { return ( } label="Handover brief" tone="amber" text={m.content.replace(/^HANDOVER BRIEF\n?/, '')} /> ) } // A lone dash is the co-pilot deliberately staying quiet — rendering it // as an empty suggestion box would read as a bug. if (m.content.trim() === '—' || m.content.trim() === '-') return null return ( } label="Suggested — yours to relay" tone="indigo" text={m.content} /> ) })}
    ) } function Bubble({ icon, label, text, tone, }: { icon: React.ReactNode label: string text: string tone: 'slate' | 'indigo' | 'amber' }) { const skin = { slate: 'border-slate-200 bg-slate-50 text-slate-700', indigo: 'border-indigo-200 bg-indigo-50 text-indigo-900', amber: 'border-amber-200 bg-amber-50 text-amber-900', }[tone] return (
    {icon} {label}

    {text}

    ) } function when(ts?: string): string { if (!ts) return '' const d = new Date(ts) if (Number.isNaN(d.getTime())) return '' return d.toLocaleString(undefined, { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit', }) }