apteva 0.1.0

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 (47) hide show
  1. package/LICENSE +63 -0
  2. package/README.md +84 -0
  3. package/bin/agent-linux-amd64 +0 -0
  4. package/bin/apteva.js +144 -0
  5. package/dist/App.g02zmbqf.js +213 -0
  6. package/dist/App.g02zmbqf.js.map +37 -0
  7. package/dist/App.mq6jqare.js +1 -0
  8. package/dist/apteva-kit.css +1 -0
  9. package/dist/index.html +14 -0
  10. package/dist/styles.css +1 -0
  11. package/package.json +65 -0
  12. package/src/binary.ts +116 -0
  13. package/src/crypto.ts +152 -0
  14. package/src/db.ts +446 -0
  15. package/src/providers.ts +255 -0
  16. package/src/routes/api.ts +380 -0
  17. package/src/routes/static.ts +47 -0
  18. package/src/server.ts +134 -0
  19. package/src/web/App.tsx +218 -0
  20. package/src/web/components/agents/AgentCard.tsx +71 -0
  21. package/src/web/components/agents/AgentsView.tsx +69 -0
  22. package/src/web/components/agents/ChatPanel.tsx +63 -0
  23. package/src/web/components/agents/CreateAgentModal.tsx +128 -0
  24. package/src/web/components/agents/index.ts +4 -0
  25. package/src/web/components/common/Icons.tsx +61 -0
  26. package/src/web/components/common/LoadingSpinner.tsx +44 -0
  27. package/src/web/components/common/Modal.tsx +16 -0
  28. package/src/web/components/common/Select.tsx +96 -0
  29. package/src/web/components/common/index.ts +4 -0
  30. package/src/web/components/dashboard/Dashboard.tsx +136 -0
  31. package/src/web/components/dashboard/index.ts +1 -0
  32. package/src/web/components/index.ts +11 -0
  33. package/src/web/components/layout/ErrorBanner.tsx +18 -0
  34. package/src/web/components/layout/Header.tsx +26 -0
  35. package/src/web/components/layout/Sidebar.tsx +66 -0
  36. package/src/web/components/layout/index.ts +3 -0
  37. package/src/web/components/onboarding/OnboardingWizard.tsx +344 -0
  38. package/src/web/components/onboarding/index.ts +1 -0
  39. package/src/web/components/settings/SettingsPage.tsx +285 -0
  40. package/src/web/components/settings/index.ts +1 -0
  41. package/src/web/hooks/index.ts +3 -0
  42. package/src/web/hooks/useAgents.ts +62 -0
  43. package/src/web/hooks/useOnboarding.ts +25 -0
  44. package/src/web/hooks/useProviders.ts +65 -0
  45. package/src/web/index.html +21 -0
  46. package/src/web/styles.css +23 -0
  47. package/src/web/types.ts +43 -0
