apteva 0.2.3 → 0.2.6
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.0mzj9cz9.js +213 -0
- package/dist/index.html +1 -1
- package/dist/styles.css +1 -1
- package/package.json +6 -6
- package/src/binary.ts +271 -1
- package/src/crypto.ts +53 -0
- package/src/db.ts +492 -3
- package/src/mcp-client.ts +599 -0
- package/src/providers.ts +31 -0
- package/src/routes/api.ts +832 -64
- package/src/server.ts +169 -5
- package/src/web/App.tsx +44 -2
- package/src/web/components/agents/AgentCard.tsx +53 -9
- package/src/web/components/agents/AgentPanel.tsx +381 -0
- package/src/web/components/agents/AgentsView.tsx +27 -10
- package/src/web/components/agents/CreateAgentModal.tsx +7 -7
- package/src/web/components/agents/index.ts +1 -1
- package/src/web/components/common/Icons.tsx +8 -0
- package/src/web/components/common/Modal.tsx +2 -2
- package/src/web/components/common/Select.tsx +1 -1
- package/src/web/components/common/index.ts +1 -0
- package/src/web/components/dashboard/Dashboard.tsx +74 -25
- package/src/web/components/index.ts +5 -2
- package/src/web/components/layout/Sidebar.tsx +22 -2
- package/src/web/components/mcp/McpPage.tsx +1144 -0
- package/src/web/components/mcp/index.ts +1 -0
- package/src/web/components/onboarding/OnboardingWizard.tsx +5 -1
- package/src/web/components/settings/SettingsPage.tsx +312 -82
- package/src/web/components/tasks/TasksPage.tsx +129 -0
- package/src/web/components/tasks/index.ts +1 -0
- package/src/web/components/telemetry/TelemetryPage.tsx +359 -0
- package/src/web/context/TelemetryContext.tsx +202 -0
- package/src/web/context/index.ts +2 -0
- package/src/web/hooks/useAgents.ts +23 -0
- package/src/web/styles.css +18 -0
- package/src/web/types.ts +75 -1
- package/dist/App.wfhmfhx7.js +0 -213
- package/src/web/components/agents/ChatPanel.tsx +0 -63
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { McpPage } from "./McpPage";
|
|
@@ -19,7 +19,11 @@ export function OnboardingWizard({ onComplete }: OnboardingWizardProps) {
|
|
|
19
19
|
useEffect(() => {
|
|
20
20
|
fetch("/api/providers")
|
|
21
21
|
.then(res => res.json())
|
|
22
|
-
.then(data =>
|
|
22
|
+
.then(data => {
|
|
23
|
+
// Only show LLM providers in onboarding, not integrations
|
|
24
|
+
const llmProviders = (data.providers || []).filter((p: Provider) => p.type === "llm");
|
|
25
|
+
setProviders(llmProviders);
|
|
26
|
+
});
|
|
23
27
|
}, []);
|
|
24
28
|
|
|
25
29
|
const configuredProviders = providers.filter(p => p.hasKey);
|
|
@@ -126,43 +126,87 @@ function ProvidersSettings() {
|
|
|
126
126
|
fetchProviders();
|
|
127
127
|
};
|
|
128
128
|
|
|
129
|
-
const
|
|
129
|
+
const llmProviders = providers.filter(p => p.type === "llm");
|
|
130
|
+
const integrations = providers.filter(p => p.type === "integration");
|
|
131
|
+
const llmConfiguredCount = llmProviders.filter(p => p.hasKey).length;
|
|
132
|
+
const intConfiguredCount = integrations.filter(p => p.hasKey).length;
|
|
130
133
|
|
|
131
134
|
return (
|
|
132
|
-
<div className="max-w-4xl">
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
<
|
|
136
|
-
|
|
137
|
-
|
|
135
|
+
<div className="max-w-4xl space-y-10">
|
|
136
|
+
{/* AI Providers Section */}
|
|
137
|
+
<div>
|
|
138
|
+
<div className="mb-6">
|
|
139
|
+
<h1 className="text-2xl font-semibold mb-1">AI Providers</h1>
|
|
140
|
+
<p className="text-[#666]">
|
|
141
|
+
Manage your API keys for AI providers. {llmConfiguredCount} of {llmProviders.length} configured.
|
|
142
|
+
</p>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
<div className="grid gap-4 md:grid-cols-2">
|
|
146
|
+
{llmProviders.map(provider => (
|
|
147
|
+
<ProviderKeyCard
|
|
148
|
+
key={provider.id}
|
|
149
|
+
provider={provider}
|
|
150
|
+
isEditing={selectedProvider === provider.id}
|
|
151
|
+
apiKey={apiKey}
|
|
152
|
+
saving={saving}
|
|
153
|
+
testing={testing}
|
|
154
|
+
error={selectedProvider === provider.id ? error : null}
|
|
155
|
+
success={selectedProvider === provider.id ? success : null}
|
|
156
|
+
onStartEdit={() => {
|
|
157
|
+
setSelectedProvider(provider.id);
|
|
158
|
+
setError(null);
|
|
159
|
+
setSuccess(null);
|
|
160
|
+
}}
|
|
161
|
+
onCancelEdit={() => {
|
|
162
|
+
setSelectedProvider(null);
|
|
163
|
+
setApiKey("");
|
|
164
|
+
setError(null);
|
|
165
|
+
}}
|
|
166
|
+
onApiKeyChange={setApiKey}
|
|
167
|
+
onSave={saveKey}
|
|
168
|
+
onDelete={() => deleteKey(provider.id)}
|
|
169
|
+
/>
|
|
170
|
+
))}
|
|
171
|
+
</div>
|
|
138
172
|
</div>
|
|
139
173
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
174
|
+
{/* MCP Integrations Section */}
|
|
175
|
+
<div>
|
|
176
|
+
<div className="mb-6">
|
|
177
|
+
<h2 className="text-xl font-semibold mb-1">MCP Integrations</h2>
|
|
178
|
+
<p className="text-[#666]">
|
|
179
|
+
Connect to MCP gateways for tool integrations. {intConfiguredCount} of {integrations.length} configured.
|
|
180
|
+
</p>
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
<div className="grid gap-4 md:grid-cols-2">
|
|
184
|
+
{integrations.map(provider => (
|
|
185
|
+
<IntegrationKeyCard
|
|
186
|
+
key={provider.id}
|
|
187
|
+
provider={provider}
|
|
188
|
+
isEditing={selectedProvider === provider.id}
|
|
189
|
+
apiKey={apiKey}
|
|
190
|
+
saving={saving}
|
|
191
|
+
testing={testing}
|
|
192
|
+
error={selectedProvider === provider.id ? error : null}
|
|
193
|
+
success={selectedProvider === provider.id ? success : null}
|
|
194
|
+
onStartEdit={() => {
|
|
195
|
+
setSelectedProvider(provider.id);
|
|
196
|
+
setError(null);
|
|
197
|
+
setSuccess(null);
|
|
198
|
+
}}
|
|
199
|
+
onCancelEdit={() => {
|
|
200
|
+
setSelectedProvider(null);
|
|
201
|
+
setApiKey("");
|
|
202
|
+
setError(null);
|
|
203
|
+
}}
|
|
204
|
+
onApiKeyChange={setApiKey}
|
|
205
|
+
onSave={saveKey}
|
|
206
|
+
onDelete={() => deleteKey(provider.id)}
|
|
207
|
+
/>
|
|
208
|
+
))}
|
|
209
|
+
</div>
|
|
166
210
|
</div>
|
|
167
211
|
</div>
|
|
168
212
|
);
|
|
@@ -184,17 +228,24 @@ interface ProviderKeyCardProps {
|
|
|
184
228
|
}
|
|
185
229
|
|
|
186
230
|
interface VersionInfo {
|
|
187
|
-
|
|
188
|
-
latest: string;
|
|
231
|
+
installed: string | null;
|
|
232
|
+
latest: string | null;
|
|
189
233
|
updateAvailable: boolean;
|
|
190
|
-
|
|
234
|
+
lastChecked: string | null;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
interface AllVersionInfo {
|
|
238
|
+
apteva: VersionInfo;
|
|
239
|
+
agent: VersionInfo;
|
|
191
240
|
}
|
|
192
241
|
|
|
193
242
|
function UpdatesSettings() {
|
|
194
|
-
const [
|
|
243
|
+
const [versions, setVersions] = useState<AllVersionInfo | null>(null);
|
|
195
244
|
const [checking, setChecking] = useState(false);
|
|
245
|
+
const [updatingAgent, setUpdatingAgent] = useState(false);
|
|
196
246
|
const [error, setError] = useState<string | null>(null);
|
|
197
|
-
const [
|
|
247
|
+
const [updateSuccess, setUpdateSuccess] = useState<string | null>(null);
|
|
248
|
+
const [copied, setCopied] = useState<string | null>(null);
|
|
198
249
|
|
|
199
250
|
const checkForUpdates = async () => {
|
|
200
251
|
setChecking(true);
|
|
@@ -203,92 +254,165 @@ function UpdatesSettings() {
|
|
|
203
254
|
const res = await fetch("/api/version");
|
|
204
255
|
if (!res.ok) throw new Error("Failed to check for updates");
|
|
205
256
|
const data = await res.json();
|
|
206
|
-
|
|
257
|
+
setVersions(data);
|
|
207
258
|
} catch (e) {
|
|
208
259
|
setError("Failed to check for updates");
|
|
209
260
|
}
|
|
210
261
|
setChecking(false);
|
|
211
262
|
};
|
|
212
263
|
|
|
264
|
+
const updateAgent = async () => {
|
|
265
|
+
setUpdatingAgent(true);
|
|
266
|
+
setError(null);
|
|
267
|
+
setUpdateSuccess(null);
|
|
268
|
+
try {
|
|
269
|
+
const res = await fetch("/api/version/update", { method: "POST" });
|
|
270
|
+
const data = await res.json();
|
|
271
|
+
if (!data.success) {
|
|
272
|
+
setError(data.error || "Update failed");
|
|
273
|
+
} else {
|
|
274
|
+
setUpdateSuccess(`Agent updated to v${data.version}. New agents will use this version.`);
|
|
275
|
+
await checkForUpdates();
|
|
276
|
+
}
|
|
277
|
+
} catch (e) {
|
|
278
|
+
setError("Failed to update agent");
|
|
279
|
+
}
|
|
280
|
+
setUpdatingAgent(false);
|
|
281
|
+
};
|
|
282
|
+
|
|
213
283
|
useEffect(() => {
|
|
214
284
|
checkForUpdates();
|
|
215
285
|
}, []);
|
|
216
286
|
|
|
217
|
-
const copyCommand = () => {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
setTimeout(() => setCopied(false), 2000);
|
|
222
|
-
}
|
|
287
|
+
const copyCommand = (cmd: string, id: string) => {
|
|
288
|
+
navigator.clipboard.writeText(cmd);
|
|
289
|
+
setCopied(id);
|
|
290
|
+
setTimeout(() => setCopied(null), 2000);
|
|
223
291
|
};
|
|
224
292
|
|
|
293
|
+
const hasAnyUpdate = versions?.apteva.updateAvailable || versions?.agent.updateAvailable;
|
|
294
|
+
|
|
225
295
|
return (
|
|
226
296
|
<div className="max-w-2xl">
|
|
227
297
|
<div className="mb-6">
|
|
228
298
|
<h1 className="text-2xl font-semibold mb-1">Updates</h1>
|
|
229
299
|
<p className="text-[#666]">
|
|
230
|
-
Check for new versions of apteva.
|
|
300
|
+
Check for new versions of apteva and the agent binary.
|
|
231
301
|
</p>
|
|
232
302
|
</div>
|
|
233
303
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
<div className="
|
|
304
|
+
{checking && !versions ? (
|
|
305
|
+
<div className="text-[#666]">Checking for updates...</div>
|
|
306
|
+
) : error && !versions ? (
|
|
307
|
+
<div className="text-red-400">{error}</div>
|
|
308
|
+
) : versions ? (
|
|
309
|
+
<div className="space-y-6">
|
|
310
|
+
{updateSuccess && (
|
|
311
|
+
<div className="bg-green-500/10 border border-green-500/30 rounded-lg p-4 text-green-400">
|
|
312
|
+
{updateSuccess}
|
|
313
|
+
</div>
|
|
314
|
+
)}
|
|
315
|
+
|
|
316
|
+
{error && (
|
|
317
|
+
<div className="bg-red-500/10 border border-red-500/30 rounded-lg p-4 text-red-400">
|
|
318
|
+
{error}
|
|
319
|
+
</div>
|
|
320
|
+
)}
|
|
321
|
+
|
|
322
|
+
{/* Apteva App Version */}
|
|
323
|
+
<div className="bg-[#111] border border-[#1a1a1a] rounded-lg p-5">
|
|
324
|
+
<div className="flex items-center justify-between mb-4">
|
|
242
325
|
<div>
|
|
243
|
-
<
|
|
244
|
-
<
|
|
326
|
+
<h3 className="font-medium text-lg">apteva</h3>
|
|
327
|
+
<p className="text-sm text-[#666]">The app you're running</p>
|
|
245
328
|
</div>
|
|
246
329
|
<div className="text-right">
|
|
247
|
-
<div className="text-
|
|
248
|
-
|
|
330
|
+
<div className="text-xl font-mono">v{versions.apteva.installed || "?"}</div>
|
|
331
|
+
{versions.apteva.updateAvailable && (
|
|
332
|
+
<div className="text-sm text-[#f97316]">→ v{versions.apteva.latest}</div>
|
|
333
|
+
)}
|
|
249
334
|
</div>
|
|
250
335
|
</div>
|
|
251
336
|
|
|
252
|
-
{
|
|
337
|
+
{versions.apteva.updateAvailable ? (
|
|
253
338
|
<div className="bg-[#f97316]/10 border border-[#f97316]/30 rounded-lg p-4">
|
|
254
|
-
<div className="flex items-center gap-2 text-[#f97316] font-medium mb-2">
|
|
255
|
-
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
256
|
-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
|
257
|
-
</svg>
|
|
258
|
-
Update available!
|
|
259
|
-
</div>
|
|
260
339
|
<p className="text-sm text-[#888] mb-3">
|
|
261
|
-
|
|
340
|
+
Update by running:
|
|
262
341
|
</p>
|
|
263
342
|
<div className="flex items-center gap-2">
|
|
264
|
-
<code className="flex-1 bg-[#0a0a0a] px-3 py-2 rounded font-mono text-sm">
|
|
265
|
-
|
|
343
|
+
<code className="flex-1 bg-[#0a0a0a] px-3 py-2 rounded font-mono text-sm text-[#888]">
|
|
344
|
+
npx apteva@latest
|
|
266
345
|
</code>
|
|
267
346
|
<button
|
|
268
|
-
onClick={copyCommand}
|
|
347
|
+
onClick={() => copyCommand("npx apteva@latest", "apteva")}
|
|
269
348
|
className="px-3 py-2 bg-[#1a1a1a] hover:bg-[#222] rounded text-sm"
|
|
270
349
|
>
|
|
271
|
-
{copied ? "Copied!" : "Copy"}
|
|
350
|
+
{copied === "apteva" ? "Copied!" : "Copy"}
|
|
272
351
|
</button>
|
|
273
352
|
</div>
|
|
274
353
|
</div>
|
|
275
354
|
) : (
|
|
276
|
-
<div className="
|
|
277
|
-
<CheckIcon className="w-
|
|
278
|
-
|
|
355
|
+
<div className="flex items-center gap-2 text-green-400 text-sm">
|
|
356
|
+
<CheckIcon className="w-4 h-4" />
|
|
357
|
+
Up to date
|
|
279
358
|
</div>
|
|
280
359
|
)}
|
|
360
|
+
</div>
|
|
361
|
+
|
|
362
|
+
{/* Agent Binary Version */}
|
|
363
|
+
<div className="bg-[#111] border border-[#1a1a1a] rounded-lg p-5">
|
|
364
|
+
<div className="flex items-center justify-between mb-4">
|
|
365
|
+
<div>
|
|
366
|
+
<h3 className="font-medium text-lg">Agent Binary</h3>
|
|
367
|
+
<p className="text-sm text-[#666]">The Go binary that runs agents</p>
|
|
368
|
+
</div>
|
|
369
|
+
<div className="text-right">
|
|
370
|
+
<div className="text-xl font-mono">v{versions.agent.installed || "?"}</div>
|
|
371
|
+
{versions.agent.updateAvailable && (
|
|
372
|
+
<div className="text-sm text-[#f97316]">→ v{versions.agent.latest}</div>
|
|
373
|
+
)}
|
|
374
|
+
</div>
|
|
375
|
+
</div>
|
|
281
376
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
377
|
+
{versions.agent.updateAvailable ? (
|
|
378
|
+
<div className="bg-[#f97316]/10 border border-[#f97316]/30 rounded-lg p-4">
|
|
379
|
+
<p className="text-sm text-[#888] mb-3">
|
|
380
|
+
A new version is available. Stop all agents before updating.
|
|
381
|
+
</p>
|
|
382
|
+
<div className="flex items-center gap-2">
|
|
383
|
+
<button
|
|
384
|
+
onClick={updateAgent}
|
|
385
|
+
disabled={updatingAgent}
|
|
386
|
+
className="px-4 py-2 bg-[#f97316] text-black rounded font-medium text-sm disabled:opacity-50"
|
|
387
|
+
>
|
|
388
|
+
{updatingAgent ? "Updating..." : "Update Agent"}
|
|
389
|
+
</button>
|
|
390
|
+
</div>
|
|
391
|
+
</div>
|
|
392
|
+
) : (
|
|
393
|
+
<div className="flex items-center gap-2 text-green-400 text-sm">
|
|
394
|
+
<CheckIcon className="w-4 h-4" />
|
|
395
|
+
Up to date
|
|
396
|
+
</div>
|
|
397
|
+
)}
|
|
289
398
|
</div>
|
|
290
|
-
|
|
291
|
-
|
|
399
|
+
|
|
400
|
+
{!hasAnyUpdate && !updateSuccess && (
|
|
401
|
+
<div className="bg-green-500/10 border border-green-500/30 rounded-lg p-4 flex items-center gap-2 text-green-400">
|
|
402
|
+
<CheckIcon className="w-5 h-5" />
|
|
403
|
+
Everything is up to date!
|
|
404
|
+
</div>
|
|
405
|
+
)}
|
|
406
|
+
|
|
407
|
+
<button
|
|
408
|
+
onClick={checkForUpdates}
|
|
409
|
+
disabled={checking}
|
|
410
|
+
className="text-sm text-[#666] hover:text-[#888] disabled:opacity-50"
|
|
411
|
+
>
|
|
412
|
+
{checking ? "Checking..." : "Check for updates"}
|
|
413
|
+
</button>
|
|
414
|
+
</div>
|
|
415
|
+
) : null}
|
|
292
416
|
</div>
|
|
293
417
|
);
|
|
294
418
|
}
|
|
@@ -398,3 +522,109 @@ function ProviderKeyCard({
|
|
|
398
522
|
</div>
|
|
399
523
|
);
|
|
400
524
|
}
|
|
525
|
+
|
|
526
|
+
function IntegrationKeyCard({
|
|
527
|
+
provider,
|
|
528
|
+
isEditing,
|
|
529
|
+
apiKey,
|
|
530
|
+
saving,
|
|
531
|
+
testing,
|
|
532
|
+
error,
|
|
533
|
+
success,
|
|
534
|
+
onStartEdit,
|
|
535
|
+
onCancelEdit,
|
|
536
|
+
onApiKeyChange,
|
|
537
|
+
onSave,
|
|
538
|
+
onDelete,
|
|
539
|
+
}: ProviderKeyCardProps) {
|
|
540
|
+
return (
|
|
541
|
+
<div className={`bg-[#111] border rounded-lg p-4 ${
|
|
542
|
+
provider.hasKey ? 'border-[#f97316]/20' : 'border-[#1a1a1a]'
|
|
543
|
+
}`}>
|
|
544
|
+
<div className="flex items-center justify-between mb-2">
|
|
545
|
+
<div>
|
|
546
|
+
<h3 className="font-medium">{provider.name}</h3>
|
|
547
|
+
<p className="text-sm text-[#666]">{provider.description || "MCP integration"}</p>
|
|
548
|
+
</div>
|
|
549
|
+
{provider.hasKey ? (
|
|
550
|
+
<span className="text-[#f97316] text-xs flex items-center gap-1 bg-[#f97316]/10 px-2 py-1 rounded">
|
|
551
|
+
<CheckIcon className="w-3 h-3" />
|
|
552
|
+
{provider.keyHint}
|
|
553
|
+
</span>
|
|
554
|
+
) : (
|
|
555
|
+
<span className="text-[#666] text-xs bg-[#1a1a1a] px-2 py-1 rounded">
|
|
556
|
+
Not configured
|
|
557
|
+
</span>
|
|
558
|
+
)}
|
|
559
|
+
</div>
|
|
560
|
+
|
|
561
|
+
{provider.hasKey ? (
|
|
562
|
+
<div className="flex items-center justify-between mt-3 pt-3 border-t border-[#1a1a1a]">
|
|
563
|
+
<a
|
|
564
|
+
href={provider.docsUrl}
|
|
565
|
+
target="_blank"
|
|
566
|
+
rel="noopener noreferrer"
|
|
567
|
+
className="text-sm text-[#3b82f6] hover:underline"
|
|
568
|
+
>
|
|
569
|
+
View docs
|
|
570
|
+
</a>
|
|
571
|
+
<button
|
|
572
|
+
onClick={onDelete}
|
|
573
|
+
className="text-red-400 hover:text-red-300 text-sm"
|
|
574
|
+
>
|
|
575
|
+
Remove key
|
|
576
|
+
</button>
|
|
577
|
+
</div>
|
|
578
|
+
) : (
|
|
579
|
+
<div className="mt-3 pt-3 border-t border-[#1a1a1a]">
|
|
580
|
+
{isEditing ? (
|
|
581
|
+
<div className="space-y-3">
|
|
582
|
+
<input
|
|
583
|
+
type="password"
|
|
584
|
+
value={apiKey}
|
|
585
|
+
onChange={e => onApiKeyChange(e.target.value)}
|
|
586
|
+
placeholder="Enter API key..."
|
|
587
|
+
autoFocus
|
|
588
|
+
className="w-full bg-[#0a0a0a] border border-[#333] rounded px-3 py-2 focus:outline-none focus:border-[#f97316]"
|
|
589
|
+
/>
|
|
590
|
+
{error && <p className="text-red-400 text-sm">{error}</p>}
|
|
591
|
+
{success && <p className="text-green-400 text-sm">{success}</p>}
|
|
592
|
+
<div className="flex gap-2">
|
|
593
|
+
<button
|
|
594
|
+
onClick={onCancelEdit}
|
|
595
|
+
className="flex-1 px-3 py-1.5 border border-[#333] rounded text-sm hover:border-[#666]"
|
|
596
|
+
>
|
|
597
|
+
Cancel
|
|
598
|
+
</button>
|
|
599
|
+
<button
|
|
600
|
+
onClick={onSave}
|
|
601
|
+
disabled={!apiKey || saving}
|
|
602
|
+
className="flex-1 px-3 py-1.5 bg-[#f97316] text-black rounded text-sm font-medium disabled:opacity-50"
|
|
603
|
+
>
|
|
604
|
+
{testing ? "Validating..." : saving ? "Saving..." : "Save"}
|
|
605
|
+
</button>
|
|
606
|
+
</div>
|
|
607
|
+
</div>
|
|
608
|
+
) : (
|
|
609
|
+
<div className="flex items-center justify-between">
|
|
610
|
+
<a
|
|
611
|
+
href={provider.docsUrl}
|
|
612
|
+
target="_blank"
|
|
613
|
+
rel="noopener noreferrer"
|
|
614
|
+
className="text-sm text-[#3b82f6] hover:underline"
|
|
615
|
+
>
|
|
616
|
+
Get API key
|
|
617
|
+
</a>
|
|
618
|
+
<button
|
|
619
|
+
onClick={onStartEdit}
|
|
620
|
+
className="text-sm text-[#f97316] hover:text-[#fb923c]"
|
|
621
|
+
>
|
|
622
|
+
+ Add key
|
|
623
|
+
</button>
|
|
624
|
+
</div>
|
|
625
|
+
)}
|
|
626
|
+
</div>
|
|
627
|
+
)}
|
|
628
|
+
</div>
|
|
629
|
+
);
|
|
630
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
|
+
import { TasksIcon } from "../common/Icons";
|
|
3
|
+
import type { Task } from "../../types";
|
|
4
|
+
|
|
5
|
+
interface TasksPageProps {
|
|
6
|
+
onSelectAgent?: (agentId: string) => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function TasksPage({ onSelectAgent }: TasksPageProps) {
|
|
10
|
+
const [tasks, setTasks] = useState<Task[]>([]);
|
|
11
|
+
const [loading, setLoading] = useState(true);
|
|
12
|
+
const [filter, setFilter] = useState<string>("all");
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
fetchTasks();
|
|
16
|
+
// Refresh every 10 seconds
|
|
17
|
+
const interval = setInterval(fetchTasks, 10000);
|
|
18
|
+
return () => clearInterval(interval);
|
|
19
|
+
}, [filter]);
|
|
20
|
+
|
|
21
|
+
const fetchTasks = async () => {
|
|
22
|
+
try {
|
|
23
|
+
const res = await fetch(`/api/tasks?status=${filter}`);
|
|
24
|
+
const data = await res.json();
|
|
25
|
+
setTasks(data.tasks || []);
|
|
26
|
+
} catch (e) {
|
|
27
|
+
console.error("Failed to fetch tasks:", e);
|
|
28
|
+
} finally {
|
|
29
|
+
setLoading(false);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const statusColors: Record<string, string> = {
|
|
34
|
+
pending: "bg-yellow-500/20 text-yellow-400",
|
|
35
|
+
running: "bg-blue-500/20 text-blue-400",
|
|
36
|
+
completed: "bg-green-500/20 text-green-400",
|
|
37
|
+
failed: "bg-red-500/20 text-red-400",
|
|
38
|
+
cancelled: "bg-gray-500/20 text-gray-400",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const filterOptions = [
|
|
42
|
+
{ value: "all", label: "All" },
|
|
43
|
+
{ value: "pending", label: "Pending" },
|
|
44
|
+
{ value: "running", label: "Running" },
|
|
45
|
+
{ value: "completed", label: "Completed" },
|
|
46
|
+
{ value: "failed", label: "Failed" },
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<div className="flex-1 p-6 overflow-auto">
|
|
51
|
+
<div className="max-w-4xl mx-auto">
|
|
52
|
+
<div className="flex items-center justify-between mb-6">
|
|
53
|
+
<div>
|
|
54
|
+
<h1 className="text-2xl font-semibold mb-1">Tasks</h1>
|
|
55
|
+
<p className="text-[#666]">
|
|
56
|
+
View tasks from all running agents
|
|
57
|
+
</p>
|
|
58
|
+
</div>
|
|
59
|
+
<div className="flex gap-2">
|
|
60
|
+
{filterOptions.map(opt => (
|
|
61
|
+
<button
|
|
62
|
+
key={opt.value}
|
|
63
|
+
onClick={() => setFilter(opt.value)}
|
|
64
|
+
className={`px-3 py-1.5 rounded text-sm transition ${
|
|
65
|
+
filter === opt.value
|
|
66
|
+
? "bg-[#f97316] text-black"
|
|
67
|
+
: "bg-[#1a1a1a] hover:bg-[#222]"
|
|
68
|
+
}`}
|
|
69
|
+
>
|
|
70
|
+
{opt.label}
|
|
71
|
+
</button>
|
|
72
|
+
))}
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
{loading ? (
|
|
77
|
+
<div className="text-center py-12 text-[#666]">Loading tasks...</div>
|
|
78
|
+
) : tasks.length === 0 ? (
|
|
79
|
+
<div className="text-center py-12">
|
|
80
|
+
<TasksIcon className="w-12 h-12 mx-auto mb-4 text-[#333]" />
|
|
81
|
+
<p className="text-[#666]">No tasks found</p>
|
|
82
|
+
<p className="text-sm text-[#444] mt-1">
|
|
83
|
+
Tasks will appear here when agents create them
|
|
84
|
+
</p>
|
|
85
|
+
</div>
|
|
86
|
+
) : (
|
|
87
|
+
<div className="space-y-3">
|
|
88
|
+
{tasks.map(task => (
|
|
89
|
+
<div
|
|
90
|
+
key={`${task.agentId}-${task.id}`}
|
|
91
|
+
className="bg-[#111] border border-[#1a1a1a] rounded-lg p-4 hover:border-[#333] transition"
|
|
92
|
+
>
|
|
93
|
+
<div className="flex items-start justify-between mb-2">
|
|
94
|
+
<div className="flex-1">
|
|
95
|
+
<h3 className="font-medium">{task.title}</h3>
|
|
96
|
+
<p className="text-sm text-[#666]">
|
|
97
|
+
{task.agentName}
|
|
98
|
+
{task.execute_at && (
|
|
99
|
+
<span className="ml-2">
|
|
100
|
+
· Scheduled: {new Date(task.execute_at).toLocaleString()}
|
|
101
|
+
</span>
|
|
102
|
+
)}
|
|
103
|
+
</p>
|
|
104
|
+
</div>
|
|
105
|
+
<span className={`px-2 py-1 rounded text-xs font-medium ${statusColors[task.status] || statusColors.pending}`}>
|
|
106
|
+
{task.status}
|
|
107
|
+
</span>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
{task.description && (
|
|
111
|
+
<p className="text-sm text-[#888] mb-2 line-clamp-2">
|
|
112
|
+
{task.description}
|
|
113
|
+
</p>
|
|
114
|
+
)}
|
|
115
|
+
|
|
116
|
+
<div className="flex items-center gap-4 text-xs text-[#555]">
|
|
117
|
+
<span>Type: {task.type}</span>
|
|
118
|
+
<span>Priority: {task.priority}</span>
|
|
119
|
+
{task.recurrence && <span>Recurrence: {task.recurrence}</span>}
|
|
120
|
+
<span>Created: {new Date(task.created_at).toLocaleString()}</span>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
))}
|
|
124
|
+
</div>
|
|
125
|
+
)}
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { TasksPage } from "./TasksPage";
|