From 131d761936f214b19db043b476ee6665cc01c1a2 Mon Sep 17 00:00:00 2001 From: Agent Zero Date: Mon, 17 Aug 2026 18:34:29 +0200 Subject: [PATCH] feat(F): F-UI-CHAT AgentChat, F-UI-LOG AgentRunLog, F-UI-MON AgentMonitor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- frontend/src/components/agents/AgentChat.tsx | 147 ++++++++++++++++++ .../src/components/agents/AgentMonitor.tsx | 86 ++++++++++ .../src/components/agents/AgentRunLog.tsx | 82 ++++++++++ 3 files changed, 315 insertions(+) create mode 100644 frontend/src/components/agents/AgentChat.tsx create mode 100644 frontend/src/components/agents/AgentMonitor.tsx create mode 100644 frontend/src/components/agents/AgentRunLog.tsx diff --git a/frontend/src/components/agents/AgentChat.tsx b/frontend/src/components/agents/AgentChat.tsx new file mode 100644 index 0000000..f6bf270 --- /dev/null +++ b/frontend/src/components/agents/AgentChat.tsx @@ -0,0 +1,147 @@ +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 | 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([]); + const [input, setInput] = useState(''); + const [steps, setSteps] = useState([]); + const [isRunning, setIsRunning] = useState(false); + const [totalCost, setTotalCost] = useState(0); + const [extendedTrace, setExtendedTrace] = useState(false); + const [dryRun, setDryRun] = useState(false); + const eventSourceRef = useRef(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 ( +
+
+

{agentName}

+
+ + + {totalCost > 0 && ( + + + {totalCost.toFixed(6)} + + )} +
+
+ +
+ {messages.map((msg, i) => ( +
+

{msg}

+
+ ))} + {extendedTrace && steps.map((step) => ( +
+
+ Step {step.step_number} + ${step.cost_usd.toFixed(6)} +
+ {step.thought &&

Thought: {step.thought}

} + {step.action &&

Action: {step.action}

} + {step.observation &&

Observation: {step.observation}

} +
+ ))} +
+ +
+
+ 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 ? ( + + ) : ( + + )} +
+
+
+ ); +} diff --git a/frontend/src/components/agents/AgentMonitor.tsx b/frontend/src/components/agents/AgentMonitor.tsx new file mode 100644 index 0000000..7ccf97d --- /dev/null +++ b/frontend/src/components/agents/AgentMonitor.tsx @@ -0,0 +1,86 @@ +import { useTranslation } from 'react-i18next'; +import { useQuery } from '@tanstack/react-query'; +import { ChartBarIcon, ExclamationTriangleIcon, CurrencyDollarIcon, ClockIcon } from '@heroicons/react/24/outline'; + +export function AgentMonitor() { + const { t } = useTranslation(); + + const { data: stats, isLoading } = useQuery({ + queryKey: ['agent-monitor-stats'], + queryFn: async () => { + const res = await fetch('/api/v1/agents/monitor/stats'); + if (!res.ok) throw new Error('Failed to fetch stats'); + return res.json() as Promise<{ active_runs: number; total_budget_usd: number; runs_per_hour: number; error_rate: number }>; + }, + refetchInterval: 5000, + }); + + const { data: activeRuns } = useQuery({ + queryKey: ['agent-active-runs'], + queryFn: async () => { + const res = await fetch('/api/v1/agents/runs/recent?status=running&limit=20'); + if (!res.ok) throw new Error('Failed to fetch runs'); + return res.json() as Promise>; + }, + refetchInterval: 5000, + }); + + return ( +
+

{t('agents.monitoring')}

+ +
+
+
+ + {t('agents.activeRuns')} +
+

{stats?.active_runs ?? 0}

+
+
+
+ + {t('agents.totalBudget')} +
+

${(stats?.total_budget_usd ?? 0).toFixed(4)}

+
+
+
+ + {t('agents.runsPerHour')} +
+

{stats?.runs_per_hour ?? 0}

+
+
+
+ + {t('agents.errorRate')} +
+

{(stats?.error_rate ?? 0).toFixed(1)}%

+
+
+ +
+

{t('agents.activeRunsList')}

+ {isLoading &&

{t('common.loading')}

} +
+ {activeRuns?.map((run) => ( +
+
+

{run.agent_name}

+

{new Date(run.started_at).toLocaleString()}

+
+
+ ${run.cost_usd.toFixed(6)} + {run.status} +
+
+ ))} + {(!activeRuns || activeRuns.length === 0) && !isLoading && ( +

{t('agents.noActiveRuns')}

+ )} +
+
+
+ ); +} diff --git a/frontend/src/components/agents/AgentRunLog.tsx b/frontend/src/components/agents/AgentRunLog.tsx new file mode 100644 index 0000000..c83c694 --- /dev/null +++ b/frontend/src/components/agents/AgentRunLog.tsx @@ -0,0 +1,82 @@ +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useQuery } from '@tanstack/react-query'; +import { ArrowDownTrayIcon } from '@heroicons/react/24/outline'; + +interface RunStep { + id: string; + step_number: number; + thought: string | null; + action: string | null; + action_input: Record | null; + observation: string | null; + cost_usd: number; + created_at: string; +} + +interface AgentRunLogProps { + agentId: string; + runId: string; +} + +export function AgentRunLog({ agentId, runId }: AgentRunLogProps) { + const { t } = useTranslation(); + const [filterStatus, setFilterStatus] = useState('all'); + + const { data: steps, isLoading } = useQuery({ + queryKey: ['agent-run-steps', agentId, runId], + queryFn: async () => { + const res = await fetch(`/api/v1/agents/${agentId}/runs/${runId}/steps`); + if (!res.ok) throw new Error('Failed to fetch steps'); + return res.json() as Promise; + }, + }); + + const handleExport = (format: 'json' | 'csv') => { + if (!steps) return; + const data = format === 'json' ? JSON.stringify(steps, null, 2) : + 'step,thought,action,observation,cost,timestamp\n' + + steps.map((s) => `${s.step_number},${s.thought || ''},${s.action || ''},${s.observation || ''},${s.cost_usd},${s.created_at}`).join('\n'); + const blob = new Blob([data], { type: format === 'json' ? 'application/json' : 'text/csv' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `agent-run-${runId}.${format}`; + a.click(); + URL.revokeObjectURL(url); + }; + + return ( +
+
+

{t('agents.runLog')}

+
+ + +
+
+
+ {isLoading &&

{t('common.loading')}

} + {steps?.map((step) => ( +
+
+ Step {step.step_number} +
+ ${step.cost_usd.toFixed(6)} + {new Date(step.created_at).toLocaleString()} +
+
+ {step.thought &&

Thought: {step.thought}

} + {step.action &&

Action: {step.action}

} + {step.action_input &&
{JSON.stringify(step.action_input, null, 2)}
} + {step.observation &&

Observation: {step.observation}

} +
+ ))} +
+
+ ); +}