mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-14 12:12:23 +00:00
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:
@@ -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<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 wasAnsweredRef = useRef(callState === 'active');
|
||||
|
||||
@@ -235,7 +240,15 @@ export const ActiveCallCard = ({ lead, callerPhone, missedCallId, onCallComplete
|
||||
{fullName && <p className="text-xs text-tertiary">{phoneDisplay}</p>}
|
||||
</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>
|
||||
|
||||
{/* Call controls */}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<OzonetelState>('offline');
|
||||
const [supervisorPresence, setSupervisorPresence] = useState<SupervisorPresence>('none');
|
||||
const prevStateRef = useRef<OzonetelState>('offline');
|
||||
const esRef = useRef<EventSource | null>(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 };
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user