mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-14 20:12:25 +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 { 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,8 +240,16 @@ 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>
|
||||||
|
<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>
|
<Badge size="md" color="success" type="pill-color">{formatDuration(callDuration)}</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Call controls */}
|
{/* Call controls */}
|
||||||
<div className="mt-3 flex items-center gap-1.5 flex-wrap">
|
<div className="mt-3 flex items-center gap-1.5 flex-wrap">
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 };
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user