import type { ReactNode } from 'react'; import { useRef, useEffect } from 'react'; import { useThemeTokens } from '@/providers/theme-token-provider'; import { useChat } from '@ai-sdk/react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faPaperPlaneTop, faSparkles, faUserHeadset } from '@fortawesome/pro-duotone-svg-icons'; const API_URL = import.meta.env.VITE_API_URL ?? 'http://localhost:4100'; type CallerContext = { type?: string; callerPhone?: string; leadId?: string; leadName?: string; }; interface AiChatPanelProps { callerContext?: CallerContext; onChatStart?: () => void; } // Supervisor has different quick-action prompts than the CC agent — they // ask about team metrics, not patient / doctor info. Hardcoded here rather // than in theme tokens because the prompts map 1:1 to the supervisor tool // set in ai-chat.controller.ts (get_agent_performance, get_call_summary, // get_campaign_stats) — changing the tools means changing these prompts. const SUPERVISOR_QUICK_ACTIONS = [ { label: 'Agent performance', prompt: 'Show me agent performance this week.' }, { label: 'Call summary', prompt: 'Summarize call activity this week.' }, { label: 'Campaign stats', prompt: 'How are the campaigns performing?' }, { label: 'Who needs attention?', prompt: 'Which agents are underperforming or need attention?' }, ]; const SUPERVISOR_INTRO = 'Ask me about agent performance, call trends, or campaign stats.'; export const AiChatPanel = ({ callerContext, onChatStart }: AiChatPanelProps) => { const { tokens } = useThemeTokens(); const isSupervisor = callerContext?.type === 'supervisor'; const quickActions = isSupervisor ? SUPERVISOR_QUICK_ACTIONS : tokens.ai.quickActions; const introText = isSupervisor ? SUPERVISOR_INTRO : 'Ask me about doctors, clinics, packages, or patient info.'; const messagesEndRef = useRef(null); const chatStartedRef = useRef(false); const token = localStorage.getItem('helix_access_token') ?? ''; const { messages, input, handleSubmit, handleInputChange, isLoading, append, setMessages } = useChat({ api: `${API_URL}/api/ai/stream`, streamProtocol: 'text', headers: { 'Authorization': `Bearer ${token}`, }, body: { context: callerContext, }, }); useEffect(() => { const el = messagesEndRef.current; if (el?.parentElement) { el.parentElement.scrollTop = el.parentElement.scrollHeight; } if (messages.length > 0 && !chatStartedRef.current) { chatStartedRef.current = true; onChatStart?.(); } }, [messages, onChatStart]); // Auto-fire a patient-summary request when a caller with a leadId appears // on the panel. Resets whenever the caller changes (new incoming call) or // the call ends (leadId clears), so each call starts fresh. The sidecar's // AI agent inspects the leadId and replies with appointment/disposition/ // notes history when the caller is a returning patient. const autoFiredForLeadRef = useRef(null); useEffect(() => { const leadId = callerContext?.leadId ?? null; // Call ended or no caller — wipe the panel so the next caller's // context doesn't bleed over and the agent isn't staring at a stale // summary in the worklist view between calls. if (!leadId) { if (autoFiredForLeadRef.current !== null) { autoFiredForLeadRef.current = null; setMessages([]); chatStartedRef.current = false; } return; } if (autoFiredForLeadRef.current === leadId) return; // New caller — clear any prior chat state and fire the summary prompt. autoFiredForLeadRef.current = leadId; setMessages([]); chatStartedRef.current = false; const name = callerContext?.leadName ?? 'this caller'; append({ role: 'user', content: `Give me a quick summary of ${name} — prior appointments, last disposition, any outstanding notes. If net-new, say so.`, }); }, [callerContext?.leadId, callerContext?.leadName, append, setMessages]); const handleQuickAction = (prompt: string) => { append({ role: 'user', content: prompt }); }; return (
{messages.length === 0 && (

{introText}

{quickActions.map((action) => ( ))}
)} {messages.map((msg) => (
{msg.role === 'assistant' && (
AI
)}
))} {isLoading && (
)}
); }; // Tool result cards will be added in Phase 2 when SDK versions are aligned for data stream protocol const parseLine = (text: string): ReactNode[] => { const parts: ReactNode[] = []; const boldPattern = /\*\*(.+?)\*\*/g; let lastIndex = 0; let match: RegExpExecArray | null; while ((match = boldPattern.exec(text)) !== null) { if (match.index > lastIndex) parts.push(text.slice(lastIndex, match.index)); parts.push({match[1]}); lastIndex = boldPattern.lastIndex; } if (lastIndex < text.length) parts.push(text.slice(lastIndex)); return parts.length > 0 ? parts : [text]; }; const MessageContent = ({ content }: { content: string }) => { if (!content) return null; const lines = content.split('\n'); return (
{lines.map((line, i) => { if (line.trim().length === 0) return
; if (line.trimStart().startsWith('- ')) { return (
{parseLine(line.replace(/^\s*-\s*/, ''))}
); } return

{parseLine(line)}

; })}
); };