apteva 0.4.57 → 0.7.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 (142) hide show
  1. package/README.md +216 -54
  2. package/cli.js +35 -0
  3. package/install.js +92 -0
  4. package/package.json +12 -79
  5. package/LICENSE +0 -63
  6. package/bin/apteva.js +0 -196
  7. package/dist/ActivityPage.kxzzb4yc.js +0 -3
  8. package/dist/ApiDocsPage.zq998hbm.js +0 -4
  9. package/dist/App.55rea8mn.js +0 -61
  10. package/dist/App.5ywb23z4.js +0 -53
  11. package/dist/App.6thds120.js +0 -4
  12. package/dist/App.9tctxzqm.js +0 -8
  13. package/dist/App.a8r8ttaz.js +0 -4
  14. package/dist/App.agsv5bje.js +0 -4
  15. package/dist/App.cepapqmx.js +0 -4
  16. package/dist/App.dp041gb3.js +0 -221
  17. package/dist/App.fds72zb5.js +0 -4
  18. package/dist/App.fg9qj2dq.js +0 -4
  19. package/dist/App.ndfejbm9.js +0 -4
  20. package/dist/App.nxmfmq1h.js +0 -13
  21. package/dist/App.qdfyt8ba.js +0 -4
  22. package/dist/App.x2d0ygt6.js +0 -4
  23. package/dist/App.yt9p4nr3.js +0 -20
  24. package/dist/App.zn4mw16t.js +0 -1
  25. package/dist/ConnectionsPage.8r96ryw7.js +0 -3
  26. package/dist/McpPage.3cwh0gnd.js +0 -3
  27. package/dist/SettingsPage.ykgdh5ev.js +0 -3
  28. package/dist/SkillsPage.4np1s65b.js +0 -3
  29. package/dist/TasksPage.4g08t7p6.js +0 -3
  30. package/dist/TelemetryPage.72w9pwcp.js +0 -3
  31. package/dist/TestsPage.z4fk3r7r.js +0 -3
  32. package/dist/ThreadsPage.63tcajeh.js +0 -3
  33. package/dist/apteva-kit.css +0 -1
  34. package/dist/icon.png +0 -0
  35. package/dist/index.html +0 -16
  36. package/dist/styles.css +0 -1
  37. package/scripts/postinstall.mjs +0 -102
  38. package/src/auth/index.ts +0 -394
  39. package/src/auth/middleware.ts +0 -213
  40. package/src/binary.ts +0 -536
  41. package/src/channels/index.ts +0 -40
  42. package/src/channels/telegram.ts +0 -311
  43. package/src/crypto.ts +0 -301
  44. package/src/db-tests.ts +0 -174
  45. package/src/db.ts +0 -3133
  46. package/src/integrations/agentdojo.ts +0 -559
  47. package/src/integrations/composio.ts +0 -437
  48. package/src/integrations/index.ts +0 -87
  49. package/src/integrations/skillsmp.ts +0 -318
  50. package/src/mcp-client.ts +0 -605
  51. package/src/mcp-handler.ts +0 -394
  52. package/src/mcp-platform.ts +0 -2403
  53. package/src/openapi.ts +0 -2410
  54. package/src/providers.ts +0 -597
  55. package/src/routes/api/agent-utils.ts +0 -890
  56. package/src/routes/api/agents.ts +0 -916
  57. package/src/routes/api/api-keys.ts +0 -95
  58. package/src/routes/api/channels.ts +0 -182
  59. package/src/routes/api/helpers.ts +0 -12
  60. package/src/routes/api/integrations.ts +0 -639
  61. package/src/routes/api/mcp.ts +0 -574
  62. package/src/routes/api/meta-agent.ts +0 -195
  63. package/src/routes/api/projects.ts +0 -112
  64. package/src/routes/api/providers.ts +0 -424
  65. package/src/routes/api/skills.ts +0 -537
  66. package/src/routes/api/system.ts +0 -333
  67. package/src/routes/api/telemetry.ts +0 -203
  68. package/src/routes/api/tests.ts +0 -148
  69. package/src/routes/api/triggers.ts +0 -518
  70. package/src/routes/api/users.ts +0 -148
  71. package/src/routes/api/webhooks.ts +0 -171
  72. package/src/routes/api.ts +0 -53
  73. package/src/routes/auth.ts +0 -251
  74. package/src/routes/share.ts +0 -86
  75. package/src/routes/static.ts +0 -131
  76. package/src/server.ts +0 -642
  77. package/src/test-runner.ts +0 -598
  78. package/src/triggers/agentdojo.ts +0 -253
  79. package/src/triggers/composio.ts +0 -264
  80. package/src/triggers/index.ts +0 -71
  81. package/src/tui/AgentList.tsx +0 -145
  82. package/src/tui/App.tsx +0 -102
  83. package/src/tui/Login.tsx +0 -104
  84. package/src/tui/api.ts +0 -72
  85. package/src/tui/index.tsx +0 -7
  86. package/src/web/App.tsx +0 -455
  87. package/src/web/components/activity/ActivityPage.tsx +0 -314
  88. package/src/web/components/activity/index.ts +0 -1
  89. package/src/web/components/agents/AgentCard.tsx +0 -189
  90. package/src/web/components/agents/AgentPanel.tsx +0 -2244
  91. package/src/web/components/agents/AgentsView.tsx +0 -180
  92. package/src/web/components/agents/CreateAgentModal.tsx +0 -475
  93. package/src/web/components/agents/index.ts +0 -4
  94. package/src/web/components/api/ApiDocsPage.tsx +0 -842
  95. package/src/web/components/auth/CreateAccountStep.tsx +0 -176
  96. package/src/web/components/auth/LoginPage.tsx +0 -91
  97. package/src/web/components/auth/index.ts +0 -2
  98. package/src/web/components/common/Icons.tsx +0 -250
  99. package/src/web/components/common/LoadingSpinner.tsx +0 -44
  100. package/src/web/components/common/Modal.tsx +0 -199
  101. package/src/web/components/common/Select.tsx +0 -97
  102. package/src/web/components/common/index.ts +0 -20
  103. package/src/web/components/connections/ConnectionsPage.tsx +0 -54
  104. package/src/web/components/connections/IntegrationsTab.tsx +0 -170
  105. package/src/web/components/connections/OverviewTab.tsx +0 -137
  106. package/src/web/components/connections/TriggersTab.tsx +0 -1346
  107. package/src/web/components/dashboard/Dashboard.tsx +0 -572
  108. package/src/web/components/dashboard/index.ts +0 -1
  109. package/src/web/components/index.ts +0 -21
  110. package/src/web/components/layout/ErrorBanner.tsx +0 -18
  111. package/src/web/components/layout/Header.tsx +0 -332
  112. package/src/web/components/layout/Sidebar.tsx +0 -231
  113. package/src/web/components/layout/index.ts +0 -3
  114. package/src/web/components/mcp/IntegrationsPanel.tsx +0 -857
  115. package/src/web/components/mcp/McpPage.tsx +0 -2515
  116. package/src/web/components/mcp/index.ts +0 -1
  117. package/src/web/components/meta-agent/MetaAgent.tsx +0 -245
  118. package/src/web/components/onboarding/OnboardingWizard.tsx +0 -404
  119. package/src/web/components/onboarding/index.ts +0 -1
  120. package/src/web/components/settings/SettingsPage.tsx +0 -2776
  121. package/src/web/components/settings/index.ts +0 -1
  122. package/src/web/components/skills/SkillsPage.tsx +0 -1200
  123. package/src/web/components/tasks/TasksPage.tsx +0 -1116
  124. package/src/web/components/tasks/index.ts +0 -1
  125. package/src/web/components/telemetry/TelemetryPage.tsx +0 -1129
  126. package/src/web/components/tests/TestsPage.tsx +0 -594
  127. package/src/web/components/threads/ThreadsPage.tsx +0 -315
  128. package/src/web/context/AuthContext.tsx +0 -242
  129. package/src/web/context/ProjectContext.tsx +0 -214
  130. package/src/web/context/TelemetryContext.tsx +0 -299
  131. package/src/web/context/ThemeContext.tsx +0 -90
  132. package/src/web/context/UIModeContext.tsx +0 -49
  133. package/src/web/context/index.ts +0 -12
  134. package/src/web/hooks/index.ts +0 -3
  135. package/src/web/hooks/useAgents.ts +0 -115
  136. package/src/web/hooks/useOnboarding.ts +0 -20
  137. package/src/web/hooks/useProviders.ts +0 -75
  138. package/src/web/icon.png +0 -0
  139. package/src/web/index.html +0 -16
  140. package/src/web/styles.css +0 -118
  141. package/src/web/themes.ts +0 -162
  142. package/src/web/types.ts +0 -298
