From ee9da619c15c9dbcf50965a62762959b9dc25773 Mon Sep 17 00:00:00 2001 From: saridsa2 Date: Sun, 12 Apr 2026 16:16:31 +0530 Subject: [PATCH] feat(frontend): supervisor presence indicator on agent call card MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- src/components/call-desk/active-call-card.tsx | 15 +++++++++++++- .../call-desk/agent-status-toggle.tsx | 2 +- src/components/layout/sidebar.tsx | 2 +- src/hooks/use-agent-state.ts | 20 +++++++++++++++++-- 4 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src/components/call-desk/active-call-card.tsx b/src/components/call-desk/active-call-card.tsx index 77bec51..6f6df65 100644 --- a/src/components/call-desk/active-call-card.tsx +++ b/src/components/call-desk/active-call-card.tsx @@ -18,6 +18,7 @@ import { EnquiryForm } from './enquiry-form'; import { formatPhone } from '@/lib/format'; import { apiClient } from '@/lib/api-client'; import { useAuth } from '@/providers/auth-provider'; +import { useAgentState } from '@/hooks/use-agent-state'; import { cx } from '@/utils/cx'; import { notify } from '@/lib/toast'; import type { Lead, CallDisposition } from '@/types/entities'; @@ -49,6 +50,10 @@ export const ActiveCallCard = ({ lead, callerPhone, missedCallId, onCallComplete const [callerDisconnected, setCallerDisconnected] = useState(false); const [suggestedDisposition, setSuggestedDisposition] = useState(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 wasAnsweredRef = useRef(callState === 'active'); @@ -235,7 +240,15 @@ export const ActiveCallCard = ({ lead, callerPhone, missedCallId, onCallComplete {fullName &&

{phoneDisplay}

} - {formatDuration(callDuration)} +
+ {supervisorPresence === 'whisper' && ( + Supervisor coaching + )} + {supervisorPresence === 'barge' && ( + Supervisor on call + )} + {formatDuration(callDuration)} +
{/* Call controls */} diff --git a/src/components/call-desk/agent-status-toggle.tsx b/src/components/call-desk/agent-status-toggle.tsx index 171a718..90ae72f 100644 --- a/src/components/call-desk/agent-status-toggle.tsx +++ b/src/components/call-desk/agent-status-toggle.tsx @@ -33,7 +33,7 @@ type AgentStatusToggleProps = { export const AgentStatusToggle = ({ isRegistered, connectionStatus }: AgentStatusToggleProps) => { const agentConfig = localStorage.getItem('helix_agent_config'); const agentId = agentConfig ? JSON.parse(agentConfig).ozonetelAgentId : null; - const ozonetelState = useAgentState(agentId); + const { state: ozonetelState } = useAgentState(agentId); const [menuOpen, setMenuOpen] = useState(false); const [changing, setChanging] = useState(false); diff --git a/src/components/layout/sidebar.tsx b/src/components/layout/sidebar.tsx index 3fa6708..5b56b55 100644 --- a/src/components/layout/sidebar.tsx +++ b/src/components/layout/sidebar.tsx @@ -132,7 +132,7 @@ export const Sidebar = ({ activeUrl = "/" }: SidebarProps) => { const [collapsed, setCollapsed] = useAtom(sidebarCollapsedAtom); 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 ozonetelState = useAgentState(agentId); + const { state: ozonetelState } = useAgentState(agentId); const avatarStatus: 'online' | 'offline' = ozonetelState === 'ready' ? 'online' : 'offline'; const width = collapsed ? COLLAPSED_WIDTH : EXPANDED_WIDTH; diff --git a/src/hooks/use-agent-state.ts b/src/hooks/use-agent-state.ts index 2eaf288..f9148c6 100644 --- a/src/hooks/use-agent-state.ts +++ b/src/hooks/use-agent-state.ts @@ -2,11 +2,13 @@ import { useState, useEffect, useRef } from 'react'; import { notify } from '@/lib/toast'; 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'; -export const useAgentState = (agentId: string | null): OzonetelState => { +export const useAgentState = (agentId: string | null): { state: OzonetelState; supervisorPresence: SupervisorPresence } => { const [state, setState] = useState('offline'); + const [supervisorPresence, setSupervisorPresence] = useState('none'); const prevStateRef = useRef('offline'); const esRef = useRef(null); @@ -56,6 +58,20 @@ export const useAgentState = (agentId: string | null): OzonetelState => { 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; setState(data.state); } catch { @@ -74,5 +90,5 @@ export const useAgentState = (agentId: string | null): OzonetelState => { }; }, [agentId]); - return state; + return { state, supervisorPresence }; };