feat(frontend): supervisor presence indicator on agent call card

- useAgentState hook returns { state, supervisorPresence }
- SSE events: supervisor-whisper → "Supervisor coaching" (blue badge)
  supervisor-barge → "Supervisor on call" (brand badge)
  supervisor-left → badge disappears
- Listen mode is silent — no badge shown
- Updated call sites: sidebar.tsx, agent-status-toggle.tsx destructure

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-12 16:16:31 +05:30
parent 42d1a03f9d
commit ee9da619c1
4 changed files with 34 additions and 5 deletions

View File

@@ -18,6 +18,7 @@ import { EnquiryForm } from './enquiry-form';
import { formatPhone } from '@/lib/format'; import { formatPhone } from '@/lib/format';
import { apiClient } from '@/lib/api-client'; import { apiClient } from '@/lib/api-client';
import { useAuth } from '@/providers/auth-provider'; import { useAuth } from '@/providers/auth-provider';
import { useAgentState } from '@/hooks/use-agent-state';
import { cx } from '@/utils/cx'; import { cx } from '@/utils/cx';
import { notify } from '@/lib/toast'; import { notify } from '@/lib/toast';
import type { Lead, CallDisposition } from '@/types/entities'; import type { Lead, CallDisposition } from '@/types/entities';
@@ -49,6 +50,10 @@ export const ActiveCallCard = ({ lead, callerPhone, missedCallId, onCallComplete
const [callerDisconnected, setCallerDisconnected] = useState(false); const [callerDisconnected, setCallerDisconnected] = useState(false);
const [suggestedDisposition, setSuggestedDisposition] = useState<CallDisposition | null>(null); const [suggestedDisposition, setSuggestedDisposition] = useState<CallDisposition | null>(null);
const agentConfig = localStorage.getItem('helix_agent_config');
const agentIdForState = agentConfig ? (() => { try { return JSON.parse(agentConfig).ozonetelAgentId; } catch { return null; } })() : null;
const { supervisorPresence } = useAgentState(agentIdForState);
const callDirectionRef = useRef(callState === 'ringing-out' ? 'OUTBOUND' : 'INBOUND'); const callDirectionRef = useRef(callState === 'ringing-out' ? 'OUTBOUND' : 'INBOUND');
const wasAnsweredRef = useRef(callState === 'active'); const wasAnsweredRef = useRef(callState === 'active');
@@ -235,7 +240,15 @@ export const ActiveCallCard = ({ lead, callerPhone, missedCallId, onCallComplete
{fullName && <p className="text-xs text-tertiary">{phoneDisplay}</p>} {fullName && <p className="text-xs text-tertiary">{phoneDisplay}</p>}
</div> </div>
</div> </div>
<Badge size="md" color="success" type="pill-color">{formatDuration(callDuration)}</Badge> <div className="flex items-center gap-2">
{supervisorPresence === 'whisper' && (
<Badge size="sm" color="blue" type="pill-color">Supervisor coaching</Badge>
)}
{supervisorPresence === 'barge' && (
<Badge size="sm" color="brand" type="pill-color">Supervisor on call</Badge>
)}
<Badge size="md" color="success" type="pill-color">{formatDuration(callDuration)}</Badge>
</div>
</div> </div>
{/* Call controls */} {/* Call controls */}

View File

@@ -33,7 +33,7 @@ type AgentStatusToggleProps = {
export const AgentStatusToggle = ({ isRegistered, connectionStatus }: AgentStatusToggleProps) => { export const AgentStatusToggle = ({ isRegistered, connectionStatus }: AgentStatusToggleProps) => {
const agentConfig = localStorage.getItem('helix_agent_config'); const agentConfig = localStorage.getItem('helix_agent_config');
const agentId = agentConfig ? JSON.parse(agentConfig).ozonetelAgentId : null; const agentId = agentConfig ? JSON.parse(agentConfig).ozonetelAgentId : null;
const ozonetelState = useAgentState(agentId); const { state: ozonetelState } = useAgentState(agentId);
const [menuOpen, setMenuOpen] = useState(false); const [menuOpen, setMenuOpen] = useState(false);
const [changing, setChanging] = useState(false); const [changing, setChanging] = useState(false);

View File

@@ -132,7 +132,7 @@ export const Sidebar = ({ activeUrl = "/" }: SidebarProps) => {
const [collapsed, setCollapsed] = useAtom(sidebarCollapsedAtom); const [collapsed, setCollapsed] = useAtom(sidebarCollapsedAtom);
const agentConfig = typeof window !== 'undefined' ? localStorage.getItem('helix_agent_config') : null; const agentConfig = typeof window !== 'undefined' ? localStorage.getItem('helix_agent_config') : null;
const agentId = agentConfig ? (() => { try { return JSON.parse(agentConfig).ozonetelAgentId; } catch { return null; } })() : null; const agentId = agentConfig ? (() => { try { return JSON.parse(agentConfig).ozonetelAgentId; } catch { return null; } })() : null;
const ozonetelState = useAgentState(agentId); const { state: ozonetelState } = useAgentState(agentId);
const avatarStatus: 'online' | 'offline' = ozonetelState === 'ready' ? 'online' : 'offline'; const avatarStatus: 'online' | 'offline' = ozonetelState === 'ready' ? 'online' : 'offline';
const width = collapsed ? COLLAPSED_WIDTH : EXPANDED_WIDTH; const width = collapsed ? COLLAPSED_WIDTH : EXPANDED_WIDTH;

View File

@@ -2,11 +2,13 @@ import { useState, useEffect, useRef } from 'react';
import { notify } from '@/lib/toast'; import { notify } from '@/lib/toast';
export type OzonetelState = 'ready' | 'break' | 'training' | 'calling' | 'in-call' | 'acw' | 'offline'; export type OzonetelState = 'ready' | 'break' | 'training' | 'calling' | 'in-call' | 'acw' | 'offline';
export type SupervisorPresence = 'none' | 'whisper' | 'barge';
const API_URL = import.meta.env.VITE_API_URL ?? 'http://localhost:4100'; const API_URL = import.meta.env.VITE_API_URL ?? 'http://localhost:4100';
export const useAgentState = (agentId: string | null): OzonetelState => { export const useAgentState = (agentId: string | null): { state: OzonetelState; supervisorPresence: SupervisorPresence } => {
const [state, setState] = useState<OzonetelState>('offline'); const [state, setState] = useState<OzonetelState>('offline');
const [supervisorPresence, setSupervisorPresence] = useState<SupervisorPresence>('none');
const prevStateRef = useRef<OzonetelState>('offline'); const prevStateRef = useRef<OzonetelState>('offline');
const esRef = useRef<EventSource | null>(null); const esRef = useRef<EventSource | null>(null);
@@ -56,6 +58,20 @@ export const useAgentState = (agentId: string | null): OzonetelState => {
return; return;
} }
// Supervisor presence events — don't replace agent state
if (data.state === 'supervisor-whisper') {
setSupervisorPresence('whisper');
return;
}
if (data.state === 'supervisor-barge') {
setSupervisorPresence('barge');
return;
}
if (data.state === 'supervisor-left') {
setSupervisorPresence('none');
return;
}
prevStateRef.current = data.state; prevStateRef.current = data.state;
setState(data.state); setState(data.state);
} catch { } catch {
@@ -74,5 +90,5 @@ export const useAgentState = (agentId: string | null): OzonetelState => {
}; };
}, [agentId]); }, [agentId]);
return state; return { state, supervisorPresence };
}; };