@@ -1,2244 +0,0 @@
1
- import React, { useState, useEffect } from "react";
2
- import { Chat, convertApiMessages } from "@apteva/apteva-kit";
3
- import { CloseIcon, MemoryIcon, TasksIcon, VisionIcon, OperatorIcon, McpIcon, RealtimeIcon, FilesIcon, MultiAgentIcon, RecurringIcon, ScheduledIcon, TaskOnceIcon } from "../common/Icons";
4
- import { formatCron, formatRelativeTime, TrajectoryView } from "../tasks/TasksPage";
5
- import { Select } from "../common/Select";
6
- import { useConfirm } from "../common/Modal";
7
- import { useTelemetry, useTheme, useUIMode } from "../../context";
8
- import { useAuth } from "../../context";
9
- import type { Agent, Provider, AgentFeatures, McpServer, SkillSummary, MultiAgentConfig, OperatorConfig, RealtimeConfig, Task } from "../../types";
10
- import { getMultiAgentConfig, getOperatorConfig, getRealtimeConfig, isRealtimeEnabled, REALTIME_PROVIDERS } from "../../types";
11
-
12
- type Tab = "chat" | "threads" | "tasks" | "memory" | "files" | "settings";
13
-
14
- interface AgentPanelProps {
15
- agent: Agent;
16
- providers: Provider[];
17
- onClose: () => void;
18
- onStartAgent: (e?: React.MouseEvent) => void;
19
- onUpdateAgent: (updates: Partial<Agent>) => Promise<{ error?: string }>;
20
- onDeleteAgent: () => void;
21
- }
22
-
23
- const FEATURE_CONFIG = [
24
- { key: "memory" as keyof AgentFeatures, label: "Memory", description: "Persistent recall", icon: MemoryIcon },
25
- { key: "tasks" as keyof AgentFeatures, label: "Tasks", description: "Schedule and execute tasks", icon: TasksIcon },
26
- { key: "files" as keyof AgentFeatures, label: "Files", description: "File storage and management", icon: FilesIcon },
27
- { key: "vision" as keyof AgentFeatures, label: "Vision", description: "Process images and PDFs", icon: VisionIcon },
28
- { key: "operator" as keyof AgentFeatures, label: "Operator", description: "Browser automation", icon: OperatorIcon },
29
- { key: "mcp" as keyof AgentFeatures, label: "MCP", description: "External tools/services", icon: McpIcon },
30
- { key: "realtime" as keyof AgentFeatures, label: "Realtime", description: "Voice conversations", icon: RealtimeIcon },
31
- { key: "agents" as keyof AgentFeatures, label: "Multi-Agent", description: "Communicate with peer agents", icon: MultiAgentIcon },
32
- ];
33
-
34
- export function AgentPanel({ agent, providers, onClose, onStartAgent, onUpdateAgent, onDeleteAgent }: AgentPanelProps) {
35
- const [activeTab, setActiveTab] = useState<Tab>("chat");
36
- const { isDev } = useUIMode();
37
-
38
- return (
39
- <div className="w-full h-full flex flex-col overflow-hidden bg-[var(--color-bg)] border-l border-[var(--color-border)]">
40
- {/* Header with tabs */}
41
- <div className="border-b border-[var(--color-border)] flex items-center">
42
- {/* Scrollable tabs */}
43
- <div className="flex-1 overflow-x-auto scrollbar-hide px-2 md:px-4">
44
- <div className="flex gap-1">
45
- <TabButton active={activeTab === "chat"} onClick={() => setActiveTab("chat")}>
46
- Chat
47
- </TabButton>
48
- <TabButton active={activeTab === "threads"} onClick={() => setActiveTab("threads")}>
49
- Threads
50
- </TabButton>
51
- <TabButton active={activeTab === "tasks"} onClick={() => setActiveTab("tasks")}>
52
- Tasks
53
- </TabButton>
54
- <TabButton active={activeTab === "memory"} onClick={() => setActiveTab("memory")}>
55
- Memory
56
- </TabButton>
57
- <TabButton active={activeTab === "files"} onClick={() => setActiveTab("files")}>
58
- Files
59
- </TabButton>
60
- {isDev && (
61
- <TabButton active={activeTab === "settings"} onClick={() => setActiveTab("settings")}>
62
- Settings
63
- </TabButton>
64
- )}
65
- </div>
66
- </div>
67
-
68
- {/* Close button - fixed on right */}
69
- <button
70
- onClick={onClose}
71
- className="text-[var(--color-text-muted)] hover:text-[var(--color-text)] transition p-2 flex-shrink-0 mr-2"
72
- >
73
- <CloseIcon />
74
- </button>
75
- </div>
76
-
77
- {/* Tab content */}
78
- <div className="flex-1 min-h-0 flex flex-col overflow-hidden">
79
- {activeTab === "chat" && (
80
- <ChatTab agent={agent} onStartAgent={onStartAgent} />
81
- )}
82
- {activeTab === "threads" && (
83
- <ThreadsTab agent={agent} />
84
- )}
85
- {activeTab === "tasks" && (
86
- <TasksTab agent={agent} />
87
- )}
88
- {activeTab === "memory" && (
89
- <MemoryTab agent={agent} />
90
- )}
91
- {activeTab === "files" && (
92
- <FilesTab agent={agent} />
93
- )}
94
- {activeTab === "settings" && (
95
- <SettingsTab agent={agent} providers={providers} onUpdateAgent={onUpdateAgent} onDeleteAgent={onDeleteAgent} />
96
- )}
97
- </div>
98
- </div>
99
- );
100
- }
101
-
102
- function TabButton({ active, onClick, children }: { active: boolean; onClick: () => void; children: React.ReactNode }) {
103
- return (
104
- <button
105
- onClick={onClick}
106
- className={`px-4 py-3 text-sm font-medium border-b-2 transition ${
107
- active
108
- ? "border-[var(--color-accent)] text-[var(--color-text)]"
109
- : "border-transparent text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]"
110
- }`}
111
- >
112
- {children}
113
- </button>
114
- );
115
- }
116
-
117
- function ChatTab({ agent, onStartAgent }: { agent: Agent; onStartAgent: (e?: React.MouseEvent) => void }) {
118
- const { theme } = useTheme();
119
- const { accessToken } = useAuth();
120
- if (agent.status === "running" && agent.port) {
121
- return (
122
- <Chat
123
- agentId="default"
124
- apiUrl={`/api/agents/${agent.id}`}
125
- apiKey={accessToken || undefined}
126
- placeholder="Message this agent..."
127
- context={agent.systemPrompt}
128
- variant="terminal"
129
- theme={theme.id as "light" | "dark"}
130
- headerTitle={agent.name}
131
- enableVoice={isRealtimeEnabled(agent.features)}
132
- voiceProvider={getRealtimeConfig(agent.features).ttsProvider}
133
- />
134
- );
135
- }
136
-
137
- return (
138
- <div className="flex-1 flex items-center justify-center text-[var(--color-text-muted)]">
139
- <div className="text-center">
140
- <p className="text-lg mb-2">Agent is not running</p>
141
- <button
142
- onClick={onStartAgent}
143
- className="bg-[#3b82f6]/20 text-[#3b82f6] hover:bg-[#3b82f6]/30 px-4 py-2 rounded font-medium transition"
144
- >
145
- Start Agent
146
- </button>
147
- </div>
148
- </div>
149
- );
150
- }
151
-
152
- interface Thread {
153
- id: string;
154
- title?: string;
155
- created_at: string;
156
- updated_at: string;
157
- message_count?: number;
158
- }
159
-
160
- function ThreadsTab({ agent }: { agent: Agent }) {
161
- const { theme: themeObj } = useTheme();
162
- const [threads, setThreads] = useState<Thread[]>([]);
163
- const [loading, setLoading] = useState(true);
164
- const [error, setError] = useState<string | null>(null);
165
- const [selectedThread, setSelectedThread] = useState<string | null>(null);
166
- const [initialMessages, setInitialMessages] = useState<any[]>([]);
167
- const [loadingMessages, setLoadingMessages] = useState(false);
168
- const { confirm, ConfirmDialog } = useConfirm();
169
-
170
- // Reset state when agent changes
171
- useEffect(() => {
172
- setThreads([]);
173
- setSelectedThread(null);
174
- setError(null);
175
- setLoading(true);
176
- }, [agent.id]);
177
-
178
- useEffect(() => {
179
- if (agent.status !== "running") {
180
- setLoading(false);
181
- return;
182
- }
183
-
184
- const fetchThreads = async () => {
185
- try {
186
- const res = await fetch(`/api/agents/${agent.id}/threads`);
187
- if (!res.ok) throw new Error("Failed to fetch threads");
188
- const data = await res.json();
189
- setThreads(data.threads || []);
190
- setError(null);
191
- } catch (err) {
192
- setError(err instanceof Error ? err.message : "Failed to load threads");
193
- } finally {
194
- setLoading(false);
195
- }
196
- };
197
-
198
- fetchThreads();
199
- }, [agent.id, agent.status]);
200
-
201
- const openThread = async (threadId: string) => {
202
- setLoadingMessages(true);
203
- setSelectedThread(threadId);
204
- try {
205
- const res = await fetch(`/api/agents/${agent.id}/threads/${threadId}/messages`);
206
- if (res.ok) {
207
- const data = await res.json();
208
- setInitialMessages(convertApiMessages(data.messages || []));
209
- } else {
210
- setInitialMessages([]);
211
- }
212
- } catch {
213
- setInitialMessages([]);
214
- }
215
- setLoadingMessages(false);
216
- };
217
-
218
- const deleteThread = async (threadId: string, e: React.MouseEvent) => {
219
- e.stopPropagation();
220
- const confirmed = await confirm("Delete this thread?", { confirmText: "Delete", title: "Delete Thread" });
221
- if (!confirmed) return;
222
-
223
- try {
224
- await fetch(`/api/agents/${agent.id}/threads/${threadId}`, { method: "DELETE" });
225
- setThreads(prev => prev.filter(t => t.id !== threadId));
226
- if (selectedThread === threadId) {
227
- setSelectedThread(null);
228
- }
229
- } catch {
230
- // Ignore errors
231
- }
232
- };
233
-
234
- if (agent.status !== "running") {
235
- return (
236
- <div className="flex-1 flex items-center justify-center text-[var(--color-text-muted)]">
237
- <p>Start the agent to view threads</p>
238
- </div>
239
- );
240
- }
241
-
242
- if (loading) {
243
- return (
244
- <div className="flex-1 flex items-center justify-center text-[var(--color-text-muted)]">
245
- <p>Loading threads...</p>
246
- </div>
247
- );
248
- }
249
-
250
- if (error) {
251
- return (
252
- <div className="flex-1 flex items-center justify-center text-red-400">
253
- <p>{error}</p>
254
- </div>
255
- );
256
- }
257
-
258
- // Show live chat for selected thread
259
- if (selectedThread) {
260
- return (
261
- <>
262
- {ConfirmDialog}
263
- <div className="flex-1 flex flex-col overflow-hidden">
264
- {loadingMessages ? (
265
- <div className="flex-1 flex items-center justify-center text-[var(--color-text-muted)]">Loading messages...</div>
266
- ) : (
267
- <Chat
268
- key={selectedThread}
269
- agentId="default"
270
- apiUrl={`/api/agents/${agent.id}`}
271
- threadId={selectedThread}
272
- initialMessages={initialMessages}
273
- placeholder="Continue this conversation..."
274
- context={agent.systemPrompt}
275
- variant="terminal"
276
- theme={themeObj.id as "light" | "dark"}
277
- showHeader={true}
278
- onHeaderBack={() => { setSelectedThread(null); setInitialMessages([]); }}
279
- />
280
- )}
281
- </div>
282
- </>
283
- );
284
- }
285
-
286
- // Show threads list (full width)
287
- return (
288
- <>
289
- {ConfirmDialog}
290
- <div className="flex-1 overflow-auto">
291
- {threads.length === 0 ? (
292
- <div className="flex items-center justify-center h-full text-[var(--color-text-muted)]">
293
- <p>No conversation threads yet</p>
294
- </div>
295
- ) : (
296
- <div className="divide-y divide-[var(--color-border)]">
297
- {threads.map(thread => (
298
- <div
299
- key={thread.id}
300
- onClick={() => openThread(thread.id)}
301
- className="p-4 cursor-pointer hover:bg-[var(--color-surface)] transition flex items-center justify-between"
302
- >
303
- <div className="flex-1 min-w-0">
304
- <p className="text-sm font-medium truncate">
305
- {thread.title || `Thread ${thread.id.slice(0, 8)}`}
306
- </p>
307
- <p className="text-xs text-[var(--color-text-muted)] mt-1">
308
- {new Date(thread.updated_at || thread.created_at).toLocaleString()}
309
- {thread.message_count !== undefined && ` • ${thread.message_count} messages`}
310
- </p>
311
- </div>
312
- <button
313
- onClick={(e) => deleteThread(thread.id, e)}
314
- className="text-[var(--color-text-muted)] hover:text-red-400 text-lg ml-4"
315
- >
316
- ×
317
- </button>
318
- </div>
319
- ))}
320
- </div>
321
- )}
322
- </div>
323
- </>
324
- );
325
- }
326
-
327
- function TasksTab({ agent }: { agent: Agent }) {
328
- const { authFetch } = useAuth();
329
- const [tasks, setTasks] = useState<Task[]>([]);
330
- const [loading, setLoading] = useState(true);
331
- const [error, setError] = useState<string | null>(null);
332
- const [filter, setFilter] = useState<string>("all");
333
- const [selectedTask, setSelectedTask] = useState<Task | null>(null);
334
- const [loadingTask, setLoadingTask] = useState(false);
335
- const [showCreateForm, setShowCreateForm] = useState(false);
336
- const [executing, setExecuting] = useState(false);
337
- const [deleting, setDeleting] = useState(false);
338
- const [editing, setEditing] = useState(false);
339
- const [saving, setSaving] = useState(false);
340
- const [editForm, setEditForm] = useState({ title: "", description: "", type: "once" as "once" | "recurring", priority: 5, execute_at: "", recurrence: "" });
341
- const { confirm, ConfirmDialog } = useConfirm();
342
- const { events } = useTelemetry({ agent_id: agent.id, category: "task" });
343
-
344
- // Reset state when agent changes
345
- useEffect(() => {
346
- setTasks([]);
347
- setError(null);
348
- setLoading(true);
349
- setSelectedTask(null);
350
- }, [agent.id]);
351
-
352
- const fetchTasks = async () => {
353
- if (agent.status !== "running") {
354
- setLoading(false);
355
- return;
356
- }
357
-
358
- try {
359
- const res = await fetch(`/api/agents/${agent.id}/tasks?status=${filter}`);
360
- if (!res.ok) throw new Error("Failed to fetch tasks");
361
- const data = await res.json();
362
- setTasks(data.tasks || []);
363
- setError(null);
364
- } catch (err) {
365
- setError(err instanceof Error ? err.message : "Failed to load tasks");
366
- } finally {
367
- setLoading(false);
368
- }
369
- };
370
-
371
- const selectTask = async (task: Task) => {
372
- setSelectedTask(task);
373
- setLoadingTask(true);
374
- try {
375
- const res = await authFetch(`/api/tasks/${task.agentId || agent.id}/${task.id}`);
376
- if (res.ok) {
377
- const data = await res.json();
378
- if (data.task) {
379
- setSelectedTask({ ...data.task, agentId: task.agentId || agent.id, agentName: task.agentName || agent.name });
380
- }
381
- }
382
- } catch (e) {
383
- console.error("Failed to fetch task details:", e);
384
- } finally {
385
- setLoadingTask(false);
386
- }
387
- };
388
-
389
- const handleExecuteTask = async () => {
390
- if (!selectedTask || executing) return;
391
- setExecuting(true);
392
- try {
393
- await authFetch(`/api/tasks/${agent.id}/${selectedTask.id}/execute`, { method: "POST" });
394
- setSelectedTask(null);
395
- fetchTasks();
396
- } catch (e) {
397
- console.error("Failed to execute task:", e);
398
- } finally {
399
- setExecuting(false);
400
- }
401
- };
402
-
403
- const handleDeleteTask = async () => {
404
- if (!selectedTask || deleting) return;
405
- const ok = await confirm(`Are you sure you want to delete "${selectedTask.title}"?`, {
406
- title: "Delete Task",
407
- confirmText: "Delete",
408
- confirmVariant: "danger",
409
- });
410
- if (!ok) return;
411
- setDeleting(true);
412
- try {
413
- await authFetch(`/api/tasks/${agent.id}/${selectedTask.id}`, { method: "DELETE" });
414
- setSelectedTask(null);
415
- fetchTasks();
416
- } catch (e) {
417
- console.error("Failed to delete task:", e);
418
- } finally {
419
- setDeleting(false);
420
- }
421
- };
422
-
423
- const handleCreateTask = async (data: { title: string; description: string; type: string; priority: number; execute_at?: string; recurrence?: string }) => {
424
- try {
425
- const body: Record<string, unknown> = { ...data };
426
- if (data.execute_at) body.execute_at = new Date(data.execute_at).toISOString();
427
- const res = await authFetch(`/api/tasks/${agent.id}`, {
428
- method: "POST",
429
- headers: { "Content-Type": "application/json" },
430
- body: JSON.stringify(body),
431
- });
432
- if (res.ok) {
433
- setShowCreateForm(false);
434
- fetchTasks();
435
- }
436
- } catch (e) {
437
- console.error("Failed to create task:", e);
438
- }
439
- };
440
-
441
- const startEditing = (task: Task) => {
442
- setEditForm({
443
- title: task.title,
444
- description: task.description || "",
445
- type: task.type as "once" | "recurring",
446
- priority: task.priority,
447
- execute_at: task.execute_at ? new Date(task.execute_at).toISOString().slice(0, 16) : "",
448
- recurrence: task.recurrence || "",
449
- });
450
- setEditing(true);
451
- };
452
-
453
- const handleUpdateTask = async () => {
454
- if (!selectedTask || saving) return;
455
- setSaving(true);
456
- try {
457
- const body: Record<string, unknown> = {
458
- title: editForm.title.trim(),
459
- description: editForm.description.trim() || undefined,
460
- type: editForm.type,
461
- priority: editForm.priority,
462
- };
463
- if (editForm.type === "once" && editForm.execute_at) {
464
- body.execute_at = new Date(editForm.execute_at).toISOString();
465
- }
466
- if (editForm.type === "recurring" && editForm.recurrence.trim()) {
467
- body.recurrence = editForm.recurrence.trim();
468
- }
469
- const res = await authFetch(`/api/tasks/${agent.id}/${selectedTask.id}`, {
470
- method: "PUT",
471
- headers: { "Content-Type": "application/json" },
472
- body: JSON.stringify(body),
473
- });
474
- if (res.ok) {
475
- setEditing(false);
476
- setSelectedTask(null);
477
- fetchTasks();
478
- }
479
- } catch (e) {
480
- console.error("Failed to update task:", e);
481
- } finally {
482
- setSaving(false);
483
- }
484
- };
485
-
486
- // Refetch when agent changes, filter changes, or task telemetry arrives
487
- useEffect(() => {
488
- setLoading(true);
489
- fetchTasks();
490
- }, [agent.id, agent.status, filter, events.length]);
491
-
492
- const statusColors: Record<string, string> = {
493
- pending: "bg-yellow-500/20 text-yellow-400",
494
- running: "bg-blue-500/20 text-blue-400",
495
- completed: "bg-green-500/20 text-green-400",
496
- failed: "bg-red-500/20 text-red-400",
497
- cancelled: "bg-gray-500/20 text-gray-400",
498
- };
499
-
500
- if (agent.status !== "running") {
501
- return (
502
- <div className="flex-1 flex items-center justify-center text-[var(--color-text-muted)]">
503
- <p>Start the agent to view tasks</p>
504
- </div>
505
- );
506
- }
507
-
508
- if (!agent.features?.tasks) {
509
- return (
510
- <div className="flex-1 flex items-center justify-center text-[var(--color-text-muted)]">
511
- <div className="text-center">
512
- <p className="mb-2">Tasks feature is not enabled</p>
513
- <p className="text-sm">Enable it in Settings to schedule tasks</p>
514
- </div>
515
- </div>
516
- );
517
- }
518
-
519
- if (loading) {
520
- return (
521
- <div className="flex-1 flex items-center justify-center text-[var(--color-text-muted)]">
522
- <p>Loading tasks...</p>
523
- </div>
524
- );
525
- }
526
-
527
- if (error) {
528
- return (
529
- <div className="flex-1 flex items-center justify-center text-red-400">
530
- <p>{error}</p>
531
- </div>
532
- );
533
- }
534
-
535
- const filterOptions = [
536
- { value: "all", label: "All" },
537
- { value: "pending", label: "Pending" },
538
- { value: "running", label: "Running" },
539
- { value: "completed", label: "Completed" },
540
- { value: "failed", label: "Failed" },
541
- ];
542
-
543
- // Show task detail view when a task is selected
544
- if (selectedTask) {
545
- return (
546
- <div className="flex-1 flex flex-col overflow-hidden">
547
- {ConfirmDialog}
548
- {/* Back button + actions */}
549
- <div className="px-4 pt-3 pb-2 border-b border-[var(--color-border)] shrink-0 flex items-center justify-between">
550
- <button
551
- onClick={() => { setSelectedTask(null); setEditing(false); }}
552
- className="text-sm text-[var(--color-text-muted)] hover:text-[var(--color-text)] transition flex items-center gap-1"
553
- >
554
- <span>←</span> {editing ? "Cancel" : "Back to tasks"}
555
- </button>
556
- <div className="flex items-center gap-2">
557
- {editing ? (
558
- <>
559
- <button
560
- onClick={() => setEditing(false)}
561
- className="text-[var(--color-text-muted)] hover:text-[var(--color-text)] text-sm transition"
562
- >
563
- Cancel
564
- </button>
565
- <button
566
- onClick={handleUpdateTask}
567
- disabled={saving || !editForm.title.trim()}
568
- className="px-3 py-1 rounded text-sm bg-[var(--color-accent)] text-black hover:opacity-90 transition disabled:opacity-50"
569
- >
570
- {saving ? "Saving..." : "Save"}
571
- </button>
572
- </>
573
- ) : (
574
- <>
575
- {(selectedTask.status === "pending" || selectedTask.status === "completed" || selectedTask.status === "failed") && (
576
- <button
577
- onClick={() => startEditing(selectedTask)}
578
- title="Edit task"
579
- className="text-[var(--color-text-muted)] hover:text-[var(--color-accent)] transition"
580
- >
581
- <svg className="w-4.5 h-4.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" /></svg>
582
- </button>
583
- )}
584
- {(selectedTask.status === "pending" || selectedTask.status === "completed") && (
585
- <button
586
- onClick={handleExecuteTask}
587
- disabled={executing}
588
- title="Execute now"
589
- className="text-[var(--color-accent)] hover:opacity-80 transition disabled:opacity-50"
590
- >
591
- <svg className="w-4.5 h-4.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" /><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
592
- </button>
593
- )}
594
- <button
595
- onClick={handleDeleteTask}
596
- disabled={deleting}
597
- title="Delete task"
598
- className="text-red-400 hover:text-red-300 transition disabled:opacity-50"
599
- >
600
- <svg className="w-4.5 h-4.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg>
601
- </button>
602
- </>
603
- )}
604
- </div>
605
- </div>
606
-
607
- {/* Task detail content */}
608
- <div className="flex-1 overflow-auto p-4 space-y-4">
609
- {(() => {
610
- const inputClass = "w-full bg-[var(--color-bg)] border border-[var(--color-border-light)] rounded px-2 py-1.5 text-sm focus:outline-none focus:border-[var(--color-accent)] text-[var(--color-text)]";
611
- return <>
612
- {/* Title & Status */}
613
- <div>
614
- <div className="flex items-start justify-between gap-2 mb-1">
615
- {editing ? (
616
- <input
617
- type="text"
618
- value={editForm.title}
619
- onChange={e => setEditForm({ ...editForm, title: e.target.value })}
620
- className={`${inputClass} text-lg font-medium`}
621
- placeholder="Task title"
622
- />
623
- ) : (
624
- <h3 className="text-lg font-medium">{selectedTask.title}</h3>
625
- )}
626
- {!editing && (
627
- <span className={`px-2 py-1 rounded text-xs font-medium flex-shrink-0 ${statusColors[selectedTask.status]}`}>
628
- {selectedTask.status}
629
- </span>
630
- )}
631
- </div>
632
- </div>
633
-
634
- {/* Description */}
635
- {editing ? (
636
- <div>
637
- <h4 className="text-xs text-[var(--color-text-muted)] uppercase tracking-wider mb-1">Description</h4>
638
- <textarea
639
- value={editForm.description}
640
- onChange={e => setEditForm({ ...editForm, description: e.target.value })}
641
- className={`${inputClass} resize-none`}
642
- rows={3}
643
- placeholder="Task description..."
644
- />
645
- </div>
646
- ) : selectedTask.description ? (
647
- <div>
648
- <h4 className="text-xs text-[var(--color-text-muted)] uppercase tracking-wider mb-1">Description</h4>
649
- <p className="text-sm text-[var(--color-text-secondary)] whitespace-pre-wrap">{selectedTask.description}</p>
650
- </div>
651
- ) : null}
652
-
653
- {/* Metadata */}
654
- {editing ? (
655
- <div className="grid grid-cols-2 gap-3">
656
- <div>
657
- <label className="text-xs text-[var(--color-text-muted)] uppercase tracking-wider mb-1 block">Type</label>
658
- <select
659
- value={editForm.type}
660
- onChange={e => setEditForm({ ...editForm, type: e.target.value as "once" | "recurring" })}
661
- className={inputClass}
662
- >
663
- <option value="once">One-time</option>
664
- <option value="recurring">Recurring</option>
665
- </select>
666
- </div>
667
- <div>
668
- <label className="text-xs text-[var(--color-text-muted)] uppercase tracking-wider mb-1 block">Priority</label>
669
- <input
670
- type="number"
671
- min={1}
672
- max={10}
673
- value={editForm.priority}
674
- onChange={e => setEditForm({ ...editForm, priority: Number(e.target.value) })}
675
- className={inputClass}
676
- />
677
- </div>
678
- </div>
679
- ) : (
680
- <div className="grid grid-cols-2 gap-3 text-sm">
681
- <div>
682
- <span className="text-[var(--color-text-muted)]">Type</span>
683
- <p className="capitalize">{selectedTask.type}</p>
684
- </div>
685
- <div>
686
- <span className="text-[var(--color-text-muted)]">Priority</span>
687
- <p>{selectedTask.priority}</p>
688
- </div>
689
- {selectedTask.recurrence && (
690
- <div>
691
- <span className="text-[var(--color-text-muted)]">Recurrence</span>
692
- <p>{formatCron(selectedTask.recurrence)}</p>
693
- <p className="text-xs text-[var(--color-text-faint)] mt-0.5 font-mono">{selectedTask.recurrence}</p>
694
- </div>
695
- )}
696
- </div>
697
- )}
698
-
699
- {/* Schedule (edit mode) */}
700
- {editing && editForm.type === "once" && (
701
- <div>
702
- <label className="text-xs text-[var(--color-text-muted)] uppercase tracking-wider mb-1 block">Schedule</label>
703
- <input
704
- type="datetime-local"
705
- value={editForm.execute_at}
706
- onChange={e => setEditForm({ ...editForm, execute_at: e.target.value })}
707
- className={inputClass}
708
- />
709
- </div>
710
- )}
711
- {editing && editForm.type === "recurring" && (
712
- <div>
713
- <label className="text-xs text-[var(--color-text-muted)] uppercase tracking-wider mb-1 block">Cron Schedule</label>
714
- <input
715
- type="text"
716
- value={editForm.recurrence}
717
- onChange={e => setEditForm({ ...editForm, recurrence: e.target.value })}
718
- className={`${inputClass} font-mono`}
719
- placeholder="*/30 * * * *"
720
- />
721
- <p className="text-xs text-[var(--color-text-faint)] mt-1">e.g. */30 * * * * = every 30 min</p>
722
- </div>
723
- )}
724
-
725
- {/* Timestamps (view mode only) */}
726
- {!editing && (
727
- <div className="space-y-2 text-sm">
728
- <div className="flex justify-between">
729
- <span className="text-[var(--color-text-muted)]">Created</span>
730
- <span>{new Date(selectedTask.created_at).toLocaleString()}</span>
731
- </div>
732
- {selectedTask.execute_at && (
733
- <div className="flex justify-between">
734
- <span className="text-[var(--color-text-muted)]">Scheduled</span>
735
- <span className="text-[var(--color-accent)]">{formatRelativeTime(selectedTask.execute_at)}</span>
736
- </div>
737
- )}
738
- {selectedTask.executed_at && (
739
- <div className="flex justify-between">
740
- <span className="text-[var(--color-text-muted)]">Started</span>
741
- <span>{new Date(selectedTask.executed_at).toLocaleString()}</span>
742
- </div>
743
- )}
744
- {selectedTask.completed_at && (
745
- <div className="flex justify-between">
746
- <span className="text-[var(--color-text-muted)]">Completed</span>
747
- <span>{new Date(selectedTask.completed_at).toLocaleString()}</span>
748
- </div>
749
- )}
750
- {selectedTask.next_run && (
751
- <div className="flex justify-between">
752
- <span className="text-[var(--color-text-muted)]">Next Run</span>
753
- <span className="text-[var(--color-accent)]">{formatRelativeTime(selectedTask.next_run)}</span>
754
- </div>
755
- )}
756
- </div>
757
- )}
758
-
759
- {/* Error */}
760
- {!editing && selectedTask.status === "failed" && selectedTask.error && (
761
- <div className="min-w-0">
762
- <h4 className="text-xs text-red-400 uppercase tracking-wider mb-1">Error</h4>
763
- <div className="bg-red-500/10 border border-red-500/20 rounded p-3 overflow-x-auto">
764
- <pre className="text-sm text-red-400 whitespace-pre-wrap break-words">{selectedTask.error}</pre>
765
- </div>
766
- </div>
767
- )}
768
-
769
- {/* Result */}
770
- {!editing && selectedTask.status === "completed" && selectedTask.result && (
771
- <div className="min-w-0">
772
- <h4 className="text-xs text-green-400 uppercase tracking-wider mb-1">Result</h4>
773
- <div className="bg-green-500/10 border border-green-500/20 rounded p-3 overflow-x-auto">
774
- <pre className="text-sm text-green-400 whitespace-pre-wrap break-words">
775
- {typeof selectedTask.result === "string" ? selectedTask.result : JSON.stringify(selectedTask.result, null, 2)}
776
- </pre>
777
- </div>
778
- </div>
779
- )}
780
-
781
- {/* Trajectory */}
782
- {!editing && loadingTask && !selectedTask.trajectory && (
783
- <div>
784
- <h4 className="text-xs text-[var(--color-text-muted)] uppercase tracking-wider mb-2">Trajectory</h4>
785
- <div className="text-sm text-[var(--color-text-faint)]">Loading trajectory...</div>
786
- </div>
787
- )}
788
- {!editing && selectedTask.trajectory && selectedTask.trajectory.length > 0 && (
789
- <div>
790
- <h4 className="text-xs text-[var(--color-text-muted)] uppercase tracking-wider mb-2">
791
- Trajectory ({selectedTask.trajectory.length} steps)
792
- </h4>
793
- <TrajectoryView trajectory={selectedTask.trajectory} />
794
- </div>
795
- )}
796
- </>;
797
- })()}
798
- </div>
799
- </div>
800
- );
801
- }
802
-
803
- return (
804
- <div className="flex-1 overflow-auto p-4">
805
- {/* Create Task Button + Filter tabs */}
806
- <div className="flex items-center justify-between mb-4">
807
- <div className="flex gap-2">
808
- {filterOptions.map(opt => (
809
- <button
810
- key={opt.value}
811
- onClick={() => setFilter(opt.value)}
812
- className={`px-3 py-1.5 rounded text-sm transition ${
813
- filter === opt.value
814
- ? "bg-[var(--color-accent)] text-black"
815
- : "bg-[var(--color-surface-raised)] hover:bg-[var(--color-surface-raised)]"
816
- }`}
817
- >
818
- {opt.label}
819
- </button>
820
- ))}
821
- </div>
822
- <button
823
- onClick={() => setShowCreateForm(!showCreateForm)}
824
- className="px-3 py-1.5 rounded text-sm bg-[var(--color-accent)] text-black hover:opacity-90 transition flex items-center gap-1 flex-shrink-0"
825
- >
826
- <svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" /></svg>
827
- New
828
- </button>
829
- </div>
830
-
831
- {/* Inline Create Form */}
832
- {showCreateForm && (
833
- <AgentCreateTaskForm
834
- onSubmit={handleCreateTask}
835
- onCancel={() => setShowCreateForm(false)}
836
- />
837
- )}
838
-
839
- {tasks.length === 0 ? (
840
- <div className="text-center py-10">
841
- <TasksIcon className="w-10 h-10 mx-auto mb-3 text-[var(--color-border-light)]" />
842
- <p className="text-[var(--color-text-muted)]">No {filter === "all" ? "" : filter + " "}tasks</p>
843
- <p className="text-sm text-[var(--color-text-faint)] mt-1">Tasks will appear here when created</p>
844
- </div>
845
- ) : (
846
- <div className="space-y-3">
847
- {tasks.map(task => (
848
- <div
849
- key={task.id}
850
- onClick={() => selectTask(task)}
851
- className="bg-[var(--color-surface)] card p-4 cursor-pointer hover:border-[var(--color-border-light)] transition"
852
- >
853
- <div className="flex items-start justify-between mb-2">
854
- <div className="flex-1 min-w-0">
855
- <h3 className="font-medium">{task.title || task.name}</h3>
856
- </div>
857
- <span className={`px-2 py-1 rounded text-xs font-medium ml-2 ${statusColors[task.status] || statusColors.pending}`}>
858
- {task.status}
859
- </span>
860
- </div>
861
-
862
- {task.description && (
863
- <p className="text-sm text-[var(--color-text-secondary)] mb-2 line-clamp-2">{task.description}</p>
864
- )}
865
-
866
- <div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-[var(--color-text-faint)]">
867
- <span className="flex items-center gap-1">
868
- {task.type === "recurring"
869
- ? <RecurringIcon className="w-3.5 h-3.5" />
870
- : task.execute_at
871
- ? <ScheduledIcon className="w-3.5 h-3.5" />
872
- : <TaskOnceIcon className="w-3.5 h-3.5" />
873
- }
874
- {task.type === "recurring" && task.recurrence ? formatCron(task.recurrence) : task.type || "once"}
875
- </span>
876
- {task.priority !== undefined && (
877
- <span>Priority: {task.priority}</span>
878
- )}
879
- {task.next_run && (
880
- <span className="text-[var(--color-accent)]">{formatRelativeTime(task.next_run)}</span>
881
- )}
882
- {!task.next_run && task.execute_at && (
883
- <span className="text-[var(--color-accent)]">{formatRelativeTime(task.execute_at)}</span>
884
- )}
885
- <span>Created: {new Date(task.created_at).toLocaleDateString()}</span>
886
- </div>
887
-
888
- {task.status === "completed" && task.result && (
889
- <div className="mt-3 bg-green-500/10 border border-green-500/20 rounded p-3">
890
- <h4 className="text-xs text-green-400 uppercase tracking-wider mb-1">Result</h4>
891
- <pre className="text-sm text-green-400 whitespace-pre-wrap break-words">
892
- {typeof task.result === "string" ? task.result : JSON.stringify(task.result, null, 2)}
893
- </pre>
894
- </div>
895
- )}
896
-
897
- {task.status === "failed" && task.error && (
898
- <div className="mt-3 bg-red-500/10 border border-red-500/20 rounded p-3">
899
- <h4 className="text-xs text-red-400 uppercase tracking-wider mb-1">Error</h4>
900
- <pre className="text-sm text-red-400 whitespace-pre-wrap break-words">{task.error}</pre>
901
- </div>
902
- )}
903
- </div>
904
- ))}
905
- </div>
906
- )}
907
- </div>
908
- );
909
- }
910
-
911
- function AgentCreateTaskForm({ onSubmit, onCancel }: {
912
- onSubmit: (data: { title: string; description: string; type: string; priority: number; execute_at?: string; recurrence?: string }) => void;
913
- onCancel: () => void;
914
- }) {
915
- const [title, setTitle] = useState("");
916
- const [description, setDescription] = useState("");
917
- const [type, setType] = useState("once");
918
- const [priority, setPriority] = useState(5);
919
- const [executeAt, setExecuteAt] = useState("");
920
- const [recurrence, setRecurrence] = useState("");
921
-
922
- return (
923
- <div className="bg-[var(--color-surface)] border border-[var(--color-accent)]/30 rounded-lg p-3 mb-4 space-y-3">
924
- <input
925
- type="text"
926
- value={title}
927
- onChange={e => setTitle(e.target.value)}
928
- className="w-full bg-[var(--color-bg)] border border-[var(--color-border)] rounded px-3 py-1.5 text-sm"
929
- placeholder="Task title..."
930
- autoFocus
931
- />
932
- <textarea
933
- value={description}
934
- onChange={e => setDescription(e.target.value)}
935
- className="w-full bg-[var(--color-bg)] border border-[var(--color-border)] rounded px-3 py-1.5 text-sm resize-none"
936
- rows={2}
937
- placeholder="Description (optional)..."
938
- />
939
- <div className="grid grid-cols-2 gap-2">
940
- <select
941
- value={type}
942
- onChange={e => setType(e.target.value)}
943
- className="bg-[var(--color-bg)] border border-[var(--color-border)] rounded px-2 py-1.5 text-sm"
944
- >
945
- <option value="once">One-time</option>
946
- <option value="recurring">Recurring</option>
947
- </select>
948
- <input
949
- type="number"
950
- min={1}
951
- max={10}
952
- value={priority}
953
- onChange={e => setPriority(Number(e.target.value))}
954
- className="bg-[var(--color-bg)] border border-[var(--color-border)] rounded px-2 py-1.5 text-sm"
955
- placeholder="Priority"
956
- />
957
- </div>
958
- {type === "once" && (
959
- <input
960
- type="datetime-local"
961
- value={executeAt}
962
- onChange={e => setExecuteAt(e.target.value)}
963
- className="w-full bg-[var(--color-bg)] border border-[var(--color-border)] rounded px-3 py-1.5 text-sm"
964
- />
965
- )}
966
- {type === "recurring" && (
967
- <input
968
- type="text"
969
- value={recurrence}
970
- onChange={e => setRecurrence(e.target.value)}
971
- className="w-full bg-[var(--color-bg)] border border-[var(--color-border)] rounded px-3 py-1.5 text-sm font-mono"
972
- placeholder="*/30 * * * * (cron)"
973
- />
974
- )}
975
- <div className="flex justify-end gap-2">
976
- <button onClick={onCancel} className="px-3 py-1.5 rounded text-sm bg-[var(--color-surface-raised)] hover:bg-[var(--color-border)] transition">Cancel</button>
977
- <button
978
- onClick={() => title.trim() && onSubmit({ title: title.trim(), description: description.trim(), type, priority, execute_at: executeAt || undefined, recurrence: recurrence || undefined })}
979
- disabled={!title.trim()}
980
- className="px-3 py-1.5 rounded text-sm bg-[var(--color-accent)] text-black hover:opacity-90 transition disabled:opacity-50"
981
- >Create</button>
982
- </div>
983
- </div>
984
- );
985
- }
986
-
987
- interface Memory {
988
- id: string;
989
- content: string;
990
- type: string;
991
- importance: number;
992
- thread_id?: string;
993
- created_at: string;
994
- }
995
-
996
- function MemoryTab({ agent }: { agent: Agent }) {
997
- const [memories, setMemories] = useState<Memory[]>([]);
998
- const [loading, setLoading] = useState(true);
999
- const [error, setError] = useState<string | null>(null);
1000
- const [enabled, setEnabled] = useState(false);
1001
- const { confirm, ConfirmDialog } = useConfirm();
1002
-
1003
- // Reset state when agent changes
1004
- useEffect(() => {
1005
- setMemories([]);
1006
- setError(null);
1007
- setLoading(true);
1008
- }, [agent.id]);
1009
-
1010
- const fetchMemories = async () => {
1011
- if (agent.status !== "running") {
1012
- setLoading(false);
1013
- return;
1014
- }
1015
-
1016
- try {
1017
- const res = await fetch(`/api/agents/${agent.id}/memories`);
1018
- if (!res.ok) throw new Error("Failed to fetch memories");
1019
- const data = await res.json();
1020
- setMemories(data.memories || []);
1021
- setEnabled(data.enabled ?? false);
1022
- setError(null);
1023
- } catch (err) {
1024
- setError(err instanceof Error ? err.message : "Failed to load memories");
1025
- } finally {
1026
- setLoading(false);
1027
- }
1028
- };
1029
-
1030
- useEffect(() => {
1031
- fetchMemories();
1032
- }, [agent.id, agent.status]);
1033
-
1034
- const deleteMemory = async (memoryId: string) => {
1035
- try {
1036
- await fetch(`/api/agents/${agent.id}/memories/${memoryId}`, { method: "DELETE" });
1037
- setMemories(prev => prev.filter(m => m.id !== memoryId));
1038
- } catch {
1039
- // Ignore errors
1040
- }
1041
- };
1042
-
1043
- const clearAllMemories = async () => {
1044
- const confirmed = await confirm("Clear all memories?", { confirmText: "Clear", title: "Clear Memories" });
1045
- if (!confirmed) return;
1046
- try {
1047
- await fetch(`/api/agents/${agent.id}/memories`, { method: "DELETE" });
1048
- setMemories([]);
1049
- } catch {
1050
- // Ignore errors
1051
- }
1052
- };
1053
-
1054
- if (!agent.features?.memory) {
1055
- return (
1056
- <div className="flex-1 flex items-center justify-center text-[var(--color-text-muted)]">
1057
- <div className="text-center">
1058
- <p className="mb-2">Memory feature is not enabled</p>
1059
- <p className="text-sm">Enable it in Settings to persist knowledge</p>
1060
- </div>
1061
- </div>
1062
- );
1063
- }
1064
-
1065
- if (agent.status !== "running") {
1066
- return (
1067
- <div className="flex-1 flex items-center justify-center text-[var(--color-text-muted)]">
1068
- <p>Start the agent to view memories</p>
1069
- </div>
1070
- );
1071
- }
1072
-
1073
- if (loading) {
1074
- return (
1075
- <div className="flex-1 flex items-center justify-center text-[var(--color-text-muted)]">
1076
- <p>Loading memories...</p>
1077
- </div>
1078
- );
1079
- }
1080
-
1081
- if (error) {
1082
- return (
1083
- <div className="flex-1 flex items-center justify-center text-red-400">
1084
- <p>{error}</p>
1085
- </div>
1086
- );
1087
- }
1088
-
1089
- if (!enabled) {
1090
- return (
1091
- <div className="flex-1 flex items-center justify-center text-[var(--color-text-muted)]">
1092
- <div className="text-center">
1093
- <p className="mb-2">Memory system not initialized</p>
1094
- <p className="text-sm">Check OPENAI_API_KEY for embeddings</p>
1095
- </div>
1096
- </div>
1097
- );
1098
- }
1099
-
1100
- return (
1101
- <>
1102
- {ConfirmDialog}
1103
- <div className="flex-1 overflow-auto p-4">
1104
- <div className="flex items-center justify-between mb-4">
1105
- <h3 className="text-sm font-medium text-[var(--color-text-secondary)]">Stored Memories ({memories.length})</h3>
1106
- {memories.length > 0 && (
1107
- <button
1108
- onClick={clearAllMemories}
1109
- className="text-xs text-red-400 hover:text-red-300"
1110
- >
1111
- Clear All
1112
- </button>
1113
- )}
1114
- </div>
1115
-
1116
- {memories.length === 0 ? (
1117
- <div className="text-center py-10 text-[var(--color-text-muted)]">
1118
- <p>No memories stored yet</p>
1119
- <p className="text-sm mt-1">The agent will remember important information from conversations</p>
1120
- </div>
1121
- ) : (
1122
- <div className="space-y-3">
1123
- {memories.map(memory => (
1124
- <div key={memory.id} className="bg-[var(--color-surface)] border border-[var(--color-border)] rounded p-3">
1125
- <div className="flex items-start justify-between gap-2">
1126
- <p className="text-sm text-[var(--color-text)] flex-1">{memory.content}</p>
1127
- <button
1128
- onClick={() => deleteMemory(memory.id)}
1129
- className="text-[var(--color-text-muted)] hover:text-red-400 text-sm flex-shrink-0"
1130
- >
1131
- ×
1132
- </button>
1133
- </div>
1134
- <div className="flex items-center gap-3 mt-2">
1135
- <span className={`text-xs px-2 py-0.5 rounded ${
1136
- memory.type === "preference"
1137
- ? "bg-purple-500/20 text-purple-400"
1138
- : memory.type === "fact"
1139
- ? "bg-green-500/20 text-green-400"
1140
- : "bg-blue-500/20 text-blue-400"
1141
- }`}>
1142
- {memory.type}
1143
- </span>
1144
- <span className="text-xs text-[var(--color-text-muted)]">
1145
- {new Date(memory.created_at).toLocaleString()}
1146
- </span>
1147
- {memory.importance && (
1148
- <span className="text-xs text-[var(--color-text-faint)]">
1149
- importance: {memory.importance.toFixed(1)}
1150
- </span>
1151
- )}
1152
- </div>
1153
- </div>
1154
- ))}
1155
- </div>
1156
- )}
1157
- </div>
1158
- </>
1159
- );
1160
- }
1161
-
1162
- interface AgentFile {
1163
- id: string;
1164
- filename: string;
1165
- mime_type: string;
1166
- file_type: string;
1167
- size_bytes: number;
1168
- source: string;
1169
- source_tool?: string;
1170
- url?: string;
1171
- created_at: string;
1172
- }
1173
-
1174
- function FilesTab({ agent }: { agent: Agent }) {
1175
- const [files, setFiles] = useState<AgentFile[]>([]);
1176
- const [loading, setLoading] = useState(true);
1177
- const [error, setError] = useState<string | null>(null);
1178
- const [uploading, setUploading] = useState(false);
1179
- const [uploadError, setUploadError] = useState<string | null>(null);
1180
- const [dragOver, setDragOver] = useState(false);
1181
- const fileInputRef = React.useRef<HTMLInputElement>(null);
1182
- const { confirm, ConfirmDialog } = useConfirm();
1183
-
1184
- // Reset state when agent changes
1185
- useEffect(() => {
1186
- setFiles([]);
1187
- setError(null);
1188
- setLoading(true);
1189
- }, [agent.id]);
1190
-
1191
- const fetchFiles = async () => {
1192
- if (agent.status !== "running") {
1193
- setLoading(false);
1194
- return;
1195
- }
1196
-
1197
- try {
1198
- const res = await fetch(`/api/agents/${agent.id}/files`);
1199
- if (!res.ok) throw new Error("Failed to fetch files");
1200
- const data = await res.json();
1201
- setFiles(data.files || []);
1202
- setError(null);
1203
- } catch (err) {
1204
- setError(err instanceof Error ? err.message : "Failed to load files");
1205
- } finally {
1206
- setLoading(false);
1207
- }
1208
- };
1209
-
1210
- useEffect(() => {
1211
- fetchFiles();
1212
- }, [agent.id, agent.status]);
1213
-
1214
- const deleteFile = async (fileId: string) => {
1215
- const confirmed = await confirm("Delete this file?", { confirmText: "Delete", title: "Delete File" });
1216
- if (!confirmed) return;
1217
- try {
1218
- await fetch(`/api/agents/${agent.id}/files/${fileId}`, { method: "DELETE" });
1219
- setFiles(prev => prev.filter(f => f.id !== fileId));
1220
- } catch {
1221
- // Ignore errors
1222
- }
1223
- };
1224
-
1225
- const downloadFile = (fileId: string, filename: string) => {
1226
- const link = document.createElement("a");
1227
- link.href = `/api/agents/${agent.id}/files/${fileId}/download`;
1228
- link.download = filename;
1229
- link.click();
1230
- };
1231
-
1232
- const formatSize = (bytes: number) => {
1233
- if (bytes < 1024) return `${bytes} B`;
1234
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
1235
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
1236
- };
1237
-
1238
- const uploadFile = async (file: File) => {
1239
- setUploading(true);
1240
- setUploadError(null);
1241
- try {
1242
- const formData = new FormData();
1243
- formData.append("file", file);
1244
-
1245
- const res = await fetch(`/api/agents/${agent.id}/files`, {
1246
- method: "POST",
1247
- body: formData,
1248
- });
1249
-
1250
- if (!res.ok) {
1251
- const data = await res.json();
1252
- throw new Error(data.error || "Upload failed");
1253
- }
1254
-
1255
- // Refresh file list
1256
- await fetchFiles();
1257
- } catch (err) {
1258
- setUploadError(err instanceof Error ? err.message : "Upload failed");
1259
- } finally {
1260
- setUploading(false);
1261
- }
1262
- };
1263
-
1264
- const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
1265
- const file = e.target.files?.[0];
1266
- if (file) {
1267
- uploadFile(file);
1268
- }
1269
- // Reset input so same file can be selected again
1270
- e.target.value = "";
1271
- };
1272
-
1273
- const handleDrop = (e: React.DragEvent) => {
1274
- e.preventDefault();
1275
- setDragOver(false);
1276
- const file = e.dataTransfer.files?.[0];
1277
- if (file) {
1278
- uploadFile(file);
1279
- }
1280
- };
1281
-
1282
- const handleDragOver = (e: React.DragEvent) => {
1283
- e.preventDefault();
1284
- setDragOver(true);
1285
- };
1286
-
1287
- const handleDragLeave = (e: React.DragEvent) => {
1288
- e.preventDefault();
1289
- setDragOver(false);
1290
- };
1291
-
1292
- const getFileIcon = (mimeType: string) => {
1293
- if (mimeType.startsWith("image/")) return "🖼";
1294
- if (mimeType.includes("pdf")) return "📕";
1295
- if (mimeType.includes("json")) return "{}";
1296
- if (mimeType.includes("javascript") || mimeType.includes("typescript")) return "⚡";
1297
- if (mimeType.startsWith("text/")) return "📄";
1298
- if (mimeType.startsWith("audio/")) return "🎵";
1299
- if (mimeType.startsWith("video/")) return "🎬";
1300
- return "📁";
1301
- };
1302
-
1303
- if (!agent.features?.files) {
1304
- return (
1305
- <div className="flex-1 flex items-center justify-center text-[var(--color-text-muted)]">
1306
- <div className="text-center">
1307
- <p className="mb-2">Files feature is not enabled</p>
1308
- <p className="text-sm">Enable it in Settings to manage files</p>
1309
- </div>
1310
- </div>
1311
- );
1312
- }
1313
-
1314
- if (agent.status !== "running") {
1315
- return (
1316
- <div className="flex-1 flex items-center justify-center text-[var(--color-text-muted)]">
1317
- <p>Start the agent to view files</p>
1318
- </div>
1319
- );
1320
- }
1321
-
1322
- if (loading) {
1323
- return (
1324
- <div className="flex-1 flex items-center justify-center text-[var(--color-text-muted)]">
1325
- <p>Loading files...</p>
1326
- </div>
1327
- );
1328
- }
1329
-
1330
- if (error) {
1331
- return (
1332
- <div className="flex-1 flex items-center justify-center text-red-400">
1333
- <p>{error}</p>
1334
- </div>
1335
- );
1336
- }
1337
-
1338
- return (
1339
- <>
1340
- {ConfirmDialog}
1341
- <div
1342
- className={`flex-1 overflow-auto p-4 transition ${dragOver ? "bg-[var(--color-accent-5)]" : ""}`}
1343
- onDrop={handleDrop}
1344
- onDragOver={handleDragOver}
1345
- onDragLeave={handleDragLeave}
1346
- >
1347
- {/* Hidden file input */}
1348
- <input
1349
- ref={fileInputRef}
1350
- type="file"
1351
- className="hidden"
1352
- onChange={handleFileSelect}
1353
- />
1354
-
1355
- <div className="flex items-center justify-between mb-4">
1356
- <h3 className="text-sm font-medium text-[var(--color-text-secondary)]">Agent Files ({files.length})</h3>
1357
- <div className="flex items-center gap-2">
1358
- <button
1359
- onClick={() => fileInputRef.current?.click()}
1360
- disabled={uploading}
1361
- className="text-xs bg-[var(--color-accent)] hover:bg-[var(--color-accent-hover)] disabled:opacity-50 text-black px-3 py-1 rounded font-medium transition"
1362
- >
1363
- {uploading ? "Uploading..." : "Upload"}
1364
- </button>
1365
- <button
1366
- onClick={fetchFiles}
1367
- className="text-xs text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]"
1368
- >
1369
- Refresh
1370
- </button>
1371
- </div>
1372
- </div>
1373
-
1374
- {uploadError && (
1375
- <div className="mb-4 text-sm bg-red-500/10 text-red-400 px-3 py-2 rounded">
1376
- {uploadError}
1377
- </div>
1378
- )}
1379
-
1380
- {dragOver && (
1381
- <div className="mb-4 border-2 border-dashed border-[var(--color-accent)] rounded-lg p-8 text-center">
1382
- <p className="text-[var(--color-accent)]">Drop file to upload</p>
1383
- </div>
1384
- )}
1385
-
1386
- {files.length === 0 && !dragOver && (
1387
- <div className="text-center py-10 text-[var(--color-text-muted)]">
1388
- <p>No files stored yet</p>
1389
- <p className="text-sm mt-1">Drop files here, click Upload, or attach files in Chat</p>
1390
- {agent.features?.memory && (
1391
- <p className="text-xs mt-2 text-[var(--color-text-faint)]">Files will be auto-ingested into memory</p>
1392
- )}
1393
- </div>
1394
- )}
1395
-
1396
- {files.length > 0 && (
1397
- <div className="space-y-2">
1398
- {files.map(file => (
1399
- <div key={file.id} className="bg-[var(--color-surface)] border border-[var(--color-border)] rounded p-3 flex items-center gap-3">
1400
- <div className="w-10 h-10 bg-[var(--color-surface-raised)] rounded flex items-center justify-center text-[var(--color-text-muted)]">
1401
- {getFileIcon(file.mime_type)}
1402
- </div>
1403
- <div className="flex-1 min-w-0">
1404
- <p className="text-sm text-[var(--color-text)] truncate">{file.filename}</p>
1405
- <p className="text-xs text-[var(--color-text-muted)]">
1406
- {formatSize(file.size_bytes)} • {new Date(file.created_at).toLocaleString()}
1407
- {file.source && file.source !== "upload" && ` • ${file.source}`}
1408
- </p>
1409
- </div>
1410
- <div className="flex items-center gap-2">
1411
- <button
1412
- onClick={() => downloadFile(file.id, file.filename)}
1413
- className="text-xs text-[var(--color-text-muted)] hover:text-[var(--color-accent)] px-2 py-1"
1414
- >
1415
-
1416
- </button>
1417
- <button
1418
- onClick={() => deleteFile(file.id)}
1419
- className="text-[var(--color-text-muted)] hover:text-red-400 text-sm"
1420
- >
1421
- ×
1422
- </button>
1423
- </div>
1424
- </div>
1425
- ))}
1426
- </div>
1427
- )}
1428
- </div>
1429
- </>
1430
- );
1431
- }
1432
-
1433
- interface AvailableSkill {
1434
- id: string;
1435
- name: string;
1436
- description: string;
1437
- version: string;
1438
- enabled: boolean;
1439
- project_id: string | null;
1440
- }
1441
-
1442
- function SettingsTab({ agent, providers, onUpdateAgent, onDeleteAgent }: {
1443
- agent: Agent;
1444
- providers: Provider[];
1445
- onUpdateAgent: (updates: Partial<Agent>) => Promise<{ error?: string }>;
1446
- onDeleteAgent: () => void;
1447
- }) {
1448
- const { authFetch, isDev } = useAuth();
1449
- const [form, setForm] = useState({
1450
- name: agent.name,
1451
- provider: agent.provider,
1452
- model: agent.model,
1453
- systemPrompt: agent.systemPrompt,
1454
- features: {
1455
- ...agent.features,
1456
- builtinTools: agent.features.builtinTools || { webSearch: false, webFetch: false },
1457
- },
1458
- mcpServers: [...(agent.mcpServers || [])],
1459
- skills: [...(agent.skills || [])],
1460
- });
1461
- const [saving, setSaving] = useState(false);
1462
- const [confirmDelete, setConfirmDelete] = useState(false);
1463
- const [message, setMessage] = useState<{ type: "success" | "error"; text: string } | null>(null);
1464
- const [availableMcpServers, setAvailableMcpServers] = useState<McpServer[]>([]);
1465
- const [availableSkills, setAvailableSkills] = useState<AvailableSkill[]>([]);
1466
- const [ollamaModels, setOllamaModels] = useState<Array<{ value: string; label: string }>>([]);
1467
- const [loadingOllamaModels, setLoadingOllamaModels] = useState(false);
1468
- const [apiKey, setApiKey] = useState<string | null>(null);
1469
- const [apiKeyFull, setApiKeyFull] = useState<string | null>(null);
1470
- const [showApiKey, setShowApiKey] = useState(false);
1471
- const [subscriptions, setSubscriptions] = useState<{ id: string; trigger_slug: string; enabled: boolean }[]>([]);
1472
- const [shareToken, setShareToken] = useState<string | null>(null);
1473
- const [shareCopied, setShareCopied] = useState(false);
1474
-
1475
- // Fetch subscriptions for this agent
1476
- useEffect(() => {
1477
- authFetch(`/api/subscriptions?agent_id=${agent.id}`)
1478
- .then(res => res.ok ? res.json() : { subscriptions: [] })
1479
- .then(data => setSubscriptions(data.subscriptions || []))
1480
- .catch(() => {});
1481
- }, [agent.id, authFetch]);
1482
-
1483
- // Fetch available MCP servers
1484
- useEffect(() => {
1485
- const fetchMcpServers = async () => {
1486
- try {
1487
- const res = await authFetch("/api/mcp/servers");
1488
- const data = await res.json();
1489
- setAvailableMcpServers(data.servers || []);
1490
- } catch (e) {
1491
- console.error("Failed to fetch MCP servers:", e);
1492
- }
1493
- };
1494
- fetchMcpServers();
1495
- }, [authFetch]);
1496
-
1497
- // Fetch API key
1498
- useEffect(() => {
1499
- const fetchApiKey = async () => {
1500
- try {
1501
- const res = await authFetch(`/api/agents/${agent.id}/api-key`);
1502
- if (res.ok) {
1503
- const data = await res.json();
1504
- setApiKey(data.apiKey);
1505
- setApiKeyFull(data.fullKey || null);
1506
- }
1507
- } catch (e) {
1508
- // Ignore - not critical
1509
- }
1510
- };
1511
- fetchApiKey();
1512
- }, [agent.id, authFetch]);
1513
-
1514
- // Fetch share token
1515
- useEffect(() => {
1516
- const fetchShareToken = async () => {
1517
- try {
1518
- const res = await authFetch(`/api/agents/${agent.id}/share-token`);
1519
- if (res.ok) {
1520
- const data = await res.json();
1521
- setShareToken(data.token || null);
1522
- }
1523
- } catch {}
1524
- };
1525
- fetchShareToken();
1526
- }, [agent.id, authFetch]);
1527
-
1528
- // Fetch available skills
1529
- useEffect(() => {
1530
- const fetchSkills = async () => {
1531
- try {
1532
- const res = await authFetch("/api/skills");
1533
- const data = await res.json();
1534
- setAvailableSkills(data.skills || []);
1535
- } catch (e) {
1536
- console.error("Failed to fetch skills:", e);
1537
- }
1538
- };
1539
- fetchSkills();
1540
- }, [authFetch]);
1541
-
1542
- // Fetch Ollama models when Ollama is selected
1543
- useEffect(() => {
1544
- if (form.provider === "ollama") {
1545
- setLoadingOllamaModels(true);
1546
- authFetch("/api/providers/ollama/models")
1547
- .then(res => res.json())
1548
- .then(data => {
1549
- if (data.models && data.models.length > 0) {
1550
- setOllamaModels(data.models.map((m: { value: string; label?: string }) => ({
1551
- value: m.value,
1552
- label: m.label || m.value,
1553
- })));
1554
- }
1555
- })
1556
- .catch(() => setOllamaModels([]))
1557
- .finally(() => setLoadingOllamaModels(false));
1558
- } else {
1559
- setOllamaModels([]);
1560
- }
1561
- }, [form.provider, authFetch]);
1562
-
1563
- // Reset form when agent changes
1564
- useEffect(() => {
1565
- setForm({
1566
- name: agent.name,
1567
- provider: agent.provider,
1568
- model: agent.model,
1569
- systemPrompt: agent.systemPrompt,
1570
- features: {
1571
- ...agent.features,
1572
- builtinTools: agent.features.builtinTools || { webSearch: false, webFetch: false },
1573
- },
1574
- mcpServers: [...(agent.mcpServers || [])],
1575
- skills: [...(agent.skills || [])],
1576
- });
1577
- setMessage(null);
1578
- }, [agent.id]);
1579
-
1580
- const selectedProvider = providers.find(p => p.id === form.provider);
1581
-
1582
- const providerOptions = providers
1583
- .filter(p => p.hasKey && p.type === "llm")
1584
- .map(p => ({ value: p.id, label: p.name }));
1585
-
1586
- const modelOptions = form.provider === "ollama" && ollamaModels.length > 0
1587
- ? ollamaModels
1588
- : selectedProvider?.models.map(m => ({
1589
- value: m.value,
1590
- label: m.label,
1591
- recommended: m.recommended,
1592
- })) || [];
1593
-
1594
- const handleProviderChange = (providerId: string) => {
1595
- const provider = providers.find(p => p.id === providerId);
1596
- const defaultModel = provider?.models.find(m => m.recommended)?.value || provider?.models[0]?.value || "";
1597
- setForm(prev => ({ ...prev, provider: providerId, model: defaultModel }));
1598
- };
1599
-
1600
- const toggleFeature = (key: keyof AgentFeatures) => {
1601
- if (key === "agents") {
1602
- // Special handling for agents feature - convert to MultiAgentConfig
1603
- setForm(prev => {
1604
- const isEnabled = typeof prev.features.agents === "boolean"
1605
- ? prev.features.agents
1606
- : (prev.features.agents as MultiAgentConfig)?.enabled ?? false;
1607
- if (isEnabled) {
1608
- return { ...prev, features: { ...prev.features, agents: false } };
1609
- } else {
1610
- return {
1611
- ...prev,
1612
- features: {
1613
- ...prev.features,
1614
- agents: { enabled: true, group: agent.projectId || undefined },
1615
- },
1616
- };
1617
- }
1618
- });
1619
- } else if (key === "operator") {
1620
- // Special handling for operator feature - convert to OperatorConfig
1621
- setForm(prev => {
1622
- const opConfig = getOperatorConfig(prev.features);
1623
- if (opConfig.enabled) {
1624
- return { ...prev, features: { ...prev.features, operator: false } };
1625
- } else {
1626
- return {
1627
- ...prev,
1628
- features: {
1629
- ...prev.features,
1630
- operator: { enabled: true },
1631
- },
1632
- };
1633
- }
1634
- });
1635
- } else if (key === "realtime") {
1636
- // Special handling for realtime feature - convert to RealtimeConfig
1637
- setForm(prev => {
1638
- if (isRealtimeEnabled(prev.features)) {
1639
- return { ...prev, features: { ...prev.features, realtime: false } };
1640
- } else {
1641
- return {
1642
- ...prev,
1643
- features: {
1644
- ...prev.features,
1645
- realtime: { enabled: true },
1646
- },
1647
- };
1648
- }
1649
- });
1650
- } else {
1651
- setForm(prev => ({
1652
- ...prev,
1653
- features: { ...prev.features, [key]: !prev.features[key] },
1654
- }));
1655
- }
1656
- };
1657
-
1658
- // Helper to check if agents feature is enabled
1659
- const isAgentsEnabled = () => {
1660
- const agentsVal = form.features.agents;
1661
- if (typeof agentsVal === "boolean") return agentsVal;
1662
- return (agentsVal as MultiAgentConfig)?.enabled ?? false;
1663
- };
1664
-
1665
- // Helper to check if operator feature is enabled
1666
- const isOperatorEnabled = () => {
1667
- return getOperatorConfig(form.features).enabled;
1668
- };
1669
-
1670
- // Get current operator config
1671
- const getOperatorCfg = (): OperatorConfig => {
1672
- return getOperatorConfig(form.features);
1673
- };
1674
-
1675
- // Get browser providers from the providers list
1676
- const browserProviders = providers.filter(p => p.type === "browser" && p.hasKey);
1677
-
1678
- // Set operator browser provider
1679
- const setOperatorBrowserProvider = (browserProvider: string) => {
1680
- setForm(prev => {
1681
- const current = getOperatorConfig(prev.features);
1682
- return {
1683
- ...prev,
1684
- features: {
1685
- ...prev.features,
1686
- operator: { ...current, enabled: true, browser_provider: browserProvider },
1687
- },
1688
- };
1689
- });
1690
- };
1691
-
1692
- const toggleMcpServer = (serverId: string) => {
1693
- setForm(prev => ({
1694
- ...prev,
1695
- mcpServers: prev.mcpServers.includes(serverId)
1696
- ? prev.mcpServers.filter(id => id !== serverId)
1697
- : [...prev.mcpServers, serverId],
1698
- }));
1699
- };
1700
-
1701
- const toggleSkill = (skillId: string) => {
1702
- setForm(prev => ({
1703
- ...prev,
1704
- skills: prev.skills.includes(skillId)
1705
- ? prev.skills.filter(id => id !== skillId)
1706
- : [...prev.skills, skillId],
1707
- }));
1708
- };
1709
-
1710
- const handleSave = async () => {
1711
- setSaving(true);
1712
- setMessage(null);
1713
- const result = await onUpdateAgent(form);
1714
- setSaving(false);
1715
- if (result.error) {
1716
- setMessage({ type: "error", text: result.error });
1717
- } else {
1718
- setMessage({ type: "success", text: "Settings saved" });
1719
- setTimeout(() => setMessage(null), 2000);
1720
- }
1721
- };
1722
-
1723
- const hasChanges =
1724
- form.name !== agent.name ||
1725
- form.provider !== agent.provider ||
1726
- form.model !== agent.model ||
1727
- form.systemPrompt !== agent.systemPrompt ||
1728
- JSON.stringify(form.features) !== JSON.stringify(agent.features) ||
1729
- JSON.stringify(form.mcpServers.sort()) !== JSON.stringify((agent.mcpServers || []).sort()) ||
1730
- JSON.stringify(form.skills.sort()) !== JSON.stringify((agent.skills || []).sort());
1731
-
1732
- return (
1733
- <div className="flex-1 overflow-auto p-4">
1734
- <div className="space-y-4">
1735
- <FormField label="Name">
1736
- <input
1737
- type="text"
1738
- value={form.name}
1739
- onChange={(e) => setForm(prev => ({ ...prev, name: e.target.value }))}
1740
- className="w-full bg-[var(--color-bg)] border border-[var(--color-border-light)] rounded px-3 py-2 focus:outline-none focus:border-[var(--color-accent)] text-[var(--color-text)]"
1741
- />
1742
- </FormField>
1743
-
1744
- <FormField label="Provider">
1745
- <Select
1746
- value={form.provider}
1747
- options={providerOptions}
1748
- onChange={handleProviderChange}
1749
- />
1750
- </FormField>
1751
-
1752
- <FormField label="Model">
1753
- {loadingOllamaModels ? (
1754
- <div className="text-sm text-[var(--color-text-muted)] py-2">Loading Ollama models...</div>
1755
- ) : form.provider === "ollama" && modelOptions.length === 0 ? (
1756
- <div className="text-sm text-yellow-400/80 py-2">
1757
- No models found. Run <code className="bg-[var(--color-surface-raised)] px-1 rounded">ollama pull llama3.3</code> to download a model.
1758
- </div>
1759
- ) : (
1760
- <Select
1761
- value={form.model}
1762
- options={modelOptions}
1763
- onChange={(value) => setForm(prev => ({ ...prev, model: value }))}
1764
- />
1765
- )}
1766
- </FormField>
1767
-
1768
- <FormField label="System Prompt">
1769
- <textarea
1770
- value={form.systemPrompt}
1771
- onChange={(e) => setForm(prev => ({ ...prev, systemPrompt: e.target.value }))}
1772
- className="w-full bg-[var(--color-bg)] border border-[var(--color-border-light)] rounded px-3 py-2 h-24 resize-none focus:outline-none focus:border-[var(--color-accent)] text-[var(--color-text)]"
1773
- />
1774
- </FormField>
1775
-
1776
- <FormField label="Features">
1777
- <div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
1778
- {FEATURE_CONFIG.map(({ key, label, description, icon: Icon }) => {
1779
- // For agents/operator features, check the enabled property of the config
1780
- const isEnabled = key === "agents" ? isAgentsEnabled()
1781
- : key === "operator" ? isOperatorEnabled()
1782
- : key === "realtime" ? isRealtimeEnabled(form.features)
1783
- : !!form.features[key];
1784
- return (
1785
- <button
1786
- key={key}
1787
- type="button"
1788
- onClick={() => toggleFeature(key)}
1789
- className={`flex items-center gap-3 p-3 rounded border text-left transition ${
1790
- isEnabled
1791
- ? "border-[var(--color-accent)] bg-[var(--color-accent-10)]"
1792
- : "border-[var(--color-border-light)] hover:border-[var(--color-border-light)]"
1793
- }`}
1794
- >
1795
- <Icon className={`w-5 h-5 flex-shrink-0 ${isEnabled ? "text-[var(--color-accent)]" : "text-[var(--color-text-muted)]"}`} />
1796
- <div className="flex-1 min-w-0">
1797
- <div className={`text-sm font-medium ${isEnabled ? "text-[var(--color-accent)]" : ""}`}>
1798
- {label}
1799
- </div>
1800
- <div className="text-xs text-[var(--color-text-muted)]">{description}</div>
1801
- </div>
1802
- </button>
1803
- );
1804
- })}
1805
- </div>
1806
- </FormField>
1807
-
1808
- {/* Operator Browser Provider - shown when operator is enabled */}
1809
- {isOperatorEnabled() && (
1810
- <FormField label="Browser Provider">
1811
- {browserProviders.length > 0 ? (
1812
- <Select
1813
- value={getOperatorCfg().browser_provider || ""}
1814
- options={[
1815
- { value: "", label: "Auto (first available)" },
1816
- ...browserProviders.map(p => ({
1817
- value: p.id,
1818
- label: p.name,
1819
- })),
1820
- ]}
1821
- onChange={(value) => setOperatorBrowserProvider(value)}
1822
- />
1823
- ) : (
1824
- <p className="text-sm text-[var(--color-text-muted)] p-3 border border-[var(--color-border-light)] rounded bg-[var(--color-bg)]">
1825
- No browser providers configured. Go to Settings &rarr; Providers to add one.
1826
- </p>
1827
- )}
1828
- </FormField>
1829
- )}
1830
-
1831
- {/* Voice Configuration - shown when Realtime is enabled */}
1832
- {isRealtimeEnabled(form.features) && (() => {
1833
- const rtConfig = getRealtimeConfig(form.features);
1834
- const hasOpenAI = providers.some(p => p.id === "openai" && p.hasKey);
1835
- const hasGemini = providers.some(p => p.id === "gemini" && p.hasKey);
1836
- const voiceProviders = providers.filter(p => p.type === "voice" && p.hasKey);
1837
- const hasStandard = voiceProviders.length > 0;
1838
- const currentMode = rtConfig.provider || "standard";
1839
-
1840
- const updateRt = (updates: Partial<RealtimeConfig>) => {
1841
- setForm(prev => ({
1842
- ...prev,
1843
- features: {
1844
- ...prev.features,
1845
- realtime: { ...getRealtimeConfig(prev.features), ...updates },
1846
- },
1847
- }));
1848
- };
1849
-
1850
- const sttOptions = voiceProviders
1851
- .filter(p => !(p as any).voiceSubtype || (p as any).voiceSubtype === "stt" || (p as any).voiceSubtype === "both")
1852
- .map(p => ({ value: p.id, label: p.name }));
1853
- const ttsOptions = voiceProviders
1854
- .filter(p => !(p as any).voiceSubtype || (p as any).voiceSubtype === "tts" || (p as any).voiceSubtype === "both")
1855
- .map(p => ({ value: p.id, label: p.name }));
1856
-
1857
- return (
1858
- <div className="p-3 bg-[var(--color-surface)] rounded border border-[var(--color-border-light)] space-y-3">
1859
- <p className="text-xs text-[var(--color-text-muted)] font-medium uppercase tracking-wider">Voice Configuration</p>
1860
- <FormField label="Voice Mode">
1861
- <div className="flex gap-2 flex-wrap">
1862
- {hasOpenAI && (
1863
- <button type="button" onClick={() => updateRt({ provider: "openai", model: "gpt-realtime", voice: "alloy" })}
1864
- className={`px-3 py-1.5 text-sm rounded border transition ${currentMode === "openai" ? "border-[var(--color-accent)] bg-[var(--color-accent-10)] text-[var(--color-accent)]" : "border-[var(--color-border-light)]"}`}>
1865
- OpenAI Realtime
1866
- </button>
1867
- )}
1868
- {hasGemini && (
1869
- <button type="button" onClick={() => updateRt({ provider: "gemini", geminiVoice: "Kore" })}
1870
- className={`px-3 py-1.5 text-sm rounded border transition ${currentMode === "gemini" ? "border-[var(--color-accent)] bg-[var(--color-accent-10)] text-[var(--color-accent)]" : "border-[var(--color-border-light)]"}`}>
1871
- Gemini Live
1872
- </button>
1873
- )}
1874
- {hasStandard && (
1875
- <button type="button" onClick={() => updateRt({ provider: "standard" })}
1876
- className={`px-3 py-1.5 text-sm rounded border transition ${currentMode === "standard" ? "border-[var(--color-accent)] bg-[var(--color-accent-10)] text-[var(--color-accent)]" : "border-[var(--color-border-light)]"}`}>
1877
- Standard (STT+LLM+TTS)
1878
- </button>
1879
- )}
1880
- </div>
1881
- </FormField>
1882
- {currentMode === "openai" && (
1883
- <>
1884
- <FormField label="Realtime Model">
1885
- <Select value={rtConfig.model || "gpt-realtime"} options={REALTIME_PROVIDERS.openai.models.map(m => ({ value: m.value, label: m.label, recommended: m.recommended }))} onChange={(value) => updateRt({ model: value })} placeholder="Select model..." />
1886
- </FormField>
1887
- <FormField label="Voice">
1888
- <Select value={rtConfig.voice || "alloy"} options={REALTIME_PROVIDERS.openai.voices.map(v => ({ value: v.value, label: v.label, recommended: v.recommended }))} onChange={(value) => updateRt({ voice: value })} placeholder="Select voice..." />
1889
- </FormField>
1890
- </>
1891
- )}
1892
- {currentMode === "gemini" && (
1893
- <>
1894
- <FormField label="Realtime Model">
1895
- <Select value={rtConfig.geminiModel || REALTIME_PROVIDERS.gemini.models[0].value} options={REALTIME_PROVIDERS.gemini.models.map(m => ({ value: m.value, label: m.label, recommended: m.recommended }))} onChange={(value) => updateRt({ geminiModel: value })} placeholder="Select model..." />
1896
- </FormField>
1897
- <FormField label="Voice">
1898
- <Select value={rtConfig.geminiVoice || "Kore"} options={REALTIME_PROVIDERS.gemini.voices.map(v => ({ value: v.value, label: v.label, recommended: v.recommended }))} onChange={(value) => updateRt({ geminiVoice: value })} placeholder="Select voice..." />
1899
- </FormField>
1900
- <div className="flex items-center gap-2">
1901
- <button type="button" onClick={() => updateRt({ googleSearch: !rtConfig.googleSearch })}
1902
- className={`flex items-center gap-2 px-3 py-1.5 text-sm rounded border transition ${rtConfig.googleSearch ? "border-[var(--color-accent)] bg-[var(--color-accent-10)] text-[var(--color-accent)]" : "border-[var(--color-border-light)]"}`}>
1903
- <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>
1904
- Google Search
1905
- </button>
1906
- <span className="text-xs text-[var(--color-text-faint)]">Enable search grounding</span>
1907
- </div>
1908
- </>
1909
- )}
1910
- {currentMode === "standard" && (
1911
- <>
1912
- {sttOptions.length > 0 && (
1913
- <FormField label="STT Provider">
1914
- <Select value={rtConfig.sttProvider || (sttOptions[0]?.value ?? "")} options={sttOptions} onChange={(value) => updateRt({ sttProvider: value })} placeholder="Select STT provider..." />
1915
- </FormField>
1916
- )}
1917
- {ttsOptions.length > 0 && (
1918
- <FormField label="TTS Provider">
1919
- <Select value={rtConfig.ttsProvider || (ttsOptions[0]?.value ?? "")} options={ttsOptions} onChange={(value) => updateRt({ ttsProvider: value })} placeholder="Select TTS provider..." />
1920
- </FormField>
1921
- )}
1922
- </>
1923
- )}
1924
- </div>
1925
- );
1926
- })()}
1927
-
1928
- {/* Agent Built-in Tools - Anthropic only */}
1929
- {form.provider === "anthropic" && (
1930
- <FormField label="Agent Built-in Tools">
1931
- <div className="flex flex-wrap gap-2">
1932
- <button
1933
- type="button"
1934
- onClick={() => setForm(prev => ({
1935
- ...prev,
1936
- features: {
1937
- ...prev.features,
1938
- builtinTools: {
1939
- ...prev.features.builtinTools,
1940
- webSearch: !prev.features.builtinTools?.webSearch,
1941
- },
1942
- },
1943
- }))}
1944
- className={`flex items-center gap-2 px-3 py-2 rounded border transition ${
1945
- form.features.builtinTools?.webSearch
1946
- ? "border-[var(--color-accent)] bg-[var(--color-accent-10)] text-[var(--color-accent)]"
1947
- : "border-[var(--color-border-light)] hover:border-[var(--color-border-light)] text-[var(--color-text-secondary)]"
1948
- }`}
1949
- >
1950
- <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1951
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
1952
- </svg>
1953
- <span className="text-sm">Web Search</span>
1954
- </button>
1955
- <button
1956
- type="button"
1957
- onClick={() => setForm(prev => ({
1958
- ...prev,
1959
- features: {
1960
- ...prev.features,
1961
- builtinTools: {
1962
- ...prev.features.builtinTools,
1963
- webFetch: !prev.features.builtinTools?.webFetch,
1964
- },
1965
- },
1966
- }))}
1967
- className={`flex items-center gap-2 px-3 py-2 rounded border transition ${
1968
- form.features.builtinTools?.webFetch
1969
- ? "border-[var(--color-accent)] bg-[var(--color-accent-10)] text-[var(--color-accent)]"
1970
- : "border-[var(--color-border-light)] hover:border-[var(--color-border-light)] text-[var(--color-text-secondary)]"
1971
- }`}
1972
- >
1973
- <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1974
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" />
1975
- </svg>
1976
- <span className="text-sm">Web Fetch</span>
1977
- </button>
1978
- </div>
1979
- <p className="text-xs text-[var(--color-text-faint)] mt-2">
1980
- Provider-native tools for real-time web access
1981
- </p>
1982
- </FormField>
1983
- )}
1984
-
1985
- {/* MCP Server Selection - shown when MCP is enabled */}
1986
- {form.features.mcp && (
1987
- <FormField label="MCP Servers">
1988
- {availableMcpServers.length === 0 ? (
1989
- <p className="text-sm text-[var(--color-text-muted)]">
1990
- No MCP servers configured. Add servers in the MCP page first.
1991
- </p>
1992
- ) : (
1993
- <div className="space-y-2">
1994
- {availableMcpServers
1995
- .filter(server => server.project_id === null || server.project_id === agent.projectId)
1996
- .map(server => {
1997
- const isRemote = server.type === "http" && server.url;
1998
- const isAvailable = isRemote || server.status === "running";
1999
- const serverInfo = isRemote
2000
- ? `${server.source || "remote"} • http`
2001
- : `${server.type} • ${server.package || server.command || "custom"}${server.status === "running" && server.port ? ` • :${server.port}` : ""}`;
2002
- return (
2003
- <button
2004
- key={server.id}
2005
- type="button"
2006
- onClick={() => toggleMcpServer(server.id)}
2007
- className={`w-full flex items-center gap-3 p-3 rounded border text-left transition ${
2008
- form.mcpServers.includes(server.id)
2009
- ? "border-[var(--color-accent)] bg-[var(--color-accent-10)]"
2010
- : "border-[var(--color-border-light)] hover:border-[var(--color-border-light)]"
2011
- }`}
2012
- >
2013
- <div className={`w-2 h-2 rounded-full flex-shrink-0 ${
2014
- isAvailable ? "bg-green-400" : "bg-[var(--color-scrollbar)]"
2015
- }`} />
2016
- <div className="flex-1 min-w-0">
2017
- <div className="flex items-center gap-2">
2018
- <span className={`text-sm font-medium ${form.mcpServers.includes(server.id) ? "text-[var(--color-accent)]" : ""}`}>
2019
- {server.name}
2020
- </span>
2021
- {server.project_id === null && (
2022
- <span className="text-[10px] text-[var(--color-text-muted)] bg-[var(--color-surface-raised)] px-1.5 py-0.5 rounded">Global</span>
2023
- )}
2024
- </div>
2025
- <div className="text-xs text-[var(--color-text-muted)]">{serverInfo}</div>
2026
- </div>
2027
- <div className={`text-xs px-2 py-0.5 rounded ${
2028
- isAvailable
2029
- ? "bg-green-500/20 text-green-400"
2030
- : "bg-[var(--color-surface-raised)] text-[var(--color-text-muted)]"
2031
- }`}>
2032
- {isRemote ? "remote" : server.status}
2033
- </div>
2034
- </button>
2035
- );
2036
- })}
2037
- <p className="text-xs text-[var(--color-text-muted)] mt-2">
2038
- Remote servers are always available. Local servers must be running.
2039
- </p>
2040
- </div>
2041
- )}
2042
- </FormField>
2043
- )}
2044
-
2045
- {/* Skills Selection */}
2046
- <FormField label="Skills">
2047
- {availableSkills.length === 0 ? (
2048
- <p className="text-sm text-[var(--color-text-muted)]">
2049
- No skills configured. Add skills in the Skills page first.
2050
- </p>
2051
- ) : (
2052
- <div className="space-y-2">
2053
- {availableSkills
2054
- .filter(s => s.enabled && (s.project_id === null || s.project_id === agent.projectId))
2055
- .map(skill => (
2056
- <button
2057
- key={skill.id}
2058
- type="button"
2059
- onClick={() => toggleSkill(skill.id)}
2060
- className={`w-full flex items-center gap-3 p-3 rounded border text-left transition ${
2061
- form.skills.includes(skill.id)
2062
- ? "border-[var(--color-accent)] bg-[var(--color-accent-10)]"
2063
- : "border-[var(--color-border-light)] hover:border-[var(--color-border-light)]"
2064
- }`}
2065
- >
2066
- <div className="flex-1 min-w-0">
2067
- <div className="flex items-center gap-2">
2068
- <span className={`text-sm font-medium ${form.skills.includes(skill.id) ? "text-[var(--color-accent)]" : ""}`}>
2069
- {skill.name}
2070
- </span>
2071
- {skill.project_id === null && (
2072
- <span className="text-[10px] text-[var(--color-text-muted)] bg-[var(--color-surface-raised)] px-1.5 py-0.5 rounded">Global</span>
2073
- )}
2074
- </div>
2075
- <div className="text-xs text-[var(--color-text-muted)]">{skill.description}</div>
2076
- </div>
2077
- <div className="text-xs px-2 py-0.5 rounded bg-[var(--color-surface-raised)] text-[var(--color-text-muted)]">
2078
- v{skill.version}
2079
- </div>
2080
- </button>
2081
- ))}
2082
- <p className="text-xs text-[var(--color-text-muted)] mt-2">
2083
- Skills provide reusable instructions for the agent.
2084
- </p>
2085
- </div>
2086
- )}
2087
- </FormField>
2088
-
2089
- {message && (
2090
- <div className={`text-sm px-3 py-2 rounded ${
2091
- message.type === "success"
2092
- ? "bg-green-500/10 text-green-400"
2093
- : "bg-red-500/10 text-red-400"
2094
- }`}>
2095
- {message.text}
2096
- </div>
2097
- )}
2098
-
2099
- <button
2100
- onClick={handleSave}
2101
- disabled={!hasChanges || saving || !form.name}
2102
- className="w-full bg-[var(--color-accent)] hover:bg-[var(--color-accent-hover)] disabled:opacity-50 disabled:cursor-not-allowed text-black px-4 py-2 rounded font-medium transition"
2103
- >
2104
- {saving ? "Saving..." : "Save Changes"}
2105
- </button>
2106
-
2107
- {agent.status === "running" && hasChanges && (
2108
- <p className="text-xs text-[var(--color-text-muted)] text-center">
2109
- Changes will be applied to the running agent
2110
- </p>
2111
- )}
2112
-
2113
- {/* Subscriptions */}
2114
- <div className="mt-8 pt-6 border-t border-[var(--color-border-light)]">
2115
- <p className="text-sm text-[var(--color-text-muted)] mb-3">Subscriptions</p>
2116
- {subscriptions.length === 0 ? (
2117
- <p className="text-xs text-[var(--color-text-faint)]">No subscriptions. Set up triggers in Connections to have this agent listen to external events.</p>
2118
- ) : (
2119
- <div className="space-y-2">
2120
- {subscriptions.map(sub => (
2121
- <div key={sub.id} className="flex items-center gap-2 px-3 py-2 bg-[var(--color-surface)] rounded border border-[var(--color-border)]">
2122
- <span className={`w-2 h-2 rounded-full shrink-0 ${sub.enabled ? "bg-cyan-400" : "bg-[var(--color-scrollbar)]"}`} />
2123
- <span className={`text-sm flex-1 ${sub.enabled ? "text-cyan-400" : "text-[var(--color-text-muted)]"}`}>
2124
- {sub.trigger_slug.replace(/_/g, " ")}
2125
- </span>
2126
- <span className={`text-[10px] px-1.5 py-0.5 rounded ${sub.enabled ? "bg-cyan-500/10 text-cyan-400" : "bg-[var(--color-surface-raised)] text-[var(--color-text-faint)]"}`}>
2127
- {sub.enabled ? "active" : "disabled"}
2128
- </span>
2129
- </div>
2130
- ))}
2131
- </div>
2132
- )}
2133
- </div>
2134
-
2135
- {/* Developer Info (dev mode only) */}
2136
- {apiKey && (
2137
- <div className="mt-8 pt-6 border-t border-[var(--color-border-light)]">
2138
- <p className="text-sm text-[var(--color-text-muted)] mb-3">Developer Info</p>
2139
- <div className="space-y-2">
2140
- <div className="flex items-center justify-between">
2141
- <span className="text-xs text-[var(--color-text-muted)]">Agent ID</span>
2142
- <code className="text-xs bg-[var(--color-surface-raised)] px-2 py-1 rounded text-[var(--color-text-secondary)]">{agent.id}</code>
2143
- </div>
2144
- <div className="flex items-center justify-between">
2145
- <span className="text-xs text-[var(--color-text-muted)]">Port</span>
2146
- <code className="text-xs bg-[var(--color-surface-raised)] px-2 py-1 rounded text-[var(--color-text-secondary)]">{agent.port || "N/A"}</code>
2147
- </div>
2148
- <div className="flex flex-col gap-1">
2149
- <div className="flex items-center justify-between">
2150
- <span className="text-xs text-[var(--color-text-muted)]">API Key</span>
2151
- <button
2152
- onClick={() => setShowApiKey(!showApiKey)}
2153
- className="text-xs text-[var(--color-accent)] hover:text-[var(--color-accent-hover)]"
2154
- >
2155
- {showApiKey ? "Hide" : "Show"}
2156
- </button>
2157
- </div>
2158
- <code className="text-xs bg-[var(--color-surface-raised)] px-2 py-1 rounded text-[var(--color-text-secondary)] break-all">
2159
- {showApiKey ? (apiKeyFull || apiKey) : apiKey}
2160
- </code>
2161
- </div>
2162
- {agent.status === "running" && agent.port && (
2163
- <div className="flex flex-col gap-1 mt-2">
2164
- <span className="text-xs text-[var(--color-text-muted)]">Test with curl</span>
2165
- <code className="text-xs bg-[var(--color-surface-raised)] px-2 py-1.5 rounded text-[var(--color-text-muted)] break-all">
2166
- curl -H "X-API-Key: {showApiKey ? (apiKeyFull || apiKey) : "***"}" http://localhost:{agent.port}/config
2167
- </code>
2168
- </div>
2169
- )}
2170
- </div>
2171
- </div>
2172
- )}
2173
-
2174
- {/* Share Link */}
2175
- {shareToken && (
2176
- <div className="mt-8 pt-6 border-t border-[var(--color-border-light)]">
2177
- <p className="text-sm text-[var(--color-text-muted)] mb-3">Share Link</p>
2178
- <p className="text-xs text-[var(--color-text-faint)] mb-3">
2179
- Anyone with this link can chat with this agent. No login required. Regenerate the API key to invalidate.
2180
- </p>
2181
- <div className="flex gap-2">
2182
- <code className="flex-1 text-xs bg-[var(--color-surface-raised)] px-3 py-2 rounded text-[var(--color-text-secondary)] break-all border border-[var(--color-border-light)]">
2183
- {`${window.location.origin}/share/${shareToken}`}
2184
- </code>
2185
- <button
2186
- onClick={() => {
2187
- navigator.clipboard.writeText(`${window.location.origin}/share/${shareToken}`);
2188
- setShareCopied(true);
2189
- setTimeout(() => setShareCopied(false), 2000);
2190
- }}
2191
- className="px-3 py-2 text-xs bg-[var(--color-surface-raised)] hover:bg-[var(--color-surface-raised)] border border-[var(--color-border-light)] rounded text-[var(--color-text-secondary)] hover:text-[var(--color-text)] transition flex-shrink-0"
2192
- >
2193
- {shareCopied ? "Copied!" : "Copy"}
2194
- </button>
2195
- </div>
2196
- <div className="mt-3">
2197
- <p className="text-xs text-[var(--color-text-faint)] mb-1">Embed</p>
2198
- <code className="block text-xs bg-[var(--color-surface-raised)] px-3 py-2 rounded text-[var(--color-text-muted)] break-all border border-[var(--color-border-light)]">
2199
- {`<iframe src="${window.location.origin}/share/${shareToken}" width="400" height="600" style="border:none; border-radius:12px;" />`}
2200
- </code>
2201
- </div>
2202
- </div>
2203
- )}
2204
-
2205
- {/* Danger Zone */}
2206
- <div className="mt-8 pt-6 border-t border-[var(--color-border-light)]">
2207
- <p className="text-sm text-[var(--color-text-muted)] mb-3">Danger Zone</p>
2208
- {confirmDelete ? (
2209
- <div className="flex gap-2">
2210
- <button
2211
- onClick={() => setConfirmDelete(false)}
2212
- className="flex-1 border border-[var(--color-border-light)] hover:border-[var(--color-scrollbar)] px-4 py-2 rounded font-medium transition"
2213
- >
2214
- Cancel
2215
- </button>
2216
- <button
2217
- onClick={onDeleteAgent}
2218
- className="flex-1 bg-red-500/20 text-red-400 hover:bg-red-500/30 px-4 py-2 rounded font-medium transition"
2219
- >
2220
- Confirm Delete
2221
- </button>
2222
- </div>
2223
- ) : (
2224
- <button
2225
- onClick={() => setConfirmDelete(true)}
2226
- className="w-full border border-red-500/30 text-red-400/70 hover:border-red-500/50 hover:text-red-400 px-4 py-2 rounded font-medium transition"
2227
- >
2228
- Delete Agent
2229
- </button>
2230
- )}
2231
- </div>
2232
- </div>
2233
- </div>
2234
- );
2235
- }
2236
-
2237
- function FormField({ label, children }: { label: string; children: React.ReactNode }) {
2238
- return (
2239
- <div>
2240
- <label className="block text-sm text-[var(--color-text-muted)] mb-1">{label}</label>
2241
- {children}
2242
- </div>
2243
- );
2244
- }