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.
Files changed (41) hide show
  1. package/dist/App.hzbfeg94.js +217 -0
  2. package/dist/index.html +3 -1
  3. package/dist/styles.css +1 -1
  4. package/package.json +1 -1
  5. package/src/auth/index.ts +386 -0
  6. package/src/auth/middleware.ts +183 -0
  7. package/src/binary.ts +19 -1
  8. package/src/db.ts +570 -32
  9. package/src/routes/api.ts +913 -38
  10. package/src/routes/auth.ts +242 -0
  11. package/src/server.ts +60 -8
  12. package/src/web/App.tsx +61 -19
  13. package/src/web/components/agents/AgentCard.tsx +30 -41
  14. package/src/web/components/agents/AgentPanel.tsx +751 -11
  15. package/src/web/components/agents/AgentsView.tsx +81 -9
  16. package/src/web/components/agents/CreateAgentModal.tsx +28 -1
  17. package/src/web/components/auth/CreateAccountStep.tsx +176 -0
  18. package/src/web/components/auth/LoginPage.tsx +91 -0
  19. package/src/web/components/auth/index.ts +2 -0
  20. package/src/web/components/common/Icons.tsx +48 -0
  21. package/src/web/components/common/Modal.tsx +1 -1
  22. package/src/web/components/dashboard/Dashboard.tsx +91 -31
  23. package/src/web/components/index.ts +3 -0
  24. package/src/web/components/layout/Header.tsx +145 -15
  25. package/src/web/components/layout/Sidebar.tsx +81 -43
  26. package/src/web/components/mcp/McpPage.tsx +261 -32
  27. package/src/web/components/onboarding/OnboardingWizard.tsx +64 -8
  28. package/src/web/components/settings/SettingsPage.tsx +404 -18
  29. package/src/web/components/tasks/TasksPage.tsx +21 -19
  30. package/src/web/components/telemetry/TelemetryPage.tsx +271 -81
  31. package/src/web/context/AuthContext.tsx +230 -0
  32. package/src/web/context/ProjectContext.tsx +182 -0
  33. package/src/web/context/TelemetryContext.tsx +98 -76
  34. package/src/web/context/index.ts +5 -0
  35. package/src/web/hooks/useAgents.ts +18 -6
  36. package/src/web/hooks/useOnboarding.ts +20 -4
  37. package/src/web/hooks/useProviders.ts +15 -5
  38. package/src/web/icon.png +0 -0
  39. package/src/web/styles.css +12 -0
  40. package/src/web/types.ts +6 -0
  41. 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 justify-between px-4">
34
- <div className="flex gap-1">
35
- <TabButton active={activeTab === "chat"} onClick={() => setActiveTab("chat")}>
36
- Chat
37
- </TabButton>
38
- <TabButton active={activeTab === "settings"} onClick={() => setActiveTab("settings")}>
39
- Settings
40
- </TabButton>
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[];