apteva 0.2.6 → 0.2.8
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/App.hzbfeg94.js +217 -0
- package/dist/index.html +3 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/src/auth/index.ts +386 -0
- package/src/auth/middleware.ts +183 -0
- package/src/binary.ts +19 -1
- package/src/db.ts +570 -32
- package/src/routes/api.ts +913 -38
- package/src/routes/auth.ts +242 -0
- package/src/server.ts +60 -8
- package/src/web/App.tsx +61 -19
- package/src/web/components/agents/AgentCard.tsx +30 -41
- package/src/web/components/agents/AgentPanel.tsx +751 -11
- package/src/web/components/agents/AgentsView.tsx +81 -9
- package/src/web/components/agents/CreateAgentModal.tsx +28 -1
- package/src/web/components/auth/CreateAccountStep.tsx +176 -0
- package/src/web/components/auth/LoginPage.tsx +91 -0
- package/src/web/components/auth/index.ts +2 -0
- package/src/web/components/common/Icons.tsx +48 -0
- package/src/web/components/common/Modal.tsx +1 -1
- package/src/web/components/dashboard/Dashboard.tsx +91 -31
- package/src/web/components/index.ts +3 -0
- package/src/web/components/layout/Header.tsx +145 -15
- package/src/web/components/layout/Sidebar.tsx +81 -43
- package/src/web/components/mcp/McpPage.tsx +261 -32
- package/src/web/components/onboarding/OnboardingWizard.tsx +64 -8
- package/src/web/components/settings/SettingsPage.tsx +404 -18
- package/src/web/components/tasks/TasksPage.tsx +21 -19
- package/src/web/components/telemetry/TelemetryPage.tsx +271 -81
- package/src/web/context/AuthContext.tsx +230 -0
- package/src/web/context/ProjectContext.tsx +182 -0
- package/src/web/context/TelemetryContext.tsx +98 -76
- package/src/web/context/index.ts +5 -0
- package/src/web/hooks/useAgents.ts +18 -6
- package/src/web/hooks/useOnboarding.ts +20 -4
- package/src/web/hooks/useProviders.ts +15 -5
- package/src/web/icon.png +0 -0
- package/src/web/styles.css +12 -0
- package/src/web/types.ts +6 -0
- package/dist/App.0mzj9cz9.js +0 -213
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import React, { useState, useEffect } from "react";
|
|
2
2
|
import { Chat } from "@apteva/apteva-kit";
|
|
3
|
-
import { CloseIcon, MemoryIcon, TasksIcon, VisionIcon, OperatorIcon, McpIcon, RealtimeIcon } from "../common/Icons";
|
|
3
|
+
import { CloseIcon, MemoryIcon, TasksIcon, VisionIcon, OperatorIcon, McpIcon, RealtimeIcon, FilesIcon, MultiAgentIcon } from "../common/Icons";
|
|
4
4
|
import { Select } from "../common/Select";
|
|
5
5
|
import type { Agent, Provider, AgentFeatures, McpServer } from "../../types";
|
|
6
6
|
|
|
7
|
-
type Tab = "chat" | "settings";
|
|
7
|
+
type Tab = "chat" | "threads" | "tasks" | "memory" | "files" | "settings";
|
|
8
8
|
|
|
9
9
|
interface AgentPanelProps {
|
|
10
10
|
agent: Agent;
|
|
@@ -18,10 +18,12 @@ interface AgentPanelProps {
|
|
|
18
18
|
const FEATURE_CONFIG = [
|
|
19
19
|
{ key: "memory" as keyof AgentFeatures, label: "Memory", description: "Persistent recall", icon: MemoryIcon },
|
|
20
20
|
{ key: "tasks" as keyof AgentFeatures, label: "Tasks", description: "Schedule and execute tasks", icon: TasksIcon },
|
|
21
|
+
{ key: "files" as keyof AgentFeatures, label: "Files", description: "File storage and management", icon: FilesIcon },
|
|
21
22
|
{ key: "vision" as keyof AgentFeatures, label: "Vision", description: "Process images and PDFs", icon: VisionIcon },
|
|
22
23
|
{ key: "operator" as keyof AgentFeatures, label: "Operator", description: "Browser automation", icon: OperatorIcon },
|
|
23
24
|
{ key: "mcp" as keyof AgentFeatures, label: "MCP", description: "External tools/services", icon: McpIcon },
|
|
24
25
|
{ key: "realtime" as keyof AgentFeatures, label: "Realtime", description: "Voice conversations", icon: RealtimeIcon },
|
|
26
|
+
{ key: "agents" as keyof AgentFeatures, label: "Multi-Agent", description: "Communicate with peer agents", icon: MultiAgentIcon },
|
|
25
27
|
];
|
|
26
28
|
|
|
27
29
|
export function AgentPanel({ agent, providers, onClose, onStartAgent, onUpdateAgent, onDeleteAgent }: AgentPanelProps) {
|
|
@@ -30,18 +32,35 @@ export function AgentPanel({ agent, providers, onClose, onStartAgent, onUpdateAg
|
|
|
30
32
|
return (
|
|
31
33
|
<div className="w-full h-full flex flex-col overflow-hidden bg-[#0a0a0a] border-l border-[#1a1a1a]">
|
|
32
34
|
{/* Header with tabs */}
|
|
33
|
-
<div className="border-b border-[#1a1a1a] flex items-center
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
35
|
+
<div className="border-b border-[#1a1a1a] flex items-center">
|
|
36
|
+
{/* Scrollable tabs */}
|
|
37
|
+
<div className="flex-1 overflow-x-auto scrollbar-hide px-2 md:px-4">
|
|
38
|
+
<div className="flex gap-1">
|
|
39
|
+
<TabButton active={activeTab === "chat"} onClick={() => setActiveTab("chat")}>
|
|
40
|
+
Chat
|
|
41
|
+
</TabButton>
|
|
42
|
+
<TabButton active={activeTab === "threads"} onClick={() => setActiveTab("threads")}>
|
|
43
|
+
Threads
|
|
44
|
+
</TabButton>
|
|
45
|
+
<TabButton active={activeTab === "tasks"} onClick={() => setActiveTab("tasks")}>
|
|
46
|
+
Tasks
|
|
47
|
+
</TabButton>
|
|
48
|
+
<TabButton active={activeTab === "memory"} onClick={() => setActiveTab("memory")}>
|
|
49
|
+
Memory
|
|
50
|
+
</TabButton>
|
|
51
|
+
<TabButton active={activeTab === "files"} onClick={() => setActiveTab("files")}>
|
|
52
|
+
Files
|
|
53
|
+
</TabButton>
|
|
54
|
+
<TabButton active={activeTab === "settings"} onClick={() => setActiveTab("settings")}>
|
|
55
|
+
Settings
|
|
56
|
+
</TabButton>
|
|
57
|
+
</div>
|
|
41
58
|
</div>
|
|
59
|
+
|
|
60
|
+
{/* Close button - fixed on right */}
|
|
42
61
|
<button
|
|
43
62
|
onClick={onClose}
|
|
44
|
-
className="text-[#666] hover:text-[#e0e0e0] transition p-2"
|
|
63
|
+
className="text-[#666] hover:text-[#e0e0e0] transition p-2 flex-shrink-0 mr-2"
|
|
45
64
|
>
|
|
46
65
|
<CloseIcon />
|
|
47
66
|
</button>
|
|
@@ -52,6 +71,18 @@ export function AgentPanel({ agent, providers, onClose, onStartAgent, onUpdateAg
|
|
|
52
71
|
{activeTab === "chat" && (
|
|
53
72
|
<ChatTab agent={agent} onStartAgent={onStartAgent} />
|
|
54
73
|
)}
|
|
74
|
+
{activeTab === "threads" && (
|
|
75
|
+
<ThreadsTab agent={agent} />
|
|
76
|
+
)}
|
|
77
|
+
{activeTab === "tasks" && (
|
|
78
|
+
<TasksTab agent={agent} />
|
|
79
|
+
)}
|
|
80
|
+
{activeTab === "memory" && (
|
|
81
|
+
<MemoryTab agent={agent} />
|
|
82
|
+
)}
|
|
83
|
+
{activeTab === "files" && (
|
|
84
|
+
<FilesTab agent={agent} />
|
|
85
|
+
)}
|
|
55
86
|
{activeTab === "settings" && (
|
|
56
87
|
<SettingsTab agent={agent} providers={providers} onUpdateAgent={onUpdateAgent} onDeleteAgent={onDeleteAgent} />
|
|
57
88
|
)}
|
|
@@ -104,6 +135,715 @@ function ChatTab({ agent, onStartAgent }: { agent: Agent; onStartAgent: (e?: Rea
|
|
|
104
135
|
);
|
|
105
136
|
}
|
|
106
137
|
|
|
138
|
+
interface Thread {
|
|
139
|
+
id: string;
|
|
140
|
+
title?: string;
|
|
141
|
+
created_at: string;
|
|
142
|
+
updated_at: string;
|
|
143
|
+
message_count?: number;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function ThreadsTab({ agent }: { agent: Agent }) {
|
|
147
|
+
const [threads, setThreads] = useState<Thread[]>([]);
|
|
148
|
+
const [loading, setLoading] = useState(true);
|
|
149
|
+
const [error, setError] = useState<string | null>(null);
|
|
150
|
+
const [selectedThread, setSelectedThread] = useState<string | null>(null);
|
|
151
|
+
const [messages, setMessages] = useState<Array<{ role: string; content: string; created_at: string }>>([]);
|
|
152
|
+
const [loadingMessages, setLoadingMessages] = useState(false);
|
|
153
|
+
|
|
154
|
+
// Reset state when agent changes
|
|
155
|
+
useEffect(() => {
|
|
156
|
+
setThreads([]);
|
|
157
|
+
setSelectedThread(null);
|
|
158
|
+
setMessages([]);
|
|
159
|
+
setError(null);
|
|
160
|
+
setLoading(true);
|
|
161
|
+
}, [agent.id]);
|
|
162
|
+
|
|
163
|
+
useEffect(() => {
|
|
164
|
+
if (agent.status !== "running") {
|
|
165
|
+
setLoading(false);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const fetchThreads = async () => {
|
|
170
|
+
try {
|
|
171
|
+
const res = await fetch(`/api/agents/${agent.id}/threads`);
|
|
172
|
+
if (!res.ok) throw new Error("Failed to fetch threads");
|
|
173
|
+
const data = await res.json();
|
|
174
|
+
setThreads(data.threads || []);
|
|
175
|
+
setError(null);
|
|
176
|
+
} catch (err) {
|
|
177
|
+
setError(err instanceof Error ? err.message : "Failed to load threads");
|
|
178
|
+
} finally {
|
|
179
|
+
setLoading(false);
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
fetchThreads();
|
|
184
|
+
}, [agent.id, agent.status]);
|
|
185
|
+
|
|
186
|
+
const loadMessages = async (threadId: string) => {
|
|
187
|
+
setSelectedThread(threadId);
|
|
188
|
+
setLoadingMessages(true);
|
|
189
|
+
try {
|
|
190
|
+
const res = await fetch(`/api/agents/${agent.id}/threads/${threadId}/messages`);
|
|
191
|
+
if (!res.ok) throw new Error("Failed to fetch messages");
|
|
192
|
+
const data = await res.json();
|
|
193
|
+
setMessages(data.messages || []);
|
|
194
|
+
} catch {
|
|
195
|
+
setMessages([]);
|
|
196
|
+
} finally {
|
|
197
|
+
setLoadingMessages(false);
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const deleteThread = async (threadId: string, e: React.MouseEvent) => {
|
|
202
|
+
e.stopPropagation();
|
|
203
|
+
if (!confirm("Delete this thread?")) return;
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
await fetch(`/api/agents/${agent.id}/threads/${threadId}`, { method: "DELETE" });
|
|
207
|
+
setThreads(prev => prev.filter(t => t.id !== threadId));
|
|
208
|
+
if (selectedThread === threadId) {
|
|
209
|
+
setSelectedThread(null);
|
|
210
|
+
setMessages([]);
|
|
211
|
+
}
|
|
212
|
+
} catch {
|
|
213
|
+
// Ignore errors
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
if (agent.status !== "running") {
|
|
218
|
+
return (
|
|
219
|
+
<div className="flex-1 flex items-center justify-center text-[#666]">
|
|
220
|
+
<p>Start the agent to view threads</p>
|
|
221
|
+
</div>
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (loading) {
|
|
226
|
+
return (
|
|
227
|
+
<div className="flex-1 flex items-center justify-center text-[#666]">
|
|
228
|
+
<p>Loading threads...</p>
|
|
229
|
+
</div>
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (error) {
|
|
234
|
+
return (
|
|
235
|
+
<div className="flex-1 flex items-center justify-center text-red-400">
|
|
236
|
+
<p>{error}</p>
|
|
237
|
+
</div>
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Show messages view when a thread is selected
|
|
242
|
+
if (selectedThread) {
|
|
243
|
+
const selectedThreadData = threads.find(t => t.id === selectedThread);
|
|
244
|
+
return (
|
|
245
|
+
<div className="flex-1 flex flex-col overflow-hidden">
|
|
246
|
+
{/* Header with back button */}
|
|
247
|
+
<div className="flex items-center gap-3 px-4 py-3 border-b border-[#1a1a1a]">
|
|
248
|
+
<button
|
|
249
|
+
onClick={() => { setSelectedThread(null); setMessages([]); }}
|
|
250
|
+
className="text-[#666] hover:text-[#e0e0e0] transition text-lg"
|
|
251
|
+
>
|
|
252
|
+
←
|
|
253
|
+
</button>
|
|
254
|
+
<div className="flex-1">
|
|
255
|
+
<p className="text-sm font-medium">
|
|
256
|
+
{selectedThreadData?.title || `Thread ${selectedThread.slice(0, 8)}`}
|
|
257
|
+
</p>
|
|
258
|
+
<p className="text-xs text-[#666]">
|
|
259
|
+
{selectedThreadData && new Date(selectedThreadData.updated_at || selectedThreadData.created_at).toLocaleString()}
|
|
260
|
+
</p>
|
|
261
|
+
</div>
|
|
262
|
+
<button
|
|
263
|
+
onClick={(e) => deleteThread(selectedThread, e)}
|
|
264
|
+
className="text-[#666] hover:text-red-400 text-sm px-2 py-1"
|
|
265
|
+
>
|
|
266
|
+
Delete
|
|
267
|
+
</button>
|
|
268
|
+
</div>
|
|
269
|
+
|
|
270
|
+
{/* Messages */}
|
|
271
|
+
<div className="flex-1 overflow-auto p-4">
|
|
272
|
+
{loadingMessages ? (
|
|
273
|
+
<p className="text-[#666]">Loading messages...</p>
|
|
274
|
+
) : messages.length === 0 ? (
|
|
275
|
+
<p className="text-[#666]">No messages in this thread</p>
|
|
276
|
+
) : (
|
|
277
|
+
<div className="space-y-4">
|
|
278
|
+
{messages.map((msg, i) => (
|
|
279
|
+
<div key={i} className={`${msg.role === "user" ? "text-right" : ""}`}>
|
|
280
|
+
<div
|
|
281
|
+
className={`inline-block max-w-[80%] p-3 rounded ${
|
|
282
|
+
msg.role === "user"
|
|
283
|
+
? "bg-[#f97316]/20 text-[#f97316]"
|
|
284
|
+
: "bg-[#1a1a1a] text-[#e0e0e0]"
|
|
285
|
+
}`}
|
|
286
|
+
>
|
|
287
|
+
<p className="text-sm whitespace-pre-wrap">{msg.content}</p>
|
|
288
|
+
<p className="text-xs text-[#666] mt-1">
|
|
289
|
+
{new Date(msg.created_at).toLocaleTimeString()}
|
|
290
|
+
</p>
|
|
291
|
+
</div>
|
|
292
|
+
</div>
|
|
293
|
+
))}
|
|
294
|
+
</div>
|
|
295
|
+
)}
|
|
296
|
+
</div>
|
|
297
|
+
</div>
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Show threads list (full width)
|
|
302
|
+
return (
|
|
303
|
+
<div className="flex-1 overflow-auto">
|
|
304
|
+
{threads.length === 0 ? (
|
|
305
|
+
<div className="flex items-center justify-center h-full text-[#666]">
|
|
306
|
+
<p>No conversation threads yet</p>
|
|
307
|
+
</div>
|
|
308
|
+
) : (
|
|
309
|
+
<div className="divide-y divide-[#1a1a1a]">
|
|
310
|
+
{threads.map(thread => (
|
|
311
|
+
<div
|
|
312
|
+
key={thread.id}
|
|
313
|
+
onClick={() => loadMessages(thread.id)}
|
|
314
|
+
className="p-4 cursor-pointer hover:bg-[#111] transition flex items-center justify-between"
|
|
315
|
+
>
|
|
316
|
+
<div className="flex-1 min-w-0">
|
|
317
|
+
<p className="text-sm font-medium truncate">
|
|
318
|
+
{thread.title || `Thread ${thread.id.slice(0, 8)}`}
|
|
319
|
+
</p>
|
|
320
|
+
<p className="text-xs text-[#666] mt-1">
|
|
321
|
+
{new Date(thread.updated_at || thread.created_at).toLocaleString()}
|
|
322
|
+
{thread.message_count !== undefined && ` • ${thread.message_count} messages`}
|
|
323
|
+
</p>
|
|
324
|
+
</div>
|
|
325
|
+
<button
|
|
326
|
+
onClick={(e) => deleteThread(thread.id, e)}
|
|
327
|
+
className="text-[#666] hover:text-red-400 text-lg ml-4"
|
|
328
|
+
>
|
|
329
|
+
×
|
|
330
|
+
</button>
|
|
331
|
+
</div>
|
|
332
|
+
))}
|
|
333
|
+
</div>
|
|
334
|
+
)}
|
|
335
|
+
</div>
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
interface Task {
|
|
340
|
+
id: string;
|
|
341
|
+
name: string;
|
|
342
|
+
description?: string;
|
|
343
|
+
status: "pending" | "running" | "completed" | "failed";
|
|
344
|
+
created_at: string;
|
|
345
|
+
updated_at?: string;
|
|
346
|
+
scheduled_at?: string;
|
|
347
|
+
completed_at?: string;
|
|
348
|
+
result?: string;
|
|
349
|
+
error?: string;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function TasksTab({ agent }: { agent: Agent }) {
|
|
353
|
+
const [tasks, setTasks] = useState<Task[]>([]);
|
|
354
|
+
const [loading, setLoading] = useState(true);
|
|
355
|
+
const [error, setError] = useState<string | null>(null);
|
|
356
|
+
const [filter, setFilter] = useState<"all" | "pending" | "running" | "completed">("all");
|
|
357
|
+
|
|
358
|
+
// Reset state when agent changes
|
|
359
|
+
useEffect(() => {
|
|
360
|
+
setTasks([]);
|
|
361
|
+
setError(null);
|
|
362
|
+
setLoading(true);
|
|
363
|
+
}, [agent.id]);
|
|
364
|
+
|
|
365
|
+
const fetchTasks = async () => {
|
|
366
|
+
if (agent.status !== "running") {
|
|
367
|
+
setLoading(false);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
try {
|
|
372
|
+
const res = await fetch(`/api/agents/${agent.id}/tasks?status=${filter}`);
|
|
373
|
+
if (!res.ok) throw new Error("Failed to fetch tasks");
|
|
374
|
+
const data = await res.json();
|
|
375
|
+
setTasks(data.tasks || []);
|
|
376
|
+
setError(null);
|
|
377
|
+
} catch (err) {
|
|
378
|
+
setError(err instanceof Error ? err.message : "Failed to load tasks");
|
|
379
|
+
} finally {
|
|
380
|
+
setLoading(false);
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
useEffect(() => {
|
|
385
|
+
setLoading(true);
|
|
386
|
+
fetchTasks();
|
|
387
|
+
}, [agent.id, agent.status, filter]);
|
|
388
|
+
|
|
389
|
+
// Auto-refresh every 5 seconds when agent is running
|
|
390
|
+
useEffect(() => {
|
|
391
|
+
if (agent.status !== "running") return;
|
|
392
|
+
const interval = setInterval(fetchTasks, 5000);
|
|
393
|
+
return () => clearInterval(interval);
|
|
394
|
+
}, [agent.id, agent.status, filter]);
|
|
395
|
+
|
|
396
|
+
const getStatusColor = (status: Task["status"]) => {
|
|
397
|
+
switch (status) {
|
|
398
|
+
case "pending": return "bg-yellow-500/20 text-yellow-400";
|
|
399
|
+
case "running": return "bg-blue-500/20 text-blue-400";
|
|
400
|
+
case "completed": return "bg-green-500/20 text-green-400";
|
|
401
|
+
case "failed": return "bg-red-500/20 text-red-400";
|
|
402
|
+
default: return "bg-[#222] text-[#666]";
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
if (agent.status !== "running") {
|
|
407
|
+
return (
|
|
408
|
+
<div className="flex-1 flex items-center justify-center text-[#666]">
|
|
409
|
+
<p>Start the agent to view tasks</p>
|
|
410
|
+
</div>
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (!agent.features?.tasks) {
|
|
415
|
+
return (
|
|
416
|
+
<div className="flex-1 flex items-center justify-center text-[#666]">
|
|
417
|
+
<div className="text-center">
|
|
418
|
+
<p className="mb-2">Tasks feature is not enabled</p>
|
|
419
|
+
<p className="text-sm">Enable it in Settings to schedule tasks</p>
|
|
420
|
+
</div>
|
|
421
|
+
</div>
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (loading) {
|
|
426
|
+
return (
|
|
427
|
+
<div className="flex-1 flex items-center justify-center text-[#666]">
|
|
428
|
+
<p>Loading tasks...</p>
|
|
429
|
+
</div>
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (error) {
|
|
434
|
+
return (
|
|
435
|
+
<div className="flex-1 flex items-center justify-center text-red-400">
|
|
436
|
+
<p>{error}</p>
|
|
437
|
+
</div>
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return (
|
|
442
|
+
<div className="flex-1 overflow-auto p-4">
|
|
443
|
+
{/* Filter tabs */}
|
|
444
|
+
<div className="flex gap-2 mb-4">
|
|
445
|
+
{(["all", "pending", "running", "completed"] as const).map(status => (
|
|
446
|
+
<button
|
|
447
|
+
key={status}
|
|
448
|
+
onClick={() => setFilter(status)}
|
|
449
|
+
className={`px-3 py-1 text-xs rounded transition ${
|
|
450
|
+
filter === status
|
|
451
|
+
? "bg-[#f97316] text-black"
|
|
452
|
+
: "bg-[#1a1a1a] text-[#666] hover:text-[#888]"
|
|
453
|
+
}`}
|
|
454
|
+
>
|
|
455
|
+
{status.charAt(0).toUpperCase() + status.slice(1)}
|
|
456
|
+
</button>
|
|
457
|
+
))}
|
|
458
|
+
</div>
|
|
459
|
+
|
|
460
|
+
{tasks.length === 0 ? (
|
|
461
|
+
<div className="text-center py-10 text-[#666]">
|
|
462
|
+
<p>No {filter === "all" ? "" : filter + " "}tasks</p>
|
|
463
|
+
</div>
|
|
464
|
+
) : (
|
|
465
|
+
<div className="space-y-3">
|
|
466
|
+
{tasks.map(task => (
|
|
467
|
+
<div key={task.id} className="bg-[#111] border border-[#1a1a1a] rounded p-3">
|
|
468
|
+
<div className="flex items-start justify-between">
|
|
469
|
+
<div className="flex-1 min-w-0">
|
|
470
|
+
<p className="text-sm font-medium text-[#e0e0e0]">{task.name}</p>
|
|
471
|
+
{task.description && (
|
|
472
|
+
<p className="text-xs text-[#666] mt-1 line-clamp-2">{task.description}</p>
|
|
473
|
+
)}
|
|
474
|
+
</div>
|
|
475
|
+
<span className={`text-xs px-2 py-0.5 rounded ml-2 ${getStatusColor(task.status)}`}>
|
|
476
|
+
{task.status}
|
|
477
|
+
</span>
|
|
478
|
+
</div>
|
|
479
|
+
|
|
480
|
+
<div className="flex items-center gap-4 mt-2 text-xs text-[#666]">
|
|
481
|
+
<span>Created: {new Date(task.created_at).toLocaleString()}</span>
|
|
482
|
+
{task.scheduled_at && (
|
|
483
|
+
<span>Scheduled: {new Date(task.scheduled_at).toLocaleString()}</span>
|
|
484
|
+
)}
|
|
485
|
+
</div>
|
|
486
|
+
|
|
487
|
+
{task.status === "completed" && task.result && (
|
|
488
|
+
<div className="mt-2 p-2 bg-[#0a0a0a] rounded text-xs text-[#888]">
|
|
489
|
+
<p className="text-[#666] mb-1">Result:</p>
|
|
490
|
+
<p className="whitespace-pre-wrap">{task.result}</p>
|
|
491
|
+
</div>
|
|
492
|
+
)}
|
|
493
|
+
|
|
494
|
+
{task.status === "failed" && task.error && (
|
|
495
|
+
<div className="mt-2 p-2 bg-red-500/10 rounded text-xs text-red-400">
|
|
496
|
+
<p className="text-red-400/70 mb-1">Error:</p>
|
|
497
|
+
<p className="whitespace-pre-wrap">{task.error}</p>
|
|
498
|
+
</div>
|
|
499
|
+
)}
|
|
500
|
+
</div>
|
|
501
|
+
))}
|
|
502
|
+
</div>
|
|
503
|
+
)}
|
|
504
|
+
</div>
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
interface Memory {
|
|
509
|
+
id: string;
|
|
510
|
+
content: string;
|
|
511
|
+
type: string;
|
|
512
|
+
importance: number;
|
|
513
|
+
thread_id?: string;
|
|
514
|
+
created_at: string;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function MemoryTab({ agent }: { agent: Agent }) {
|
|
518
|
+
const [memories, setMemories] = useState<Memory[]>([]);
|
|
519
|
+
const [loading, setLoading] = useState(true);
|
|
520
|
+
const [error, setError] = useState<string | null>(null);
|
|
521
|
+
const [enabled, setEnabled] = useState(false);
|
|
522
|
+
|
|
523
|
+
// Reset state when agent changes
|
|
524
|
+
useEffect(() => {
|
|
525
|
+
setMemories([]);
|
|
526
|
+
setError(null);
|
|
527
|
+
setLoading(true);
|
|
528
|
+
}, [agent.id]);
|
|
529
|
+
|
|
530
|
+
const fetchMemories = async () => {
|
|
531
|
+
if (agent.status !== "running") {
|
|
532
|
+
setLoading(false);
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
try {
|
|
537
|
+
const res = await fetch(`/api/agents/${agent.id}/memories`);
|
|
538
|
+
if (!res.ok) throw new Error("Failed to fetch memories");
|
|
539
|
+
const data = await res.json();
|
|
540
|
+
setMemories(data.memories || []);
|
|
541
|
+
setEnabled(data.enabled ?? false);
|
|
542
|
+
setError(null);
|
|
543
|
+
} catch (err) {
|
|
544
|
+
setError(err instanceof Error ? err.message : "Failed to load memories");
|
|
545
|
+
} finally {
|
|
546
|
+
setLoading(false);
|
|
547
|
+
}
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
useEffect(() => {
|
|
551
|
+
fetchMemories();
|
|
552
|
+
}, [agent.id, agent.status]);
|
|
553
|
+
|
|
554
|
+
const deleteMemory = async (memoryId: string) => {
|
|
555
|
+
try {
|
|
556
|
+
await fetch(`/api/agents/${agent.id}/memories/${memoryId}`, { method: "DELETE" });
|
|
557
|
+
setMemories(prev => prev.filter(m => m.id !== memoryId));
|
|
558
|
+
} catch {
|
|
559
|
+
// Ignore errors
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
const clearAllMemories = async () => {
|
|
564
|
+
if (!confirm("Clear all memories?")) return;
|
|
565
|
+
try {
|
|
566
|
+
await fetch(`/api/agents/${agent.id}/memories`, { method: "DELETE" });
|
|
567
|
+
setMemories([]);
|
|
568
|
+
} catch {
|
|
569
|
+
// Ignore errors
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
if (!agent.features?.memory) {
|
|
574
|
+
return (
|
|
575
|
+
<div className="flex-1 flex items-center justify-center text-[#666]">
|
|
576
|
+
<div className="text-center">
|
|
577
|
+
<p className="mb-2">Memory feature is not enabled</p>
|
|
578
|
+
<p className="text-sm">Enable it in Settings to persist knowledge</p>
|
|
579
|
+
</div>
|
|
580
|
+
</div>
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
if (agent.status !== "running") {
|
|
585
|
+
return (
|
|
586
|
+
<div className="flex-1 flex items-center justify-center text-[#666]">
|
|
587
|
+
<p>Start the agent to view memories</p>
|
|
588
|
+
</div>
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (loading) {
|
|
593
|
+
return (
|
|
594
|
+
<div className="flex-1 flex items-center justify-center text-[#666]">
|
|
595
|
+
<p>Loading memories...</p>
|
|
596
|
+
</div>
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
if (error) {
|
|
601
|
+
return (
|
|
602
|
+
<div className="flex-1 flex items-center justify-center text-red-400">
|
|
603
|
+
<p>{error}</p>
|
|
604
|
+
</div>
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
if (!enabled) {
|
|
609
|
+
return (
|
|
610
|
+
<div className="flex-1 flex items-center justify-center text-[#666]">
|
|
611
|
+
<div className="text-center">
|
|
612
|
+
<p className="mb-2">Memory system not initialized</p>
|
|
613
|
+
<p className="text-sm">Check OPENAI_API_KEY for embeddings</p>
|
|
614
|
+
</div>
|
|
615
|
+
</div>
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
return (
|
|
620
|
+
<div className="flex-1 overflow-auto p-4">
|
|
621
|
+
<div className="flex items-center justify-between mb-4">
|
|
622
|
+
<h3 className="text-sm font-medium text-[#888]">Stored Memories ({memories.length})</h3>
|
|
623
|
+
{memories.length > 0 && (
|
|
624
|
+
<button
|
|
625
|
+
onClick={clearAllMemories}
|
|
626
|
+
className="text-xs text-red-400 hover:text-red-300"
|
|
627
|
+
>
|
|
628
|
+
Clear All
|
|
629
|
+
</button>
|
|
630
|
+
)}
|
|
631
|
+
</div>
|
|
632
|
+
|
|
633
|
+
{memories.length === 0 ? (
|
|
634
|
+
<div className="text-center py-10 text-[#666]">
|
|
635
|
+
<p>No memories stored yet</p>
|
|
636
|
+
<p className="text-sm mt-1">The agent will remember important information from conversations</p>
|
|
637
|
+
</div>
|
|
638
|
+
) : (
|
|
639
|
+
<div className="space-y-3">
|
|
640
|
+
{memories.map(memory => (
|
|
641
|
+
<div key={memory.id} className="bg-[#111] border border-[#1a1a1a] rounded p-3">
|
|
642
|
+
<div className="flex items-start justify-between gap-2">
|
|
643
|
+
<p className="text-sm text-[#e0e0e0] flex-1">{memory.content}</p>
|
|
644
|
+
<button
|
|
645
|
+
onClick={() => deleteMemory(memory.id)}
|
|
646
|
+
className="text-[#666] hover:text-red-400 text-sm flex-shrink-0"
|
|
647
|
+
>
|
|
648
|
+
×
|
|
649
|
+
</button>
|
|
650
|
+
</div>
|
|
651
|
+
<div className="flex items-center gap-3 mt-2">
|
|
652
|
+
<span className={`text-xs px-2 py-0.5 rounded ${
|
|
653
|
+
memory.type === "preference"
|
|
654
|
+
? "bg-purple-500/20 text-purple-400"
|
|
655
|
+
: memory.type === "fact"
|
|
656
|
+
? "bg-green-500/20 text-green-400"
|
|
657
|
+
: "bg-blue-500/20 text-blue-400"
|
|
658
|
+
}`}>
|
|
659
|
+
{memory.type}
|
|
660
|
+
</span>
|
|
661
|
+
<span className="text-xs text-[#666]">
|
|
662
|
+
{new Date(memory.created_at).toLocaleString()}
|
|
663
|
+
</span>
|
|
664
|
+
{memory.importance && (
|
|
665
|
+
<span className="text-xs text-[#555]">
|
|
666
|
+
importance: {memory.importance.toFixed(1)}
|
|
667
|
+
</span>
|
|
668
|
+
)}
|
|
669
|
+
</div>
|
|
670
|
+
</div>
|
|
671
|
+
))}
|
|
672
|
+
</div>
|
|
673
|
+
)}
|
|
674
|
+
</div>
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
interface AgentFile {
|
|
679
|
+
id: string;
|
|
680
|
+
filename: string;
|
|
681
|
+
mime_type: string;
|
|
682
|
+
file_type: string;
|
|
683
|
+
size_bytes: number;
|
|
684
|
+
source: string;
|
|
685
|
+
source_tool?: string;
|
|
686
|
+
url?: string;
|
|
687
|
+
created_at: string;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function FilesTab({ agent }: { agent: Agent }) {
|
|
691
|
+
const [files, setFiles] = useState<AgentFile[]>([]);
|
|
692
|
+
const [loading, setLoading] = useState(true);
|
|
693
|
+
const [error, setError] = useState<string | null>(null);
|
|
694
|
+
|
|
695
|
+
// Reset state when agent changes
|
|
696
|
+
useEffect(() => {
|
|
697
|
+
setFiles([]);
|
|
698
|
+
setError(null);
|
|
699
|
+
setLoading(true);
|
|
700
|
+
}, [agent.id]);
|
|
701
|
+
|
|
702
|
+
const fetchFiles = async () => {
|
|
703
|
+
if (agent.status !== "running") {
|
|
704
|
+
setLoading(false);
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
try {
|
|
709
|
+
const res = await fetch(`/api/agents/${agent.id}/files`);
|
|
710
|
+
if (!res.ok) throw new Error("Failed to fetch files");
|
|
711
|
+
const data = await res.json();
|
|
712
|
+
setFiles(data.files || []);
|
|
713
|
+
setError(null);
|
|
714
|
+
} catch (err) {
|
|
715
|
+
setError(err instanceof Error ? err.message : "Failed to load files");
|
|
716
|
+
} finally {
|
|
717
|
+
setLoading(false);
|
|
718
|
+
}
|
|
719
|
+
};
|
|
720
|
+
|
|
721
|
+
useEffect(() => {
|
|
722
|
+
fetchFiles();
|
|
723
|
+
}, [agent.id, agent.status]);
|
|
724
|
+
|
|
725
|
+
const deleteFile = async (fileId: string) => {
|
|
726
|
+
if (!confirm("Delete this file?")) return;
|
|
727
|
+
try {
|
|
728
|
+
await fetch(`/api/agents/${agent.id}/files/${fileId}`, { method: "DELETE" });
|
|
729
|
+
setFiles(prev => prev.filter(f => f.id !== fileId));
|
|
730
|
+
} catch {
|
|
731
|
+
// Ignore errors
|
|
732
|
+
}
|
|
733
|
+
};
|
|
734
|
+
|
|
735
|
+
const downloadFile = (fileId: string, filename: string) => {
|
|
736
|
+
const link = document.createElement("a");
|
|
737
|
+
link.href = `/api/agents/${agent.id}/files/${fileId}/download`;
|
|
738
|
+
link.download = filename;
|
|
739
|
+
link.click();
|
|
740
|
+
};
|
|
741
|
+
|
|
742
|
+
const formatSize = (bytes: number) => {
|
|
743
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
744
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
745
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
746
|
+
};
|
|
747
|
+
|
|
748
|
+
const getFileIcon = (mimeType: string) => {
|
|
749
|
+
if (mimeType.startsWith("image/")) return "🖼";
|
|
750
|
+
if (mimeType.includes("pdf")) return "📕";
|
|
751
|
+
if (mimeType.includes("json")) return "{}";
|
|
752
|
+
if (mimeType.includes("javascript") || mimeType.includes("typescript")) return "⚡";
|
|
753
|
+
if (mimeType.startsWith("text/")) return "📄";
|
|
754
|
+
if (mimeType.startsWith("audio/")) return "🎵";
|
|
755
|
+
if (mimeType.startsWith("video/")) return "🎬";
|
|
756
|
+
return "📁";
|
|
757
|
+
};
|
|
758
|
+
|
|
759
|
+
if (!agent.features?.files) {
|
|
760
|
+
return (
|
|
761
|
+
<div className="flex-1 flex items-center justify-center text-[#666]">
|
|
762
|
+
<div className="text-center">
|
|
763
|
+
<p className="mb-2">Files feature is not enabled</p>
|
|
764
|
+
<p className="text-sm">Enable it in Settings to manage files</p>
|
|
765
|
+
</div>
|
|
766
|
+
</div>
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
if (agent.status !== "running") {
|
|
771
|
+
return (
|
|
772
|
+
<div className="flex-1 flex items-center justify-center text-[#666]">
|
|
773
|
+
<p>Start the agent to view files</p>
|
|
774
|
+
</div>
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
if (loading) {
|
|
779
|
+
return (
|
|
780
|
+
<div className="flex-1 flex items-center justify-center text-[#666]">
|
|
781
|
+
<p>Loading files...</p>
|
|
782
|
+
</div>
|
|
783
|
+
);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
if (error) {
|
|
787
|
+
return (
|
|
788
|
+
<div className="flex-1 flex items-center justify-center text-red-400">
|
|
789
|
+
<p>{error}</p>
|
|
790
|
+
</div>
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
return (
|
|
795
|
+
<div className="flex-1 overflow-auto p-4">
|
|
796
|
+
<div className="flex items-center justify-between mb-4">
|
|
797
|
+
<h3 className="text-sm font-medium text-[#888]">Agent Files ({files.length})</h3>
|
|
798
|
+
<button
|
|
799
|
+
onClick={fetchFiles}
|
|
800
|
+
className="text-xs text-[#666] hover:text-[#888]"
|
|
801
|
+
>
|
|
802
|
+
Refresh
|
|
803
|
+
</button>
|
|
804
|
+
</div>
|
|
805
|
+
|
|
806
|
+
{files.length === 0 ? (
|
|
807
|
+
<div className="text-center py-10 text-[#666]">
|
|
808
|
+
<p>No files stored yet</p>
|
|
809
|
+
<p className="text-sm mt-1">Files created or uploaded by the agent will appear here</p>
|
|
810
|
+
</div>
|
|
811
|
+
) : (
|
|
812
|
+
<div className="space-y-2">
|
|
813
|
+
{files.map(file => (
|
|
814
|
+
<div key={file.id} className="bg-[#111] border border-[#1a1a1a] rounded p-3 flex items-center gap-3">
|
|
815
|
+
<div className="w-10 h-10 bg-[#1a1a1a] rounded flex items-center justify-center text-[#666]">
|
|
816
|
+
{getFileIcon(file.mime_type)}
|
|
817
|
+
</div>
|
|
818
|
+
<div className="flex-1 min-w-0">
|
|
819
|
+
<p className="text-sm text-[#e0e0e0] truncate">{file.filename}</p>
|
|
820
|
+
<p className="text-xs text-[#666]">
|
|
821
|
+
{formatSize(file.size_bytes)} • {new Date(file.created_at).toLocaleString()}
|
|
822
|
+
{file.source && file.source !== "upload" && ` • ${file.source}`}
|
|
823
|
+
</p>
|
|
824
|
+
</div>
|
|
825
|
+
<div className="flex items-center gap-2">
|
|
826
|
+
<button
|
|
827
|
+
onClick={() => downloadFile(file.id, file.filename)}
|
|
828
|
+
className="text-xs text-[#666] hover:text-[#f97316] px-2 py-1"
|
|
829
|
+
>
|
|
830
|
+
↓
|
|
831
|
+
</button>
|
|
832
|
+
<button
|
|
833
|
+
onClick={() => deleteFile(file.id)}
|
|
834
|
+
className="text-[#666] hover:text-red-400 text-sm"
|
|
835
|
+
>
|
|
836
|
+
×
|
|
837
|
+
</button>
|
|
838
|
+
</div>
|
|
839
|
+
</div>
|
|
840
|
+
))}
|
|
841
|
+
</div>
|
|
842
|
+
)}
|
|
843
|
+
</div>
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
|
|
107
847
|
function SettingsTab({ agent, providers, onUpdateAgent, onDeleteAgent }: {
|
|
108
848
|
agent: Agent;
|
|
109
849
|
providers: Provider[];
|