// packages/web/src/components/chat/ChatAssistant.tsx // // ICC Chat Assistant — conversational AI interface // Renders messages, inline transaction tables, stats, and scan progress // // Usage: // import ChatAssistant from '@/components/chat/ChatAssistant'; // refetchTransactions()} /> import { useState, useRef, useEffect, type KeyboardEvent } from 'react'; import { useChatAssistant, type ChatMessage } from '../../hooks/useChatAssistant'; interface ChatAssistantProps { onScanTriggered?: () => void; className?: string; } export default function ChatAssistant({ onScanTriggered, className = '' }: ChatAssistantProps) { const { messages, isLoading, sendMessage, clearHistory } = useChatAssistant(); const [input, setInput] = useState(''); const [isOpen, setIsOpen] = useState(false); const messagesEndRef = useRef(null); const inputRef = useRef(null); // Auto-scroll to bottom on new messages useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages]); // Focus input when panel opens useEffect(() => { if (isOpen) inputRef.current?.focus(); }, [isOpen]); // Notify parent when scan is triggered useEffect(() => { const lastMsg = messages[messages.length - 1]; if (lastMsg?.type === 'scan_started' && onScanTriggered) { onScanTriggered(); } }, [messages, onScanTriggered]); const handleSend = () => { if (input.trim()) { sendMessage(input); setInput(''); } }; const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSend(); } }; // Quick action buttons const quickActions = [ { label: "Scanner aujourd'hui", command: "Scanne les courriels d'aujourd'hui" }, { label: 'Scanner 7 jours', command: 'Scanne les courriels des 7 derniers jours' }, { label: 'Transactions', command: 'Montre les transactions récentes' }, { label: 'Statistiques', command: 'Montre les statistiques' }, ]; return ( <> {/* ═══ FLOATING BUTTON ═══ */} {/* ═══ CHAT PANEL ═══ */}
{/* ─── HEADER ─── */}

Assistant ICC

Gestion des virements Interac

{/* ─── MESSAGES ─── */}
{messages.map((msg) => ( ))} {/* Typing indicator */} {isLoading && (
)}
{/* ─── QUICK ACTIONS ─── */}
{quickActions.map((action) => ( ))}
{/* ─── INPUT ─── */}
setInput(e.target.value)} onKeyDown={handleKeyDown} placeholder="Écris une commande..." disabled={isLoading} className="flex-1 bg-transparent text-sm text-slate-700 placeholder:text-slate-400 focus:outline-none py-1.5 disabled:opacity-50" />
); } // ═══════════════════════════════════════════ // MESSAGE BUBBLE — renders different message types // ═══════════════════════════════════════════ function MessageBubble({ message }: { message: ChatMessage }) { const isUser = message.role === 'user'; return (
{/* Avatar */} {!isUser && (
)}
{/* Bubble */}
{/* INLINE DATA DISPLAYS */} {message.data && message.type === 'transactions' && ( )} {message.data && message.type === 'search_results' && ( )} {message.data && message.type === 'stats' && ( )} {/* Timestamp */}
{message.timestamp.toLocaleTimeString('fr-CA', { hour: '2-digit', minute: '2-digit' })}
); } // ═══════════════════════════════════════════ // FORMATTED TEXT — renders markdown-lite (bold, newlines) // ═══════════════════════════════════════════ function FormattedText({ text }: { text: string }) { // Simple markdown: **bold**, newlines const parts = text.split('\n').map((line, i) => { const formatted = line.replace( /\*\*(.*?)\*\*/g, '$1' ); return ( {i > 0 &&
} ); }); return <>{parts}; } // ═══════════════════════════════════════════ // TRANSACTION TABLE — inline scrollable table // ═══════════════════════════════════════════ function TransactionTable({ transactions }: { transactions: any[] }) { if (!transactions || transactions.length === 0) return null; return (
{transactions.slice(0, 15).map((tx: any, i: number) => ( ))}
Date Expéditeur Montant Succursale Statut
{tx.date ? new Date(tx.date).toLocaleDateString('fr-CA', { day: '2-digit', month: 'short', }) : '—'} {tx.sender || '—'} {tx.amount ? `${Number(tx.amount).toFixed(2)} $` : '—'} {tx.branch || '—'}
{transactions.length > 15 && (
+ {transactions.length - 15} autres transactions
)}
); } // ═══════════════════════════════════════════ // STATS CARD — compact stats display // ═══════════════════════════════════════════ function StatsCard({ stats }: { stats: any }) { if (!stats) return null; return (
{/* Top-level stats */}
{stats.totalTransactions}
Virements
{Number(stats.totalAmount).toLocaleString('fr-CA', { minimumFractionDigits: 2 })} $
Total
{/* Top branches */} {stats.byBranch && stats.byBranch.length > 0 && (
Top succursales
{stats.byBranch.slice(0, 5).map((b: any, i: number) => (
{b.branch} {b.count} · {Number(b.total).toFixed(0)} $
))}
)}
); } // ═══════════════════════════════════════════ // STATUS BADGE // ═══════════════════════════════════════════ function StatusBadge({ status }: { status: string }) { const colors: Record = { deposited: 'bg-green-100 text-green-700', pending: 'bg-amber-100 text-amber-700', expired: 'bg-slate-100 text-slate-500', cancelled: 'bg-red-100 text-red-600', }; return ( {status || '—'} ); }