131d761936
- 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
148 lines
6.4 KiB
TypeScript
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>
|
|
);
|
|
}
|