apteva 0.2.7 → 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 +561 -32
- package/src/routes/api.ts +901 -35
- package/src/routes/auth.ts +242 -0
- package/src/server.ts +46 -5
- package/src/web/App.tsx +61 -19
- package/src/web/components/agents/AgentCard.tsx +24 -22
- 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 +70 -22
- package/src/web/components/index.ts +3 -0
- package/src/web/components/layout/Header.tsx +135 -18
- 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 +320 -21
- package/src/web/components/tasks/TasksPage.tsx +21 -19
- package/src/web/components/telemetry/TelemetryPage.tsx +163 -61
- package/src/web/context/AuthContext.tsx +230 -0
- package/src/web/context/ProjectContext.tsx +182 -0
- 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.3kb50qa3.js +0 -213
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import React, { useState, useEffect } from "react";
|
|
2
|
-
import { useAgentActivity } from "../../context";
|
|
1
|
+
import React, { useState, useEffect, useCallback, useMemo } from "react";
|
|
2
|
+
import { useAgentActivity, useAuth, useProjects } from "../../context";
|
|
3
3
|
import type { Agent, Provider, Route, DashboardStats, Task } from "../../types";
|
|
4
4
|
|
|
5
5
|
interface DashboardProps {
|
|
@@ -19,20 +19,32 @@ export function Dashboard({
|
|
|
19
19
|
onNavigate,
|
|
20
20
|
onSelectAgent,
|
|
21
21
|
}: DashboardProps) {
|
|
22
|
+
const { authFetch } = useAuth();
|
|
23
|
+
const { currentProjectId } = useProjects();
|
|
22
24
|
const [stats, setStats] = useState<DashboardStats | null>(null);
|
|
23
25
|
const [recentTasks, setRecentTasks] = useState<Task[]>([]);
|
|
24
26
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
// Filter agents by current project
|
|
28
|
+
const filteredAgents = useMemo(() => {
|
|
29
|
+
if (!currentProjectId) return agents; // "All Projects"
|
|
30
|
+
if (currentProjectId === "unassigned") return agents.filter(a => !a.projectId);
|
|
31
|
+
return agents.filter(a => a.projectId === currentProjectId);
|
|
32
|
+
}, [agents, currentProjectId]);
|
|
30
33
|
|
|
31
|
-
const
|
|
34
|
+
const filteredRunningCount = useMemo(() => {
|
|
35
|
+
return filteredAgents.filter(a => a.status === "running").length;
|
|
36
|
+
}, [filteredAgents]);
|
|
37
|
+
|
|
38
|
+
// Get agent IDs for filtering tasks
|
|
39
|
+
const projectAgentIds = useMemo(() => {
|
|
40
|
+
return new Set(filteredAgents.map(a => a.id));
|
|
41
|
+
}, [filteredAgents]);
|
|
42
|
+
|
|
43
|
+
const fetchDashboardData = useCallback(async () => {
|
|
32
44
|
try {
|
|
33
45
|
const [dashRes, tasksRes] = await Promise.all([
|
|
34
|
-
|
|
35
|
-
|
|
46
|
+
authFetch("/api/dashboard"),
|
|
47
|
+
authFetch("/api/tasks?status=all"),
|
|
36
48
|
]);
|
|
37
49
|
|
|
38
50
|
if (dashRes.ok) {
|
|
@@ -47,15 +59,38 @@ export function Dashboard({
|
|
|
47
59
|
} catch (e) {
|
|
48
60
|
console.error("Failed to fetch dashboard data:", e);
|
|
49
61
|
}
|
|
50
|
-
};
|
|
62
|
+
}, [authFetch]);
|
|
51
63
|
|
|
52
|
-
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
fetchDashboardData();
|
|
66
|
+
const interval = setInterval(fetchDashboardData, 10000);
|
|
67
|
+
return () => clearInterval(interval);
|
|
68
|
+
}, [fetchDashboardData]);
|
|
69
|
+
|
|
70
|
+
// Filter tasks by project agents
|
|
71
|
+
const filteredTasks = useMemo(() => {
|
|
72
|
+
if (!currentProjectId) return recentTasks;
|
|
73
|
+
return recentTasks.filter(t => projectAgentIds.has(t.agentId));
|
|
74
|
+
}, [recentTasks, currentProjectId, projectAgentIds]);
|
|
75
|
+
|
|
76
|
+
// Calculate task stats from filtered tasks
|
|
77
|
+
const taskStats = useMemo(() => {
|
|
78
|
+
if (!currentProjectId) {
|
|
79
|
+
return stats?.tasks || { total: 0, pending: 0, running: 0, completed: 0 };
|
|
80
|
+
}
|
|
81
|
+
// When filtering by project, calculate from filtered tasks
|
|
82
|
+
const total = filteredTasks.length;
|
|
83
|
+
const pending = filteredTasks.filter(t => t.status === "pending").length;
|
|
84
|
+
const running = filteredTasks.filter(t => t.status === "running").length;
|
|
85
|
+
const completed = filteredTasks.filter(t => t.status === "completed").length;
|
|
86
|
+
return { total, pending, running, completed };
|
|
87
|
+
}, [stats, currentProjectId, filteredTasks]);
|
|
53
88
|
|
|
54
89
|
return (
|
|
55
90
|
<div className="flex-1 overflow-auto p-6">
|
|
56
91
|
{/* Stats Cards */}
|
|
57
92
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4 mb-6">
|
|
58
|
-
<StatCard label="Agents" value={
|
|
93
|
+
<StatCard label="Agents" value={filteredAgents.length} subValue={`${filteredRunningCount} running`} />
|
|
59
94
|
<StatCard label="Tasks" value={taskStats.total} subValue={`${taskStats.pending} pending`} />
|
|
60
95
|
<StatCard label="Completed" value={taskStats.completed} color="text-green-400" />
|
|
61
96
|
<StatCard label="Providers" value={configuredProviders.length} color="text-[#f97316]" />
|
|
@@ -70,12 +105,12 @@ export function Dashboard({
|
|
|
70
105
|
>
|
|
71
106
|
{loading ? (
|
|
72
107
|
<div className="p-4 text-center text-[#666]">Loading...</div>
|
|
73
|
-
) :
|
|
108
|
+
) : filteredAgents.length === 0 ? (
|
|
74
109
|
<div className="p-4 text-center text-[#666]">No agents yet</div>
|
|
75
110
|
) : (
|
|
76
111
|
<div className="divide-y divide-[#1a1a1a]">
|
|
77
|
-
{
|
|
78
|
-
<AgentListItem key={agent.id} agent={agent} onSelect={() => onSelectAgent(agent)} />
|
|
112
|
+
{filteredAgents.slice(0, 5).map((agent) => (
|
|
113
|
+
<AgentListItem key={agent.id} agent={agent} onSelect={() => onSelectAgent(agent)} showProject={!currentProjectId} />
|
|
79
114
|
))}
|
|
80
115
|
</div>
|
|
81
116
|
)}
|
|
@@ -87,14 +122,14 @@ export function Dashboard({
|
|
|
87
122
|
actionLabel="View All"
|
|
88
123
|
onAction={() => onNavigate("tasks")}
|
|
89
124
|
>
|
|
90
|
-
{
|
|
125
|
+
{filteredTasks.length === 0 ? (
|
|
91
126
|
<div className="p-4 text-center text-[#666]">
|
|
92
127
|
<p>No tasks yet</p>
|
|
93
128
|
<p className="text-sm text-[#444] mt-1">Tasks will appear when agents create them</p>
|
|
94
129
|
</div>
|
|
95
130
|
) : (
|
|
96
131
|
<div className="divide-y divide-[#1a1a1a]">
|
|
97
|
-
{
|
|
132
|
+
{filteredTasks.map((task) => (
|
|
98
133
|
<div
|
|
99
134
|
key={`${task.agentId}-${task.id}`}
|
|
100
135
|
className="px-4 py-3 flex items-center justify-between"
|
|
@@ -155,20 +190,33 @@ function DashboardCard({ title, actionLabel, onAction, children }: DashboardCard
|
|
|
155
190
|
);
|
|
156
191
|
}
|
|
157
192
|
|
|
158
|
-
function AgentListItem({ agent, onSelect }: { agent: Agent; onSelect: () => void }) {
|
|
193
|
+
function AgentListItem({ agent, onSelect, showProject }: { agent: Agent; onSelect: () => void; showProject?: boolean }) {
|
|
159
194
|
const { isActive } = useAgentActivity(agent.id);
|
|
195
|
+
const { projects } = useProjects();
|
|
196
|
+
const project = agent.projectId ? projects.find(p => p.id === agent.projectId) : null;
|
|
160
197
|
|
|
161
198
|
return (
|
|
162
199
|
<div
|
|
163
200
|
onClick={onSelect}
|
|
164
201
|
className="px-4 py-3 hover:bg-[#1a1a1a] cursor-pointer flex items-center justify-between"
|
|
165
202
|
>
|
|
166
|
-
<div>
|
|
203
|
+
<div className="flex-1 min-w-0">
|
|
167
204
|
<p className="font-medium">{agent.name}</p>
|
|
168
|
-
<
|
|
205
|
+
<div className="flex items-center gap-2 text-sm text-[#666]">
|
|
206
|
+
<span>{agent.provider}</span>
|
|
207
|
+
{showProject && project && (
|
|
208
|
+
<>
|
|
209
|
+
<span className="text-[#444]">·</span>
|
|
210
|
+
<span className="flex items-center gap-1">
|
|
211
|
+
<span className="w-2 h-2 rounded-full" style={{ backgroundColor: project.color }} />
|
|
212
|
+
{project.name}
|
|
213
|
+
</span>
|
|
214
|
+
</>
|
|
215
|
+
)}
|
|
216
|
+
</div>
|
|
169
217
|
</div>
|
|
170
218
|
<span
|
|
171
|
-
className={`w-2 h-2 rounded-full ${
|
|
219
|
+
className={`w-2 h-2 rounded-full flex-shrink-0 ${
|
|
172
220
|
agent.status === "running"
|
|
173
221
|
? isActive
|
|
174
222
|
? "bg-green-400 animate-pulse"
|
|
@@ -4,6 +4,9 @@ export { LoadingSpinner, Modal, Select, CheckIcon, CloseIcon, DashboardIcon, Age
|
|
|
4
4
|
// Layout components
|
|
5
5
|
export { Header, Sidebar, ErrorBanner } from "./layout";
|
|
6
6
|
|
|
7
|
+
// Auth components
|
|
8
|
+
export { LoginPage, CreateAccountStep } from "./auth";
|
|
9
|
+
|
|
7
10
|
// Feature components
|
|
8
11
|
export { OnboardingWizard } from "./onboarding";
|
|
9
12
|
export { SettingsPage } from "./settings";
|
|
@@ -1,37 +1,154 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { useTelemetryContext } from "../../context";
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { useTelemetryContext, useAuth, useProjects } from "../../context";
|
|
3
|
+
import { MenuIcon, ChevronDownIcon } from "../common/Icons";
|
|
3
4
|
|
|
4
5
|
interface HeaderProps {
|
|
5
|
-
|
|
6
|
-
canCreateAgent: boolean;
|
|
6
|
+
onMenuClick?: () => void;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
export function Header({
|
|
9
|
+
export function Header({ onMenuClick }: HeaderProps) {
|
|
10
10
|
const { connected } = useTelemetryContext();
|
|
11
|
+
const { user, logout } = useAuth();
|
|
12
|
+
const { projects, currentProjectId, currentProject, setCurrentProjectId, unassignedCount } = useProjects();
|
|
13
|
+
const [showUserMenu, setShowUserMenu] = useState(false);
|
|
14
|
+
const [showProjectMenu, setShowProjectMenu] = useState(false);
|
|
15
|
+
|
|
16
|
+
const handleLogout = async () => {
|
|
17
|
+
await logout();
|
|
18
|
+
setShowUserMenu(false);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const handleProjectSelect = (projectId: string | null) => {
|
|
22
|
+
setCurrentProjectId(projectId);
|
|
23
|
+
setShowProjectMenu(false);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const getProjectLabel = () => {
|
|
27
|
+
if (currentProjectId === null) return "All Projects";
|
|
28
|
+
if (currentProjectId === "unassigned") return "Unassigned";
|
|
29
|
+
return currentProject?.name || "Select Project";
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const getProjectColor = () => {
|
|
33
|
+
if (currentProjectId === null) return "#666";
|
|
34
|
+
if (currentProjectId === "unassigned") return "#888";
|
|
35
|
+
return currentProject?.color || "#6366f1";
|
|
36
|
+
};
|
|
11
37
|
|
|
12
38
|
return (
|
|
13
|
-
<header className="border-b border-[#1a1a1a] px-6 py-4 flex-shrink-0">
|
|
39
|
+
<header className="border-b border-[#1a1a1a] px-4 md:px-6 py-4 flex-shrink-0">
|
|
14
40
|
<div className="flex items-center justify-between">
|
|
15
|
-
<div className="flex items-center gap-
|
|
16
|
-
|
|
17
|
-
<
|
|
41
|
+
<div className="flex items-center gap-3">
|
|
42
|
+
{/* Hamburger menu button - mobile only */}
|
|
43
|
+
<button
|
|
44
|
+
onClick={onMenuClick}
|
|
45
|
+
className="p-2 -ml-2 text-[#666] hover:text-[#e0e0e0] transition md:hidden"
|
|
46
|
+
>
|
|
47
|
+
<MenuIcon />
|
|
48
|
+
</button>
|
|
49
|
+
<div className="flex items-center gap-2">
|
|
50
|
+
<span className="text-[#f97316]">>_</span>
|
|
51
|
+
<span className="text-xl tracking-wider">apteva</span>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
{/* Project Selector */}
|
|
55
|
+
{projects.length > 0 && (
|
|
56
|
+
<div className="relative ml-2 md:ml-4">
|
|
57
|
+
<button
|
|
58
|
+
onClick={() => setShowProjectMenu(!showProjectMenu)}
|
|
59
|
+
className="flex items-center gap-2 px-3 py-1.5 rounded border border-[#222] bg-[#111] hover:bg-[#1a1a1a] transition text-sm"
|
|
60
|
+
>
|
|
61
|
+
<span
|
|
62
|
+
className="w-2.5 h-2.5 rounded-full"
|
|
63
|
+
style={{ backgroundColor: getProjectColor() }}
|
|
64
|
+
/>
|
|
65
|
+
<span className="hidden sm:inline max-w-[120px] md:max-w-[180px] truncate">
|
|
66
|
+
{getProjectLabel()}
|
|
67
|
+
</span>
|
|
68
|
+
<ChevronDownIcon />
|
|
69
|
+
</button>
|
|
70
|
+
{showProjectMenu && (
|
|
71
|
+
<div className="absolute left-0 top-full mt-1 w-56 bg-[#111] border border-[#222] rounded-lg shadow-xl z-50">
|
|
72
|
+
<div className="py-1 max-h-64 overflow-y-auto">
|
|
73
|
+
<button
|
|
74
|
+
onClick={() => handleProjectSelect(null)}
|
|
75
|
+
className={`w-full px-4 py-2 text-left text-sm flex items-center gap-2 hover:bg-[#1a1a1a] transition ${
|
|
76
|
+
currentProjectId === null ? "bg-[#1a1a1a] text-[#f97316]" : ""
|
|
77
|
+
}`}
|
|
78
|
+
>
|
|
79
|
+
<span className="w-2.5 h-2.5 rounded-full bg-[#666]" />
|
|
80
|
+
All Projects
|
|
81
|
+
</button>
|
|
82
|
+
{projects.map(project => (
|
|
83
|
+
<button
|
|
84
|
+
key={project.id}
|
|
85
|
+
onClick={() => handleProjectSelect(project.id)}
|
|
86
|
+
className={`w-full px-4 py-2 text-left text-sm flex items-center gap-2 hover:bg-[#1a1a1a] transition ${
|
|
87
|
+
currentProjectId === project.id ? "bg-[#1a1a1a] text-[#f97316]" : ""
|
|
88
|
+
}`}
|
|
89
|
+
>
|
|
90
|
+
<span
|
|
91
|
+
className="w-2.5 h-2.5 rounded-full flex-shrink-0"
|
|
92
|
+
style={{ backgroundColor: project.color }}
|
|
93
|
+
/>
|
|
94
|
+
<span className="truncate">{project.name}</span>
|
|
95
|
+
<span className="ml-auto text-xs text-[#666]">{project.agentCount}</span>
|
|
96
|
+
</button>
|
|
97
|
+
))}
|
|
98
|
+
{unassignedCount > 0 && (
|
|
99
|
+
<button
|
|
100
|
+
onClick={() => handleProjectSelect("unassigned")}
|
|
101
|
+
className={`w-full px-4 py-2 text-left text-sm flex items-center gap-2 hover:bg-[#1a1a1a] transition ${
|
|
102
|
+
currentProjectId === "unassigned" ? "bg-[#1a1a1a] text-[#f97316]" : ""
|
|
103
|
+
}`}
|
|
104
|
+
>
|
|
105
|
+
<span className="w-2.5 h-2.5 rounded-full bg-[#888]" />
|
|
106
|
+
<span className="truncate">Unassigned</span>
|
|
107
|
+
<span className="ml-auto text-xs text-[#666]">{unassignedCount}</span>
|
|
108
|
+
</button>
|
|
109
|
+
)}
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
)}
|
|
113
|
+
</div>
|
|
114
|
+
)}
|
|
18
115
|
</div>
|
|
19
|
-
<div className="flex items-center gap-4">
|
|
116
|
+
<div className="flex items-center gap-3 md:gap-4">
|
|
20
117
|
<div className="flex items-center gap-2">
|
|
21
118
|
<span
|
|
22
119
|
className={`w-2 h-2 rounded-full ${connected ? "bg-green-400" : "bg-red-400"}`}
|
|
23
120
|
/>
|
|
24
|
-
<span className="text-xs text-[#666]">
|
|
121
|
+
<span className="text-xs text-[#666] hidden sm:inline">
|
|
25
122
|
{connected ? "Live" : "Offline"}
|
|
26
123
|
</span>
|
|
27
124
|
</div>
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
125
|
+
{user && (
|
|
126
|
+
<div className="relative">
|
|
127
|
+
<button
|
|
128
|
+
onClick={() => setShowUserMenu(!showUserMenu)}
|
|
129
|
+
className="flex items-center gap-2 px-2 md:px-3 py-2 rounded hover:bg-[#1a1a1a] transition"
|
|
130
|
+
>
|
|
131
|
+
<div className="w-8 h-8 rounded-full bg-[#f97316] flex items-center justify-center text-black font-medium text-sm">
|
|
132
|
+
{user.username.charAt(0).toUpperCase()}
|
|
133
|
+
</div>
|
|
134
|
+
<span className="text-sm text-[#888] hidden sm:block">{user.username}</span>
|
|
135
|
+
</button>
|
|
136
|
+
{showUserMenu && (
|
|
137
|
+
<div className="absolute right-0 top-full mt-1 w-48 bg-[#111] border border-[#222] rounded-lg shadow-xl z-50">
|
|
138
|
+
<div className="px-4 py-3 border-b border-[#222]">
|
|
139
|
+
<p className="text-sm font-medium">{user.username}</p>
|
|
140
|
+
<p className="text-xs text-[#f97316] mt-1">{user.role}</p>
|
|
141
|
+
</div>
|
|
142
|
+
<button
|
|
143
|
+
onClick={handleLogout}
|
|
144
|
+
className="w-full px-4 py-2 text-left text-sm text-red-400 hover:bg-[#1a1a1a] transition"
|
|
145
|
+
>
|
|
146
|
+
Sign out
|
|
147
|
+
</button>
|
|
148
|
+
</div>
|
|
149
|
+
)}
|
|
150
|
+
</div>
|
|
151
|
+
)}
|
|
35
152
|
</div>
|
|
36
153
|
</div>
|
|
37
154
|
</header>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { DashboardIcon, AgentsIcon, TasksIcon, McpIcon, TelemetryIcon, SettingsIcon } from "../common/Icons";
|
|
2
|
+
import { DashboardIcon, AgentsIcon, TasksIcon, McpIcon, TelemetryIcon, SettingsIcon, CloseIcon } from "../common/Icons";
|
|
3
3
|
import type { Route } from "../../types";
|
|
4
4
|
|
|
5
5
|
interface SidebarProps {
|
|
@@ -7,52 +7,90 @@ interface SidebarProps {
|
|
|
7
7
|
agentCount: number;
|
|
8
8
|
taskCount?: number;
|
|
9
9
|
onNavigate: (route: Route) => void;
|
|
10
|
+
isOpen?: boolean;
|
|
11
|
+
onClose?: () => void;
|
|
10
12
|
}
|
|
11
13
|
|
|
12
|
-
export function Sidebar({ route, agentCount, taskCount, onNavigate }: SidebarProps) {
|
|
14
|
+
export function Sidebar({ route, agentCount, taskCount, onNavigate, isOpen, onClose }: SidebarProps) {
|
|
15
|
+
const handleNavigate = (newRoute: Route) => {
|
|
16
|
+
onNavigate(newRoute);
|
|
17
|
+
onClose?.();
|
|
18
|
+
};
|
|
19
|
+
|
|
13
20
|
return (
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
onClick={() => onNavigate("dashboard")}
|
|
21
|
-
/>
|
|
22
|
-
<NavButton
|
|
23
|
-
icon={<AgentsIcon />}
|
|
24
|
-
label="Agents"
|
|
25
|
-
active={route === "agents"}
|
|
26
|
-
onClick={() => onNavigate("agents")}
|
|
27
|
-
badge={agentCount > 0 ? String(agentCount) : undefined}
|
|
28
|
-
/>
|
|
29
|
-
<NavButton
|
|
30
|
-
icon={<TasksIcon />}
|
|
31
|
-
label="Tasks"
|
|
32
|
-
active={route === "tasks"}
|
|
33
|
-
onClick={() => onNavigate("tasks")}
|
|
34
|
-
badge={taskCount && taskCount > 0 ? String(taskCount) : undefined}
|
|
35
|
-
/>
|
|
36
|
-
<NavButton
|
|
37
|
-
icon={<McpIcon />}
|
|
38
|
-
label="MCP"
|
|
39
|
-
active={route === "mcp"}
|
|
40
|
-
onClick={() => onNavigate("mcp")}
|
|
41
|
-
/>
|
|
42
|
-
<NavButton
|
|
43
|
-
icon={<TelemetryIcon />}
|
|
44
|
-
label="Telemetry"
|
|
45
|
-
active={route === "telemetry"}
|
|
46
|
-
onClick={() => onNavigate("telemetry")}
|
|
21
|
+
<>
|
|
22
|
+
{/* Mobile overlay backdrop */}
|
|
23
|
+
{isOpen && (
|
|
24
|
+
<div
|
|
25
|
+
className="fixed inset-0 bg-black/60 z-40 md:hidden"
|
|
26
|
+
onClick={onClose}
|
|
47
27
|
/>
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
28
|
+
)}
|
|
29
|
+
|
|
30
|
+
{/* Sidebar - hidden on mobile unless open, always visible on md+ */}
|
|
31
|
+
<aside
|
|
32
|
+
className={`
|
|
33
|
+
fixed inset-y-0 left-0 z-50 w-64 bg-[#0a0a0a] border-r border-[#1a1a1a] p-4 transform transition-transform duration-200 ease-in-out
|
|
34
|
+
md:relative md:w-56 md:translate-x-0 md:z-auto
|
|
35
|
+
${isOpen ? "translate-x-0" : "-translate-x-full"}
|
|
36
|
+
`}
|
|
37
|
+
>
|
|
38
|
+
{/* Mobile header with close button */}
|
|
39
|
+
<div className="flex items-center justify-between mb-4 md:hidden">
|
|
40
|
+
<div className="flex items-center gap-2">
|
|
41
|
+
<span className="text-[#f97316]">>_</span>
|
|
42
|
+
<span className="text-lg tracking-wider">apteva</span>
|
|
43
|
+
</div>
|
|
44
|
+
<button
|
|
45
|
+
onClick={onClose}
|
|
46
|
+
className="p-2 text-[#666] hover:text-[#e0e0e0] transition"
|
|
47
|
+
>
|
|
48
|
+
<CloseIcon />
|
|
49
|
+
</button>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<nav className="space-y-1">
|
|
53
|
+
<NavButton
|
|
54
|
+
icon={<DashboardIcon />}
|
|
55
|
+
label="Dashboard"
|
|
56
|
+
active={route === "dashboard"}
|
|
57
|
+
onClick={() => handleNavigate("dashboard")}
|
|
58
|
+
/>
|
|
59
|
+
<NavButton
|
|
60
|
+
icon={<AgentsIcon />}
|
|
61
|
+
label="Agents"
|
|
62
|
+
active={route === "agents"}
|
|
63
|
+
onClick={() => handleNavigate("agents")}
|
|
64
|
+
badge={agentCount > 0 ? String(agentCount) : undefined}
|
|
65
|
+
/>
|
|
66
|
+
<NavButton
|
|
67
|
+
icon={<TasksIcon />}
|
|
68
|
+
label="Tasks"
|
|
69
|
+
active={route === "tasks"}
|
|
70
|
+
onClick={() => handleNavigate("tasks")}
|
|
71
|
+
badge={taskCount && taskCount > 0 ? String(taskCount) : undefined}
|
|
72
|
+
/>
|
|
73
|
+
<NavButton
|
|
74
|
+
icon={<McpIcon />}
|
|
75
|
+
label="MCP"
|
|
76
|
+
active={route === "mcp"}
|
|
77
|
+
onClick={() => handleNavigate("mcp")}
|
|
78
|
+
/>
|
|
79
|
+
<NavButton
|
|
80
|
+
icon={<TelemetryIcon />}
|
|
81
|
+
label="Telemetry"
|
|
82
|
+
active={route === "telemetry"}
|
|
83
|
+
onClick={() => handleNavigate("telemetry")}
|
|
84
|
+
/>
|
|
85
|
+
<NavButton
|
|
86
|
+
icon={<SettingsIcon />}
|
|
87
|
+
label="Settings"
|
|
88
|
+
active={route === "settings"}
|
|
89
|
+
onClick={() => handleNavigate("settings")}
|
|
90
|
+
/>
|
|
91
|
+
</nav>
|
|
92
|
+
</aside>
|
|
93
|
+
</>
|
|
56
94
|
);
|
|
57
95
|
}
|
|
58
96
|
|