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.
- package/README.md +216 -54
- package/cli.js +35 -0
- package/install.js +92 -0
- package/package.json +12 -79
- package/LICENSE +0 -63
- package/bin/apteva.js +0 -196
- package/dist/ActivityPage.kxzzb4yc.js +0 -3
- package/dist/ApiDocsPage.zq998hbm.js +0 -4
- package/dist/App.55rea8mn.js +0 -61
- package/dist/App.5ywb23z4.js +0 -53
- package/dist/App.6thds120.js +0 -4
- package/dist/App.9tctxzqm.js +0 -8
- package/dist/App.a8r8ttaz.js +0 -4
- package/dist/App.agsv5bje.js +0 -4
- package/dist/App.cepapqmx.js +0 -4
- package/dist/App.dp041gb3.js +0 -221
- package/dist/App.fds72zb5.js +0 -4
- package/dist/App.fg9qj2dq.js +0 -4
- package/dist/App.ndfejbm9.js +0 -4
- package/dist/App.nxmfmq1h.js +0 -13
- package/dist/App.qdfyt8ba.js +0 -4
- package/dist/App.x2d0ygt6.js +0 -4
- package/dist/App.yt9p4nr3.js +0 -20
- package/dist/App.zn4mw16t.js +0 -1
- package/dist/ConnectionsPage.8r96ryw7.js +0 -3
- package/dist/McpPage.3cwh0gnd.js +0 -3
- package/dist/SettingsPage.ykgdh5ev.js +0 -3
- package/dist/SkillsPage.4np1s65b.js +0 -3
- package/dist/TasksPage.4g08t7p6.js +0 -3
- package/dist/TelemetryPage.72w9pwcp.js +0 -3
- package/dist/TestsPage.z4fk3r7r.js +0 -3
- package/dist/ThreadsPage.63tcajeh.js +0 -3
- package/dist/apteva-kit.css +0 -1
- package/dist/icon.png +0 -0
- package/dist/index.html +0 -16
- package/dist/styles.css +0 -1
- package/scripts/postinstall.mjs +0 -102
- package/src/auth/index.ts +0 -394
- package/src/auth/middleware.ts +0 -213
- package/src/binary.ts +0 -536
- package/src/channels/index.ts +0 -40
- package/src/channels/telegram.ts +0 -311
- package/src/crypto.ts +0 -301
- package/src/db-tests.ts +0 -174
- package/src/db.ts +0 -3133
- package/src/integrations/agentdojo.ts +0 -559
- package/src/integrations/composio.ts +0 -437
- package/src/integrations/index.ts +0 -87
- package/src/integrations/skillsmp.ts +0 -318
- package/src/mcp-client.ts +0 -605
- package/src/mcp-handler.ts +0 -394
- package/src/mcp-platform.ts +0 -2403
- package/src/openapi.ts +0 -2410
- package/src/providers.ts +0 -597
- package/src/routes/api/agent-utils.ts +0 -890
- package/src/routes/api/agents.ts +0 -916
- package/src/routes/api/api-keys.ts +0 -95
- package/src/routes/api/channels.ts +0 -182
- package/src/routes/api/helpers.ts +0 -12
- package/src/routes/api/integrations.ts +0 -639
- package/src/routes/api/mcp.ts +0 -574
- package/src/routes/api/meta-agent.ts +0 -195
- package/src/routes/api/projects.ts +0 -112
- package/src/routes/api/providers.ts +0 -424
- package/src/routes/api/skills.ts +0 -537
- package/src/routes/api/system.ts +0 -333
- package/src/routes/api/telemetry.ts +0 -203
- package/src/routes/api/tests.ts +0 -148
- package/src/routes/api/triggers.ts +0 -518
- package/src/routes/api/users.ts +0 -148
- package/src/routes/api/webhooks.ts +0 -171
- package/src/routes/api.ts +0 -53
- package/src/routes/auth.ts +0 -251
- package/src/routes/share.ts +0 -86
- package/src/routes/static.ts +0 -131
- package/src/server.ts +0 -642
- package/src/test-runner.ts +0 -598
- package/src/triggers/agentdojo.ts +0 -253
- package/src/triggers/composio.ts +0 -264
- package/src/triggers/index.ts +0 -71
- package/src/tui/AgentList.tsx +0 -145
- package/src/tui/App.tsx +0 -102
- package/src/tui/Login.tsx +0 -104
- package/src/tui/api.ts +0 -72
- package/src/tui/index.tsx +0 -7
- package/src/web/App.tsx +0 -455
- package/src/web/components/activity/ActivityPage.tsx +0 -314
- package/src/web/components/activity/index.ts +0 -1
- package/src/web/components/agents/AgentCard.tsx +0 -189
- package/src/web/components/agents/AgentPanel.tsx +0 -2244
- package/src/web/components/agents/AgentsView.tsx +0 -180
- package/src/web/components/agents/CreateAgentModal.tsx +0 -475
- package/src/web/components/agents/index.ts +0 -4
- package/src/web/components/api/ApiDocsPage.tsx +0 -842
- package/src/web/components/auth/CreateAccountStep.tsx +0 -176
- package/src/web/components/auth/LoginPage.tsx +0 -91
- package/src/web/components/auth/index.ts +0 -2
- package/src/web/components/common/Icons.tsx +0 -250
- package/src/web/components/common/LoadingSpinner.tsx +0 -44
- package/src/web/components/common/Modal.tsx +0 -199
- package/src/web/components/common/Select.tsx +0 -97
- package/src/web/components/common/index.ts +0 -20
- package/src/web/components/connections/ConnectionsPage.tsx +0 -54
- package/src/web/components/connections/IntegrationsTab.tsx +0 -170
- package/src/web/components/connections/OverviewTab.tsx +0 -137
- package/src/web/components/connections/TriggersTab.tsx +0 -1346
- package/src/web/components/dashboard/Dashboard.tsx +0 -572
- package/src/web/components/dashboard/index.ts +0 -1
- package/src/web/components/index.ts +0 -21
- package/src/web/components/layout/ErrorBanner.tsx +0 -18
- package/src/web/components/layout/Header.tsx +0 -332
- package/src/web/components/layout/Sidebar.tsx +0 -231
- package/src/web/components/layout/index.ts +0 -3
- package/src/web/components/mcp/IntegrationsPanel.tsx +0 -857
- package/src/web/components/mcp/McpPage.tsx +0 -2515
- package/src/web/components/mcp/index.ts +0 -1
- package/src/web/components/meta-agent/MetaAgent.tsx +0 -245
- package/src/web/components/onboarding/OnboardingWizard.tsx +0 -404
- package/src/web/components/onboarding/index.ts +0 -1
- package/src/web/components/settings/SettingsPage.tsx +0 -2776
- package/src/web/components/settings/index.ts +0 -1
- package/src/web/components/skills/SkillsPage.tsx +0 -1200
- package/src/web/components/tasks/TasksPage.tsx +0 -1116
- package/src/web/components/tasks/index.ts +0 -1
- package/src/web/components/telemetry/TelemetryPage.tsx +0 -1129
- package/src/web/components/tests/TestsPage.tsx +0 -594
- package/src/web/components/threads/ThreadsPage.tsx +0 -315
- package/src/web/context/AuthContext.tsx +0 -242
- package/src/web/context/ProjectContext.tsx +0 -214
- package/src/web/context/TelemetryContext.tsx +0 -299
- package/src/web/context/ThemeContext.tsx +0 -90
- package/src/web/context/UIModeContext.tsx +0 -49
- package/src/web/context/index.ts +0 -12
- package/src/web/hooks/index.ts +0 -3
- package/src/web/hooks/useAgents.ts +0 -115
- package/src/web/hooks/useOnboarding.ts +0 -20
- package/src/web/hooks/useProviders.ts +0 -75
- package/src/web/icon.png +0 -0
- package/src/web/index.html +0 -16
- package/src/web/styles.css +0 -118
- package/src/web/themes.ts +0 -162
- 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 → 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
|
-
}
|