apteva 0.2.7 → 0.2.9
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.m4hg4bxq.js +218 -0
- package/dist/index.html +4 -2
- 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 +688 -45
- package/src/integrations/composio.ts +437 -0
- package/src/integrations/index.ts +80 -0
- package/src/openapi.ts +1724 -0
- package/src/routes/api.ts +1476 -118
- package/src/routes/auth.ts +242 -0
- package/src/server.ts +121 -11
- package/src/web/App.tsx +64 -19
- package/src/web/components/agents/AgentCard.tsx +24 -22
- package/src/web/components/agents/AgentPanel.tsx +810 -45
- package/src/web/components/agents/AgentsView.tsx +81 -9
- package/src/web/components/agents/CreateAgentModal.tsx +28 -1
- package/src/web/components/api/ApiDocsPage.tsx +583 -0
- 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 +56 -0
- package/src/web/components/common/Modal.tsx +184 -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 +87 -43
- package/src/web/components/mcp/IntegrationsPanel.tsx +743 -0
- package/src/web/components/mcp/McpPage.tsx +451 -63
- package/src/web/components/onboarding/OnboardingWizard.tsx +64 -8
- package/src/web/components/settings/SettingsPage.tsx +340 -26
- package/src/web/components/tasks/TasksPage.tsx +22 -20
- 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/index.html +1 -1
- package/src/web/styles.css +12 -0
- package/src/web/types.ts +10 -1
- package/dist/App.3kb50qa3.js +0 -213
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
|
|
3
|
+
interface CreateAccountStepProps {
|
|
4
|
+
onComplete: (user: { username: string }) => void;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function CreateAccountStep({ onComplete }: CreateAccountStepProps) {
|
|
8
|
+
const [username, setUsername] = useState("");
|
|
9
|
+
const [password, setPassword] = useState("");
|
|
10
|
+
const [confirmPassword, setConfirmPassword] = useState("");
|
|
11
|
+
const [email, setEmail] = useState("");
|
|
12
|
+
const [showEmail, setShowEmail] = useState(false);
|
|
13
|
+
const [error, setError] = useState<string | null>(null);
|
|
14
|
+
const [loading, setLoading] = useState(false);
|
|
15
|
+
|
|
16
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
17
|
+
e.preventDefault();
|
|
18
|
+
setError(null);
|
|
19
|
+
|
|
20
|
+
// Validate passwords match
|
|
21
|
+
if (password !== confirmPassword) {
|
|
22
|
+
setError("Passwords do not match");
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
setLoading(true);
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const res = await fetch("/api/onboarding/user", {
|
|
30
|
+
method: "POST",
|
|
31
|
+
headers: { "Content-Type": "application/json" },
|
|
32
|
+
body: JSON.stringify({
|
|
33
|
+
username,
|
|
34
|
+
password,
|
|
35
|
+
...(email && { email }), // Only include if provided
|
|
36
|
+
}),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const data = await res.json();
|
|
40
|
+
|
|
41
|
+
if (!res.ok) {
|
|
42
|
+
setError(data.error || "Failed to create account");
|
|
43
|
+
setLoading(false);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Auto-login after account creation
|
|
48
|
+
const loginRes = await fetch("/api/auth/login", {
|
|
49
|
+
method: "POST",
|
|
50
|
+
headers: { "Content-Type": "application/json" },
|
|
51
|
+
credentials: "include",
|
|
52
|
+
body: JSON.stringify({ username, password }),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (!loginRes.ok) {
|
|
56
|
+
setError("Account created but login failed. Please try logging in.");
|
|
57
|
+
setLoading(false);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const loginData = await loginRes.json();
|
|
62
|
+
|
|
63
|
+
// Store token for subsequent requests
|
|
64
|
+
if (loginData.accessToken) {
|
|
65
|
+
sessionStorage.setItem("accessToken", loginData.accessToken);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
onComplete({ username });
|
|
69
|
+
} catch (e) {
|
|
70
|
+
setError("Failed to create account");
|
|
71
|
+
setLoading(false);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<>
|
|
77
|
+
<h2 className="text-2xl font-semibold mb-2">Create your account</h2>
|
|
78
|
+
<p className="text-[#666] mb-6">
|
|
79
|
+
Set up your admin account to get started with apteva.
|
|
80
|
+
</p>
|
|
81
|
+
|
|
82
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
83
|
+
<div>
|
|
84
|
+
<label htmlFor="username" className="block text-sm text-[#888] mb-1">
|
|
85
|
+
Username
|
|
86
|
+
</label>
|
|
87
|
+
<input
|
|
88
|
+
id="username"
|
|
89
|
+
type="text"
|
|
90
|
+
value={username}
|
|
91
|
+
onChange={e => setUsername(e.target.value)}
|
|
92
|
+
placeholder="Choose a username"
|
|
93
|
+
autoFocus
|
|
94
|
+
required
|
|
95
|
+
className="w-full bg-[#0a0a0a] border border-[#333] rounded px-4 py-3 focus:outline-none focus:border-[#f97316]"
|
|
96
|
+
/>
|
|
97
|
+
<p className="text-xs text-[#666] mt-1">3-20 characters, letters, numbers, underscore</p>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<div>
|
|
101
|
+
<label htmlFor="password" className="block text-sm text-[#888] mb-1">
|
|
102
|
+
Password
|
|
103
|
+
</label>
|
|
104
|
+
<input
|
|
105
|
+
id="password"
|
|
106
|
+
type="password"
|
|
107
|
+
value={password}
|
|
108
|
+
onChange={e => setPassword(e.target.value)}
|
|
109
|
+
placeholder="Enter a password"
|
|
110
|
+
required
|
|
111
|
+
className="w-full bg-[#0a0a0a] border border-[#333] rounded px-4 py-3 focus:outline-none focus:border-[#f97316]"
|
|
112
|
+
/>
|
|
113
|
+
<p className="text-xs text-[#666] mt-1">Min 8 characters, uppercase, lowercase, number</p>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<div>
|
|
117
|
+
<label htmlFor="confirmPassword" className="block text-sm text-[#888] mb-1">
|
|
118
|
+
Confirm Password
|
|
119
|
+
</label>
|
|
120
|
+
<input
|
|
121
|
+
id="confirmPassword"
|
|
122
|
+
type="password"
|
|
123
|
+
value={confirmPassword}
|
|
124
|
+
onChange={e => setConfirmPassword(e.target.value)}
|
|
125
|
+
placeholder="Confirm your password"
|
|
126
|
+
required
|
|
127
|
+
className="w-full bg-[#0a0a0a] border border-[#333] rounded px-4 py-3 focus:outline-none focus:border-[#f97316]"
|
|
128
|
+
/>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
{!showEmail ? (
|
|
132
|
+
<button
|
|
133
|
+
type="button"
|
|
134
|
+
onClick={() => setShowEmail(true)}
|
|
135
|
+
className="text-sm text-[#666] hover:text-[#888] transition"
|
|
136
|
+
>
|
|
137
|
+
+ Add email for password recovery (optional)
|
|
138
|
+
</button>
|
|
139
|
+
) : (
|
|
140
|
+
<div>
|
|
141
|
+
<label htmlFor="email" className="block text-sm text-[#888] mb-1">
|
|
142
|
+
Email <span className="text-[#666]">(optional)</span>
|
|
143
|
+
</label>
|
|
144
|
+
<input
|
|
145
|
+
id="email"
|
|
146
|
+
type="email"
|
|
147
|
+
value={email}
|
|
148
|
+
onChange={e => setEmail(e.target.value)}
|
|
149
|
+
placeholder="For password recovery only"
|
|
150
|
+
className="w-full bg-[#0a0a0a] border border-[#333] rounded px-4 py-3 focus:outline-none focus:border-[#f97316]"
|
|
151
|
+
/>
|
|
152
|
+
<p className="text-xs text-[#666] mt-1">Only used for password recovery, never shared</p>
|
|
153
|
+
</div>
|
|
154
|
+
)}
|
|
155
|
+
|
|
156
|
+
{error && (
|
|
157
|
+
<div className="p-3 bg-red-500/10 border border-red-500/30 rounded text-red-400 text-sm">
|
|
158
|
+
{error}
|
|
159
|
+
</div>
|
|
160
|
+
)}
|
|
161
|
+
|
|
162
|
+
<button
|
|
163
|
+
type="submit"
|
|
164
|
+
disabled={loading || !username || !password || !confirmPassword}
|
|
165
|
+
className="w-full bg-[#f97316] hover:bg-[#fb923c] disabled:opacity-50 disabled:cursor-not-allowed text-black px-4 py-3 rounded font-medium transition"
|
|
166
|
+
>
|
|
167
|
+
{loading ? "Creating account..." : "Create Account"}
|
|
168
|
+
</button>
|
|
169
|
+
</form>
|
|
170
|
+
|
|
171
|
+
<p className="text-xs text-[#666] mt-4 text-center">
|
|
172
|
+
This will be your admin account with full access to apteva.
|
|
173
|
+
</p>
|
|
174
|
+
</>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { useAuth } from "../../context/AuthContext";
|
|
3
|
+
|
|
4
|
+
export function LoginPage() {
|
|
5
|
+
const { login } = useAuth();
|
|
6
|
+
const [username, setUsername] = useState("");
|
|
7
|
+
const [password, setPassword] = useState("");
|
|
8
|
+
const [error, setError] = useState<string | null>(null);
|
|
9
|
+
const [loading, setLoading] = useState(false);
|
|
10
|
+
|
|
11
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
12
|
+
e.preventDefault();
|
|
13
|
+
setError(null);
|
|
14
|
+
setLoading(true);
|
|
15
|
+
|
|
16
|
+
const result = await login(username, password);
|
|
17
|
+
|
|
18
|
+
if (!result.success) {
|
|
19
|
+
setError(result.error || "Login failed");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
setLoading(false);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div className="min-h-screen bg-[#0a0a0a] text-[#e0e0e0] font-mono flex items-center justify-center p-8">
|
|
27
|
+
<div className="w-full max-w-md">
|
|
28
|
+
{/* Logo */}
|
|
29
|
+
<div className="text-center mb-8">
|
|
30
|
+
<div className="flex items-center justify-center gap-2 mb-2">
|
|
31
|
+
<span className="text-[#f97316] text-3xl">>_</span>
|
|
32
|
+
<span className="text-3xl tracking-wider">apteva</span>
|
|
33
|
+
</div>
|
|
34
|
+
<p className="text-[#666]">Run AI agents locally</p>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<div className="bg-[#111] rounded-lg border border-[#1a1a1a] p-8">
|
|
38
|
+
<h2 className="text-2xl font-semibold mb-2">Welcome back</h2>
|
|
39
|
+
<p className="text-[#666] mb-6">Sign in to continue to apteva</p>
|
|
40
|
+
|
|
41
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
42
|
+
<div>
|
|
43
|
+
<label htmlFor="username" className="block text-sm text-[#888] mb-1">
|
|
44
|
+
Username
|
|
45
|
+
</label>
|
|
46
|
+
<input
|
|
47
|
+
id="username"
|
|
48
|
+
type="text"
|
|
49
|
+
value={username}
|
|
50
|
+
onChange={e => setUsername(e.target.value)}
|
|
51
|
+
placeholder="Enter your username"
|
|
52
|
+
autoFocus
|
|
53
|
+
required
|
|
54
|
+
className="w-full bg-[#0a0a0a] border border-[#333] rounded px-4 py-3 focus:outline-none focus:border-[#f97316]"
|
|
55
|
+
/>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<div>
|
|
59
|
+
<label htmlFor="password" className="block text-sm text-[#888] mb-1">
|
|
60
|
+
Password
|
|
61
|
+
</label>
|
|
62
|
+
<input
|
|
63
|
+
id="password"
|
|
64
|
+
type="password"
|
|
65
|
+
value={password}
|
|
66
|
+
onChange={e => setPassword(e.target.value)}
|
|
67
|
+
placeholder="Enter your password"
|
|
68
|
+
required
|
|
69
|
+
className="w-full bg-[#0a0a0a] border border-[#333] rounded px-4 py-3 focus:outline-none focus:border-[#f97316]"
|
|
70
|
+
/>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
{error && (
|
|
74
|
+
<div className="p-3 bg-red-500/10 border border-red-500/30 rounded text-red-400 text-sm">
|
|
75
|
+
{error}
|
|
76
|
+
</div>
|
|
77
|
+
)}
|
|
78
|
+
|
|
79
|
+
<button
|
|
80
|
+
type="submit"
|
|
81
|
+
disabled={loading || !username || !password}
|
|
82
|
+
className="w-full bg-[#f97316] hover:bg-[#fb923c] disabled:opacity-50 disabled:cursor-not-allowed text-black px-4 py-3 rounded font-medium transition"
|
|
83
|
+
>
|
|
84
|
+
{loading ? "Signing in..." : "Sign In"}
|
|
85
|
+
</button>
|
|
86
|
+
</form>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
@@ -117,3 +117,59 @@ export function TelemetryIcon({ className = "w-4 h-4" }: IconProps) {
|
|
|
117
117
|
</svg>
|
|
118
118
|
);
|
|
119
119
|
}
|
|
120
|
+
|
|
121
|
+
export function ApiIcon({ className = "w-4 h-4" }: IconProps) {
|
|
122
|
+
return (
|
|
123
|
+
<svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
124
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
|
|
125
|
+
</svg>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function FilesIcon({ className = "w-4 h-4" }: IconProps) {
|
|
130
|
+
return (
|
|
131
|
+
<svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
132
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
|
|
133
|
+
</svg>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function MultiAgentIcon({ className = "w-4 h-4" }: IconProps) {
|
|
138
|
+
return (
|
|
139
|
+
<svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
140
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
|
|
141
|
+
</svg>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function MenuIcon({ className = "w-5 h-5" }: IconProps) {
|
|
146
|
+
return (
|
|
147
|
+
<svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
148
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
|
149
|
+
</svg>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function ChevronDownIcon({ className = "w-4 h-4" }: IconProps) {
|
|
154
|
+
return (
|
|
155
|
+
<svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
156
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
157
|
+
</svg>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function FolderIcon({ className = "w-4 h-4" }: IconProps) {
|
|
162
|
+
return (
|
|
163
|
+
<svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
164
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
|
|
165
|
+
</svg>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function PlusIcon({ className = "w-4 h-4" }: IconProps) {
|
|
170
|
+
return (
|
|
171
|
+
<svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
172
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
|
173
|
+
</svg>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
@@ -8,9 +8,192 @@ interface ModalProps {
|
|
|
8
8
|
export function Modal({ children, onClose }: ModalProps) {
|
|
9
9
|
return (
|
|
10
10
|
<div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4">
|
|
11
|
-
<div className="bg-[#111] rounded p-6 w-full max-w-xl border border-[#1a1a1a] max-h-[90vh] overflow-y-auto">
|
|
11
|
+
<div className="bg-[#111] rounded p-6 w-full max-w-xl lg:max-w-2xl border border-[#1a1a1a] max-h-[90vh] overflow-y-auto">
|
|
12
12
|
{children}
|
|
13
13
|
</div>
|
|
14
14
|
</div>
|
|
15
15
|
);
|
|
16
16
|
}
|
|
17
|
+
|
|
18
|
+
// Confirmation Modal - replaces browser confirm()
|
|
19
|
+
interface ConfirmModalProps {
|
|
20
|
+
title?: string;
|
|
21
|
+
message: string;
|
|
22
|
+
confirmText?: string;
|
|
23
|
+
cancelText?: string;
|
|
24
|
+
confirmVariant?: "danger" | "primary";
|
|
25
|
+
onConfirm: () => void;
|
|
26
|
+
onCancel: () => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function ConfirmModal({
|
|
30
|
+
title,
|
|
31
|
+
message,
|
|
32
|
+
confirmText = "Confirm",
|
|
33
|
+
cancelText = "Cancel",
|
|
34
|
+
confirmVariant = "danger",
|
|
35
|
+
onConfirm,
|
|
36
|
+
onCancel,
|
|
37
|
+
}: ConfirmModalProps) {
|
|
38
|
+
return (
|
|
39
|
+
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
|
40
|
+
<div className="bg-[#111] border border-[#333] rounded-lg p-6 w-full max-w-sm">
|
|
41
|
+
{title && <h3 className="font-medium mb-2">{title}</h3>}
|
|
42
|
+
<p className="text-sm text-[#ccc] mb-4">{message}</p>
|
|
43
|
+
<div className="flex gap-2">
|
|
44
|
+
<button
|
|
45
|
+
onClick={onCancel}
|
|
46
|
+
className="flex-1 text-sm bg-[#1a1a1a] hover:bg-[#222] border border-[#333] px-4 py-2 rounded transition"
|
|
47
|
+
>
|
|
48
|
+
{cancelText}
|
|
49
|
+
</button>
|
|
50
|
+
<button
|
|
51
|
+
onClick={onConfirm}
|
|
52
|
+
className={`flex-1 text-sm text-white px-4 py-2 rounded transition ${
|
|
53
|
+
confirmVariant === "danger"
|
|
54
|
+
? "bg-red-500 hover:bg-red-600"
|
|
55
|
+
: "bg-[#f97316] hover:bg-[#ea580c]"
|
|
56
|
+
}`}
|
|
57
|
+
>
|
|
58
|
+
{confirmText}
|
|
59
|
+
</button>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Alert Modal - replaces browser alert()
|
|
67
|
+
interface AlertModalProps {
|
|
68
|
+
title?: string;
|
|
69
|
+
message: string;
|
|
70
|
+
buttonText?: string;
|
|
71
|
+
variant?: "error" | "success" | "info";
|
|
72
|
+
onClose: () => void;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function AlertModal({
|
|
76
|
+
title,
|
|
77
|
+
message,
|
|
78
|
+
buttonText = "OK",
|
|
79
|
+
variant = "info",
|
|
80
|
+
onClose,
|
|
81
|
+
}: AlertModalProps) {
|
|
82
|
+
const iconColors = {
|
|
83
|
+
error: "bg-red-500/20 text-red-400",
|
|
84
|
+
success: "bg-green-500/20 text-green-400",
|
|
85
|
+
info: "bg-blue-500/20 text-blue-400",
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const icons = {
|
|
89
|
+
error: "✕",
|
|
90
|
+
success: "✓",
|
|
91
|
+
info: "ℹ",
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
|
96
|
+
<div className="bg-[#111] border border-[#333] rounded-lg p-6 w-full max-w-sm text-center">
|
|
97
|
+
<div
|
|
98
|
+
className={`w-12 h-12 rounded-full flex items-center justify-center mx-auto mb-3 ${iconColors[variant]}`}
|
|
99
|
+
>
|
|
100
|
+
<span className="text-xl">{icons[variant]}</span>
|
|
101
|
+
</div>
|
|
102
|
+
{title && <h3 className="font-medium mb-2">{title}</h3>}
|
|
103
|
+
<p className="text-sm text-[#ccc] mb-4">{message}</p>
|
|
104
|
+
<button
|
|
105
|
+
onClick={onClose}
|
|
106
|
+
className="w-full text-sm bg-[#1a1a1a] hover:bg-[#222] border border-[#333] px-4 py-2 rounded transition"
|
|
107
|
+
>
|
|
108
|
+
{buttonText}
|
|
109
|
+
</button>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Hook for using confirmation dialogs
|
|
116
|
+
import { useState, useCallback } from "react";
|
|
117
|
+
|
|
118
|
+
interface UseConfirmOptions {
|
|
119
|
+
title?: string;
|
|
120
|
+
confirmText?: string;
|
|
121
|
+
cancelText?: string;
|
|
122
|
+
confirmVariant?: "danger" | "primary";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function useConfirm() {
|
|
126
|
+
const [state, setState] = useState<{
|
|
127
|
+
message: string;
|
|
128
|
+
options: UseConfirmOptions;
|
|
129
|
+
resolve: (value: boolean) => void;
|
|
130
|
+
} | null>(null);
|
|
131
|
+
|
|
132
|
+
const confirm = useCallback((message: string, options: UseConfirmOptions = {}) => {
|
|
133
|
+
return new Promise<boolean>((resolve) => {
|
|
134
|
+
setState({ message, options, resolve });
|
|
135
|
+
});
|
|
136
|
+
}, []);
|
|
137
|
+
|
|
138
|
+
const handleConfirm = useCallback(() => {
|
|
139
|
+
state?.resolve(true);
|
|
140
|
+
setState(null);
|
|
141
|
+
}, [state]);
|
|
142
|
+
|
|
143
|
+
const handleCancel = useCallback(() => {
|
|
144
|
+
state?.resolve(false);
|
|
145
|
+
setState(null);
|
|
146
|
+
}, [state]);
|
|
147
|
+
|
|
148
|
+
const ConfirmDialog = state ? (
|
|
149
|
+
<ConfirmModal
|
|
150
|
+
title={state.options.title}
|
|
151
|
+
message={state.message}
|
|
152
|
+
confirmText={state.options.confirmText}
|
|
153
|
+
cancelText={state.options.cancelText}
|
|
154
|
+
confirmVariant={state.options.confirmVariant}
|
|
155
|
+
onConfirm={handleConfirm}
|
|
156
|
+
onCancel={handleCancel}
|
|
157
|
+
/>
|
|
158
|
+
) : null;
|
|
159
|
+
|
|
160
|
+
return { confirm, ConfirmDialog };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Hook for using alert dialogs
|
|
164
|
+
interface UseAlertOptions {
|
|
165
|
+
title?: string;
|
|
166
|
+
buttonText?: string;
|
|
167
|
+
variant?: "error" | "success" | "info";
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function useAlert() {
|
|
171
|
+
const [state, setState] = useState<{
|
|
172
|
+
message: string;
|
|
173
|
+
options: UseAlertOptions;
|
|
174
|
+
resolve: () => void;
|
|
175
|
+
} | null>(null);
|
|
176
|
+
|
|
177
|
+
const alert = useCallback((message: string, options: UseAlertOptions = {}) => {
|
|
178
|
+
return new Promise<void>((resolve) => {
|
|
179
|
+
setState({ message, options, resolve });
|
|
180
|
+
});
|
|
181
|
+
}, []);
|
|
182
|
+
|
|
183
|
+
const handleClose = useCallback(() => {
|
|
184
|
+
state?.resolve();
|
|
185
|
+
setState(null);
|
|
186
|
+
}, [state]);
|
|
187
|
+
|
|
188
|
+
const AlertDialog = state ? (
|
|
189
|
+
<AlertModal
|
|
190
|
+
title={state.options.title}
|
|
191
|
+
message={state.message}
|
|
192
|
+
buttonText={state.options.buttonText}
|
|
193
|
+
variant={state.options.variant}
|
|
194
|
+
onClose={handleClose}
|
|
195
|
+
/>
|
|
196
|
+
) : null;
|
|
197
|
+
|
|
198
|
+
return { alert, AlertDialog };
|
|
199
|
+
}
|
|
@@ -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"
|