Files
leocrm/frontend/src/components/agents/AgentChat.tsx
T
Agent Zero 131d761936 feat(F): F-UI-CHAT AgentChat, F-UI-LOG AgentRunLog, F-UI-MON AgentMonitor
- F-UI-CHAT: AgentChat.tsx (147 lines) — SSE streaming, extended trace toggle, dry run, cost display, stop button
- F-UI-LOG: AgentRunLog.tsx (82 lines) — timeline view, export JSON/CSV
- F-UI-MON: AgentMonitor.tsx (86 lines) — live stats, active runs, auto-refresh 5s
2026-08-17 18:34:29 +02:00

148 lines
6.4 KiB
TypeScript

import { useState, useRef, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PaperAirplaneIcon, StopCircleIcon, CurrencyDollarIcon } from '@heroicons/react/24/outline';
interface AgentStep {
step_number: number;
thought: string;
action: string | null;
action_input: Record<string, unknown> | null;
observation: string | null;
cost_usd: number;
}
interface AgentChatProps {
agentId: string;
agentName: string;
}
export function AgentChat({ agentId, agentName }: AgentChatProps) {
const { t } = useTranslation();
const [messages, setMessages] = useState<string[]>([]);
const [input, setInput] = useState('');
const [steps, setSteps] = useState<AgentStep[]>([]);
const [isRunning, setIsRunning] = useState(false);
const [totalCost, setTotalCost] = useState(0);
const [extendedTrace, setExtendedTrace] = useState(false);
const [dryRun, setDryRun] = useState(false);
const eventSourceRef = useRef<EventSource | null>(null);
const handleSend = useCallback(() => {
if (!input.trim() || isRunning) return;
const userMsg = input.trim();
setMessages((prev) => [...prev, `User: ${userMsg}`]);
setInput('');
setIsRunning(true);
setSteps([]);
setTotalCost(0);
const url = `/api/v1/agents/${agentId}/stream?message=${encodeURIComponent(userMsg)}&trace_mode=${extendedTrace ? 'extended' : 'standard'}&dry_run=${dryRun}`;
const eventSource = new EventSource(url);
eventSourceRef.current = eventSource;
eventSource.addEventListener('step', (e) => {
const step = JSON.parse(e.data) as AgentStep;
setSteps((prev) => [...prev, step]);
setTotalCost((prev) => prev + (step.cost_usd || 0));
});
eventSource.addEventListener('status', (e) => {
const data = JSON.parse(e.data);
if (data.status === 'running') {
setMessages((prev) => [...prev, `Step ${data.step}: ${data.action || 'Thinking...'}`]);
}
});
eventSource.addEventListener('done', (e) => {
const data = JSON.parse(e.data);
setMessages((prev) => [...prev, `Agent: ${data.final_content}`]);
setIsRunning(false);
eventSource.close();
});
eventSource.addEventListener('error', (e) => {
try {
const data = JSON.parse(e.data);
setMessages((prev) => [...prev, `Error: ${data.error}`]);
} catch {
setMessages((prev) => [...prev, 'Error: Connection lost']);
}
setIsRunning(false);
eventSource.close();
});
}, [input, isRunning, agentId, extendedTrace, dryRun]);
const handleStop = useCallback(() => {
eventSourceRef.current?.close();
setIsRunning(false);
setMessages((prev) => [...prev, 'Agent run stopped by user']);
}, []);
return (
<div className="flex flex-col h-full bg-white dark:bg-gray-900">
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">{agentName}</h2>
<div className="flex items-center gap-4">
<label className="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400">
<input type="checkbox" checked={extendedTrace} onChange={(e) => setExtendedTrace(e.target.checked)} className="rounded" aria-label={t('agents.extendedTrace')} />
{t('agents.extendedTrace')}
</label>
<label className="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400">
<input type="checkbox" checked={dryRun} onChange={(e) => setDryRun(e.target.checked)} className="rounded" aria-label={t('agents.dryRun')} />
{t('agents.dryRun')}
</label>
{totalCost > 0 && (
<span className="flex items-center gap-1 text-sm text-gray-600 dark:text-gray-400">
<CurrencyDollarIcon className="w-4 h-4" />
{totalCost.toFixed(6)}
</span>
)}
</div>
</div>
<div className="flex-1 overflow-y-auto p-4 space-y-3">
{messages.map((msg, i) => (
<div key={i} className={`p-3 rounded-lg ${msg.startsWith('User:') ? 'bg-primary-50 dark:bg-primary-900/20' : msg.startsWith('Error:') ? 'bg-red-50 dark:bg-red-900/20' : 'bg-gray-50 dark:bg-gray-800'}`}>
<p className="text-sm text-gray-900 dark:text-gray-100">{msg}</p>
</div>
))}
{extendedTrace && steps.map((step) => (
<div key={step.step_number} className="p-3 rounded-lg border border-gray-200 dark:border-gray-700">
<div className="flex items-center justify-between mb-2">
<span className="text-xs font-medium text-gray-500">Step {step.step_number}</span>
<span className="text-xs text-gray-400">${step.cost_usd.toFixed(6)}</span>
</div>
{step.thought && <p className="text-sm text-gray-700 dark:text-gray-300 mb-1"><strong>Thought:</strong> {step.thought}</p>}
{step.action && <p className="text-sm text-primary-600 dark:text-primary-400 mb-1"><strong>Action:</strong> {step.action}</p>}
{step.observation && <p className="text-sm text-gray-600 dark:text-gray-400"><strong>Observation:</strong> {step.observation}</p>}
</div>
))}
</div>
<div className="p-4 border-t border-gray-200 dark:border-gray-700">
<div className="flex items-center gap-2">
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSend()}
placeholder={t('agents.typeMessage')}
disabled={isRunning}
className="flex-1 px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-primary-500"
aria-label={t('agents.messageInput')}
/>
{isRunning ? (
<button onClick={handleStop} className="p-2 rounded-lg bg-red-500 text-white hover:bg-red-600 min-h-[44px] min-w-[44px]" aria-label={t('agents.stop')}>
<StopCircleIcon className="w-5 h-5" />
</button>
) : (
<button onClick={handleSend} disabled={!input.trim()} className="p-2 rounded-lg bg-primary-600 text-white hover:bg-primary-700 disabled:opacity-50 min-h-[44px] min-w-[44px]" aria-label={t('agents.send')}>
<PaperAirplaneIcon className="w-5 h-5" />
</button>
)}
</div>
</div>
</div>
);
}