apteva 0.4.20 → 0.4.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ActivityPage.cycn14ck.js +3 -0
- package/dist/{ApiDocsPage.kf6bbwkk.js → ApiDocsPage.3q5x9hhg.js} +2 -2
- package/dist/App.0wwyytz2.js +4 -0
- package/dist/{App.c90t3dxg.js → App.2prdcxgq.js} +3 -3
- package/dist/{App.2yy66bnp.js → App.40azyqz6.js} +3 -3
- package/dist/App.6ftxk387.js +4 -0
- package/dist/{App.jfx3der4.js → App.9bzz8dqh.js} +3 -3
- package/dist/App.a7h91mxr.js +4 -0
- package/dist/{App.7v1w3ys9.js → App.e54ynjf2.js} +3 -3
- package/dist/{App.edwahsvz.js → App.fq11mvc7.js} +2 -2
- package/dist/{App.2jmkqm8c.js → App.h6k4j1w9.js} +3 -3
- package/dist/App.jq5tmjws.js +267 -0
- package/dist/{App.q3bpx15d.js → App.k377qek6.js} +2 -2
- package/dist/{App.039re6cf.js → App.r2c5nw36.js} +3 -3
- package/dist/{App.n4jb3c22.js → App.sb2fg71h.js} +3 -3
- package/dist/App.wnap3h7r.js +4 -0
- package/dist/ConnectionsPage.6fyhqfhz.js +3 -0
- package/dist/McpPage.hk2qt1qt.js +3 -0
- package/dist/SettingsPage.gwpx9v7v.js +3 -0
- package/dist/SkillsPage.j5zech2z.js +3 -0
- package/dist/TasksPage.65dcf4vw.js +3 -0
- package/dist/TelemetryPage.07xrbd7k.js +3 -0
- package/dist/TestsPage.q6zfephf.js +3 -0
- package/dist/index.html +1 -1
- package/dist/styles.css +1 -1
- package/package.json +2 -2
- package/src/integrations/agentdojo.ts +1 -1
- package/src/providers.ts +2 -0
- package/src/routes/api/triggers.ts +45 -5
- package/src/web/App.tsx +1 -0
- package/src/web/components/activity/ActivityPage.tsx +347 -212
- package/src/web/components/agents/AgentCard.tsx +32 -3
- package/src/web/components/agents/AgentPanel.tsx +188 -4
- package/src/web/components/connections/IntegrationsTab.tsx +57 -31
- package/src/web/components/connections/TriggersTab.tsx +336 -159
- package/src/web/components/dashboard/Dashboard.tsx +39 -7
- package/src/web/components/layout/Header.tsx +0 -34
- package/src/web/components/layout/Sidebar.tsx +43 -3
- package/src/web/components/tasks/TasksPage.tsx +32 -6
- package/dist/ActivityPage.h769ek3a.js +0 -3
- package/dist/App.3515wsb4.js +0 -4
- package/dist/App.r0a2nmqs.js +0 -267
- package/dist/App.s2yrcz15.js +0 -4
- package/dist/App.s5j82a5j.js +0 -4
- package/dist/App.tg1b94tx.js +0 -4
- package/dist/ConnectionsPage.a67fjgbf.js +0 -3
- package/dist/McpPage.d4p3xvtk.js +0 -3
- package/dist/SettingsPage.46sqpe39.js +0 -3
- package/dist/SkillsPage.j9hkqm99.js +0 -3
- package/dist/TasksPage.6pvkb7s7.js +0 -3
- package/dist/TelemetryPage.5zq9msb5.js +0 -3
- package/dist/TestsPage.24432yqt.js +0 -3
|
@@ -1,17 +1,25 @@
|
|
|
1
|
-
import React, { useState, useMemo, useEffect, useRef } from "react";
|
|
1
|
+
import React, { useState, useMemo, useEffect, useCallback, useRef } from "react";
|
|
2
2
|
import { useAgentActivity, useAuth, useProjects, useTelemetryContext } from "../../context";
|
|
3
|
+
import { useTelemetry } from "../../context/TelemetryContext";
|
|
3
4
|
import type { TelemetryEvent } from "../../context";
|
|
4
|
-
import type { Agent } from "../../types";
|
|
5
|
-
import {
|
|
5
|
+
import type { Agent, Task, Route } from "../../types";
|
|
6
|
+
import { RecurringIcon, ScheduledIcon, TaskOnceIcon } from "../common/Icons";
|
|
6
7
|
|
|
7
8
|
interface ActivityPageProps {
|
|
8
9
|
agents: Agent[];
|
|
9
10
|
loading: boolean;
|
|
11
|
+
onNavigate?: (route: Route) => void;
|
|
10
12
|
}
|
|
11
13
|
|
|
12
|
-
export function ActivityPage({ agents, loading }: ActivityPageProps) {
|
|
14
|
+
export function ActivityPage({ agents, loading, onNavigate }: ActivityPageProps) {
|
|
15
|
+
const { authFetch } = useAuth();
|
|
13
16
|
const { currentProjectId } = useProjects();
|
|
17
|
+
const { events: realtimeEvents, statusChangeCounter } = useTelemetryContext();
|
|
14
18
|
const [selectedAgentId, setSelectedAgentId] = useState<string | null>(null);
|
|
19
|
+
const [tasks, setTasks] = useState<Task[]>([]);
|
|
20
|
+
const [historicalActivities, setHistoricalActivities] = useState<TelemetryEvent[]>([]);
|
|
21
|
+
const lastProcessedTaskEventRef = useRef<string | null>(null);
|
|
22
|
+
const { events: taskTelemetryEvents } = useTelemetry({ category: "TASK" });
|
|
15
23
|
|
|
16
24
|
const filteredAgents = useMemo(() => {
|
|
17
25
|
if (currentProjectId === null) return agents;
|
|
@@ -19,127 +27,227 @@ export function ActivityPage({ agents, loading }: ActivityPageProps) {
|
|
|
19
27
|
return agents.filter(a => a.projectId === currentProjectId);
|
|
20
28
|
}, [agents, currentProjectId]);
|
|
21
29
|
|
|
22
|
-
const
|
|
30
|
+
const sortedAgents = useMemo(() => {
|
|
31
|
+
return [...filteredAgents].sort((a, b) => {
|
|
32
|
+
if (a.status === "running" && b.status !== "running") return -1;
|
|
33
|
+
if (b.status === "running" && a.status !== "running") return 1;
|
|
34
|
+
return a.name.localeCompare(b.name);
|
|
35
|
+
});
|
|
36
|
+
}, [filteredAgents]);
|
|
23
37
|
|
|
24
|
-
|
|
25
|
-
<div className="flex-1 flex flex-col overflow-hidden">
|
|
26
|
-
{/* Top: Agent Visualization */}
|
|
27
|
-
<div className="flex-[3] min-h-0 p-6 overflow-auto">
|
|
28
|
-
<ActivityVisualization
|
|
29
|
-
agents={filteredAgents}
|
|
30
|
-
loading={loading}
|
|
31
|
-
selectedAgentId={selectedAgentId}
|
|
32
|
-
onSelectAgent={setSelectedAgentId}
|
|
33
|
-
/>
|
|
34
|
-
</div>
|
|
38
|
+
const runningCount = useMemo(() => filteredAgents.filter(a => a.status === "running").length, [filteredAgents]);
|
|
35
39
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
40
|
+
const agentIds = useMemo(() => new Set(filteredAgents.map(a => a.id)), [filteredAgents]);
|
|
41
|
+
const agentNameMap = useMemo(() => {
|
|
42
|
+
const map = new Map<string, string>();
|
|
43
|
+
filteredAgents.forEach(a => map.set(a.id, a.name));
|
|
44
|
+
return map;
|
|
45
|
+
}, [filteredAgents]);
|
|
46
|
+
|
|
47
|
+
// Fetch tasks + historical activity
|
|
48
|
+
const fetchData = useCallback(async () => {
|
|
49
|
+
const projectParam = currentProjectId ? `&project_id=${encodeURIComponent(currentProjectId)}` : "";
|
|
50
|
+
const [tasksRes, activityRes] = await Promise.all([
|
|
51
|
+
authFetch(`/api/tasks?status=all${projectParam}`).catch(() => null),
|
|
52
|
+
authFetch(`/api/telemetry/events?type=thread_activity&limit=50${projectParam}`).catch(() => null),
|
|
53
|
+
]);
|
|
54
|
+
if (tasksRes?.ok) {
|
|
55
|
+
const data = await tasksRes.json();
|
|
56
|
+
const list: Task[] = data.tasks || [];
|
|
57
|
+
list.sort((a, b) => {
|
|
58
|
+
const aPri = a.status === "running" ? 0 : a.status === "pending" ? 1 : a.status === "completed" ? 2 : 3;
|
|
59
|
+
const bPri = b.status === "running" ? 0 : b.status === "pending" ? 1 : b.status === "completed" ? 2 : 3;
|
|
60
|
+
if (aPri !== bPri) return aPri - bPri;
|
|
61
|
+
if (aPri <= 1) {
|
|
62
|
+
const aTs = (a.next_run || a.execute_at) ? new Date(a.next_run || a.execute_at!).getTime() : Infinity;
|
|
63
|
+
const bTs = (b.next_run || b.execute_at) ? new Date(b.next_run || b.execute_at!).getTime() : Infinity;
|
|
64
|
+
return aTs - bTs;
|
|
65
|
+
}
|
|
66
|
+
const aDate = a.completed_at || a.executed_at || a.created_at;
|
|
67
|
+
const bDate = b.completed_at || b.executed_at || b.created_at;
|
|
68
|
+
return new Date(bDate).getTime() - new Date(aDate).getTime();
|
|
69
|
+
});
|
|
70
|
+
setTasks(list);
|
|
71
|
+
}
|
|
72
|
+
if (activityRes?.ok) {
|
|
73
|
+
const data = await activityRes.json();
|
|
74
|
+
setHistoricalActivities(data.events || []);
|
|
75
|
+
}
|
|
76
|
+
}, [authFetch, currentProjectId]);
|
|
47
77
|
|
|
48
|
-
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
fetchData();
|
|
80
|
+
}, [fetchData, statusChangeCounter]);
|
|
81
|
+
|
|
82
|
+
// Real-time task updates from telemetry (same pattern as TasksPage)
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
if (!taskTelemetryEvents.length) return;
|
|
85
|
+
const latestEvent = taskTelemetryEvents[0];
|
|
86
|
+
if (!latestEvent || latestEvent.id === lastProcessedTaskEventRef.current) return;
|
|
87
|
+
const eventType = latestEvent.type;
|
|
88
|
+
if (eventType === "task_created" || eventType === "task_updated" || eventType === "task_deleted") {
|
|
89
|
+
lastProcessedTaskEventRef.current = latestEvent.id;
|
|
90
|
+
fetchData();
|
|
91
|
+
}
|
|
92
|
+
}, [taskTelemetryEvents, fetchData]);
|
|
93
|
+
|
|
94
|
+
// Merge realtime + historical thread_activity
|
|
95
|
+
const activities = useMemo(() => {
|
|
96
|
+
const realtimeThreadEvents = realtimeEvents.filter(e => e.type === "thread_activity");
|
|
97
|
+
const seen = new Set(realtimeThreadEvents.map(e => e.id));
|
|
98
|
+
const merged = [...realtimeThreadEvents];
|
|
99
|
+
for (const evt of historicalActivities) {
|
|
100
|
+
if (!seen.has(evt.id)) {
|
|
101
|
+
merged.push(evt);
|
|
102
|
+
seen.add(evt.id);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
let filtered = merged.filter(e => agentIds.has(e.agent_id));
|
|
106
|
+
filtered.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
107
|
+
return filtered.slice(0, 50);
|
|
108
|
+
}, [realtimeEvents, historicalActivities, agentIds]);
|
|
49
109
|
|
|
50
|
-
function ActivityVisualization({ agents, loading, selectedAgentId, onSelectAgent }: {
|
|
51
|
-
agents: Agent[];
|
|
52
|
-
loading: boolean;
|
|
53
|
-
selectedAgentId: string | null;
|
|
54
|
-
onSelectAgent: (id: string | null) => void;
|
|
55
|
-
}) {
|
|
56
110
|
if (loading) {
|
|
57
|
-
return <div className="flex items-center justify-center
|
|
111
|
+
return <div className="flex-1 flex items-center justify-center text-[#666]">Loading...</div>;
|
|
58
112
|
}
|
|
59
113
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
<
|
|
114
|
+
return (
|
|
115
|
+
<div className="flex-1 flex flex-col overflow-hidden">
|
|
116
|
+
{/* Header */}
|
|
117
|
+
<div className="px-6 pt-6 pb-4 shrink-0">
|
|
118
|
+
<div className="flex items-center justify-between">
|
|
119
|
+
<h1 className="text-xl font-semibold">Activity</h1>
|
|
120
|
+
<span className="text-sm text-[#666]">
|
|
121
|
+
{runningCount} of {filteredAgents.length} agents running
|
|
122
|
+
</span>
|
|
66
123
|
</div>
|
|
67
124
|
</div>
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
125
|
|
|
71
|
-
|
|
126
|
+
{/* Three-column layout: 1/4 | 3/8 | 3/8 */}
|
|
127
|
+
<div className="flex-1 flex min-h-0 overflow-hidden">
|
|
128
|
+
{/* Left: Agents (1/4) */}
|
|
129
|
+
<div className="flex-[2] flex flex-col overflow-hidden border-r border-[#1a1a1a]">
|
|
130
|
+
<div className="px-4 py-2.5 border-b border-[#1a1a1a] shrink-0">
|
|
131
|
+
<h3 className="text-xs font-semibold text-[#666] uppercase tracking-wider">Agents</h3>
|
|
132
|
+
</div>
|
|
133
|
+
<div className="flex-1 overflow-auto px-3 py-2">
|
|
134
|
+
{sortedAgents.length === 0 ? (
|
|
135
|
+
<p className="text-sm text-[#555] px-2 py-4 text-center">No agents found</p>
|
|
136
|
+
) : (
|
|
137
|
+
<div className="space-y-1">
|
|
138
|
+
{sortedAgents.map(agent => (
|
|
139
|
+
<AgentRow
|
|
140
|
+
key={agent.id}
|
|
141
|
+
agent={agent}
|
|
142
|
+
selected={selectedAgentId === agent.id}
|
|
143
|
+
onSelect={() => setSelectedAgentId(selectedAgentId === agent.id ? null : agent.id)}
|
|
144
|
+
/>
|
|
145
|
+
))}
|
|
146
|
+
</div>
|
|
147
|
+
)}
|
|
72
148
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
</
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
<div className="
|
|
83
|
-
|
|
84
|
-
<
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
149
|
+
{selectedAgentId && (
|
|
150
|
+
<InlineCommand
|
|
151
|
+
agent={filteredAgents.find(a => a.id === selectedAgentId) || null}
|
|
152
|
+
/>
|
|
153
|
+
)}
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
{/* Center: Activity Feed (3/8) */}
|
|
158
|
+
<div className="flex-[3] flex flex-col min-h-0 overflow-hidden border-r border-[#1a1a1a]">
|
|
159
|
+
<div className="px-4 py-2.5 border-b border-[#1a1a1a] flex items-center justify-between shrink-0">
|
|
160
|
+
<h3 className="text-xs font-semibold text-[#666] uppercase tracking-wider">Activity Feed</h3>
|
|
161
|
+
<span className="text-xs text-[#555]">{activities.length}</span>
|
|
162
|
+
</div>
|
|
163
|
+
<div className="flex-1 overflow-auto">
|
|
164
|
+
{activities.length === 0 ? (
|
|
165
|
+
<div className="p-6 text-center text-[#555] text-sm">
|
|
166
|
+
No activity yet. Agent activity will appear here in real-time.
|
|
167
|
+
</div>
|
|
168
|
+
) : (
|
|
169
|
+
<div className="divide-y divide-[#1a1a1a]">
|
|
170
|
+
{activities.map(evt => (
|
|
171
|
+
<div key={evt.id} className="px-4 py-2.5 hover:bg-[#111]/50 transition">
|
|
172
|
+
<p className="text-sm truncate">{(evt.data?.activity as string) || "Working..."}</p>
|
|
173
|
+
<div className="flex items-center gap-2 text-[10px] text-[#555] mt-0.5">
|
|
174
|
+
<span className="text-[#666]">{agentNameMap.get(evt.agent_id) || evt.agent_id}</span>
|
|
175
|
+
<span className="text-[#444]">·</span>
|
|
176
|
+
<span>{timeAgo(evt.timestamp)}</span>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
))}
|
|
180
|
+
</div>
|
|
181
|
+
)}
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
{/* Right: Tasks (3/8) */}
|
|
186
|
+
<div className="flex-[3] flex flex-col overflow-hidden">
|
|
187
|
+
<div className="px-4 py-2.5 border-b border-[#1a1a1a] flex items-center justify-between shrink-0">
|
|
188
|
+
<h3 className="text-xs font-semibold text-[#666] uppercase tracking-wider">Tasks</h3>
|
|
189
|
+
{onNavigate && (
|
|
190
|
+
<button onClick={() => onNavigate("tasks")} className="text-xs text-[#3b82f6] hover:text-[#60a5fa]">
|
|
191
|
+
View All
|
|
192
|
+
</button>
|
|
193
|
+
)}
|
|
194
|
+
</div>
|
|
195
|
+
<div className="flex-1 overflow-auto px-3 py-3">
|
|
196
|
+
{tasks.length === 0 ? (
|
|
197
|
+
<p className="text-sm text-[#555] px-2 py-4 text-center">No tasks yet</p>
|
|
198
|
+
) : (
|
|
199
|
+
<div className="space-y-2.5">
|
|
200
|
+
{tasks.map(task => (
|
|
201
|
+
<TaskCard key={`${task.agentId}-${task.id}`} task={task} />
|
|
202
|
+
))}
|
|
203
|
+
</div>
|
|
204
|
+
)}
|
|
205
|
+
</div>
|
|
91
206
|
</div>
|
|
92
207
|
</div>
|
|
93
208
|
</div>
|
|
94
209
|
);
|
|
95
210
|
}
|
|
96
211
|
|
|
97
|
-
// --- Agent
|
|
212
|
+
// --- Agent Row ---
|
|
98
213
|
|
|
99
|
-
function
|
|
214
|
+
function AgentRow({ agent, selected, onSelect }: {
|
|
100
215
|
agent: Agent;
|
|
101
216
|
selected: boolean;
|
|
102
|
-
|
|
217
|
+
onSelect: () => void;
|
|
103
218
|
}) {
|
|
104
219
|
const { isActive, type } = useAgentActivity(agent.id);
|
|
105
220
|
const isRunning = agent.status === "running";
|
|
106
221
|
|
|
107
|
-
const ringStyle = selected
|
|
108
|
-
? "ring-2 ring-[#f97316] shadow-[0_0_12px_rgba(249,115,22,0.3)]"
|
|
109
|
-
: isRunning && isActive
|
|
110
|
-
? "ring-2 ring-green-400"
|
|
111
|
-
: isRunning
|
|
112
|
-
? "ring-1 ring-[#3b82f6]/60"
|
|
113
|
-
: "ring-1 ring-[#333]";
|
|
114
|
-
|
|
115
|
-
const bgClass = isRunning
|
|
116
|
-
? isActive ? "bg-green-500/10" : "bg-[#1a1a1a]"
|
|
117
|
-
: "bg-[#111]";
|
|
118
|
-
|
|
119
|
-
const textClass = isRunning ? "text-[#e0e0e0]" : "text-[#555]";
|
|
120
|
-
|
|
121
222
|
return (
|
|
122
223
|
<button
|
|
123
|
-
onClick={
|
|
124
|
-
className=
|
|
224
|
+
onClick={onSelect}
|
|
225
|
+
className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-left transition ${
|
|
226
|
+
selected
|
|
227
|
+
? "bg-[#f97316]/10 border border-[#f97316]/30"
|
|
228
|
+
: "hover:bg-[#1a1a1a] border border-transparent"
|
|
229
|
+
}`}
|
|
125
230
|
>
|
|
126
|
-
<
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
231
|
+
<span
|
|
232
|
+
className={`w-2.5 h-2.5 rounded-full shrink-0 ${
|
|
233
|
+
isRunning && isActive
|
|
234
|
+
? "bg-green-400 animate-pulse"
|
|
235
|
+
: isRunning
|
|
236
|
+
? "bg-[#3b82f6]"
|
|
237
|
+
: "bg-[#444]"
|
|
238
|
+
}`}
|
|
239
|
+
/>
|
|
240
|
+
<div className="flex-1 min-w-0">
|
|
241
|
+
<div className="flex items-center gap-2">
|
|
242
|
+
<span className={`text-sm font-medium truncate ${isRunning ? "" : "text-[#666]"}`}>
|
|
243
|
+
{agent.name}
|
|
244
|
+
</span>
|
|
245
|
+
<span className="text-[10px] text-[#555] shrink-0">{agent.provider}</span>
|
|
246
|
+
</div>
|
|
139
247
|
{isActive && type ? (
|
|
140
|
-
<p className="text-[
|
|
248
|
+
<p className="text-[11px] text-green-400 truncate">{type}</p>
|
|
141
249
|
) : (
|
|
142
|
-
<p className={`text-[
|
|
250
|
+
<p className={`text-[11px] ${isRunning ? "text-[#555]" : "text-[#444]"}`}>
|
|
143
251
|
{isRunning ? "idle" : "stopped"}
|
|
144
252
|
</p>
|
|
145
253
|
)}
|
|
@@ -148,74 +256,9 @@ function AgentNode({ agent, selected, onClick }: {
|
|
|
148
256
|
);
|
|
149
257
|
}
|
|
150
258
|
|
|
151
|
-
// ---
|
|
152
|
-
|
|
153
|
-
const categoryColors: Record<string, string> = {
|
|
154
|
-
LLM: "bg-purple-500/20 text-purple-400",
|
|
155
|
-
TOOL: "bg-blue-500/20 text-blue-400",
|
|
156
|
-
CHAT: "bg-green-500/20 text-green-400",
|
|
157
|
-
ERROR: "bg-red-500/20 text-red-400",
|
|
158
|
-
SYSTEM: "bg-gray-500/20 text-gray-400",
|
|
159
|
-
TASK: "bg-yellow-500/20 text-yellow-400",
|
|
160
|
-
MEMORY: "bg-cyan-500/20 text-cyan-400",
|
|
161
|
-
MCP: "bg-orange-500/20 text-orange-400",
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
function LiveActivityStream({ agents }: { agents: Agent[] }) {
|
|
165
|
-
const { events } = useTelemetryContext();
|
|
166
|
-
const scrollRef = useRef<HTMLDivElement>(null);
|
|
167
|
-
|
|
168
|
-
const agentIds = useMemo(() => new Set(agents.map(a => a.id)), [agents]);
|
|
169
|
-
const agentNameMap = useMemo(() => {
|
|
170
|
-
const map = new Map<string, string>();
|
|
171
|
-
agents.forEach(a => map.set(a.id, a.name));
|
|
172
|
-
return map;
|
|
173
|
-
}, [agents]);
|
|
174
|
-
|
|
175
|
-
const filteredEvents = useMemo(() => {
|
|
176
|
-
return events
|
|
177
|
-
.filter(e => agentIds.has(e.agent_id))
|
|
178
|
-
.slice(0, 50);
|
|
179
|
-
}, [events, agentIds]);
|
|
180
|
-
|
|
181
|
-
return (
|
|
182
|
-
<div className="flex-1 flex flex-col overflow-hidden border-l border-[#1a1a1a]">
|
|
183
|
-
<div className="px-4 py-3 border-b border-[#1a1a1a] flex items-center justify-between shrink-0">
|
|
184
|
-
<h3 className="font-semibold text-sm">Live Activity</h3>
|
|
185
|
-
<span className="text-xs text-[#666]">{filteredEvents.length} events</span>
|
|
186
|
-
</div>
|
|
187
|
-
<div ref={scrollRef} className="flex-1 overflow-auto">
|
|
188
|
-
{filteredEvents.length === 0 ? (
|
|
189
|
-
<div className="p-4 text-center text-[#666] text-sm">
|
|
190
|
-
No activity yet. Events appear in real-time.
|
|
191
|
-
</div>
|
|
192
|
-
) : (
|
|
193
|
-
<div className="divide-y divide-[#1a1a1a]">
|
|
194
|
-
{filteredEvents.map(event => (
|
|
195
|
-
<div key={event.id} className="px-4 py-2 hover:bg-[#111] transition" style={{ animation: "slideIn 0.3s ease-out" }}>
|
|
196
|
-
<div className="flex items-center gap-2">
|
|
197
|
-
<span className={`px-1.5 py-0.5 rounded text-[10px] font-medium ${categoryColors[event.category] || "bg-[#222] text-[#888]"}`}>
|
|
198
|
-
{event.category}
|
|
199
|
-
</span>
|
|
200
|
-
<span className="text-xs font-medium truncate flex-1">{event.type}</span>
|
|
201
|
-
<span className="text-[10px] text-[#555] shrink-0">{timeAgo(event.timestamp)}</span>
|
|
202
|
-
</div>
|
|
203
|
-
<div className="text-[10px] text-[#555] mt-0.5">
|
|
204
|
-
{agentNameMap.get(event.agent_id) || event.agent_id}
|
|
205
|
-
{event.duration_ms ? ` \u00b7 ${event.duration_ms}ms` : ""}
|
|
206
|
-
</div>
|
|
207
|
-
</div>
|
|
208
|
-
))}
|
|
209
|
-
</div>
|
|
210
|
-
)}
|
|
211
|
-
</div>
|
|
212
|
-
</div>
|
|
213
|
-
);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// --- Quick Command Panel ---
|
|
259
|
+
// --- Inline Command ---
|
|
217
260
|
|
|
218
|
-
function
|
|
261
|
+
function InlineCommand({ agent }: { agent: Agent | null }) {
|
|
219
262
|
const { authFetch } = useAuth();
|
|
220
263
|
const [command, setCommand] = useState("");
|
|
221
264
|
const [sending, setSending] = useState(false);
|
|
@@ -226,17 +269,13 @@ function QuickCommandPanel({ agent, onClose }: { agent: Agent | null; onClose: (
|
|
|
226
269
|
setToast(null);
|
|
227
270
|
}, [agent?.id]);
|
|
228
271
|
|
|
229
|
-
if (!agent)
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
Click an agent to send a quick command
|
|
233
|
-
</div>
|
|
234
|
-
);
|
|
235
|
-
}
|
|
272
|
+
if (!agent) return null;
|
|
273
|
+
|
|
274
|
+
const isRunning = agent.status === "running";
|
|
236
275
|
|
|
237
276
|
const handleSend = async () => {
|
|
238
277
|
if (!command.trim() || sending) return;
|
|
239
|
-
if (
|
|
278
|
+
if (!isRunning) {
|
|
240
279
|
setToast("Agent is not running");
|
|
241
280
|
setTimeout(() => setToast(null), 3000);
|
|
242
281
|
return;
|
|
@@ -249,63 +288,96 @@ function QuickCommandPanel({ agent, onClose }: { agent: Agent | null; onClose: (
|
|
|
249
288
|
body: JSON.stringify({ message: command, agent_id: agent.id }),
|
|
250
289
|
});
|
|
251
290
|
if (res.ok) {
|
|
252
|
-
setToast("
|
|
291
|
+
setToast("Sent");
|
|
253
292
|
setCommand("");
|
|
254
293
|
} else {
|
|
255
294
|
const data = await res.json().catch(() => ({}));
|
|
256
|
-
setToast(data.error || "Failed
|
|
295
|
+
setToast(data.error || "Failed");
|
|
257
296
|
}
|
|
258
297
|
} catch {
|
|
259
|
-
setToast("Failed to send
|
|
298
|
+
setToast("Failed to send");
|
|
260
299
|
} finally {
|
|
261
300
|
setSending(false);
|
|
262
301
|
setTimeout(() => setToast(null), 3000);
|
|
263
302
|
}
|
|
264
303
|
};
|
|
265
304
|
|
|
266
|
-
|
|
305
|
+
return (
|
|
306
|
+
<div className="mt-2 bg-[#0a0a0a] border border-[#1a1a1a] rounded-lg p-2.5">
|
|
307
|
+
<div className="flex items-center justify-between mb-1.5">
|
|
308
|
+
<span className="text-[10px] text-[#666]">
|
|
309
|
+
Send to <span className="text-[#888]">{agent.name}</span>
|
|
310
|
+
</span>
|
|
311
|
+
{toast && (
|
|
312
|
+
<span className={`text-[10px] px-1.5 py-0.5 rounded ${
|
|
313
|
+
toast === "Sent" ? "bg-green-500/10 text-green-400" : "bg-red-500/10 text-red-400"
|
|
314
|
+
}`}>{toast}</span>
|
|
315
|
+
)}
|
|
316
|
+
</div>
|
|
317
|
+
<div className="flex gap-1.5">
|
|
318
|
+
<input
|
|
319
|
+
type="text"
|
|
320
|
+
value={command}
|
|
321
|
+
onChange={e => setCommand(e.target.value)}
|
|
322
|
+
onKeyDown={e => e.key === "Enter" && handleSend()}
|
|
323
|
+
placeholder={isRunning ? "Command..." : "Not running"}
|
|
324
|
+
disabled={sending || !isRunning}
|
|
325
|
+
autoFocus
|
|
326
|
+
className="flex-1 bg-[#111] border border-[#1a1a1a] rounded px-2 py-1.5 text-xs focus:outline-none focus:border-[#f97316] placeholder-[#444] disabled:opacity-50"
|
|
327
|
+
/>
|
|
328
|
+
<button
|
|
329
|
+
onClick={handleSend}
|
|
330
|
+
disabled={sending || !command.trim() || !isRunning}
|
|
331
|
+
className="px-2.5 py-1.5 bg-[#f97316]/20 text-[#f97316] rounded text-xs font-medium hover:bg-[#f97316]/30 transition disabled:opacity-30"
|
|
332
|
+
>
|
|
333
|
+
{sending ? "..." : "Send"}
|
|
334
|
+
</button>
|
|
335
|
+
</div>
|
|
336
|
+
</div>
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// --- Task Card (matches TasksPage style) ---
|
|
341
|
+
|
|
342
|
+
const taskStatusColors: Record<string, string> = {
|
|
343
|
+
pending: "bg-yellow-500/20 text-yellow-400",
|
|
344
|
+
running: "bg-blue-500/20 text-blue-400",
|
|
345
|
+
completed: "bg-green-500/20 text-green-400",
|
|
346
|
+
failed: "bg-red-500/20 text-red-400",
|
|
347
|
+
cancelled: "bg-gray-500/20 text-gray-400",
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
const TASK_DAY_NAMES = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
267
351
|
|
|
352
|
+
function TaskCard({ task }: { task: Task }) {
|
|
268
353
|
return (
|
|
269
|
-
<div className="
|
|
270
|
-
<div className="
|
|
271
|
-
<div className="min-w-0">
|
|
272
|
-
<
|
|
273
|
-
<p className=
|
|
274
|
-
{isRunning ? "Running" : "Stopped"}
|
|
275
|
-
</p>
|
|
354
|
+
<div className="bg-[#111] border border-[#1a1a1a] rounded-lg p-3 hover:border-[#333] transition">
|
|
355
|
+
<div className="flex items-start justify-between mb-1.5">
|
|
356
|
+
<div className="flex-1 min-w-0">
|
|
357
|
+
<h4 className="text-sm font-medium truncate">{task.title}</h4>
|
|
358
|
+
<p className="text-xs text-[#666]">{task.agentName}</p>
|
|
276
359
|
</div>
|
|
277
|
-
<
|
|
278
|
-
|
|
279
|
-
</
|
|
360
|
+
<span className={`px-1.5 py-0.5 rounded text-[10px] font-medium shrink-0 ml-2 ${taskStatusColors[task.status] || taskStatusColors.pending}`}>
|
|
361
|
+
{task.status}
|
|
362
|
+
</span>
|
|
280
363
|
</div>
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
364
|
+
|
|
365
|
+
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-[11px] text-[#555]">
|
|
366
|
+
<span className="flex items-center gap-1">
|
|
367
|
+
{task.type === "recurring"
|
|
368
|
+
? <RecurringIcon className="w-3 h-3" />
|
|
369
|
+
: task.execute_at
|
|
370
|
+
? <ScheduledIcon className="w-3 h-3" />
|
|
371
|
+
: <TaskOnceIcon className="w-3 h-3" />
|
|
372
|
+
}
|
|
373
|
+
{task.type === "recurring" && task.recurrence ? formatCronShort(task.recurrence) : task.type}
|
|
374
|
+
</span>
|
|
375
|
+
{task.next_run && (
|
|
376
|
+
<span className="text-[#f97316]">{formatTaskRelative(task.next_run)}</span>
|
|
377
|
+
)}
|
|
378
|
+
{!task.next_run && task.execute_at && (
|
|
379
|
+
<span className="text-[#f97316]">{formatTaskRelative(task.execute_at)}</span>
|
|
290
380
|
)}
|
|
291
|
-
<div className="flex gap-2">
|
|
292
|
-
<input
|
|
293
|
-
type="text"
|
|
294
|
-
value={command}
|
|
295
|
-
onChange={e => setCommand(e.target.value)}
|
|
296
|
-
onKeyDown={e => e.key === "Enter" && handleSend()}
|
|
297
|
-
placeholder={isRunning ? "Quick command..." : "Agent not running"}
|
|
298
|
-
disabled={sending || !isRunning}
|
|
299
|
-
className="flex-1 bg-[#111] border border-[#1a1a1a] rounded px-3 py-2 text-sm focus:outline-none focus:border-[#f97316] placeholder-[#444] disabled:opacity-50"
|
|
300
|
-
/>
|
|
301
|
-
<button
|
|
302
|
-
onClick={handleSend}
|
|
303
|
-
disabled={sending || !command.trim() || !isRunning}
|
|
304
|
-
className="px-3 py-2 bg-[#f97316]/20 text-[#f97316] rounded text-sm font-medium hover:bg-[#f97316]/30 transition disabled:opacity-30"
|
|
305
|
-
>
|
|
306
|
-
{sending ? "..." : "Send"}
|
|
307
|
-
</button>
|
|
308
|
-
</div>
|
|
309
381
|
</div>
|
|
310
382
|
</div>
|
|
311
383
|
);
|
|
@@ -313,6 +385,69 @@ function QuickCommandPanel({ agent, onClose }: { agent: Agent | null; onClose: (
|
|
|
313
385
|
|
|
314
386
|
// --- Helpers ---
|
|
315
387
|
|
|
388
|
+
function formatCronShort(cron: string): string {
|
|
389
|
+
try {
|
|
390
|
+
const parts = cron.trim().split(/\s+/);
|
|
391
|
+
if (parts.length !== 5) return cron;
|
|
392
|
+
const [minute, hour, dayOfMonth, month, dayOfWeek] = parts;
|
|
393
|
+
|
|
394
|
+
if (minute.startsWith("*/") && hour === "*" && dayOfMonth === "*" && month === "*" && dayOfWeek === "*") {
|
|
395
|
+
const n = parseInt(minute.slice(2));
|
|
396
|
+
return n === 1 ? "Every min" : `Every ${n}min`;
|
|
397
|
+
}
|
|
398
|
+
if (minute !== "*" && !minute.includes("/") && hour === "*" && dayOfMonth === "*" && month === "*" && dayOfWeek === "*") {
|
|
399
|
+
return "Hourly";
|
|
400
|
+
}
|
|
401
|
+
if (hour.startsWith("*/") && dayOfMonth === "*" && month === "*" && dayOfWeek === "*") {
|
|
402
|
+
const n = parseInt(hour.slice(2));
|
|
403
|
+
return n === 1 ? "Hourly" : `Every ${n}h`;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const formatTime = (h: string, m: string): string => {
|
|
407
|
+
const hr = parseInt(h);
|
|
408
|
+
const mn = parseInt(m);
|
|
409
|
+
if (isNaN(hr)) return "";
|
|
410
|
+
const ampm = hr >= 12 ? "PM" : "AM";
|
|
411
|
+
const h12 = hr === 0 ? 12 : hr > 12 ? hr - 12 : hr;
|
|
412
|
+
return `${h12}:${mn.toString().padStart(2, "0")} ${ampm}`;
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
if (hour !== "*" && !hour.includes("/") && dayOfMonth === "*" && month === "*") {
|
|
416
|
+
const timeStr = formatTime(hour, minute);
|
|
417
|
+
if (dayOfWeek === "*") return `Daily ${timeStr}`;
|
|
418
|
+
const days = dayOfWeek.split(",").map(d => TASK_DAY_NAMES[parseInt(d.trim())] || d);
|
|
419
|
+
if (days.length === 1) return `${days[0]} ${timeStr}`;
|
|
420
|
+
return `${days.join(", ")} ${timeStr}`;
|
|
421
|
+
}
|
|
422
|
+
return cron;
|
|
423
|
+
} catch {
|
|
424
|
+
return cron;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function formatTaskRelative(dateStr: string): string {
|
|
429
|
+
const date = new Date(dateStr);
|
|
430
|
+
const now = new Date();
|
|
431
|
+
const diffMs = date.getTime() - now.getTime();
|
|
432
|
+
const isFuture = diffMs > 0;
|
|
433
|
+
const absDiffMs = Math.abs(diffMs);
|
|
434
|
+
const minutes = Math.floor(absDiffMs / 60000);
|
|
435
|
+
const hours = Math.floor(absDiffMs / 3600000);
|
|
436
|
+
const timeStr = date.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
|
|
437
|
+
const isToday = date.toDateString() === now.toDateString();
|
|
438
|
+
const tomorrow = new Date(now);
|
|
439
|
+
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
440
|
+
const isTomorrow = date.toDateString() === tomorrow.toDateString();
|
|
441
|
+
|
|
442
|
+
if (isToday) {
|
|
443
|
+
if (minutes < 1) return "now";
|
|
444
|
+
if (minutes < 60) return isFuture ? `in ${minutes}m` : `${minutes}m ago`;
|
|
445
|
+
return isFuture ? `in ${hours}h (${timeStr})` : `${hours}h ago`;
|
|
446
|
+
}
|
|
447
|
+
if (isTomorrow) return `Tomorrow ${timeStr}`;
|
|
448
|
+
return `${TASK_DAY_NAMES[date.getDay()]} ${timeStr}`;
|
|
449
|
+
}
|
|
450
|
+
|
|
316
451
|
function timeAgo(timestamp: string): string {
|
|
317
452
|
const seconds = Math.floor((Date.now() - new Date(timestamp).getTime()) / 1000);
|
|
318
453
|
if (seconds < 5) return "just now";
|