190 lines
6.1 KiB
TypeScript
190 lines
6.1 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect } from "react";
|
|
|
|
interface ExecutionLog {
|
|
id: string;
|
|
templateId: string;
|
|
agent: string;
|
|
taskTitle: string;
|
|
startedAt: string;
|
|
completedAt?: string;
|
|
status: string;
|
|
output?: string;
|
|
error?: string;
|
|
}
|
|
|
|
const AGENT_COLORS: Record<string, string> = {
|
|
thoth: "#8b5cf6",
|
|
"thoth-trading": "#f59e0b",
|
|
ptah: "#10b981",
|
|
seshat: "#ec4899",
|
|
anubis: "#6366f1",
|
|
sekhmet: "#ef4444"
|
|
};
|
|
|
|
export default function ExecutionLogsPanel() {
|
|
const [logs, setLogs] = useState<ExecutionLog[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [filterAgent, setFilterAgent] = useState<string>("all");
|
|
const [selectedLog, setSelectedLog] = useState<ExecutionLog | null>(null);
|
|
|
|
useEffect(() => {
|
|
fetchLogs();
|
|
const interval = setInterval(fetchLogs, 30000); // Refresh every 30s
|
|
return () => clearInterval(interval);
|
|
}, []);
|
|
|
|
const fetchLogs = async () => {
|
|
const res = await fetch("/api/execution-logs?limit=50");
|
|
const data = await res.json();
|
|
setLogs(data);
|
|
setLoading(false);
|
|
};
|
|
|
|
const getStatusColor = (status: string) => {
|
|
switch (status) {
|
|
case "running": return "text-yellow-400 bg-yellow-400/20";
|
|
case "completed": return "text-green-400 bg-green-400/20";
|
|
case "failed": return "text-red-400 bg-red-400/20";
|
|
default: return "text-white/50 bg-white/10";
|
|
}
|
|
};
|
|
|
|
const filteredLogs = filterAgent === "all"
|
|
? logs
|
|
: logs.filter(l => l.agent === filterAgent);
|
|
|
|
const agents = [...new Set(logs.map(l => l.agent))];
|
|
|
|
if (loading) {
|
|
return <div className="p-4 text-white/50">Loading logs...</div>;
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<div className="flex justify-between items-center">
|
|
<h3 className="text-lg font-semibold">📊 Execution Logs</h3>
|
|
<div className="flex gap-2">
|
|
<select
|
|
value={filterAgent}
|
|
onChange={(e) => setFilterAgent(e.target.value)}
|
|
className="bg-black/30 border border-white/20 rounded px-3 py-1 text-sm text-white"
|
|
>
|
|
<option value="all">All Agents</option>
|
|
{agents.map(agent => (
|
|
<option key={agent} value={agent}>{agent}</option>
|
|
))}
|
|
</select>
|
|
<button
|
|
onClick={fetchLogs}
|
|
className="px-3 py-1 bg-white/10 hover:bg-white/20 rounded text-sm"
|
|
>
|
|
🔄
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Log List */}
|
|
<div className="space-y-2 max-h-96 overflow-y-auto">
|
|
{filteredLogs.map((log) => (
|
|
<div
|
|
key={log.id}
|
|
onClick={() => setSelectedLog(log)}
|
|
className={`p-3 rounded-lg cursor-pointer transition ${
|
|
selectedLog?.id === log.id
|
|
? "bg-brand-pink/20 border border-brand-pink"
|
|
: "bg-white/5 border border-white/10 hover:bg-white/10"
|
|
}`}
|
|
>
|
|
<div className="flex justify-between items-start">
|
|
<div className="flex items-center gap-2">
|
|
<span
|
|
className="px-2 py-0.5 rounded text-xs"
|
|
style={{ backgroundColor: AGENT_COLORS[log.agent] || "#666" }}
|
|
>
|
|
{log.agent}
|
|
</span>
|
|
<span className="font-medium">{log.taskTitle}</span>
|
|
</div>
|
|
<span className={`px-2 py-0.5 rounded text-xs ${getStatusColor(log.status)}`}>
|
|
{log.status}
|
|
</span>
|
|
</div>
|
|
<div className="flex gap-4 mt-1 text-xs text-white/40">
|
|
<span>{new Date(log.startedAt).toLocaleString()}</span>
|
|
{log.completedAt && (
|
|
<span>Duration: {Math.round((new Date(log.completedAt).getTime() - new Date(log.startedAt).getTime()) / 1000)}s</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
{filteredLogs.length === 0 && (
|
|
<div className="text-center py-8 text-white/50">
|
|
No execution logs yet. Run a task to see logs here.
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Log Detail */}
|
|
{selectedLog && (
|
|
<div className="mt-4 p-4 bg-black/30 rounded-lg border border-white/10">
|
|
<div className="flex justify-between items-start mb-3">
|
|
<h4 className="font-medium">{selectedLog.taskTitle}</h4>
|
|
<button
|
|
onClick={() => setSelectedLog(null)}
|
|
className="text-white/50 hover:text-white"
|
|
>
|
|
✕
|
|
</button>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4 text-sm mb-3">
|
|
<div>
|
|
<span className="text-white/50">Agent:</span>{" "}
|
|
<span style={{ color: AGENT_COLORS[selectedLog.agent] }}>
|
|
{selectedLog.agent}
|
|
</span>
|
|
</div>
|
|
<div>
|
|
<span className="text-white/50">Status:</span>{" "}
|
|
<span className={getStatusColor(selectedLog.status)}>
|
|
{selectedLog.status}
|
|
</span>
|
|
</div>
|
|
<div>
|
|
<span className="text-white/50">Started:</span>{" "}
|
|
{new Date(selectedLog.startedAt).toLocaleString()}
|
|
</div>
|
|
{selectedLog.completedAt && (
|
|
<div>
|
|
<span className="text-white/50">Completed:</span>{" "}
|
|
{new Date(selectedLog.completedAt).toLocaleString()}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{selectedLog.output && (
|
|
<div>
|
|
<span className="text-white/50 text-sm">Output:</span>
|
|
<pre className="mt-1 p-2 bg-black/50 rounded text-xs text-white/70 overflow-x-auto">
|
|
{selectedLog.output}
|
|
</pre>
|
|
</div>
|
|
)}
|
|
|
|
{selectedLog.error && (
|
|
<div className="mt-2">
|
|
<span className="text-red-400 text-sm">Error:</span>
|
|
<pre className="mt-1 p-2 bg-red-900/20 rounded text-xs text-red-300 overflow-x-auto">
|
|
{selectedLog.error}
|
|
</pre>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|