apteva 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +63 -0
- package/README.md +84 -0
- package/bin/agent-linux-amd64 +0 -0
- package/bin/apteva.js +144 -0
- package/dist/App.g02zmbqf.js +213 -0
- package/dist/App.g02zmbqf.js.map +37 -0
- package/dist/App.mq6jqare.js +1 -0
- package/dist/apteva-kit.css +1 -0
- package/dist/index.html +14 -0
- package/dist/styles.css +1 -0
- package/package.json +65 -0
- package/src/binary.ts +116 -0
- package/src/crypto.ts +152 -0
- package/src/db.ts +446 -0
- package/src/providers.ts +255 -0
- package/src/routes/api.ts +380 -0
- package/src/routes/static.ts +47 -0
- package/src/server.ts +134 -0
- package/src/web/App.tsx +218 -0
- package/src/web/components/agents/AgentCard.tsx +71 -0
- package/src/web/components/agents/AgentsView.tsx +69 -0
- package/src/web/components/agents/ChatPanel.tsx +63 -0
- package/src/web/components/agents/CreateAgentModal.tsx +128 -0
- package/src/web/components/agents/index.ts +4 -0
- package/src/web/components/common/Icons.tsx +61 -0
- package/src/web/components/common/LoadingSpinner.tsx +44 -0
- package/src/web/components/common/Modal.tsx +16 -0
- package/src/web/components/common/Select.tsx +96 -0
- package/src/web/components/common/index.ts +4 -0
- package/src/web/components/dashboard/Dashboard.tsx +136 -0
- package/src/web/components/dashboard/index.ts +1 -0
- package/src/web/components/index.ts +11 -0
- package/src/web/components/layout/ErrorBanner.tsx +18 -0
- package/src/web/components/layout/Header.tsx +26 -0
- package/src/web/components/layout/Sidebar.tsx +66 -0
- package/src/web/components/layout/index.ts +3 -0
- package/src/web/components/onboarding/OnboardingWizard.tsx +344 -0
- package/src/web/components/onboarding/index.ts +1 -0
- package/src/web/components/settings/SettingsPage.tsx +285 -0
- package/src/web/components/settings/index.ts +1 -0
- package/src/web/hooks/index.ts +3 -0
- package/src/web/hooks/useAgents.ts +62 -0
- package/src/web/hooks/useOnboarding.ts +25 -0
- package/src/web/hooks/useProviders.ts +65 -0
- package/src/web/index.html +21 -0
- package/src/web/styles.css +23 -0
- package/src/web/types.ts +43 -0
package/src/web/App.tsx
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
|
+
import { createRoot } from "react-dom/client";
|
|
3
|
+
import "@apteva/apteva-kit/styles.css";
|
|
4
|
+
|
|
5
|
+
// Types
|
|
6
|
+
import type { Agent, Provider, Route, NewAgentForm } from "./types";
|
|
7
|
+
|
|
8
|
+
// Hooks
|
|
9
|
+
import { useAgents, useProviders, useOnboarding } from "./hooks";
|
|
10
|
+
|
|
11
|
+
// Components
|
|
12
|
+
import {
|
|
13
|
+
LoadingSpinner,
|
|
14
|
+
Header,
|
|
15
|
+
Sidebar,
|
|
16
|
+
ErrorBanner,
|
|
17
|
+
OnboardingWizard,
|
|
18
|
+
SettingsPage,
|
|
19
|
+
CreateAgentModal,
|
|
20
|
+
AgentsView,
|
|
21
|
+
Dashboard,
|
|
22
|
+
} from "./components";
|
|
23
|
+
|
|
24
|
+
function App() {
|
|
25
|
+
// Onboarding state
|
|
26
|
+
const { isComplete: onboardingComplete, setIsComplete: setOnboardingComplete } = useOnboarding();
|
|
27
|
+
|
|
28
|
+
// Data hooks
|
|
29
|
+
const {
|
|
30
|
+
agents,
|
|
31
|
+
loading,
|
|
32
|
+
runningCount,
|
|
33
|
+
fetchAgents,
|
|
34
|
+
createAgent,
|
|
35
|
+
deleteAgent,
|
|
36
|
+
toggleAgent,
|
|
37
|
+
} = useAgents(onboardingComplete === true);
|
|
38
|
+
|
|
39
|
+
const {
|
|
40
|
+
providers,
|
|
41
|
+
configuredProviders,
|
|
42
|
+
fetchProviders,
|
|
43
|
+
} = useProviders(onboardingComplete === true);
|
|
44
|
+
|
|
45
|
+
// UI state
|
|
46
|
+
const [showCreate, setShowCreate] = useState(false);
|
|
47
|
+
const [selectedAgent, setSelectedAgent] = useState<Agent | null>(null);
|
|
48
|
+
const [route, setRoute] = useState<Route>("dashboard");
|
|
49
|
+
const [startError, setStartError] = useState<string | null>(null);
|
|
50
|
+
|
|
51
|
+
// Form state
|
|
52
|
+
const [newAgent, setNewAgent] = useState<NewAgentForm>({
|
|
53
|
+
name: "",
|
|
54
|
+
model: "",
|
|
55
|
+
provider: "",
|
|
56
|
+
systemPrompt: "You are a helpful assistant.",
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Set default provider when providers are loaded
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (configuredProviders.length > 0 && !newAgent.provider) {
|
|
62
|
+
const defaultProvider = configuredProviders[0];
|
|
63
|
+
const defaultModel = defaultProvider.models.find(m => m.recommended)?.value || defaultProvider.models[0]?.value || "";
|
|
64
|
+
setNewAgent(prev => ({
|
|
65
|
+
...prev,
|
|
66
|
+
provider: defaultProvider.id,
|
|
67
|
+
model: defaultModel,
|
|
68
|
+
}));
|
|
69
|
+
}
|
|
70
|
+
}, [configuredProviders, newAgent.provider]);
|
|
71
|
+
|
|
72
|
+
// Update selected agent when agents list changes
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (selectedAgent) {
|
|
75
|
+
const updated = agents.find(a => a.id === selectedAgent.id);
|
|
76
|
+
if (updated) {
|
|
77
|
+
setSelectedAgent(updated);
|
|
78
|
+
} else {
|
|
79
|
+
setSelectedAgent(null);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}, [agents, selectedAgent]);
|
|
83
|
+
|
|
84
|
+
const handleProviderChange = (providerId: string) => {
|
|
85
|
+
const provider = providers.find(p => p.id === providerId);
|
|
86
|
+
const defaultModel = provider?.models.find(m => m.recommended)?.value || provider?.models[0]?.value || "";
|
|
87
|
+
setNewAgent(prev => ({
|
|
88
|
+
...prev,
|
|
89
|
+
provider: providerId,
|
|
90
|
+
model: defaultModel,
|
|
91
|
+
}));
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const handleCreateAgent = async () => {
|
|
95
|
+
if (!newAgent.name) return;
|
|
96
|
+
await createAgent(newAgent);
|
|
97
|
+
const defaultProvider = configuredProviders[0];
|
|
98
|
+
const defaultModel = defaultProvider?.models.find(m => m.recommended)?.value || defaultProvider?.models[0]?.value || "";
|
|
99
|
+
setNewAgent({
|
|
100
|
+
name: "",
|
|
101
|
+
model: defaultModel,
|
|
102
|
+
provider: defaultProvider?.id || "",
|
|
103
|
+
systemPrompt: "You are a helpful assistant.",
|
|
104
|
+
});
|
|
105
|
+
setShowCreate(false);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const handleToggleAgent = async (agent: Agent, e?: React.MouseEvent) => {
|
|
109
|
+
e?.stopPropagation();
|
|
110
|
+
setStartError(null);
|
|
111
|
+
const result = await toggleAgent(agent);
|
|
112
|
+
if (result.error) {
|
|
113
|
+
setStartError(result.error);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const handleDeleteAgent = async (id: string, e?: React.MouseEvent) => {
|
|
118
|
+
e?.stopPropagation();
|
|
119
|
+
if (selectedAgent?.id === id) {
|
|
120
|
+
setSelectedAgent(null);
|
|
121
|
+
}
|
|
122
|
+
await deleteAgent(id);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const handleSelectAgent = (agent: Agent) => {
|
|
126
|
+
setSelectedAgent(agent);
|
|
127
|
+
setStartError(null);
|
|
128
|
+
setRoute("agents");
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const handleNavigate = (newRoute: Route) => {
|
|
132
|
+
setRoute(newRoute);
|
|
133
|
+
setSelectedAgent(null);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const handleOnboardingComplete = () => {
|
|
137
|
+
setOnboardingComplete(true);
|
|
138
|
+
fetchProviders();
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// Show loading while checking onboarding
|
|
142
|
+
if (onboardingComplete === null) {
|
|
143
|
+
return <LoadingSpinner fullScreen />;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Show onboarding if not complete
|
|
147
|
+
if (!onboardingComplete) {
|
|
148
|
+
return <OnboardingWizard onComplete={handleOnboardingComplete} />;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
<div className="min-h-screen bg-[#0a0a0a] text-[#e0e0e0] font-mono flex flex-col">
|
|
153
|
+
<Header
|
|
154
|
+
onNewAgent={() => setShowCreate(true)}
|
|
155
|
+
canCreateAgent={configuredProviders.length > 0}
|
|
156
|
+
/>
|
|
157
|
+
|
|
158
|
+
{startError && (
|
|
159
|
+
<ErrorBanner message={startError} onDismiss={() => setStartError(null)} />
|
|
160
|
+
)}
|
|
161
|
+
|
|
162
|
+
<div className="flex flex-1 overflow-hidden">
|
|
163
|
+
<Sidebar
|
|
164
|
+
route={route}
|
|
165
|
+
agentCount={agents.length}
|
|
166
|
+
onNavigate={handleNavigate}
|
|
167
|
+
/>
|
|
168
|
+
|
|
169
|
+
<main className="flex-1 overflow-hidden flex">
|
|
170
|
+
{route === "settings" && <SettingsPage />}
|
|
171
|
+
|
|
172
|
+
{route === "agents" && (
|
|
173
|
+
<AgentsView
|
|
174
|
+
agents={agents}
|
|
175
|
+
loading={loading}
|
|
176
|
+
selectedAgent={selectedAgent}
|
|
177
|
+
onSelectAgent={handleSelectAgent}
|
|
178
|
+
onCloseAgent={() => setSelectedAgent(null)}
|
|
179
|
+
onToggleAgent={handleToggleAgent}
|
|
180
|
+
onDeleteAgent={handleDeleteAgent}
|
|
181
|
+
/>
|
|
182
|
+
)}
|
|
183
|
+
|
|
184
|
+
{route === "dashboard" && (
|
|
185
|
+
<Dashboard
|
|
186
|
+
agents={agents}
|
|
187
|
+
loading={loading}
|
|
188
|
+
runningCount={runningCount}
|
|
189
|
+
configuredProviders={configuredProviders}
|
|
190
|
+
onNavigate={handleNavigate}
|
|
191
|
+
onSelectAgent={handleSelectAgent}
|
|
192
|
+
/>
|
|
193
|
+
)}
|
|
194
|
+
</main>
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
{showCreate && (
|
|
198
|
+
<CreateAgentModal
|
|
199
|
+
form={newAgent}
|
|
200
|
+
providers={providers}
|
|
201
|
+
configuredProviders={configuredProviders}
|
|
202
|
+
onFormChange={setNewAgent}
|
|
203
|
+
onProviderChange={handleProviderChange}
|
|
204
|
+
onCreate={handleCreateAgent}
|
|
205
|
+
onClose={() => setShowCreate(false)}
|
|
206
|
+
onGoToSettings={() => {
|
|
207
|
+
setShowCreate(false);
|
|
208
|
+
setRoute("settings");
|
|
209
|
+
}}
|
|
210
|
+
/>
|
|
211
|
+
)}
|
|
212
|
+
</div>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Mount the app
|
|
217
|
+
const root = createRoot(document.getElementById("root")!);
|
|
218
|
+
root.render(<App />);
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { Agent } from "../../types";
|
|
3
|
+
|
|
4
|
+
interface AgentCardProps {
|
|
5
|
+
agent: Agent;
|
|
6
|
+
selected: boolean;
|
|
7
|
+
onSelect: () => void;
|
|
8
|
+
onToggle: (e?: React.MouseEvent) => void;
|
|
9
|
+
onDelete: (e?: React.MouseEvent) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function AgentCard({ agent, selected, onSelect, onToggle, onDelete }: AgentCardProps) {
|
|
13
|
+
return (
|
|
14
|
+
<div
|
|
15
|
+
onClick={onSelect}
|
|
16
|
+
className={`bg-[#111] rounded p-5 border transition cursor-pointer ${
|
|
17
|
+
selected
|
|
18
|
+
? 'border-[#f97316]'
|
|
19
|
+
: 'border-[#1a1a1a] hover:border-[#333]'
|
|
20
|
+
}`}
|
|
21
|
+
>
|
|
22
|
+
<div className="flex items-start justify-between mb-3">
|
|
23
|
+
<div>
|
|
24
|
+
<h3 className="font-semibold text-lg">{agent.name}</h3>
|
|
25
|
+
<p className="text-sm text-[#666]">
|
|
26
|
+
{agent.provider} / {agent.model}
|
|
27
|
+
{agent.port && <span className="text-[#444]"> · :{agent.port}</span>}
|
|
28
|
+
</p>
|
|
29
|
+
</div>
|
|
30
|
+
<StatusBadge status={agent.status} />
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<p className="text-sm text-[#666] line-clamp-2 mb-4">
|
|
34
|
+
{agent.systemPrompt}
|
|
35
|
+
</p>
|
|
36
|
+
|
|
37
|
+
<div className="flex gap-2">
|
|
38
|
+
<button
|
|
39
|
+
onClick={onToggle}
|
|
40
|
+
className={`flex-1 px-3 py-1.5 rounded text-sm font-medium transition ${
|
|
41
|
+
agent.status === "running"
|
|
42
|
+
? "bg-[#f97316]/20 text-[#f97316] hover:bg-[#f97316]/30"
|
|
43
|
+
: "bg-[#3b82f6]/20 text-[#3b82f6] hover:bg-[#3b82f6]/30"
|
|
44
|
+
}`}
|
|
45
|
+
>
|
|
46
|
+
{agent.status === "running" ? "Stop" : "Start"}
|
|
47
|
+
</button>
|
|
48
|
+
<button
|
|
49
|
+
onClick={onDelete}
|
|
50
|
+
className="px-3 py-1.5 rounded text-sm font-medium bg-red-500/20 text-red-400 hover:bg-red-500/30 transition"
|
|
51
|
+
>
|
|
52
|
+
Delete
|
|
53
|
+
</button>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function StatusBadge({ status }: { status: Agent["status"] }) {
|
|
60
|
+
return (
|
|
61
|
+
<span
|
|
62
|
+
className={`px-2 py-1 rounded text-xs font-medium ${
|
|
63
|
+
status === "running"
|
|
64
|
+
? "bg-[#3b82f6]/20 text-[#3b82f6]"
|
|
65
|
+
: "bg-[#333] text-[#666]"
|
|
66
|
+
}`}
|
|
67
|
+
>
|
|
68
|
+
{status}
|
|
69
|
+
</span>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { AgentCard } from "./AgentCard";
|
|
3
|
+
import { ChatPanel } from "./ChatPanel";
|
|
4
|
+
import { LoadingSpinner } from "../common/LoadingSpinner";
|
|
5
|
+
import type { Agent } from "../../types";
|
|
6
|
+
|
|
7
|
+
interface AgentsViewProps {
|
|
8
|
+
agents: Agent[];
|
|
9
|
+
loading: boolean;
|
|
10
|
+
selectedAgent: Agent | null;
|
|
11
|
+
onSelectAgent: (agent: Agent) => void;
|
|
12
|
+
onCloseAgent: () => void;
|
|
13
|
+
onToggleAgent: (agent: Agent, e?: React.MouseEvent) => void;
|
|
14
|
+
onDeleteAgent: (id: string, e?: React.MouseEvent) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function AgentsView({
|
|
18
|
+
agents,
|
|
19
|
+
loading,
|
|
20
|
+
selectedAgent,
|
|
21
|
+
onSelectAgent,
|
|
22
|
+
onCloseAgent,
|
|
23
|
+
onToggleAgent,
|
|
24
|
+
onDeleteAgent,
|
|
25
|
+
}: AgentsViewProps) {
|
|
26
|
+
return (
|
|
27
|
+
<div className="flex-1 flex overflow-hidden">
|
|
28
|
+
{/* Agents list */}
|
|
29
|
+
<div className={`${selectedAgent ? 'w-1/2 border-r border-[#1a1a1a]' : 'flex-1'} overflow-auto p-6 transition-all`}>
|
|
30
|
+
{loading ? (
|
|
31
|
+
<LoadingSpinner message="Loading agents..." />
|
|
32
|
+
) : agents.length === 0 ? (
|
|
33
|
+
<EmptyState />
|
|
34
|
+
) : (
|
|
35
|
+
<div className={`grid gap-4 ${selectedAgent ? 'grid-cols-1 xl:grid-cols-2' : 'md:grid-cols-2 xl:grid-cols-3'}`}>
|
|
36
|
+
{agents.map((agent) => (
|
|
37
|
+
<AgentCard
|
|
38
|
+
key={agent.id}
|
|
39
|
+
agent={agent}
|
|
40
|
+
selected={selectedAgent?.id === agent.id}
|
|
41
|
+
onSelect={() => onSelectAgent(agent)}
|
|
42
|
+
onToggle={(e) => onToggleAgent(agent, e)}
|
|
43
|
+
onDelete={(e) => onDeleteAgent(agent.id, e)}
|
|
44
|
+
/>
|
|
45
|
+
))}
|
|
46
|
+
</div>
|
|
47
|
+
)}
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
{/* Chat Panel */}
|
|
51
|
+
{selectedAgent && (
|
|
52
|
+
<ChatPanel
|
|
53
|
+
agent={selectedAgent}
|
|
54
|
+
onClose={onCloseAgent}
|
|
55
|
+
onStartAgent={(e) => onToggleAgent(selectedAgent, e)}
|
|
56
|
+
/>
|
|
57
|
+
)}
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function EmptyState() {
|
|
63
|
+
return (
|
|
64
|
+
<div className="text-center py-20 text-[#666]">
|
|
65
|
+
<p className="text-lg">No agents yet</p>
|
|
66
|
+
<p className="text-sm mt-1">Create your first agent to get started</p>
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Chat } from "@apteva/apteva-kit";
|
|
3
|
+
import { CloseIcon } from "../common/Icons";
|
|
4
|
+
import type { Agent } from "../../types";
|
|
5
|
+
|
|
6
|
+
interface ChatPanelProps {
|
|
7
|
+
agent: Agent;
|
|
8
|
+
onClose: () => void;
|
|
9
|
+
onStartAgent: (e?: React.MouseEvent) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function ChatPanel({ agent, onClose, onStartAgent }: ChatPanelProps) {
|
|
13
|
+
if (agent.status === "running" && agent.port) {
|
|
14
|
+
return (
|
|
15
|
+
<div className="w-1/2 flex flex-col overflow-hidden">
|
|
16
|
+
<div className="flex-1 flex flex-col overflow-hidden relative">
|
|
17
|
+
<button
|
|
18
|
+
onClick={onClose}
|
|
19
|
+
className="absolute top-3 right-3 z-10 text-[#666] hover:text-[#e0e0e0] transition"
|
|
20
|
+
>
|
|
21
|
+
<CloseIcon />
|
|
22
|
+
</button>
|
|
23
|
+
<Chat
|
|
24
|
+
agentId="default"
|
|
25
|
+
apiUrl={`/api/agents/${agent.id}`}
|
|
26
|
+
placeholder="Message this agent..."
|
|
27
|
+
context={agent.systemPrompt}
|
|
28
|
+
variant="terminal"
|
|
29
|
+
headerTitle={agent.name}
|
|
30
|
+
/>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div className="w-1/2 flex flex-col overflow-hidden">
|
|
38
|
+
<div className="border-b border-[#1a1a1a] p-4 flex items-center justify-between">
|
|
39
|
+
<div>
|
|
40
|
+
<h2 className="font-semibold">{agent.name}</h2>
|
|
41
|
+
<p className="text-sm text-[#666]">{agent.provider} / {agent.model}</p>
|
|
42
|
+
</div>
|
|
43
|
+
<button
|
|
44
|
+
onClick={onClose}
|
|
45
|
+
className="text-[#666] hover:text-[#e0e0e0] transition"
|
|
46
|
+
>
|
|
47
|
+
<CloseIcon />
|
|
48
|
+
</button>
|
|
49
|
+
</div>
|
|
50
|
+
<div className="flex-1 flex items-center justify-center text-[#666]">
|
|
51
|
+
<div className="text-center">
|
|
52
|
+
<p className="text-lg mb-2">Agent is not running</p>
|
|
53
|
+
<button
|
|
54
|
+
onClick={onStartAgent}
|
|
55
|
+
className="bg-[#3b82f6]/20 text-[#3b82f6] hover:bg-[#3b82f6]/30 px-4 py-2 rounded font-medium transition"
|
|
56
|
+
>
|
|
57
|
+
Start Agent
|
|
58
|
+
</button>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Modal } from "../common/Modal";
|
|
3
|
+
import { Select } from "../common/Select";
|
|
4
|
+
import type { Provider, NewAgentForm } from "../../types";
|
|
5
|
+
|
|
6
|
+
interface CreateAgentModalProps {
|
|
7
|
+
form: NewAgentForm;
|
|
8
|
+
providers: Provider[];
|
|
9
|
+
configuredProviders: Provider[];
|
|
10
|
+
onFormChange: (form: NewAgentForm) => void;
|
|
11
|
+
onProviderChange: (providerId: string) => void;
|
|
12
|
+
onCreate: () => void;
|
|
13
|
+
onClose: () => void;
|
|
14
|
+
onGoToSettings: () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function CreateAgentModal({
|
|
18
|
+
form,
|
|
19
|
+
providers,
|
|
20
|
+
configuredProviders,
|
|
21
|
+
onFormChange,
|
|
22
|
+
onProviderChange,
|
|
23
|
+
onCreate,
|
|
24
|
+
onClose,
|
|
25
|
+
onGoToSettings,
|
|
26
|
+
}: CreateAgentModalProps) {
|
|
27
|
+
const selectedProvider = providers.find(p => p.id === form.provider);
|
|
28
|
+
|
|
29
|
+
const providerOptions = configuredProviders.map(p => ({
|
|
30
|
+
value: p.id,
|
|
31
|
+
label: p.name,
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
const modelOptions = selectedProvider?.models.map(m => ({
|
|
35
|
+
value: m.value,
|
|
36
|
+
label: m.label,
|
|
37
|
+
recommended: m.recommended,
|
|
38
|
+
})) || [];
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<Modal>
|
|
42
|
+
<h2 className="text-xl font-semibold mb-4">Create New Agent</h2>
|
|
43
|
+
|
|
44
|
+
{configuredProviders.length === 0 ? (
|
|
45
|
+
<NoProvidersMessage onGoToSettings={onGoToSettings} />
|
|
46
|
+
) : (
|
|
47
|
+
<>
|
|
48
|
+
<div className="space-y-4">
|
|
49
|
+
<FormField label="Name">
|
|
50
|
+
<input
|
|
51
|
+
type="text"
|
|
52
|
+
value={form.name}
|
|
53
|
+
onChange={(e) => onFormChange({ ...form, name: e.target.value })}
|
|
54
|
+
className="w-full bg-[#0a0a0a] border border-[#222] rounded px-3 py-2 focus:outline-none focus:border-[#f97316] text-[#e0e0e0]"
|
|
55
|
+
placeholder="My Agent"
|
|
56
|
+
/>
|
|
57
|
+
</FormField>
|
|
58
|
+
|
|
59
|
+
<FormField label="Provider">
|
|
60
|
+
<Select
|
|
61
|
+
value={form.provider}
|
|
62
|
+
options={providerOptions}
|
|
63
|
+
onChange={onProviderChange}
|
|
64
|
+
placeholder="Select provider..."
|
|
65
|
+
/>
|
|
66
|
+
</FormField>
|
|
67
|
+
|
|
68
|
+
<FormField label="Model">
|
|
69
|
+
<Select
|
|
70
|
+
value={form.model}
|
|
71
|
+
options={modelOptions}
|
|
72
|
+
onChange={(value) => onFormChange({ ...form, model: value })}
|
|
73
|
+
placeholder="Select model..."
|
|
74
|
+
/>
|
|
75
|
+
</FormField>
|
|
76
|
+
|
|
77
|
+
<FormField label="System Prompt">
|
|
78
|
+
<textarea
|
|
79
|
+
value={form.systemPrompt}
|
|
80
|
+
onChange={(e) => onFormChange({ ...form, systemPrompt: e.target.value })}
|
|
81
|
+
className="w-full bg-[#0a0a0a] border border-[#222] rounded px-3 py-2 h-24 resize-none focus:outline-none focus:border-[#f97316] text-[#e0e0e0]"
|
|
82
|
+
/>
|
|
83
|
+
</FormField>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<div className="flex gap-3 mt-6">
|
|
87
|
+
<button
|
|
88
|
+
onClick={onClose}
|
|
89
|
+
className="flex-1 border border-[#333] hover:border-[#f97316] hover:text-[#f97316] px-4 py-2 rounded font-medium transition"
|
|
90
|
+
>
|
|
91
|
+
Cancel
|
|
92
|
+
</button>
|
|
93
|
+
<button
|
|
94
|
+
onClick={onCreate}
|
|
95
|
+
disabled={!form.name}
|
|
96
|
+
className="flex-1 bg-[#f97316] hover:bg-[#fb923c] disabled:opacity-50 text-black px-4 py-2 rounded font-medium transition"
|
|
97
|
+
>
|
|
98
|
+
Create
|
|
99
|
+
</button>
|
|
100
|
+
</div>
|
|
101
|
+
</>
|
|
102
|
+
)}
|
|
103
|
+
</Modal>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function FormField({ label, children }: { label: string; children: React.ReactNode }) {
|
|
108
|
+
return (
|
|
109
|
+
<div>
|
|
110
|
+
<label className="block text-sm text-[#666] mb-1">{label}</label>
|
|
111
|
+
{children}
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function NoProvidersMessage({ onGoToSettings }: { onGoToSettings: () => void }) {
|
|
117
|
+
return (
|
|
118
|
+
<div className="text-center py-6">
|
|
119
|
+
<p className="text-[#666] mb-4">No API keys configured. Add a provider key first.</p>
|
|
120
|
+
<button
|
|
121
|
+
onClick={onGoToSettings}
|
|
122
|
+
className="bg-[#f97316] hover:bg-[#fb923c] text-black px-4 py-2 rounded font-medium transition"
|
|
123
|
+
>
|
|
124
|
+
Go to Settings
|
|
125
|
+
</button>
|
|
126
|
+
</div>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
interface IconProps {
|
|
4
|
+
className?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function CheckIcon({ className = "w-4 h-4" }: IconProps) {
|
|
8
|
+
return (
|
|
9
|
+
<svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
10
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
11
|
+
</svg>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function CloseIcon({ className = "w-5 h-5" }: IconProps) {
|
|
16
|
+
return (
|
|
17
|
+
<svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
18
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
19
|
+
</svg>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function DashboardIcon({ className = "w-5 h-5" }: IconProps) {
|
|
24
|
+
return (
|
|
25
|
+
<svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
26
|
+
<path
|
|
27
|
+
strokeLinecap="round"
|
|
28
|
+
strokeLinejoin="round"
|
|
29
|
+
strokeWidth={2}
|
|
30
|
+
d="M4 5a1 1 0 011-1h14a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM4 13a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6zM16 13a1 1 0 011-1h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6z"
|
|
31
|
+
/>
|
|
32
|
+
</svg>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function AgentsIcon({ className = "w-5 h-5" }: IconProps) {
|
|
37
|
+
return (
|
|
38
|
+
<svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
39
|
+
<path
|
|
40
|
+
strokeLinecap="round"
|
|
41
|
+
strokeLinejoin="round"
|
|
42
|
+
strokeWidth={2}
|
|
43
|
+
d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
|
|
44
|
+
/>
|
|
45
|
+
</svg>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function SettingsIcon({ className = "w-5 h-5" }: IconProps) {
|
|
50
|
+
return (
|
|
51
|
+
<svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
52
|
+
<path
|
|
53
|
+
strokeLinecap="round"
|
|
54
|
+
strokeLinejoin="round"
|
|
55
|
+
strokeWidth={2}
|
|
56
|
+
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
|
|
57
|
+
/>
|
|
58
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
59
|
+
</svg>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
interface LoadingSpinnerProps {
|
|
4
|
+
message?: string;
|
|
5
|
+
fullScreen?: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function LoadingSpinner({ message = "Loading...", fullScreen = false }: LoadingSpinnerProps) {
|
|
9
|
+
const content = (
|
|
10
|
+
<div className="flex items-center gap-3 text-[#666]">
|
|
11
|
+
<svg className="animate-spin h-5 w-5" viewBox="0 0 24 24">
|
|
12
|
+
<circle
|
|
13
|
+
className="opacity-25"
|
|
14
|
+
cx="12"
|
|
15
|
+
cy="12"
|
|
16
|
+
r="10"
|
|
17
|
+
stroke="currentColor"
|
|
18
|
+
strokeWidth="4"
|
|
19
|
+
fill="none"
|
|
20
|
+
/>
|
|
21
|
+
<path
|
|
22
|
+
className="opacity-75"
|
|
23
|
+
fill="currentColor"
|
|
24
|
+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
25
|
+
/>
|
|
26
|
+
</svg>
|
|
27
|
+
<span>{message}</span>
|
|
28
|
+
</div>
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
if (fullScreen) {
|
|
32
|
+
return (
|
|
33
|
+
<div className="min-h-screen bg-[#0a0a0a] text-[#e0e0e0] font-mono flex items-center justify-center">
|
|
34
|
+
{content}
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div className="flex items-center justify-center py-20">
|
|
41
|
+
{content}
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
interface ModalProps {
|
|
4
|
+
children: React.ReactNode;
|
|
5
|
+
onClose?: () => void;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function Modal({ children, onClose }: ModalProps) {
|
|
9
|
+
return (
|
|
10
|
+
<div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50">
|
|
11
|
+
<div className="bg-[#111] rounded p-6 w-full max-w-md border border-[#1a1a1a]">
|
|
12
|
+
{children}
|
|
13
|
+
</div>
|
|
14
|
+
</div>
|
|
15
|
+
);
|
|
16
|
+
}
|