gmail-agentic / frontend /components /AgentRunTimeline.tsx
spoodermon's picture
Initial commit
c1f8e04
Raw
History Blame Contribute Delete
5.08 kB
import React, { useState, useEffect } from 'react';
import { Mail, Search, Sparkles, Database, ShieldCheck, Terminal, AlertCircle } from 'lucide-react';
import { API_BASE_URL } from '@/app/config';
interface AgentLogItem {
step: number;
thought: string;
action: string;
result: string;
timestamp: string;
}
interface AgentRunTimelineProps {
runId: string;
}
export default function AgentRunTimeline({ runId }: AgentRunTimelineProps) {
const [logs, setLogs] = useState<AgentLogItem[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(false);
useEffect(() => {
const fetchLogs = async () => {
setIsLoading(true);
setError(false);
const token = localStorage.getItem('token') || '';
try {
const response = await fetch(`${API_BASE_URL}/api/agent/runs/${runId}/logs`, {
headers: {
'Authorization': `Bearer ${token}`,
},
});
if (!response.ok) {
throw new Error('Failed to fetch run logs');
}
const data = await response.json();
setLogs(data);
} catch (err) {
console.error(err);
setError(true);
} finally {
setIsLoading(false);
}
};
if (runId) {
fetchLogs();
// Poll every 3 seconds while active
const interval = setInterval(fetchLogs, 3000);
return () => clearInterval(interval);
}
}, [runId]);
const getActionIcon = (action: string) => {
switch (action.toUpperCase()) {
case 'READ_EMAIL':
return <Mail size={14} className="text-cyan-400" />;
case 'RETRIEVE_CONTEXT':
return <Database size={14} className="text-indigo-400" />;
case 'LLM_TRIAGE':
return <Sparkles size={14} className="text-purple-400" />;
case 'COMMIT_CHANGES':
return <ShieldCheck size={14} className="text-emerald-400" />;
default:
return <Terminal size={14} className="text-slate-400" />;
}
};
const getActionColor = (action: string) => {
switch (action.toUpperCase()) {
case 'READ_EMAIL':
return 'border-cyan-500/30 bg-cyan-950/20';
case 'RETRIEVE_CONTEXT':
return 'border-indigo-500/30 bg-indigo-950/20';
case 'LLM_TRIAGE':
return 'border-purple-500/30 bg-purple-950/20';
case 'COMMIT_CHANGES':
return 'border-emerald-500/30 bg-emerald-950/20';
default:
return 'border-slate-800 bg-slate-950/40';
}
};
if (isLoading && logs.length === 0) {
return (
<div className="flex items-center justify-center p-8 text-slate-500">
<div className="flex items-center gap-2">
<div className="h-4 w-4 animate-spin rounded-full border border-purple-500 border-t-transparent" />
<span className="text-xs">Loading audit trail...</span>
</div>
</div>
);
}
if (error && logs.length === 0) {
return (
<div className="flex items-center justify-center p-6 text-red-400 gap-2 border border-red-950/30 bg-red-950/10 rounded-xl">
<AlertCircle size={16} />
<span className="text-xs">Could not fetch logs for this agent execution.</span>
</div>
);
}
return (
<div className="flex flex-col gap-4">
<h4 className="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2 flex items-center gap-1.5">
<Terminal size={14} />
<span>Agent Thinking Audit Trail</span>
</h4>
{logs.length === 0 ? (
<p className="text-xs text-slate-500 italic">No agent log steps found for this execution run.</p>
) : (
<div className="relative border-l border-slate-800 ml-3.5 pl-6 flex flex-col gap-6.5">
{logs.map((log) => (
<div key={log.step} className="relative group">
{/* Vertical line connector bullet */}
<div className={`absolute -left-[35px] top-0 h-7 w-7 rounded-full border flex items-center justify-center transition-transform group-hover:scale-110 ${getActionColor(log.action)}`}>
{getActionIcon(log.action)}
</div>
{/* Node Contents */}
<div>
<div className="flex items-center gap-2.5 mb-1">
<span className="text-xs font-semibold text-slate-200">
Step {log.step}: {log.thought}
</span>
<span className="text-[10px] text-slate-500 font-mono">
{new Date(log.timestamp).toLocaleTimeString()}
</span>
</div>
<div className="bg-slate-950/35 border border-slate-900/60 p-3 rounded-xl text-[11px] text-slate-400 font-mono leading-relaxed max-w-full overflow-x-auto">
<span className="text-slate-500 block mb-1">Action: {log.action}</span>
<span className="text-slate-300 block">{log.result}</span>
</div>
</div>
</div>
))}
</div>
)}
</div>
);
}