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
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import React, { useState, useEffect } from "react";
|
|
2
2
|
import { CheckIcon } from "../common/Icons";
|
|
3
|
+
import { CreateAccountStep } from "../auth";
|
|
3
4
|
import type { Provider } from "../../types";
|
|
4
5
|
|
|
5
6
|
interface OnboardingWizardProps {
|
|
6
7
|
onComplete: () => void;
|
|
8
|
+
needsAccount?: boolean; // Whether to show account creation step
|
|
7
9
|
}
|
|
8
10
|
|
|
9
|
-
export function OnboardingWizard({ onComplete }: OnboardingWizardProps) {
|
|
10
|
-
|
|
11
|
+
export function OnboardingWizard({ onComplete, needsAccount = false }: OnboardingWizardProps) {
|
|
12
|
+
// Step 0 = account creation (if needed), Step 1 = add keys, Step 2 = complete
|
|
13
|
+
const [step, setStep] = useState(needsAccount ? 0 : 1);
|
|
11
14
|
const [providers, setProviders] = useState<Provider[]>([]);
|
|
12
15
|
const [selectedProvider, setSelectedProvider] = useState<string | null>(null);
|
|
13
16
|
const [apiKey, setApiKey] = useState("");
|
|
@@ -15,16 +18,26 @@ export function OnboardingWizard({ onComplete }: OnboardingWizardProps) {
|
|
|
15
18
|
const [testing, setTesting] = useState(false);
|
|
16
19
|
const [error, setError] = useState<string | null>(null);
|
|
17
20
|
const [success, setSuccess] = useState<string | null>(null);
|
|
21
|
+
const [accountCreated, setAccountCreated] = useState(false);
|
|
22
|
+
|
|
23
|
+
// Get auth token from session storage (set during account creation)
|
|
24
|
+
const getAuthHeaders = (): Record<string, string> => {
|
|
25
|
+
const token = sessionStorage.getItem("accessToken");
|
|
26
|
+
return token ? { Authorization: `Bearer ${token}` } : {};
|
|
27
|
+
};
|
|
18
28
|
|
|
19
29
|
useEffect(() => {
|
|
20
|
-
fetch(
|
|
30
|
+
// Don't fetch providers until after account is created (if needed)
|
|
31
|
+
if (needsAccount && !accountCreated && step === 0) return;
|
|
32
|
+
|
|
33
|
+
fetch("/api/providers", { headers: getAuthHeaders() })
|
|
21
34
|
.then(res => res.json())
|
|
22
35
|
.then(data => {
|
|
23
36
|
// Only show LLM providers in onboarding, not integrations
|
|
24
37
|
const llmProviders = (data.providers || []).filter((p: Provider) => p.type === "llm");
|
|
25
38
|
setProviders(llmProviders);
|
|
26
39
|
});
|
|
27
|
-
}, []);
|
|
40
|
+
}, [accountCreated, step, needsAccount]);
|
|
28
41
|
|
|
29
42
|
const configuredProviders = providers.filter(p => p.hasKey);
|
|
30
43
|
|
|
@@ -38,7 +51,7 @@ export function OnboardingWizard({ onComplete }: OnboardingWizardProps) {
|
|
|
38
51
|
setTesting(true);
|
|
39
52
|
const testRes = await fetch(`/api/keys/${selectedProvider}/test`, {
|
|
40
53
|
method: "POST",
|
|
41
|
-
headers: { "Content-Type": "application/json" },
|
|
54
|
+
headers: { "Content-Type": "application/json", ...getAuthHeaders() },
|
|
42
55
|
body: JSON.stringify({ key: apiKey }),
|
|
43
56
|
});
|
|
44
57
|
const testData = await testRes.json();
|
|
@@ -52,7 +65,7 @@ export function OnboardingWizard({ onComplete }: OnboardingWizardProps) {
|
|
|
52
65
|
|
|
53
66
|
const saveRes = await fetch(`/api/keys/${selectedProvider}`, {
|
|
54
67
|
method: "POST",
|
|
55
|
-
headers: { "Content-Type": "application/json" },
|
|
68
|
+
headers: { "Content-Type": "application/json", ...getAuthHeaders() },
|
|
56
69
|
body: JSON.stringify({ key: apiKey }),
|
|
57
70
|
});
|
|
58
71
|
const saveData = await saveRes.json();
|
|
@@ -62,7 +75,7 @@ export function OnboardingWizard({ onComplete }: OnboardingWizardProps) {
|
|
|
62
75
|
} else {
|
|
63
76
|
setSuccess("API key saved successfully!");
|
|
64
77
|
setApiKey("");
|
|
65
|
-
const res = await fetch("/api/providers");
|
|
78
|
+
const res = await fetch("/api/providers", { headers: getAuthHeaders() });
|
|
66
79
|
const data = await res.json();
|
|
67
80
|
setProviders(data.providers || []);
|
|
68
81
|
setSelectedProvider(null);
|
|
@@ -74,10 +87,43 @@ export function OnboardingWizard({ onComplete }: OnboardingWizardProps) {
|
|
|
74
87
|
};
|
|
75
88
|
|
|
76
89
|
const completeOnboarding = async () => {
|
|
77
|
-
|
|
90
|
+
// Create a default project for the user
|
|
91
|
+
try {
|
|
92
|
+
const projectRes = await fetch("/api/projects", {
|
|
93
|
+
method: "POST",
|
|
94
|
+
headers: { "Content-Type": "application/json", ...getAuthHeaders() },
|
|
95
|
+
body: JSON.stringify({
|
|
96
|
+
name: "My Project",
|
|
97
|
+
description: "Default project for organizing agents",
|
|
98
|
+
color: "#f97316", // Orange - matches brand color
|
|
99
|
+
}),
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
if (projectRes.ok) {
|
|
103
|
+
const data = await projectRes.json();
|
|
104
|
+
// Set this project as the current project in localStorage
|
|
105
|
+
if (data.project?.id) {
|
|
106
|
+
localStorage.setItem("apteva_current_project", data.project.id);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
} catch (e) {
|
|
110
|
+
// Don't block onboarding if project creation fails
|
|
111
|
+
console.error("Failed to create default project:", e);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
await fetch("/api/onboarding/complete", { method: "POST", headers: getAuthHeaders() });
|
|
78
115
|
onComplete();
|
|
79
116
|
};
|
|
80
117
|
|
|
118
|
+
const handleAccountCreated = () => {
|
|
119
|
+
setAccountCreated(true);
|
|
120
|
+
setStep(1);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// Calculate total steps and current progress
|
|
124
|
+
const totalSteps = needsAccount ? 3 : 2;
|
|
125
|
+
const currentStep = needsAccount ? step : step - 1;
|
|
126
|
+
|
|
81
127
|
return (
|
|
82
128
|
<div className="min-h-screen bg-[#0a0a0a] text-[#e0e0e0] font-mono flex items-center justify-center p-8">
|
|
83
129
|
<div className="w-full max-w-2xl">
|
|
@@ -92,12 +138,22 @@ export function OnboardingWizard({ onComplete }: OnboardingWizardProps) {
|
|
|
92
138
|
|
|
93
139
|
{/* Progress */}
|
|
94
140
|
<div className="flex items-center justify-center gap-2 mb-8">
|
|
141
|
+
{needsAccount && (
|
|
142
|
+
<>
|
|
143
|
+
<div className={`w-3 h-3 rounded-full ${step >= 0 ? 'bg-[#f97316]' : 'bg-[#333]'}`} />
|
|
144
|
+
<div className={`w-16 h-0.5 ${step >= 1 ? 'bg-[#f97316]' : 'bg-[#333]'}`} />
|
|
145
|
+
</>
|
|
146
|
+
)}
|
|
95
147
|
<div className={`w-3 h-3 rounded-full ${step >= 1 ? 'bg-[#f97316]' : 'bg-[#333]'}`} />
|
|
96
148
|
<div className={`w-16 h-0.5 ${step >= 2 ? 'bg-[#f97316]' : 'bg-[#333]'}`} />
|
|
97
149
|
<div className={`w-3 h-3 rounded-full ${step >= 2 ? 'bg-[#f97316]' : 'bg-[#333]'}`} />
|
|
98
150
|
</div>
|
|
99
151
|
|
|
100
152
|
<div className="bg-[#111] rounded-lg border border-[#1a1a1a] p-8">
|
|
153
|
+
{step === 0 && needsAccount && (
|
|
154
|
+
<CreateAccountStep onComplete={handleAccountCreated} />
|
|
155
|
+
)}
|
|
156
|
+
|
|
101
157
|
{step === 1 && (
|
|
102
158
|
<Step1AddKeys
|
|
103
159
|
providers={providers}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import React, { useState, useEffect } from "react";
|
|
2
|
-
import { CheckIcon } from "../common/Icons";
|
|
2
|
+
import { CheckIcon, CloseIcon, PlusIcon } from "../common/Icons";
|
|
3
|
+
import { Modal, useConfirm } from "../common/Modal";
|
|
4
|
+
import { useProjects, useAuth, type Project } from "../../context";
|
|
3
5
|
import type { Provider } from "../../types";
|
|
4
6
|
|
|
5
|
-
type SettingsTab = "providers" | "updates" | "data";
|
|
7
|
+
type SettingsTab = "providers" | "projects" | "updates" | "data";
|
|
6
8
|
|
|
7
9
|
export function SettingsPage() {
|
|
8
10
|
const [activeTab, setActiveTab] = useState<SettingsTab>("providers");
|
|
@@ -18,6 +20,11 @@ export function SettingsPage() {
|
|
|
18
20
|
active={activeTab === "providers"}
|
|
19
21
|
onClick={() => setActiveTab("providers")}
|
|
20
22
|
/>
|
|
23
|
+
<SettingsNavItem
|
|
24
|
+
label="Projects"
|
|
25
|
+
active={activeTab === "projects"}
|
|
26
|
+
onClick={() => setActiveTab("projects")}
|
|
27
|
+
/>
|
|
21
28
|
<SettingsNavItem
|
|
22
29
|
label="Updates"
|
|
23
30
|
active={activeTab === "updates"}
|
|
@@ -34,6 +41,7 @@ export function SettingsPage() {
|
|
|
34
41
|
{/* Settings Content */}
|
|
35
42
|
<div className="flex-1 overflow-auto p-6">
|
|
36
43
|
{activeTab === "providers" && <ProvidersSettings />}
|
|
44
|
+
{activeTab === "projects" && <ProjectsSettings />}
|
|
37
45
|
{activeTab === "updates" && <UpdatesSettings />}
|
|
38
46
|
{activeTab === "data" && <DataSettings />}
|
|
39
47
|
</div>
|
|
@@ -65,6 +73,7 @@ function SettingsNavItem({
|
|
|
65
73
|
}
|
|
66
74
|
|
|
67
75
|
function ProvidersSettings() {
|
|
76
|
+
const { authFetch } = useAuth();
|
|
68
77
|
const [providers, setProviders] = useState<Provider[]>([]);
|
|
69
78
|
const [selectedProvider, setSelectedProvider] = useState<string | null>(null);
|
|
70
79
|
const [apiKey, setApiKey] = useState("");
|
|
@@ -72,9 +81,10 @@ function ProvidersSettings() {
|
|
|
72
81
|
const [testing, setTesting] = useState(false);
|
|
73
82
|
const [error, setError] = useState<string | null>(null);
|
|
74
83
|
const [success, setSuccess] = useState<string | null>(null);
|
|
84
|
+
const { confirm, ConfirmDialog } = useConfirm();
|
|
75
85
|
|
|
76
86
|
const fetchProviders = async () => {
|
|
77
|
-
const res = await
|
|
87
|
+
const res = await authFetch("/api/providers");
|
|
78
88
|
const data = await res.json();
|
|
79
89
|
setProviders(data.providers || []);
|
|
80
90
|
};
|
|
@@ -91,7 +101,7 @@ function ProvidersSettings() {
|
|
|
91
101
|
|
|
92
102
|
try {
|
|
93
103
|
setTesting(true);
|
|
94
|
-
const testRes = await
|
|
104
|
+
const testRes = await authFetch(`/api/keys/${selectedProvider}/test`, {
|
|
95
105
|
method: "POST",
|
|
96
106
|
headers: { "Content-Type": "application/json" },
|
|
97
107
|
body: JSON.stringify({ key: apiKey }),
|
|
@@ -105,7 +115,7 @@ function ProvidersSettings() {
|
|
|
105
115
|
return;
|
|
106
116
|
}
|
|
107
117
|
|
|
108
|
-
const saveRes = await
|
|
118
|
+
const saveRes = await authFetch(`/api/keys/${selectedProvider}`, {
|
|
109
119
|
method: "POST",
|
|
110
120
|
headers: { "Content-Type": "application/json" },
|
|
111
121
|
body: JSON.stringify({ key: apiKey }),
|
|
@@ -127,8 +137,9 @@ function ProvidersSettings() {
|
|
|
127
137
|
};
|
|
128
138
|
|
|
129
139
|
const deleteKey = async (providerId: string) => {
|
|
130
|
-
|
|
131
|
-
|
|
140
|
+
const confirmed = await confirm("Are you sure you want to remove this API key?", { confirmText: "Remove", title: "Remove API Key" });
|
|
141
|
+
if (!confirmed) return;
|
|
142
|
+
await authFetch(`/api/keys/${providerId}`, { method: "DELETE" });
|
|
132
143
|
fetchProviders();
|
|
133
144
|
};
|
|
134
145
|
|
|
@@ -138,7 +149,9 @@ function ProvidersSettings() {
|
|
|
138
149
|
const intConfiguredCount = integrations.filter(p => p.hasKey).length;
|
|
139
150
|
|
|
140
151
|
return (
|
|
141
|
-
|
|
152
|
+
<>
|
|
153
|
+
{ConfirmDialog}
|
|
154
|
+
<div className="space-y-10">
|
|
142
155
|
{/* AI Providers Section */}
|
|
143
156
|
<div>
|
|
144
157
|
<div className="mb-6">
|
|
@@ -148,7 +161,7 @@ function ProvidersSettings() {
|
|
|
148
161
|
</p>
|
|
149
162
|
</div>
|
|
150
163
|
|
|
151
|
-
<div className="grid gap-4 md:grid-cols-2">
|
|
164
|
+
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
|
152
165
|
{llmProviders.map(provider => (
|
|
153
166
|
<ProviderKeyCard
|
|
154
167
|
key={provider.id}
|
|
@@ -186,7 +199,7 @@ function ProvidersSettings() {
|
|
|
186
199
|
</p>
|
|
187
200
|
</div>
|
|
188
201
|
|
|
189
|
-
<div className="grid gap-4 md:grid-cols-2">
|
|
202
|
+
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
|
190
203
|
{integrations.map(provider => (
|
|
191
204
|
<IntegrationKeyCard
|
|
192
205
|
key={provider.id}
|
|
@@ -215,6 +228,229 @@ function ProvidersSettings() {
|
|
|
215
228
|
</div>
|
|
216
229
|
</div>
|
|
217
230
|
</div>
|
|
231
|
+
</>
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const DEFAULT_PROJECT_COLORS = [
|
|
236
|
+
"#f97316", // orange
|
|
237
|
+
"#6366f1", // indigo
|
|
238
|
+
"#22c55e", // green
|
|
239
|
+
"#ef4444", // red
|
|
240
|
+
"#3b82f6", // blue
|
|
241
|
+
"#a855f7", // purple
|
|
242
|
+
"#14b8a6", // teal
|
|
243
|
+
"#f59e0b", // amber
|
|
244
|
+
];
|
|
245
|
+
|
|
246
|
+
function ProjectsSettings() {
|
|
247
|
+
const { projects, createProject, updateProject, deleteProject } = useProjects();
|
|
248
|
+
const [showModal, setShowModal] = useState(false);
|
|
249
|
+
const [editingProject, setEditingProject] = useState<Project | null>(null);
|
|
250
|
+
const { confirm, ConfirmDialog } = useConfirm();
|
|
251
|
+
|
|
252
|
+
const handleDelete = async (id: string) => {
|
|
253
|
+
const confirmed = await confirm("Are you sure you want to delete this project? Agents in this project will become unassigned.", { confirmText: "Delete", title: "Delete Project" });
|
|
254
|
+
if (!confirmed) return;
|
|
255
|
+
await deleteProject(id);
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const openCreate = () => {
|
|
259
|
+
setEditingProject(null);
|
|
260
|
+
setShowModal(true);
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const openEdit = (project: Project) => {
|
|
264
|
+
setEditingProject(project);
|
|
265
|
+
setShowModal(true);
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const closeModal = () => {
|
|
269
|
+
setShowModal(false);
|
|
270
|
+
setEditingProject(null);
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
return (
|
|
274
|
+
<>
|
|
275
|
+
{ConfirmDialog}
|
|
276
|
+
<div className="max-w-4xl w-full">
|
|
277
|
+
<div className="mb-6 flex items-center justify-between gap-4">
|
|
278
|
+
<div>
|
|
279
|
+
<h1 className="text-2xl font-semibold mb-1">Projects</h1>
|
|
280
|
+
<p className="text-[#666]">
|
|
281
|
+
Organize agents into projects for better management.
|
|
282
|
+
</p>
|
|
283
|
+
</div>
|
|
284
|
+
<button
|
|
285
|
+
onClick={openCreate}
|
|
286
|
+
className="flex items-center gap-2 bg-[#f97316] hover:bg-[#fb923c] text-black px-4 py-2 rounded font-medium transition flex-shrink-0"
|
|
287
|
+
>
|
|
288
|
+
<PlusIcon className="w-4 h-4" />
|
|
289
|
+
New Project
|
|
290
|
+
</button>
|
|
291
|
+
</div>
|
|
292
|
+
|
|
293
|
+
{/* Project List */}
|
|
294
|
+
{projects.length === 0 ? (
|
|
295
|
+
<div className="text-center py-12 text-[#666]">
|
|
296
|
+
<p className="text-lg mb-2">No projects yet</p>
|
|
297
|
+
<p className="text-sm">Create a project to organize your agents.</p>
|
|
298
|
+
</div>
|
|
299
|
+
) : (
|
|
300
|
+
<div className="space-y-3">
|
|
301
|
+
{projects.map(project => (
|
|
302
|
+
<div
|
|
303
|
+
key={project.id}
|
|
304
|
+
className="bg-[#111] border border-[#1a1a1a] rounded-lg p-4 flex items-center gap-4"
|
|
305
|
+
>
|
|
306
|
+
<div
|
|
307
|
+
className="w-4 h-4 rounded-full flex-shrink-0"
|
|
308
|
+
style={{ backgroundColor: project.color }}
|
|
309
|
+
/>
|
|
310
|
+
<div className="flex-1 min-w-0">
|
|
311
|
+
<h3 className="font-medium">{project.name}</h3>
|
|
312
|
+
{project.description && (
|
|
313
|
+
<p className="text-sm text-[#666] truncate">{project.description}</p>
|
|
314
|
+
)}
|
|
315
|
+
<p className="text-xs text-[#666] mt-1">
|
|
316
|
+
{project.agentCount} agent{project.agentCount !== 1 ? "s" : ""}
|
|
317
|
+
</p>
|
|
318
|
+
</div>
|
|
319
|
+
<div className="flex items-center gap-2">
|
|
320
|
+
<button
|
|
321
|
+
onClick={() => openEdit(project)}
|
|
322
|
+
className="text-sm text-[#888] hover:text-[#e0e0e0] px-2 py-1"
|
|
323
|
+
>
|
|
324
|
+
Edit
|
|
325
|
+
</button>
|
|
326
|
+
<button
|
|
327
|
+
onClick={() => handleDelete(project.id)}
|
|
328
|
+
className="text-sm text-red-400 hover:text-red-300 px-2 py-1"
|
|
329
|
+
>
|
|
330
|
+
Delete
|
|
331
|
+
</button>
|
|
332
|
+
</div>
|
|
333
|
+
</div>
|
|
334
|
+
))}
|
|
335
|
+
</div>
|
|
336
|
+
)}
|
|
337
|
+
|
|
338
|
+
{/* Project Modal */}
|
|
339
|
+
{showModal && (
|
|
340
|
+
<ProjectModal
|
|
341
|
+
project={editingProject}
|
|
342
|
+
onSave={async (data) => {
|
|
343
|
+
if (editingProject) {
|
|
344
|
+
const result = await updateProject(editingProject.id, data);
|
|
345
|
+
if (result) closeModal();
|
|
346
|
+
return !!result;
|
|
347
|
+
} else {
|
|
348
|
+
const result = await createProject(data);
|
|
349
|
+
if (result) closeModal();
|
|
350
|
+
return !!result;
|
|
351
|
+
}
|
|
352
|
+
}}
|
|
353
|
+
onClose={closeModal}
|
|
354
|
+
/>
|
|
355
|
+
)}
|
|
356
|
+
</div>
|
|
357
|
+
</>
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
interface ProjectModalProps {
|
|
362
|
+
project: Project | null;
|
|
363
|
+
onSave: (data: { name: string; description?: string; color: string }) => Promise<boolean>;
|
|
364
|
+
onClose: () => void;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function ProjectModal({ project, onSave, onClose }: ProjectModalProps) {
|
|
368
|
+
const [name, setName] = useState(project?.name || "");
|
|
369
|
+
const [description, setDescription] = useState(project?.description || "");
|
|
370
|
+
const [color, setColor] = useState(
|
|
371
|
+
project?.color || DEFAULT_PROJECT_COLORS[Math.floor(Math.random() * DEFAULT_PROJECT_COLORS.length)]
|
|
372
|
+
);
|
|
373
|
+
const [saving, setSaving] = useState(false);
|
|
374
|
+
const [error, setError] = useState<string | null>(null);
|
|
375
|
+
|
|
376
|
+
const handleSubmit = async () => {
|
|
377
|
+
if (!name.trim()) {
|
|
378
|
+
setError("Name is required");
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
setSaving(true);
|
|
382
|
+
setError(null);
|
|
383
|
+
const success = await onSave({ name, description: description || undefined, color });
|
|
384
|
+
setSaving(false);
|
|
385
|
+
if (!success) {
|
|
386
|
+
setError(project ? "Failed to update project" : "Failed to create project");
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
return (
|
|
391
|
+
<Modal onClose={onClose}>
|
|
392
|
+
<h2 className="text-xl font-semibold mb-6">{project ? "Edit Project" : "Create New Project"}</h2>
|
|
393
|
+
|
|
394
|
+
<div className="space-y-4">
|
|
395
|
+
<div>
|
|
396
|
+
<label className="block text-sm text-[#666] mb-1">Name</label>
|
|
397
|
+
<input
|
|
398
|
+
type="text"
|
|
399
|
+
value={name}
|
|
400
|
+
onChange={e => setName(e.target.value)}
|
|
401
|
+
className="w-full bg-[#0a0a0a] border border-[#222] rounded px-3 py-2 focus:outline-none focus:border-[#f97316]"
|
|
402
|
+
placeholder="My Project"
|
|
403
|
+
autoFocus
|
|
404
|
+
/>
|
|
405
|
+
</div>
|
|
406
|
+
|
|
407
|
+
<div>
|
|
408
|
+
<label className="block text-sm text-[#666] mb-1">Description (optional)</label>
|
|
409
|
+
<input
|
|
410
|
+
type="text"
|
|
411
|
+
value={description}
|
|
412
|
+
onChange={e => setDescription(e.target.value)}
|
|
413
|
+
className="w-full bg-[#0a0a0a] border border-[#222] rounded px-3 py-2 focus:outline-none focus:border-[#f97316]"
|
|
414
|
+
placeholder="A short description"
|
|
415
|
+
/>
|
|
416
|
+
</div>
|
|
417
|
+
|
|
418
|
+
<div>
|
|
419
|
+
<label className="block text-sm text-[#666] mb-1">Color</label>
|
|
420
|
+
<div className="flex gap-3 flex-wrap">
|
|
421
|
+
{DEFAULT_PROJECT_COLORS.map(c => (
|
|
422
|
+
<button
|
|
423
|
+
key={c}
|
|
424
|
+
type="button"
|
|
425
|
+
onClick={() => setColor(c)}
|
|
426
|
+
className={`w-10 h-10 rounded-full transition ${
|
|
427
|
+
color === c ? "ring-2 ring-white ring-offset-2 ring-offset-[#111]" : "hover:scale-110"
|
|
428
|
+
}`}
|
|
429
|
+
style={{ backgroundColor: c }}
|
|
430
|
+
/>
|
|
431
|
+
))}
|
|
432
|
+
</div>
|
|
433
|
+
</div>
|
|
434
|
+
|
|
435
|
+
{error && <p className="text-red-400 text-sm">{error}</p>}
|
|
436
|
+
</div>
|
|
437
|
+
|
|
438
|
+
<div className="flex gap-3 mt-6">
|
|
439
|
+
<button
|
|
440
|
+
onClick={onClose}
|
|
441
|
+
className="flex-1 border border-[#333] hover:border-[#f97316] hover:text-[#f97316] px-4 py-2 rounded font-medium transition"
|
|
442
|
+
>
|
|
443
|
+
Cancel
|
|
444
|
+
</button>
|
|
445
|
+
<button
|
|
446
|
+
onClick={handleSubmit}
|
|
447
|
+
disabled={saving || !name.trim()}
|
|
448
|
+
className="flex-1 bg-[#f97316] hover:bg-[#fb923c] disabled:opacity-50 text-black px-4 py-2 rounded font-medium transition"
|
|
449
|
+
>
|
|
450
|
+
{saving ? "Saving..." : project ? "Update" : "Create"}
|
|
451
|
+
</button>
|
|
452
|
+
</div>
|
|
453
|
+
</Modal>
|
|
218
454
|
);
|
|
219
455
|
}
|
|
220
456
|
|
|
@@ -243,9 +479,11 @@ interface VersionInfo {
|
|
|
243
479
|
interface AllVersionInfo {
|
|
244
480
|
apteva: VersionInfo;
|
|
245
481
|
agent: VersionInfo;
|
|
482
|
+
isDocker?: boolean;
|
|
246
483
|
}
|
|
247
484
|
|
|
248
485
|
function UpdatesSettings() {
|
|
486
|
+
const { authFetch } = useAuth();
|
|
249
487
|
const [versions, setVersions] = useState<AllVersionInfo | null>(null);
|
|
250
488
|
const [checking, setChecking] = useState(false);
|
|
251
489
|
const [updatingAgent, setUpdatingAgent] = useState(false);
|
|
@@ -257,7 +495,7 @@ function UpdatesSettings() {
|
|
|
257
495
|
setChecking(true);
|
|
258
496
|
setError(null);
|
|
259
497
|
try {
|
|
260
|
-
const res = await
|
|
498
|
+
const res = await authFetch("/api/version");
|
|
261
499
|
if (!res.ok) throw new Error("Failed to check for updates");
|
|
262
500
|
const data = await res.json();
|
|
263
501
|
setVersions(data);
|
|
@@ -272,12 +510,16 @@ function UpdatesSettings() {
|
|
|
272
510
|
setError(null);
|
|
273
511
|
setUpdateSuccess(null);
|
|
274
512
|
try {
|
|
275
|
-
const res = await
|
|
513
|
+
const res = await authFetch("/api/version/update", { method: "POST" });
|
|
276
514
|
const data = await res.json();
|
|
277
515
|
if (!data.success) {
|
|
278
516
|
setError(data.error || "Update failed");
|
|
279
517
|
} else {
|
|
280
|
-
|
|
518
|
+
const restartedCount = data.restarted?.length || 0;
|
|
519
|
+
const restartMsg = restartedCount > 0
|
|
520
|
+
? ` ${restartedCount} running agent${restartedCount > 1 ? 's' : ''} restarted.`
|
|
521
|
+
: '';
|
|
522
|
+
setUpdateSuccess(`Agent binary updated to v${data.version}.${restartMsg}`);
|
|
281
523
|
await checkForUpdates();
|
|
282
524
|
}
|
|
283
525
|
} catch (e) {
|
|
@@ -299,7 +541,7 @@ function UpdatesSettings() {
|
|
|
299
541
|
const hasAnyUpdate = versions?.apteva.updateAvailable || versions?.agent.updateAvailable;
|
|
300
542
|
|
|
301
543
|
return (
|
|
302
|
-
<div className="max-w-
|
|
544
|
+
<div className="max-w-4xl w-full">
|
|
303
545
|
<div className="mb-6">
|
|
304
546
|
<h1 className="text-2xl font-semibold mb-1">Updates</h1>
|
|
305
547
|
<p className="text-[#666]">
|
|
@@ -308,10 +550,74 @@ function UpdatesSettings() {
|
|
|
308
550
|
</div>
|
|
309
551
|
|
|
310
552
|
{checking && !versions ? (
|
|
311
|
-
<div className="text-[#666]">Checking
|
|
553
|
+
<div className="text-[#666]">Checking version info...</div>
|
|
312
554
|
) : error && !versions ? (
|
|
313
555
|
<div className="text-red-400">{error}</div>
|
|
556
|
+
) : versions?.isDocker ? (
|
|
557
|
+
/* Docker Environment */
|
|
558
|
+
<div className="space-y-6">
|
|
559
|
+
<div className="bg-blue-500/10 border border-blue-500/30 rounded-lg p-4">
|
|
560
|
+
<div className="flex items-center gap-2 text-blue-400 mb-2">
|
|
561
|
+
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
|
562
|
+
<path d="M13.983 11.078h2.119a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.119a.185.185 0 00-.185.185v1.888c0 .102.083.185.185.185m-2.954-5.43h2.118a.186.186 0 00.186-.186V3.574a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m0 2.716h2.118a.187.187 0 00.186-.186V6.29a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.887c0 .102.082.186.185.186m-2.93 0h2.12a.186.186 0 00.184-.186V6.29a.185.185 0 00-.185-.185H8.1a.185.185 0 00-.185.185v1.887c0 .102.083.186.185.186m-2.964 0h2.119a.186.186 0 00.185-.186V6.29a.186.186 0 00-.185-.185H5.136a.186.186 0 00-.186.185v1.887c0 .102.084.186.186.186m5.893 2.715h2.118a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m-2.93 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.083.185.185.185m-2.964 0h2.119a.185.185 0 00.185-.185V9.006a.185.185 0 00-.185-.186H5.136a.186.186 0 00-.186.186v1.887c0 .102.084.185.186.185m-2.92 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.186.186 0 00-.186.186v1.887c0 .102.084.185.186.185M23.763 9.89c-.065-.051-.672-.51-1.954-.51-.338.001-.676.03-1.01.087-.248-1.7-1.653-2.53-1.716-2.566l-.344-.199-.226.327c-.284.438-.49.922-.612 1.43-.23.97-.09 1.882.403 2.661-.595.332-1.55.413-1.744.42H.751a.751.751 0 00-.75.748 11.376 11.376 0 00.692 4.062c.545 1.428 1.355 2.48 2.41 3.124 1.18.723 3.1 1.137 5.275 1.137.983.003 1.963-.086 2.93-.266a12.248 12.248 0 003.823-1.389c.98-.567 1.86-1.288 2.61-2.136 1.252-1.418 1.998-2.997 2.553-4.4h.221c1.372 0 2.215-.549 2.68-1.009.309-.293.55-.65.707-1.046l.098-.288Z"/>
|
|
563
|
+
</svg>
|
|
564
|
+
<span className="font-medium">Docker Environment</span>
|
|
565
|
+
</div>
|
|
566
|
+
<p className="text-sm text-[#888]">
|
|
567
|
+
Updates are automatic when you pull a new image version.
|
|
568
|
+
</p>
|
|
569
|
+
</div>
|
|
570
|
+
|
|
571
|
+
{/* Current Version */}
|
|
572
|
+
<div className="bg-[#111] border border-[#1a1a1a] rounded-lg p-5">
|
|
573
|
+
<div className="flex items-center justify-between mb-4">
|
|
574
|
+
<div>
|
|
575
|
+
<h3 className="font-medium text-lg">Current Version</h3>
|
|
576
|
+
<p className="text-sm text-[#666]">apteva + agent binary</p>
|
|
577
|
+
</div>
|
|
578
|
+
<div className="text-right">
|
|
579
|
+
<div className="text-xl font-mono">v{versions.apteva.installed || "?"}</div>
|
|
580
|
+
</div>
|
|
581
|
+
</div>
|
|
582
|
+
|
|
583
|
+
{hasAnyUpdate ? (
|
|
584
|
+
<div className="bg-[#f97316]/10 border border-[#f97316]/30 rounded-lg p-4">
|
|
585
|
+
<p className="text-sm text-[#888] mb-3">
|
|
586
|
+
A newer version (v{versions.apteva.latest}) is available. To update:
|
|
587
|
+
</p>
|
|
588
|
+
<div className="space-y-2">
|
|
589
|
+
<code className="block bg-[#0a0a0a] px-3 py-2 rounded font-mono text-sm text-[#888]">
|
|
590
|
+
docker pull apteva/apteva:latest
|
|
591
|
+
</code>
|
|
592
|
+
<code className="block bg-[#0a0a0a] px-3 py-2 rounded font-mono text-sm text-[#888]">
|
|
593
|
+
docker compose up -d
|
|
594
|
+
</code>
|
|
595
|
+
</div>
|
|
596
|
+
<button
|
|
597
|
+
onClick={() => {
|
|
598
|
+
navigator.clipboard.writeText("docker pull apteva/apteva:latest && docker compose up -d");
|
|
599
|
+
setCopied("docker");
|
|
600
|
+
setTimeout(() => setCopied(null), 2000);
|
|
601
|
+
}}
|
|
602
|
+
className="mt-3 px-3 py-1.5 bg-[#1a1a1a] hover:bg-[#222] rounded text-sm"
|
|
603
|
+
>
|
|
604
|
+
{copied === "docker" ? "Copied!" : "Copy commands"}
|
|
605
|
+
</button>
|
|
606
|
+
</div>
|
|
607
|
+
) : (
|
|
608
|
+
<div className="flex items-center gap-2 text-green-400 text-sm">
|
|
609
|
+
<CheckIcon className="w-4 h-4" />
|
|
610
|
+
Up to date
|
|
611
|
+
</div>
|
|
612
|
+
)}
|
|
613
|
+
</div>
|
|
614
|
+
|
|
615
|
+
<p className="text-xs text-[#555]">
|
|
616
|
+
Your data is stored in a Docker volume and persists across updates.
|
|
617
|
+
</p>
|
|
618
|
+
</div>
|
|
314
619
|
) : versions ? (
|
|
620
|
+
/* Non-Docker Environment */
|
|
315
621
|
<div className="space-y-6">
|
|
316
622
|
{updateSuccess && (
|
|
317
623
|
<div className="bg-green-500/10 border border-green-500/30 rounded-lg p-4 text-green-400">
|
|
@@ -441,18 +747,22 @@ function ProviderKeyCard({
|
|
|
441
747
|
<div className={`bg-[#111] border rounded-lg p-4 ${
|
|
442
748
|
provider.hasKey ? 'border-green-500/20' : 'border-[#1a1a1a]'
|
|
443
749
|
}`}>
|
|
444
|
-
<div className="flex items-
|
|
445
|
-
<div>
|
|
750
|
+
<div className="flex items-start justify-between gap-2 mb-2">
|
|
751
|
+
<div className="min-w-0">
|
|
446
752
|
<h3 className="font-medium">{provider.name}</h3>
|
|
447
|
-
<p className="text-sm text-[#666]">
|
|
753
|
+
<p className="text-sm text-[#666] truncate">
|
|
754
|
+
{provider.type === "integration"
|
|
755
|
+
? (provider.description || "MCP integration")
|
|
756
|
+
: `${provider.models.length} models`}
|
|
757
|
+
</p>
|
|
448
758
|
</div>
|
|
449
759
|
{provider.hasKey ? (
|
|
450
|
-
<span className="text-green-400 text-xs flex items-center gap-1 bg-green-500/10 px-2 py-1 rounded">
|
|
760
|
+
<span className="text-green-400 text-xs flex items-center gap-1 bg-green-500/10 px-2 py-1 rounded whitespace-nowrap flex-shrink-0">
|
|
451
761
|
<CheckIcon className="w-3 h-3" />
|
|
452
762
|
{provider.keyHint}
|
|
453
763
|
</span>
|
|
454
764
|
) : (
|
|
455
|
-
<span className="text-[#666] text-xs bg-[#1a1a1a] px-2 py-1 rounded">
|
|
765
|
+
<span className="text-[#666] text-xs bg-[#1a1a1a] px-2 py-1 rounded whitespace-nowrap flex-shrink-0">
|
|
456
766
|
Not configured
|
|
457
767
|
</span>
|
|
458
768
|
)}
|
|
@@ -636,13 +946,15 @@ function IntegrationKeyCard({
|
|
|
636
946
|
}
|
|
637
947
|
|
|
638
948
|
function DataSettings() {
|
|
949
|
+
const { authFetch } = useAuth();
|
|
639
950
|
const [clearing, setClearing] = useState(false);
|
|
640
951
|
const [message, setMessage] = useState<{ type: "success" | "error"; text: string } | null>(null);
|
|
641
952
|
const [eventCount, setEventCount] = useState<number | null>(null);
|
|
953
|
+
const { confirm, ConfirmDialog } = useConfirm();
|
|
642
954
|
|
|
643
955
|
const fetchStats = async () => {
|
|
644
956
|
try {
|
|
645
|
-
const res = await
|
|
957
|
+
const res = await authFetch("/api/telemetry/stats");
|
|
646
958
|
const data = await res.json();
|
|
647
959
|
setEventCount(data.stats?.total_events || 0);
|
|
648
960
|
} catch {
|
|
@@ -655,15 +967,14 @@ function DataSettings() {
|
|
|
655
967
|
}, []);
|
|
656
968
|
|
|
657
969
|
const clearTelemetry = async () => {
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
}
|
|
970
|
+
const confirmed = await confirm("Are you sure you want to delete all telemetry data? This cannot be undone.", { confirmText: "Clear All", title: "Clear Telemetry Data" });
|
|
971
|
+
if (!confirmed) return;
|
|
661
972
|
|
|
662
973
|
setClearing(true);
|
|
663
974
|
setMessage(null);
|
|
664
975
|
|
|
665
976
|
try {
|
|
666
|
-
const res = await
|
|
977
|
+
const res = await authFetch("/api/telemetry/clear", { method: "POST" });
|
|
667
978
|
const data = await res.json();
|
|
668
979
|
|
|
669
980
|
if (res.ok) {
|
|
@@ -680,7 +991,9 @@ function DataSettings() {
|
|
|
680
991
|
};
|
|
681
992
|
|
|
682
993
|
return (
|
|
683
|
-
|
|
994
|
+
<>
|
|
995
|
+
{ConfirmDialog}
|
|
996
|
+
<div className="max-w-4xl w-full">
|
|
684
997
|
<div className="mb-6">
|
|
685
998
|
<h1 className="text-2xl font-semibold mb-1">Data Management</h1>
|
|
686
999
|
<p className="text-[#666]">Manage stored data and telemetry.</p>
|
|
@@ -713,5 +1026,6 @@ function DataSettings() {
|
|
|
713
1026
|
</button>
|
|
714
1027
|
</div>
|
|
715
1028
|
</div>
|
|
1029
|
+
</>
|
|
716
1030
|
);
|
|
717
1031
|
}
|