// Chat Panel — vini-agent-hub // D-06: receives scope + setScope from App; D-05: action field rendered if present (always null for now) const SUGGESTION_CHIPS = [ 'ver últimos erros', 'qual agente gastou mais essa semana', 'pause a Helena', 'executar X agora', 'status geral', ]; const ChatPanel = ({ agents, scope, setScope, activeId }) => { const [history, setHistory] = React.useState(() => { try { const raw = localStorage.getItem('hub_chat_history'); if (!raw) return []; const parsed = JSON.parse(raw); return Array.isArray(parsed) ? parsed : []; } catch { return []; } }); // [{role:'user'|'assistant', content:string}] const [input, setInput] = React.useState(""); const [sending, setSending] = React.useState(false); const [error, setError] = React.useState(null); const listRef = React.useRef(null); const textareaRef = React.useRef(null); React.useEffect(() => { if (listRef.current) listRef.current.scrollTop = listRef.current.scrollHeight; }, [history, sending]); React.useEffect(() => { try { const capped = history.slice(-50); localStorage.setItem('hub_chat_history', JSON.stringify(capped)); } catch (e) { // localStorage full or disabled — silently ignore (chat still works in-session) } }, [history]); // Resolve scope display name const scopeLabel = scope && scope.type === 'agent' && scope.agentId && Array.isArray(agents) ? (agents.find(a => a.id === scope.agentId) || {}).name || 'Agente' : null; const scopeDisplay = scopeLabel ? scopeLabel : 'Toda a equipe'; const isAgentScope = !!(scope && scope.type === 'agent'); // When in "all" scope, the toggle switches to whichever agent the detail panel // is currently showing (activeId). With no active agent, the toggle is a no-op. const canScopeToAgent = !!activeId && Array.isArray(agents) && agents.some(a => a.id === activeId); const handleToggleScope = () => { if (!setScope) return; if (isAgentScope) { setScope({ type: 'all', agentId: null }); } else if (canScopeToAgent) { setScope({ type: 'agent', agentId: activeId }); } }; const clearHistory = () => { if (history.length === 0) return; if (window.confirm('Limpar todo o histórico do chat?')) { setHistory([]); try { localStorage.removeItem('hub_chat_history'); } catch {} } }; const send = async () => { const msg = input.trim(); if (!msg || sending) return; setError(null); setSending(true); const newHistory = [...history, { role: 'user', content: msg }]; setHistory(newHistory); setInput(""); try { const token = localStorage.getItem('hub_token'); const r = await fetch('/api/v1/chat', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, }, body: JSON.stringify({ message: msg, // send history WITHOUT the just-added user message — backend appends it conversation_history: history, }), }); if (r.status === 401) { localStorage.removeItem('hub_token'); window.location = '/login'; return; } if (!r.ok) { setError(`Erro ${r.status}`); return; } const data = await r.json(); setHistory((h) => [...h, { role: 'assistant', content: data.response, action: data.action || null, }]); } catch (e) { setError('Erro de rede'); } finally { setSending(false); } }; const onKeyDown = (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); } }; const handleChipClick = (chip) => { setInput(chip); if (textareaRef.current) textareaRef.current.focus(); }; return (
{/* D-06: scope header */}
Falando sobre: {scopeDisplay} {setScope && ( )}

Chat

{history.length > 0 && ( )}
{history.length === 0 &&
Pergunte algo sobre seus agentes…
} {history.map((m, i) => (
{m.role === 'user' ? 'Você' : 'Assistente'}
{m.content}
{/* D-05: action card — renders when action is present (always null from backend now) */} {m.action && (
{m.action.description || JSON.stringify(m.action)}
)}
))} {sending &&
}
{error &&
{error}
} {/* 5 suggestion chips */}
{SUGGESTION_CHIPS.map(chip => ( ))}