@@ -0,0 +1,344 @@
1
+ import React, { useState, useEffect } from "react";
2
+ import { CheckIcon } from "../common/Icons";
3
+ import type { Provider } from "../../types";
4
+
5
+ interface OnboardingWizardProps {
6
+ onComplete: () => void;
7
+ }
8
+
9
+ export function OnboardingWizard({ onComplete }: OnboardingWizardProps) {
10
+ const [step, setStep] = useState(1);
11
+ const [providers, setProviders] = useState<Provider[]>([]);
12
+ const [selectedProvider, setSelectedProvider] = useState<string | null>(null);
13
+ const [apiKey, setApiKey] = useState("");
14
+ const [saving, setSaving] = useState(false);
15
+ const [testing, setTesting] = useState(false);
16
+ const [error, setError] = useState<string | null>(null);
17
+ const [success, setSuccess] = useState<string | null>(null);
18
+
19
+ useEffect(() => {
20
+ fetch("/api/providers")
21
+ .then(res => res.json())
22
+ .then(data => setProviders(data.providers || []));
23
+ }, []);
24
+
25
+ const configuredProviders = providers.filter(p => p.hasKey);
26
+
27
+ const saveKey = async () => {
28
+ if (!selectedProvider || !apiKey) return;
29
+ setSaving(true);
30
+ setError(null);
31
+ setSuccess(null);
32
+
33
+ try {
34
+ setTesting(true);
35
+ const testRes = await fetch(`/api/keys/${selectedProvider}/test`, {
36
+ method: "POST",
37
+ headers: { "Content-Type": "application/json" },
38
+ body: JSON.stringify({ key: apiKey }),
39
+ });
40
+ const testData = await testRes.json();
41
+ setTesting(false);
42
+
43
+ if (!testData.valid) {
44
+ setError(testData.error || "API key is invalid");
45
+ setSaving(false);
46
+ return;
47
+ }
48
+
49
+ const saveRes = await fetch(`/api/keys/${selectedProvider}`, {
50
+ method: "POST",
51
+ headers: { "Content-Type": "application/json" },
52
+ body: JSON.stringify({ key: apiKey }),
53
+ });
54
+ const saveData = await saveRes.json();
55
+
56
+ if (!saveRes.ok) {
57
+ setError(saveData.error || "Failed to save key");
58
+ } else {
59
+ setSuccess("API key saved successfully!");
60
+ setApiKey("");
61
+ const res = await fetch("/api/providers");
62
+ const data = await res.json();
63
+ setProviders(data.providers || []);
64
+ setSelectedProvider(null);
65
+ }
66
+ } catch (e) {
67
+ setError("Failed to save key");
68
+ }
69
+ setSaving(false);
70
+ };
71
+
72
+ const completeOnboarding = async () => {
73
+ await fetch("/api/onboarding/complete", { method: "POST" });
74
+ onComplete();
75
+ };
76
+
77
+ return (
78
+ <div className="min-h-screen bg-[#0a0a0a] text-[#e0e0e0] font-mono flex items-center justify-center p-8">
79
+ <div className="w-full max-w-2xl">
80
+ {/* Logo */}
81
+ <div className="text-center mb-8">
82
+ <div className="flex items-center justify-center gap-2 mb-2">
83
+ <span className="text-[#f97316] text-3xl">&gt;_</span>
84
+ <span className="text-3xl tracking-wider">apteva</span>
85
+ </div>
86
+ <p className="text-[#666]">Run AI agents locally</p>
87
+ </div>
88
+
89
+ {/* Progress */}
90
+ <div className="flex items-center justify-center gap-2 mb-8">
91
+ <div className={`w-3 h-3 rounded-full ${step >= 1 ? 'bg-[#f97316]' : 'bg-[#333]'}`} />
92
+ <div className={`w-16 h-0.5 ${step >= 2 ? 'bg-[#f97316]' : 'bg-[#333]'}`} />
93
+ <div className={`w-3 h-3 rounded-full ${step >= 2 ? 'bg-[#f97316]' : 'bg-[#333]'}`} />
94
+ </div>
95
+
96
+ <div className="bg-[#111] rounded-lg border border-[#1a1a1a] p-8">
97
+ {step === 1 && (
98
+ <Step1AddKeys
99
+ providers={providers}
100
+ configuredProviders={configuredProviders}
101
+ selectedProvider={selectedProvider}
102
+ apiKey={apiKey}
103
+ saving={saving}
104
+ testing={testing}
105
+ error={error}
106
+ success={success}
107
+ onSelectProvider={setSelectedProvider}
108
+ onApiKeyChange={setApiKey}
109
+ onSaveKey={saveKey}
110
+ onContinue={() => setStep(2)}
111
+ />
112
+ )}
113
+
114
+ {step === 2 && (
115
+ <Step2Complete
116
+ configuredProviders={configuredProviders}
117
+ onAddMore={() => setStep(1)}
118
+ onComplete={completeOnboarding}
119
+ />
120
+ )}
121
+ </div>
122
+ </div>
123
+ </div>
124
+ );
125
+ }
126
+
127
+ interface Step1Props {
128
+ providers: Provider[];
129
+ configuredProviders: Provider[];
130
+ selectedProvider: string | null;
131
+ apiKey: string;
132
+ saving: boolean;
133
+ testing: boolean;
134
+ error: string | null;
135
+ success: string | null;
136
+ onSelectProvider: (id: string | null) => void;
137
+ onApiKeyChange: (key: string) => void;
138
+ onSaveKey: () => void;
139
+ onContinue: () => void;
140
+ }
141
+
142
+ function Step1AddKeys({
143
+ providers,
144
+ configuredProviders,
145
+ selectedProvider,
146
+ apiKey,
147
+ saving,
148
+ testing,
149
+ error,
150
+ success,
151
+ onSelectProvider,
152
+ onApiKeyChange,
153
+ onSaveKey,
154
+ onContinue,
155
+ }: Step1Props) {
156
+ const selectedProviderData = providers.find(p => p.id === selectedProvider);
157
+
158
+ // When a provider is selected, show focused view with just that provider
159
+ if (selectedProvider && selectedProviderData) {
160
+ return (
161
+ <>
162
+ <h2 className="text-2xl font-semibold mb-2">Add {selectedProviderData.name} Key</h2>
163
+ <p className="text-[#666] mb-6">
164
+ Enter your API key below. It will be encrypted and stored locally.
165
+ </p>
166
+
167
+ <div className="mb-6">
168
+ <div className="p-4 rounded border border-[#f97316] bg-[#f97316]/5 mb-4">
169
+ <div className="flex items-center justify-between">
170
+ <div>
171
+ <p className="font-medium">{selectedProviderData.name}</p>
172
+ <p className="text-sm text-[#666]">
173
+ {selectedProviderData.models.length} models available
174
+ </p>
175
+ </div>
176
+ <a
177
+ href={selectedProviderData.docsUrl}
178
+ target="_blank"
179
+ rel="noopener noreferrer"
180
+ className="text-sm text-[#3b82f6] hover:underline"
181
+ >
182
+ Get API Key
183
+ </a>
184
+ </div>
185
+ </div>
186
+
187
+ <div className="space-y-3">
188
+ <input
189
+ type="password"
190
+ value={apiKey}
191
+ onChange={e => onApiKeyChange(e.target.value)}
192
+ placeholder="Enter your API key..."
193
+ autoFocus
194
+ className="w-full bg-[#0a0a0a] border border-[#333] rounded px-4 py-3 focus:outline-none focus:border-[#f97316] text-lg"
195
+ />
196
+ {error && <p className="text-red-400 text-sm">{error}</p>}
197
+ {success && <p className="text-green-400 text-sm">{success}</p>}
198
+ </div>
199
+ </div>
200
+
201
+ <div className="flex gap-3">
202
+ <button
203
+ onClick={() => {
204
+ onSelectProvider(null);
205
+ onApiKeyChange("");
206
+ }}
207
+ className="flex-1 border border-[#333] hover:border-[#666] px-4 py-3 rounded font-medium transition"
208
+ >
209
+ Back
210
+ </button>
211
+ <button
212
+ onClick={onSaveKey}
213
+ disabled={!apiKey || saving}
214
+ className="flex-1 bg-[#f97316] hover:bg-[#fb923c] disabled:opacity-50 disabled:cursor-not-allowed text-black px-4 py-3 rounded font-medium transition"
215
+ >
216
+ {testing ? "Testing..." : saving ? "Saving..." : "Save API Key"}
217
+ </button>
218
+ </div>
219
+ </>
220
+ );
221
+ }
222
+
223
+ // Default view: show all providers
224
+ return (
225
+ <>
226
+ <h2 className="text-2xl font-semibold mb-2">Welcome to Apteva</h2>
227
+ <p className="text-[#666] mb-6">
228
+ To get started, you'll need to add at least one AI provider API key.
229
+ Your keys are encrypted and stored locally.
230
+ </p>
231
+
232
+ <div className="space-y-3 mb-6">
233
+ {providers.map(provider => (
234
+ <ProviderCard
235
+ key={provider.id}
236
+ provider={provider}
237
+ selected={false}
238
+ onSelect={() => !provider.hasKey && onSelectProvider(provider.id)}
239
+ />
240
+ ))}
241
+ </div>
242
+
243
+ <button
244
+ onClick={onContinue}
245
+ disabled={configuredProviders.length === 0}
246
+ className="w-full bg-[#222] hover:bg-[#333] disabled:opacity-50 disabled:cursor-not-allowed px-4 py-3 rounded font-medium transition"
247
+ >
248
+ {configuredProviders.length === 0
249
+ ? "Add at least one API key to continue"
250
+ : `Continue with ${configuredProviders.length} provider${configuredProviders.length > 1 ? 's' : ''}`
251
+ }
252
+ </button>
253
+ </>
254
+ );
255
+ }
256
+
257
+ interface ProviderCardProps {
258
+ provider: Provider;
259
+ selected: boolean;
260
+ onSelect: () => void;
261
+ }
262
+
263
+ function ProviderCard({ provider, selected, onSelect }: ProviderCardProps) {
264
+ return (
265
+ <div
266
+ onClick={onSelect}
267
+ className={`p-4 rounded border transition cursor-pointer ${
268
+ provider.hasKey
269
+ ? 'border-green-500/30 bg-green-500/5'
270
+ : selected
271
+ ? 'border-[#f97316] bg-[#f97316]/5'
272
+ : 'border-[#222] hover:border-[#333]'
273
+ }`}
274
+ >
275
+ <div className="flex items-center justify-between">
276
+ <div>
277
+ <p className="font-medium">{provider.name}</p>
278
+ <p className="text-sm text-[#666]">
279
+ {provider.models.length} models available
280
+ </p>
281
+ </div>
282
+ {provider.hasKey ? (
283
+ <span className="text-green-400 text-sm flex items-center gap-1">
284
+ <CheckIcon />
285
+ Configured ({provider.keyHint})
286
+ </span>
287
+ ) : (
288
+ <a
289
+ href={provider.docsUrl}
290
+ target="_blank"
291
+ rel="noopener noreferrer"
292
+ onClick={e => e.stopPropagation()}
293
+ className="text-sm text-[#3b82f6] hover:underline"
294
+ >
295
+ Get API Key
296
+ </a>
297
+ )}
298
+ </div>
299
+ </div>
300
+ );
301
+ }
302
+
303
+ interface Step2Props {
304
+ configuredProviders: Provider[];
305
+ onAddMore: () => void;
306
+ onComplete: () => void;
307
+ }
308
+
309
+ function Step2Complete({ configuredProviders, onAddMore, onComplete }: Step2Props) {
310
+ return (
311
+ <>
312
+ <h2 className="text-2xl font-semibold mb-2">You're all set!</h2>
313
+ <p className="text-[#666] mb-6">
314
+ You've configured {configuredProviders.length} provider{configuredProviders.length > 1 ? 's' : ''}.
315
+ You can add more providers later in Settings.
316
+ </p>
317
+
318
+ <div className="space-y-2 mb-6">
319
+ {configuredProviders.map(provider => (
320
+ <div key={provider.id} className="flex items-center gap-3 p-3 bg-[#0a0a0a] rounded">
321
+ <CheckIcon className="w-5 h-5 text-green-400" />
322
+ <span>{provider.name}</span>
323
+ <span className="text-[#666] text-sm">({provider.keyHint})</span>
324
+ </div>
325
+ ))}
326
+ </div>
327
+
328
+ <div className="flex gap-3">
329
+ <button
330
+ onClick={onAddMore}
331
+ className="flex-1 border border-[#333] hover:border-[#f97316] px-4 py-3 rounded font-medium transition"
332
+ >
333
+ Add More
334
+ </button>
335
+ <button
336
+ onClick={onComplete}
337
+ className="flex-1 bg-[#f97316] hover:bg-[#fb923c] text-black px-4 py-3 rounded font-medium transition"
338
+ >
339
+ Start Using Apteva
340
+ </button>
341
+ </div>
342
+ </>
343
+ );
344
+ }
@@ -0,0 +1 @@
1
+ export { OnboardingWizard } from "./OnboardingWizard";
@@ -0,0 +1,285 @@
1
+ import React, { useState, useEffect } from "react";
2
+ import { CheckIcon } from "../common/Icons";
3
+ import type { Provider } from "../../types";
4
+
5
+ type SettingsTab = "providers";
6
+
7
+ export function SettingsPage() {
8
+ const [activeTab, setActiveTab] = useState<SettingsTab>("providers");
9
+
10
+ return (
11
+ <div className="flex-1 flex overflow-hidden">
12
+ {/* Settings Sidebar */}
13
+ <div className="w-48 border-r border-[#1a1a1a] p-4">
14
+ <h2 className="text-sm font-medium text-[#666] uppercase tracking-wider mb-3">Settings</h2>
15
+ <nav className="space-y-1">
16
+ <SettingsNavItem
17
+ label="Providers"
18
+ active={activeTab === "providers"}
19
+ onClick={() => setActiveTab("providers")}
20
+ />
21
+ {/* Future settings tabs can be added here */}
22
+ </nav>
23
+ </div>
24
+
25
+ {/* Settings Content */}
26
+ <div className="flex-1 overflow-auto p-6">
27
+ {activeTab === "providers" && <ProvidersSettings />}
28
+ </div>
29
+ </div>
30
+ );
31
+ }
32
+
33
+ function SettingsNavItem({
34
+ label,
35
+ active,
36
+ onClick
37
+ }: {
38
+ label: string;
39
+ active: boolean;
40
+ onClick: () => void;
41
+ }) {
42
+ return (
43
+ <button
44
+ onClick={onClick}
45
+ className={`w-full text-left px-3 py-2 rounded text-sm transition ${
46
+ active
47
+ ? "bg-[#1a1a1a] text-[#e0e0e0]"
48
+ : "text-[#666] hover:bg-[#111] hover:text-[#888]"
49
+ }`}
50
+ >
51
+ {label}
52
+ </button>
53
+ );
54
+ }
55
+
56
+ function ProvidersSettings() {
57
+ const [providers, setProviders] = useState<Provider[]>([]);
58
+ const [selectedProvider, setSelectedProvider] = useState<string | null>(null);
59
+ const [apiKey, setApiKey] = useState("");
60
+ const [saving, setSaving] = useState(false);
61
+ const [testing, setTesting] = useState(false);
62
+ const [error, setError] = useState<string | null>(null);
63
+ const [success, setSuccess] = useState<string | null>(null);
64
+
65
+ const fetchProviders = async () => {
66
+ const res = await fetch("/api/providers");
67
+ const data = await res.json();
68
+ setProviders(data.providers || []);
69
+ };
70
+
71
+ useEffect(() => {
72
+ fetchProviders();
73
+ }, []);
74
+
75
+ const saveKey = async () => {
76
+ if (!selectedProvider || !apiKey) return;
77
+ setSaving(true);
78
+ setError(null);
79
+ setSuccess(null);
80
+
81
+ try {
82
+ setTesting(true);
83
+ const testRes = await fetch(`/api/keys/${selectedProvider}/test`, {
84
+ method: "POST",
85
+ headers: { "Content-Type": "application/json" },
86
+ body: JSON.stringify({ key: apiKey }),
87
+ });
88
+ const testData = await testRes.json();
89
+ setTesting(false);
90
+
91
+ if (!testData.valid) {
92
+ setError(testData.error || "API key is invalid");
93
+ setSaving(false);
94
+ return;
95
+ }
96
+
97
+ const saveRes = await fetch(`/api/keys/${selectedProvider}`, {
98
+ method: "POST",
99
+ headers: { "Content-Type": "application/json" },
100
+ body: JSON.stringify({ key: apiKey }),
101
+ });
102
+
103
+ if (!saveRes.ok) {
104
+ const data = await saveRes.json();
105
+ setError(data.error || "Failed to save key");
106
+ } else {
107
+ setSuccess("API key saved!");
108
+ setApiKey("");
109
+ setSelectedProvider(null);
110
+ fetchProviders();
111
+ }
112
+ } catch (e) {
113
+ setError("Failed to save key");
114
+ }
115
+ setSaving(false);
116
+ };
117
+
118
+ const deleteKey = async (providerId: string) => {
119
+ if (!confirm("Are you sure you want to remove this API key?")) return;
120
+ await fetch(`/api/keys/${providerId}`, { method: "DELETE" });
121
+ fetchProviders();
122
+ };
123
+
124
+ const configuredCount = providers.filter(p => p.hasKey).length;
125
+
126
+ return (
127
+ <div className="max-w-4xl">
128
+ <div className="mb-6">
129
+ <h1 className="text-2xl font-semibold mb-1">AI Providers</h1>
130
+ <p className="text-[#666]">
131
+ Manage your API keys for AI providers. {configuredCount} of {providers.length} configured.
132
+ </p>
133
+ </div>
134
+
135
+ <div className="grid gap-4 md:grid-cols-2">
136
+ {providers.map(provider => (
137
+ <ProviderKeyCard
138
+ key={provider.id}
139
+ provider={provider}
140
+ isEditing={selectedProvider === provider.id}
141
+ apiKey={apiKey}
142
+ saving={saving}
143
+ testing={testing}
144
+ error={selectedProvider === provider.id ? error : null}
145
+ success={selectedProvider === provider.id ? success : null}
146
+ onStartEdit={() => {
147
+ setSelectedProvider(provider.id);
148
+ setError(null);
149
+ setSuccess(null);
150
+ }}
151
+ onCancelEdit={() => {
152
+ setSelectedProvider(null);
153
+ setApiKey("");
154
+ setError(null);
155
+ }}
156
+ onApiKeyChange={setApiKey}
157
+ onSave={saveKey}
158
+ onDelete={() => deleteKey(provider.id)}
159
+ />
160
+ ))}
161
+ </div>
162
+ </div>
163
+ );
164
+ }
165
+
166
+ interface ProviderKeyCardProps {
167
+ provider: Provider;
168
+ isEditing: boolean;
169
+ apiKey: string;
170
+ saving: boolean;
171
+ testing: boolean;
172
+ error: string | null;
173
+ success: string | null;
174
+ onStartEdit: () => void;
175
+ onCancelEdit: () => void;
176
+ onApiKeyChange: (key: string) => void;
177
+ onSave: () => void;
178
+ onDelete: () => void;
179
+ }
180
+
181
+ function ProviderKeyCard({
182
+ provider,
183
+ isEditing,
184
+ apiKey,
185
+ saving,
186
+ testing,
187
+ error,
188
+ success,
189
+ onStartEdit,
190
+ onCancelEdit,
191
+ onApiKeyChange,
192
+ onSave,
193
+ onDelete,
194
+ }: ProviderKeyCardProps) {
195
+ return (
196
+ <div className={`bg-[#111] border rounded-lg p-4 ${
197
+ provider.hasKey ? 'border-green-500/20' : 'border-[#1a1a1a]'
198
+ }`}>
199
+ <div className="flex items-center justify-between mb-2">
200
+ <div>
201
+ <h3 className="font-medium">{provider.name}</h3>
202
+ <p className="text-sm text-[#666]">{provider.models.length} models</p>
203
+ </div>
204
+ {provider.hasKey ? (
205
+ <span className="text-green-400 text-xs flex items-center gap-1 bg-green-500/10 px-2 py-1 rounded">
206
+ <CheckIcon className="w-3 h-3" />
207
+ {provider.keyHint}
208
+ </span>
209
+ ) : (
210
+ <span className="text-[#666] text-xs bg-[#1a1a1a] px-2 py-1 rounded">
211
+ Not configured
212
+ </span>
213
+ )}
214
+ </div>
215
+
216
+ {provider.hasKey ? (
217
+ <div className="flex items-center justify-between mt-3 pt-3 border-t border-[#1a1a1a]">
218
+ <a
219
+ href={provider.docsUrl}
220
+ target="_blank"
221
+ rel="noopener noreferrer"
222
+ className="text-sm text-[#3b82f6] hover:underline"
223
+ >
224
+ View docs
225
+ </a>
226
+ <button
227
+ onClick={onDelete}
228
+ className="text-red-400 hover:text-red-300 text-sm"
229
+ >
230
+ Remove key
231
+ </button>
232
+ </div>
233
+ ) : (
234
+ <div className="mt-3 pt-3 border-t border-[#1a1a1a]">
235
+ {isEditing ? (
236
+ <div className="space-y-3">
237
+ <input
238
+ type="password"
239
+ value={apiKey}
240
+ onChange={e => onApiKeyChange(e.target.value)}
241
+ placeholder="Enter API key..."
242
+ autoFocus
243
+ className="w-full bg-[#0a0a0a] border border-[#333] rounded px-3 py-2 focus:outline-none focus:border-[#f97316]"
244
+ />
245
+ {error && <p className="text-red-400 text-sm">{error}</p>}
246
+ {success && <p className="text-green-400 text-sm">{success}</p>}
247
+ <div className="flex gap-2">
248
+ <button
249
+ onClick={onCancelEdit}
250
+ className="flex-1 px-3 py-1.5 border border-[#333] rounded text-sm hover:border-[#666]"
251
+ >
252
+ Cancel
253
+ </button>
254
+ <button
255
+ onClick={onSave}
256
+ disabled={!apiKey || saving}
257
+ className="flex-1 px-3 py-1.5 bg-[#f97316] text-black rounded text-sm font-medium disabled:opacity-50"
258
+ >
259
+ {testing ? "Validating..." : saving ? "Saving..." : "Save"}
260
+ </button>
261
+ </div>
262
+ </div>
263
+ ) : (
264
+ <div className="flex items-center justify-between">
265
+ <a
266
+ href={provider.docsUrl}
267
+ target="_blank"
268
+ rel="noopener noreferrer"
269
+ className="text-sm text-[#3b82f6] hover:underline"
270
+ >
271
+ Get API key
272
+ </a>
273
+ <button
274
+ onClick={onStartEdit}
275
+ className="text-sm text-[#f97316] hover:text-[#fb923c]"
276
+ >
277
+ + Add key
278
+ </button>
279
+ </div>
280
+ )}
281
+ </div>
282
+ )}
283
+ </div>
284
+ );
285
+ }
@@ -0,0 +1 @@
1
+ export { SettingsPage } from "./SettingsPage";
@@ -0,0 +1,3 @@
1
+ export { useAgents } from "./useAgents";
2
+ export { useProviders } from "./useProviders";
3
+ export { useOnboarding } from "./useOnboarding";
@@ -0,0 +1,62 @@
1
+ import { useState, useEffect, useCallback } from "react";
2
+ import type { Agent } from "../types";
3
+
4
+ export function useAgents(enabled: boolean) {
5
+ const [agents, setAgents] = useState<Agent[]>([]);
6
+ const [loading, setLoading] = useState(true);
7
+
8
+ const fetchAgents = useCallback(async () => {
9
+ const res = await fetch("/api/agents");
10
+ const data = await res.json();
11
+ setAgents(data.agents || []);
12
+ setLoading(false);
13
+ }, []);
14
+
15
+ useEffect(() => {
16
+ if (enabled) {
17
+ fetchAgents();
18
+ }
19
+ }, [enabled, fetchAgents]);
20
+
21
+ const createAgent = async (agent: {
22
+ name: string;
23
+ model: string;
24
+ provider: string;
25
+ systemPrompt: string;
26
+ }) => {
27
+ await fetch("/api/agents", {
28
+ method: "POST",
29
+ headers: { "Content-Type": "application/json" },
30
+ body: JSON.stringify(agent),
31
+ });
32
+ await fetchAgents();
33
+ };
34
+
35
+ const deleteAgent = async (id: string) => {
36
+ await fetch(`/api/agents/${id}`, { method: "DELETE" });
37
+ await fetchAgents();
38
+ };
39
+
40
+ const toggleAgent = async (agent: Agent): Promise<{ error?: string }> => {
41
+ const action = agent.status === "running" ? "stop" : "start";
42
+ const res = await fetch(`/api/agents/${agent.id}/${action}`, { method: "POST" });
43
+ const data = await res.json();
44
+ await fetchAgents();
45
+ if (!res.ok && data.error) {
46
+ return { error: data.error };
47
+ }
48
+ return {};
49
+ };
50
+
51
+ const runningCount = agents.filter(a => a.status === "running").length;
52
+
53
+ return {
54
+ agents,
55
+ loading,
56
+ runningCount,
57
+ fetchAgents,
58
+ createAgent,
59
+ deleteAgent,
60
+ toggleAgent,
61
+ };
62
+ }