apteva 0.2.10 → 0.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/App.mvbdnw89.js +227 -0
- package/dist/index.html +1 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/src/auth/index.ts +11 -3
- package/src/auth/middleware.ts +1 -0
- package/src/binary.ts +7 -5
- package/src/crypto.ts +4 -0
- package/src/db.ts +437 -14
- package/src/integrations/skillsmp.ts +318 -0
- package/src/providers.ts +21 -0
- package/src/routes/api.ts +836 -16
- package/src/server.ts +58 -7
- package/src/web/App.tsx +24 -8
- package/src/web/components/agents/AgentCard.tsx +36 -11
- package/src/web/components/agents/AgentPanel.tsx +333 -24
- package/src/web/components/agents/AgentsView.tsx +1 -1
- package/src/web/components/agents/CreateAgentModal.tsx +169 -23
- package/src/web/components/common/Icons.tsx +8 -0
- package/src/web/components/common/index.ts +1 -0
- package/src/web/components/index.ts +1 -0
- package/src/web/components/layout/Header.tsx +4 -2
- package/src/web/components/layout/Sidebar.tsx +7 -1
- package/src/web/components/mcp/McpPage.tsx +602 -19
- package/src/web/components/meta-agent/MetaAgent.tsx +222 -0
- package/src/web/components/settings/SettingsPage.tsx +212 -150
- package/src/web/components/skills/SkillsPage.tsx +871 -0
- package/src/web/context/AuthContext.tsx +5 -0
- package/src/web/context/ProjectContext.tsx +26 -4
- package/src/web/types.ts +48 -3
- package/dist/App.44ge5b89.js +0 -218
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import React, { useState, useEffect, createContext, useContext, type ReactNode } from "react";
|
|
2
|
+
import { Chat } from "@apteva/apteva-kit";
|
|
3
|
+
import { useAuth } from "../../context";
|
|
4
|
+
|
|
5
|
+
interface MetaAgentStatus {
|
|
6
|
+
enabled: boolean;
|
|
7
|
+
available?: boolean;
|
|
8
|
+
reason?: string;
|
|
9
|
+
agent?: {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
status: "stopped" | "running";
|
|
13
|
+
port: number | null;
|
|
14
|
+
provider: string;
|
|
15
|
+
model: string;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface MetaAgentContextValue {
|
|
20
|
+
status: MetaAgentStatus | null;
|
|
21
|
+
isOpen: boolean;
|
|
22
|
+
isStarting: boolean;
|
|
23
|
+
error: string | null;
|
|
24
|
+
isAvailable: boolean;
|
|
25
|
+
isRunning: boolean;
|
|
26
|
+
agent: MetaAgentStatus["agent"] | undefined;
|
|
27
|
+
toggle: () => void;
|
|
28
|
+
close: () => void;
|
|
29
|
+
startAgent: () => Promise<void>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const MetaAgentContext = createContext<MetaAgentContextValue | null>(null);
|
|
33
|
+
|
|
34
|
+
export function useMetaAgent() {
|
|
35
|
+
return useContext(MetaAgentContext);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function MetaAgentProvider({ children }: { children: ReactNode }) {
|
|
39
|
+
const { authFetch, isAuthenticated, isLoading: authLoading } = useAuth();
|
|
40
|
+
const [status, setStatus] = useState<MetaAgentStatus | null>(null);
|
|
41
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
42
|
+
const [isStarting, setIsStarting] = useState(false);
|
|
43
|
+
const [error, setError] = useState<string | null>(null);
|
|
44
|
+
|
|
45
|
+
// Fetch meta agent status
|
|
46
|
+
const fetchStatus = async () => {
|
|
47
|
+
try {
|
|
48
|
+
const res = await authFetch("/api/meta-agent/status");
|
|
49
|
+
const data = await res.json();
|
|
50
|
+
setStatus(data);
|
|
51
|
+
} catch (e) {
|
|
52
|
+
console.error("[MetaAgent] Failed to fetch status:", e);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
// Only fetch when authenticated
|
|
58
|
+
if (!authLoading && isAuthenticated) {
|
|
59
|
+
fetchStatus();
|
|
60
|
+
}
|
|
61
|
+
}, [authFetch, isAuthenticated, authLoading]);
|
|
62
|
+
|
|
63
|
+
// Start the meta agent
|
|
64
|
+
const startAgent = async () => {
|
|
65
|
+
setIsStarting(true);
|
|
66
|
+
setError(null);
|
|
67
|
+
try {
|
|
68
|
+
const res = await authFetch("/api/meta-agent/start", { method: "POST" });
|
|
69
|
+
const data = await res.json();
|
|
70
|
+
if (!res.ok) {
|
|
71
|
+
setError(data.error || "Failed to start assistant");
|
|
72
|
+
} else {
|
|
73
|
+
await fetchStatus();
|
|
74
|
+
}
|
|
75
|
+
} catch (e) {
|
|
76
|
+
setError("Failed to start assistant");
|
|
77
|
+
}
|
|
78
|
+
setIsStarting(false);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const isAvailable = !!(status?.enabled && status?.available);
|
|
82
|
+
const agent = status?.agent;
|
|
83
|
+
const isRunning = !!(agent?.status === "running" && agent?.port);
|
|
84
|
+
|
|
85
|
+
const value: MetaAgentContextValue = {
|
|
86
|
+
status,
|
|
87
|
+
isOpen,
|
|
88
|
+
isStarting,
|
|
89
|
+
error,
|
|
90
|
+
isAvailable,
|
|
91
|
+
isRunning,
|
|
92
|
+
agent,
|
|
93
|
+
toggle: () => setIsOpen(!isOpen),
|
|
94
|
+
close: () => setIsOpen(false),
|
|
95
|
+
startAgent,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<MetaAgentContext.Provider value={value}>
|
|
100
|
+
{children}
|
|
101
|
+
</MetaAgentContext.Provider>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Header button component - to be used in Header.tsx
|
|
106
|
+
export function MetaAgentButton() {
|
|
107
|
+
const ctx = useMetaAgent();
|
|
108
|
+
if (!ctx?.isAvailable) return null;
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<button
|
|
112
|
+
onClick={ctx.toggle}
|
|
113
|
+
className={`hidden md:flex items-center gap-2 px-3 py-2 rounded transition ${
|
|
114
|
+
ctx.isOpen
|
|
115
|
+
? "bg-[#f97316] text-white"
|
|
116
|
+
: "bg-[#151515] hover:bg-[#1a1a1a] text-[#888] hover:text-white"
|
|
117
|
+
}`}
|
|
118
|
+
title="Apteva Assistant"
|
|
119
|
+
>
|
|
120
|
+
<AssistantIcon className="w-5 h-5" />
|
|
121
|
+
<span className="text-sm">Assistant</span>
|
|
122
|
+
{ctx.isRunning && (
|
|
123
|
+
<span className="w-2 h-2 rounded-full bg-green-400" />
|
|
124
|
+
)}
|
|
125
|
+
</button>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Chat panel component - renders as a right-side drawer
|
|
130
|
+
export function MetaAgentPanel() {
|
|
131
|
+
const ctx = useMetaAgent();
|
|
132
|
+
if (!ctx?.isAvailable || !ctx.isOpen) return null;
|
|
133
|
+
|
|
134
|
+
const { agent, isRunning, error, isStarting, startAgent, close } = ctx;
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<>
|
|
138
|
+
{/* Backdrop */}
|
|
139
|
+
<div
|
|
140
|
+
className="hidden md:block fixed inset-0 bg-black/20 z-40"
|
|
141
|
+
onClick={close}
|
|
142
|
+
/>
|
|
143
|
+
|
|
144
|
+
{/* Drawer Panel */}
|
|
145
|
+
<div className="hidden md:flex fixed top-0 right-0 h-full w-[480px] lg:w-[540px] bg-[#0a0a0a] border-l border-[#1a1a1a] shadow-2xl z-50 flex-col">
|
|
146
|
+
{/* Header */}
|
|
147
|
+
<div className="flex items-center justify-between px-4 py-3 border-b border-[#1a1a1a] bg-[#111]">
|
|
148
|
+
<div className="flex items-center gap-2">
|
|
149
|
+
<div className={`w-2 h-2 rounded-full ${isRunning ? "bg-green-400" : "bg-[#444]"}`} />
|
|
150
|
+
<span className="font-medium text-sm">Apteva Assistant</span>
|
|
151
|
+
</div>
|
|
152
|
+
<button
|
|
153
|
+
onClick={close}
|
|
154
|
+
className="text-[#666] hover:text-[#888] transition"
|
|
155
|
+
>
|
|
156
|
+
<CloseIcon />
|
|
157
|
+
</button>
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
{/* Content */}
|
|
161
|
+
<div className="flex-1 min-h-0 flex flex-col">
|
|
162
|
+
{isRunning ? (
|
|
163
|
+
<Chat
|
|
164
|
+
agentId="default"
|
|
165
|
+
apiUrl={`/api/agents/${agent!.id}`}
|
|
166
|
+
placeholder="Ask me anything about Apteva..."
|
|
167
|
+
variant="terminal"
|
|
168
|
+
showHeader={false}
|
|
169
|
+
/>
|
|
170
|
+
) : (
|
|
171
|
+
<div className="flex-1 flex flex-col items-center justify-center p-6 text-center">
|
|
172
|
+
<AssistantIcon className="w-12 h-12 text-[#333] mb-4" />
|
|
173
|
+
<h3 className="font-medium mb-2">Apteva Assistant</h3>
|
|
174
|
+
<p className="text-sm text-[#666] mb-6">
|
|
175
|
+
I can help you navigate Apteva, create agents, set up MCP servers, and more.
|
|
176
|
+
</p>
|
|
177
|
+
{error && (
|
|
178
|
+
<p className="text-sm text-red-400 mb-4">{error}</p>
|
|
179
|
+
)}
|
|
180
|
+
<button
|
|
181
|
+
onClick={startAgent}
|
|
182
|
+
disabled={isStarting}
|
|
183
|
+
className="bg-[#f97316] hover:bg-[#fb923c] disabled:opacity-50 text-white px-6 py-2 rounded font-medium transition"
|
|
184
|
+
>
|
|
185
|
+
{isStarting ? "Starting..." : "Start Assistant"}
|
|
186
|
+
</button>
|
|
187
|
+
</div>
|
|
188
|
+
)}
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
</>
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function AssistantIcon({ className = "w-6 h-6" }: { className?: string }) {
|
|
196
|
+
return (
|
|
197
|
+
<svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
198
|
+
<path
|
|
199
|
+
strokeLinecap="round"
|
|
200
|
+
strokeLinejoin="round"
|
|
201
|
+
strokeWidth={2}
|
|
202
|
+
d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"
|
|
203
|
+
/>
|
|
204
|
+
</svg>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function CloseIcon() {
|
|
209
|
+
return (
|
|
210
|
+
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
211
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
212
|
+
</svg>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function MinimizeIcon() {
|
|
217
|
+
return (
|
|
218
|
+
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
219
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
220
|
+
</svg>
|
|
221
|
+
);
|
|
222
|
+
}
|
|
@@ -7,41 +7,56 @@ import type { Provider } from "../../types";
|
|
|
7
7
|
type SettingsTab = "providers" | "projects" | "updates" | "data";
|
|
8
8
|
|
|
9
9
|
export function SettingsPage() {
|
|
10
|
+
const { projectsEnabled } = useProjects();
|
|
10
11
|
const [activeTab, setActiveTab] = useState<SettingsTab>("providers");
|
|
11
12
|
|
|
13
|
+
const tabs: { key: SettingsTab; label: string }[] = [
|
|
14
|
+
{ key: "providers", label: "Providers" },
|
|
15
|
+
...(projectsEnabled ? [{ key: "projects" as SettingsTab, label: "Projects" }] : []),
|
|
16
|
+
{ key: "updates", label: "Updates" },
|
|
17
|
+
{ key: "data", label: "Data" },
|
|
18
|
+
];
|
|
19
|
+
|
|
12
20
|
return (
|
|
13
|
-
<div className="flex-1 flex overflow-hidden">
|
|
14
|
-
{/*
|
|
15
|
-
<div className="
|
|
21
|
+
<div className="flex-1 flex flex-col md:flex-row overflow-hidden">
|
|
22
|
+
{/* Mobile: Horizontal scrolling tabs */}
|
|
23
|
+
<div className="md:hidden border-b border-[#1a1a1a] bg-[#0a0a0a]">
|
|
24
|
+
<div className="flex overflow-x-auto" style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}>
|
|
25
|
+
{tabs.map(tab => (
|
|
26
|
+
<button
|
|
27
|
+
key={tab.key}
|
|
28
|
+
onClick={() => setActiveTab(tab.key)}
|
|
29
|
+
className={`flex-shrink-0 px-4 py-3 text-sm font-medium border-b-2 transition ${
|
|
30
|
+
activeTab === tab.key
|
|
31
|
+
? "border-[#f97316] text-[#f97316]"
|
|
32
|
+
: "border-transparent text-[#666] hover:text-[#888]"
|
|
33
|
+
}`}
|
|
34
|
+
>
|
|
35
|
+
{tab.label}
|
|
36
|
+
</button>
|
|
37
|
+
))}
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
{/* Desktop: Settings Sidebar */}
|
|
42
|
+
<div className="hidden md:block w-48 border-r border-[#1a1a1a] p-4 flex-shrink-0">
|
|
16
43
|
<h2 className="text-sm font-medium text-[#666] uppercase tracking-wider mb-3">Settings</h2>
|
|
17
44
|
<nav className="space-y-1">
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
onClick={() => setActiveTab("projects")}
|
|
27
|
-
/>
|
|
28
|
-
<SettingsNavItem
|
|
29
|
-
label="Updates"
|
|
30
|
-
active={activeTab === "updates"}
|
|
31
|
-
onClick={() => setActiveTab("updates")}
|
|
32
|
-
/>
|
|
33
|
-
<SettingsNavItem
|
|
34
|
-
label="Data"
|
|
35
|
-
active={activeTab === "data"}
|
|
36
|
-
onClick={() => setActiveTab("data")}
|
|
37
|
-
/>
|
|
45
|
+
{tabs.map(tab => (
|
|
46
|
+
<SettingsNavItem
|
|
47
|
+
key={tab.key}
|
|
48
|
+
label={tab.label}
|
|
49
|
+
active={activeTab === tab.key}
|
|
50
|
+
onClick={() => setActiveTab(tab.key)}
|
|
51
|
+
/>
|
|
52
|
+
))}
|
|
38
53
|
</nav>
|
|
39
54
|
</div>
|
|
40
55
|
|
|
41
56
|
{/* Settings Content */}
|
|
42
|
-
<div className="flex-1 overflow-auto p-6">
|
|
57
|
+
<div className="flex-1 overflow-auto p-4 md:p-6">
|
|
43
58
|
{activeTab === "providers" && <ProvidersSettings />}
|
|
44
|
-
{activeTab === "projects" && <ProjectsSettings />}
|
|
59
|
+
{activeTab === "projects" && projectsEnabled && <ProjectsSettings />}
|
|
45
60
|
{activeTab === "updates" && <UpdatesSettings />}
|
|
46
61
|
{activeTab === "data" && <DataSettings />}
|
|
47
62
|
</div>
|
|
@@ -121,11 +136,22 @@ function ProvidersSettings() {
|
|
|
121
136
|
body: JSON.stringify({ key: apiKey }),
|
|
122
137
|
});
|
|
123
138
|
|
|
139
|
+
const saveData = await saveRes.json();
|
|
124
140
|
if (!saveRes.ok) {
|
|
125
|
-
|
|
126
|
-
setError(data.error || "Failed to save key");
|
|
141
|
+
setError(saveData.error || "Failed to save key");
|
|
127
142
|
} else {
|
|
128
|
-
|
|
143
|
+
// Build success message including agent restart info
|
|
144
|
+
let msg = "API key saved!";
|
|
145
|
+
if (saveData.restartedAgents && saveData.restartedAgents.length > 0) {
|
|
146
|
+
const successCount = saveData.restartedAgents.filter((a: { success: boolean }) => a.success).length;
|
|
147
|
+
const failCount = saveData.restartedAgents.length - successCount;
|
|
148
|
+
if (failCount === 0) {
|
|
149
|
+
msg += ` Restarted ${successCount} agent${successCount > 1 ? 's' : ''} with new key.`;
|
|
150
|
+
} else {
|
|
151
|
+
msg += ` Restarted ${successCount}/${saveData.restartedAgents.length} agents.`;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
setSuccess(msg);
|
|
129
155
|
setApiKey("");
|
|
130
156
|
setSelectedProvider(null);
|
|
131
157
|
fetchProviders();
|
|
@@ -148,10 +174,34 @@ function ProvidersSettings() {
|
|
|
148
174
|
const llmConfiguredCount = llmProviders.filter(p => p.hasKey).length;
|
|
149
175
|
const intConfiguredCount = integrations.filter(p => p.hasKey).length;
|
|
150
176
|
|
|
177
|
+
// Auto-dismiss success message after 5 seconds
|
|
178
|
+
useEffect(() => {
|
|
179
|
+
if (success && !selectedProvider) {
|
|
180
|
+
const timer = setTimeout(() => setSuccess(null), 5000);
|
|
181
|
+
return () => clearTimeout(timer);
|
|
182
|
+
}
|
|
183
|
+
}, [success, selectedProvider]);
|
|
184
|
+
|
|
151
185
|
return (
|
|
152
186
|
<>
|
|
153
187
|
{ConfirmDialog}
|
|
154
188
|
<div className="space-y-10">
|
|
189
|
+
{/* Global Success Banner */}
|
|
190
|
+
{success && !selectedProvider && (
|
|
191
|
+
<div className="bg-green-500/10 border border-green-500/30 rounded-lg p-4 flex items-center justify-between">
|
|
192
|
+
<div className="flex items-center gap-2 text-green-400">
|
|
193
|
+
<CheckIcon className="w-5 h-5" />
|
|
194
|
+
<span>{success}</span>
|
|
195
|
+
</div>
|
|
196
|
+
<button
|
|
197
|
+
onClick={() => setSuccess(null)}
|
|
198
|
+
className="text-green-400 hover:text-green-300"
|
|
199
|
+
>
|
|
200
|
+
<CloseIcon className="w-4 h-4" />
|
|
201
|
+
</button>
|
|
202
|
+
</div>
|
|
203
|
+
)}
|
|
204
|
+
|
|
155
205
|
{/* AI Providers Section */}
|
|
156
206
|
<div>
|
|
157
207
|
<div className="mb-6">
|
|
@@ -768,73 +818,79 @@ function ProviderKeyCard({
|
|
|
768
818
|
)}
|
|
769
819
|
</div>
|
|
770
820
|
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
<
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
className="text-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
onChange={e => onApiKeyChange(e.target.value)}
|
|
796
|
-
placeholder="Enter API key..."
|
|
797
|
-
autoFocus
|
|
798
|
-
className="w-full bg-[#0a0a0a] border border-[#333] rounded px-3 py-2 focus:outline-none focus:border-[#f97316]"
|
|
799
|
-
/>
|
|
800
|
-
{error && <p className="text-red-400 text-sm">{error}</p>}
|
|
801
|
-
{success && <p className="text-green-400 text-sm">{success}</p>}
|
|
802
|
-
<div className="flex gap-2">
|
|
803
|
-
<button
|
|
804
|
-
onClick={onCancelEdit}
|
|
805
|
-
className="flex-1 px-3 py-1.5 border border-[#333] rounded text-sm hover:border-[#666]"
|
|
806
|
-
>
|
|
807
|
-
Cancel
|
|
808
|
-
</button>
|
|
809
|
-
<button
|
|
810
|
-
onClick={onSave}
|
|
811
|
-
disabled={!apiKey || saving}
|
|
812
|
-
className="flex-1 px-3 py-1.5 bg-[#f97316] text-black rounded text-sm font-medium disabled:opacity-50"
|
|
813
|
-
>
|
|
814
|
-
{testing ? "Validating..." : saving ? "Saving..." : "Save"}
|
|
815
|
-
</button>
|
|
816
|
-
</div>
|
|
817
|
-
</div>
|
|
818
|
-
) : (
|
|
819
|
-
<div className="flex items-center justify-between">
|
|
820
|
-
<a
|
|
821
|
-
href={provider.docsUrl}
|
|
822
|
-
target="_blank"
|
|
823
|
-
rel="noopener noreferrer"
|
|
824
|
-
className="text-sm text-[#3b82f6] hover:underline"
|
|
821
|
+
<div className="mt-3 pt-3 border-t border-[#1a1a1a]">
|
|
822
|
+
{isEditing ? (
|
|
823
|
+
<div className="space-y-3">
|
|
824
|
+
<input
|
|
825
|
+
type="password"
|
|
826
|
+
value={apiKey}
|
|
827
|
+
onChange={e => onApiKeyChange(e.target.value)}
|
|
828
|
+
placeholder={provider.hasKey ? "Enter new API key..." : "Enter API key..."}
|
|
829
|
+
autoFocus
|
|
830
|
+
className="w-full bg-[#0a0a0a] border border-[#333] rounded px-3 py-2 focus:outline-none focus:border-[#f97316]"
|
|
831
|
+
/>
|
|
832
|
+
{error && <p className="text-red-400 text-sm">{error}</p>}
|
|
833
|
+
{success && <p className="text-green-400 text-sm">{success}</p>}
|
|
834
|
+
<div className="flex gap-2">
|
|
835
|
+
<button
|
|
836
|
+
onClick={onCancelEdit}
|
|
837
|
+
className="flex-1 px-3 py-1.5 border border-[#333] rounded text-sm hover:border-[#666]"
|
|
838
|
+
>
|
|
839
|
+
Cancel
|
|
840
|
+
</button>
|
|
841
|
+
<button
|
|
842
|
+
onClick={onSave}
|
|
843
|
+
disabled={!apiKey || saving}
|
|
844
|
+
className="flex-1 px-3 py-1.5 bg-[#f97316] text-black rounded text-sm font-medium disabled:opacity-50"
|
|
825
845
|
>
|
|
826
|
-
|
|
827
|
-
</
|
|
846
|
+
{testing ? "Validating..." : saving ? "Saving..." : "Save"}
|
|
847
|
+
</button>
|
|
848
|
+
</div>
|
|
849
|
+
</div>
|
|
850
|
+
) : provider.hasKey ? (
|
|
851
|
+
<div className="flex items-center justify-between">
|
|
852
|
+
<a
|
|
853
|
+
href={provider.docsUrl}
|
|
854
|
+
target="_blank"
|
|
855
|
+
rel="noopener noreferrer"
|
|
856
|
+
className="text-sm text-[#3b82f6] hover:underline"
|
|
857
|
+
>
|
|
858
|
+
View docs
|
|
859
|
+
</a>
|
|
860
|
+
<div className="flex items-center gap-3">
|
|
828
861
|
<button
|
|
829
862
|
onClick={onStartEdit}
|
|
830
|
-
className="text-sm text-[#
|
|
863
|
+
className="text-sm text-[#888] hover:text-[#e0e0e0]"
|
|
864
|
+
>
|
|
865
|
+
Update key
|
|
866
|
+
</button>
|
|
867
|
+
<button
|
|
868
|
+
onClick={onDelete}
|
|
869
|
+
className="text-red-400 hover:text-red-300 text-sm"
|
|
831
870
|
>
|
|
832
|
-
|
|
871
|
+
Remove
|
|
833
872
|
</button>
|
|
834
873
|
</div>
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
874
|
+
</div>
|
|
875
|
+
) : (
|
|
876
|
+
<div className="flex items-center justify-between">
|
|
877
|
+
<a
|
|
878
|
+
href={provider.docsUrl}
|
|
879
|
+
target="_blank"
|
|
880
|
+
rel="noopener noreferrer"
|
|
881
|
+
className="text-sm text-[#3b82f6] hover:underline"
|
|
882
|
+
>
|
|
883
|
+
Get API key
|
|
884
|
+
</a>
|
|
885
|
+
<button
|
|
886
|
+
onClick={onStartEdit}
|
|
887
|
+
className="text-sm text-[#f97316] hover:text-[#fb923c]"
|
|
888
|
+
>
|
|
889
|
+
+ Add key
|
|
890
|
+
</button>
|
|
891
|
+
</div>
|
|
892
|
+
)}
|
|
893
|
+
</div>
|
|
838
894
|
</div>
|
|
839
895
|
);
|
|
840
896
|
}
|
|
@@ -874,73 +930,79 @@ function IntegrationKeyCard({
|
|
|
874
930
|
)}
|
|
875
931
|
</div>
|
|
876
932
|
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
<
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
className="text-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
onChange={e => onApiKeyChange(e.target.value)}
|
|
902
|
-
placeholder="Enter API key..."
|
|
903
|
-
autoFocus
|
|
904
|
-
className="w-full bg-[#0a0a0a] border border-[#333] rounded px-3 py-2 focus:outline-none focus:border-[#f97316]"
|
|
905
|
-
/>
|
|
906
|
-
{error && <p className="text-red-400 text-sm">{error}</p>}
|
|
907
|
-
{success && <p className="text-green-400 text-sm">{success}</p>}
|
|
908
|
-
<div className="flex gap-2">
|
|
909
|
-
<button
|
|
910
|
-
onClick={onCancelEdit}
|
|
911
|
-
className="flex-1 px-3 py-1.5 border border-[#333] rounded text-sm hover:border-[#666]"
|
|
912
|
-
>
|
|
913
|
-
Cancel
|
|
914
|
-
</button>
|
|
915
|
-
<button
|
|
916
|
-
onClick={onSave}
|
|
917
|
-
disabled={!apiKey || saving}
|
|
918
|
-
className="flex-1 px-3 py-1.5 bg-[#f97316] text-black rounded text-sm font-medium disabled:opacity-50"
|
|
919
|
-
>
|
|
920
|
-
{testing ? "Validating..." : saving ? "Saving..." : "Save"}
|
|
921
|
-
</button>
|
|
922
|
-
</div>
|
|
923
|
-
</div>
|
|
924
|
-
) : (
|
|
925
|
-
<div className="flex items-center justify-between">
|
|
926
|
-
<a
|
|
927
|
-
href={provider.docsUrl}
|
|
928
|
-
target="_blank"
|
|
929
|
-
rel="noopener noreferrer"
|
|
930
|
-
className="text-sm text-[#3b82f6] hover:underline"
|
|
933
|
+
<div className="mt-3 pt-3 border-t border-[#1a1a1a]">
|
|
934
|
+
{isEditing ? (
|
|
935
|
+
<div className="space-y-3">
|
|
936
|
+
<input
|
|
937
|
+
type="password"
|
|
938
|
+
value={apiKey}
|
|
939
|
+
onChange={e => onApiKeyChange(e.target.value)}
|
|
940
|
+
placeholder={provider.hasKey ? "Enter new API key..." : "Enter API key..."}
|
|
941
|
+
autoFocus
|
|
942
|
+
className="w-full bg-[#0a0a0a] border border-[#333] rounded px-3 py-2 focus:outline-none focus:border-[#f97316]"
|
|
943
|
+
/>
|
|
944
|
+
{error && <p className="text-red-400 text-sm">{error}</p>}
|
|
945
|
+
{success && <p className="text-green-400 text-sm">{success}</p>}
|
|
946
|
+
<div className="flex gap-2">
|
|
947
|
+
<button
|
|
948
|
+
onClick={onCancelEdit}
|
|
949
|
+
className="flex-1 px-3 py-1.5 border border-[#333] rounded text-sm hover:border-[#666]"
|
|
950
|
+
>
|
|
951
|
+
Cancel
|
|
952
|
+
</button>
|
|
953
|
+
<button
|
|
954
|
+
onClick={onSave}
|
|
955
|
+
disabled={!apiKey || saving}
|
|
956
|
+
className="flex-1 px-3 py-1.5 bg-[#f97316] text-black rounded text-sm font-medium disabled:opacity-50"
|
|
931
957
|
>
|
|
932
|
-
|
|
933
|
-
</
|
|
958
|
+
{testing ? "Validating..." : saving ? "Saving..." : "Save"}
|
|
959
|
+
</button>
|
|
960
|
+
</div>
|
|
961
|
+
</div>
|
|
962
|
+
) : provider.hasKey ? (
|
|
963
|
+
<div className="flex items-center justify-between">
|
|
964
|
+
<a
|
|
965
|
+
href={provider.docsUrl}
|
|
966
|
+
target="_blank"
|
|
967
|
+
rel="noopener noreferrer"
|
|
968
|
+
className="text-sm text-[#3b82f6] hover:underline"
|
|
969
|
+
>
|
|
970
|
+
View docs
|
|
971
|
+
</a>
|
|
972
|
+
<div className="flex items-center gap-3">
|
|
934
973
|
<button
|
|
935
974
|
onClick={onStartEdit}
|
|
936
|
-
className="text-sm text-[#
|
|
975
|
+
className="text-sm text-[#888] hover:text-[#e0e0e0]"
|
|
976
|
+
>
|
|
977
|
+
Update key
|
|
978
|
+
</button>
|
|
979
|
+
<button
|
|
980
|
+
onClick={onDelete}
|
|
981
|
+
className="text-red-400 hover:text-red-300 text-sm"
|
|
937
982
|
>
|
|
938
|
-
|
|
983
|
+
Remove
|
|
939
984
|
</button>
|
|
940
985
|
</div>
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
986
|
+
</div>
|
|
987
|
+
) : (
|
|
988
|
+
<div className="flex items-center justify-between">
|
|
989
|
+
<a
|
|
990
|
+
href={provider.docsUrl}
|
|
991
|
+
target="_blank"
|
|
992
|
+
rel="noopener noreferrer"
|
|
993
|
+
className="text-sm text-[#3b82f6] hover:underline"
|
|
994
|
+
>
|
|
995
|
+
Get API key
|
|
996
|
+
</a>
|
|
997
|
+
<button
|
|
998
|
+
onClick={onStartEdit}
|
|
999
|
+
className="text-sm text-[#f97316] hover:text-[#fb923c]"
|
|
1000
|
+
>
|
|
1001
|
+
+ Add key
|
|
1002
|
+
</button>
|
|
1003
|
+
</div>
|
|
1004
|
+
)}
|
|
1005
|
+
</div>
|
|
944
1006
|
</div>
|
|
945
1007
|
);
|
|
946
1008
|
}
|