@harbinger-ai/harbinger 0.1.1 → 0.1.3
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/lib/chat/actions.js +416 -0
- package/lib/chat/components/agents-page.js +545 -0
- package/lib/chat/components/agents-page.jsx +571 -0
- package/lib/chat/components/app-sidebar.js +33 -1
- package/lib/chat/components/app-sidebar.jsx +37 -1
- package/lib/chat/components/findings-page.js +164 -103
- package/lib/chat/components/findings-page.jsx +156 -101
- package/lib/chat/components/icons.js +62 -0
- package/lib/chat/components/icons.jsx +62 -0
- package/lib/chat/components/index.js +3 -0
- package/lib/chat/components/mcp-page.js +383 -55
- package/lib/chat/components/mcp-page.jsx +404 -101
- package/lib/chat/components/mission-control.js +490 -0
- package/lib/chat/components/mission-control.jsx +618 -0
- package/lib/chat/components/registry-page.js +267 -133
- package/lib/chat/components/registry-page.jsx +299 -138
- package/lib/chat/components/settings-layout.js +3 -2
- package/lib/chat/components/settings-layout.jsx +2 -1
- package/lib/chat/components/settings-providers-page.js +337 -0
- package/lib/chat/components/settings-providers-page.jsx +410 -0
- package/lib/chat/components/settings-secrets-page.js +91 -66
- package/lib/chat/components/settings-secrets-page.jsx +83 -72
- package/lib/chat/components/targets-page.js +269 -200
- package/lib/chat/components/targets-page.jsx +181 -111
- package/lib/mcp/actions.js +120 -0
- package/lib/mcp/registry.js +164 -0
- package/package.json +1 -1
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
|
+
import { motion, AnimatePresence } from 'framer-motion';
|
|
5
|
+
import { CpuIcon, CheckIcon, SpinnerIcon, EyeIcon, EyeOffIcon, XIcon } from './icons.js';
|
|
6
|
+
import {
|
|
7
|
+
getLlmProviders,
|
|
8
|
+
saveLlmProvider,
|
|
9
|
+
setActiveProvider,
|
|
10
|
+
testLlmConnection,
|
|
11
|
+
getActiveProvider,
|
|
12
|
+
} from '../actions.js';
|
|
13
|
+
|
|
14
|
+
const PROVIDER_MODELS = {
|
|
15
|
+
anthropic: ['claude-sonnet-4-20250514', 'claude-opus-4-20250514', 'claude-haiku-4-5-20251001', 'claude-3-5-sonnet-20241022'],
|
|
16
|
+
openai: ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'o1', 'o1-mini', 'o3-mini'],
|
|
17
|
+
google: ['gemini-2.5-pro', 'gemini-2.5-flash', 'gemini-2.0-flash'],
|
|
18
|
+
custom: [],
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const PROVIDER_META = {
|
|
22
|
+
anthropic: { name: 'Anthropic', keyPrefix: 'sk-ant-', keyEnv: 'ANTHROPIC_API_KEY' },
|
|
23
|
+
openai: { name: 'OpenAI', keyPrefix: 'sk-', keyEnv: 'OPENAI_API_KEY' },
|
|
24
|
+
google: { name: 'Google', keyPrefix: 'AI', keyEnv: 'GOOGLE_API_KEY' },
|
|
25
|
+
custom: { name: 'Custom', keyPrefix: '', keyEnv: 'CUSTOM_API_KEY' },
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// ─── Provider Card ────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
function ProviderCard({ provider, config, isActive, onEdit, index }) {
|
|
31
|
+
const meta = PROVIDER_META[provider];
|
|
32
|
+
const hasKey = !!config?.apiKey;
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<motion.div
|
|
36
|
+
initial={{ opacity: 0, y: 8 }}
|
|
37
|
+
animate={{ opacity: 1, y: 0 }}
|
|
38
|
+
transition={{ duration: 0.25, delay: index * 0.05 }}
|
|
39
|
+
className={`rounded-lg border bg-[--card] transition-all cursor-pointer hover:border-[--cyan]/20 ${
|
|
40
|
+
isActive ? 'border-[--cyan]/30 shadow-[0_0_15px_oklch(0.7_0.17_195/8%)]' : 'border-white/[0.06]'
|
|
41
|
+
}`}
|
|
42
|
+
onClick={onEdit}
|
|
43
|
+
>
|
|
44
|
+
<div className="flex items-center gap-3 p-4">
|
|
45
|
+
<div className="flex items-center gap-1 shrink-0">
|
|
46
|
+
<div className="w-2 h-2 rounded-full bg-[#ff5f57]" />
|
|
47
|
+
<div className="w-2 h-2 rounded-full bg-[#febc2e]" />
|
|
48
|
+
<div className="w-2 h-2 rounded-full bg-[#28c840]" />
|
|
49
|
+
</div>
|
|
50
|
+
<div className="shrink-0 rounded-md bg-[--cyan]/10 p-2">
|
|
51
|
+
<CpuIcon size={16} className="text-[--cyan]" />
|
|
52
|
+
</div>
|
|
53
|
+
<div className="flex-1 min-w-0">
|
|
54
|
+
<p className="text-sm font-mono font-medium">{meta.name}</p>
|
|
55
|
+
{config?.model && (
|
|
56
|
+
<p className="text-[10px] font-mono text-muted-foreground mt-0.5 truncate">{config.model}</p>
|
|
57
|
+
)}
|
|
58
|
+
</div>
|
|
59
|
+
<div className="flex items-center gap-2 shrink-0">
|
|
60
|
+
<div className={`w-2 h-2 rounded-full ${hasKey ? 'bg-[--success]' : 'bg-muted-foreground/40'}`} />
|
|
61
|
+
<span className={`text-[10px] font-mono ${hasKey ? 'text-[--success]' : 'text-muted-foreground'}`}>
|
|
62
|
+
{hasKey ? 'configured' : 'no key'}
|
|
63
|
+
</span>
|
|
64
|
+
{isActive && (
|
|
65
|
+
<span className="inline-flex items-center rounded-full bg-[--cyan]/10 text-[--cyan] border border-[--cyan]/20 px-2 py-0.5 text-[10px] font-mono font-medium">
|
|
66
|
+
active
|
|
67
|
+
</span>
|
|
68
|
+
)}
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
</motion.div>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ─── Provider Setup Form ──────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
function ProviderSetupForm({ provider, config, isActive, onSave, onSetActive, onClose }) {
|
|
78
|
+
const meta = PROVIDER_META[provider];
|
|
79
|
+
const models = PROVIDER_MODELS[provider];
|
|
80
|
+
|
|
81
|
+
const [apiKey, setApiKey] = useState(config?.apiKey || '');
|
|
82
|
+
const [model, setModel] = useState(config?.model || models[0] || '');
|
|
83
|
+
const [maxTokens, setMaxTokens] = useState(config?.maxTokens || 4096);
|
|
84
|
+
const [baseUrl, setBaseUrl] = useState(config?.baseUrl || '');
|
|
85
|
+
const [showKey, setShowKey] = useState(false);
|
|
86
|
+
const [saving, setSaving] = useState(false);
|
|
87
|
+
const [testing, setTesting] = useState(false);
|
|
88
|
+
const [testResult, setTestResult] = useState(null);
|
|
89
|
+
|
|
90
|
+
async function handleSave() {
|
|
91
|
+
setSaving(true);
|
|
92
|
+
try {
|
|
93
|
+
await onSave(provider, { apiKey, model, maxTokens: parseInt(maxTokens), baseUrl: baseUrl || undefined });
|
|
94
|
+
} finally {
|
|
95
|
+
setSaving(false);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function handleTest() {
|
|
100
|
+
setTesting(true);
|
|
101
|
+
setTestResult(null);
|
|
102
|
+
try {
|
|
103
|
+
const result = await testLlmConnection(provider);
|
|
104
|
+
setTestResult(result);
|
|
105
|
+
} catch (err) {
|
|
106
|
+
setTestResult({ error: err.message });
|
|
107
|
+
} finally {
|
|
108
|
+
setTesting(false);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function handleSetActive() {
|
|
113
|
+
await onSetActive(provider, model);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const maskedKey = apiKey ? apiKey.slice(0, 8) + '•'.repeat(Math.max(0, apiKey.length - 12)) + apiKey.slice(-4) : '';
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<motion.div
|
|
120
|
+
initial={{ opacity: 0, height: 0 }}
|
|
121
|
+
animate={{ opacity: 1, height: 'auto' }}
|
|
122
|
+
exit={{ opacity: 0, height: 0 }}
|
|
123
|
+
transition={{ duration: 0.2 }}
|
|
124
|
+
className="overflow-hidden"
|
|
125
|
+
>
|
|
126
|
+
<div className="rounded-lg border border-[--cyan]/20 bg-[--card] p-5 mt-3">
|
|
127
|
+
<div className="flex items-center justify-between mb-4">
|
|
128
|
+
<div className="flex items-center gap-2">
|
|
129
|
+
<div className="flex items-center gap-1">
|
|
130
|
+
<div className="w-2 h-2 rounded-full bg-[#ff5f57]" />
|
|
131
|
+
<div className="w-2 h-2 rounded-full bg-[#febc2e]" />
|
|
132
|
+
<div className="w-2 h-2 rounded-full bg-[#28c840]" />
|
|
133
|
+
</div>
|
|
134
|
+
<span className="font-mono text-[10px] font-medium text-[--cyan] uppercase tracking-wider">
|
|
135
|
+
{meta.name} Setup
|
|
136
|
+
</span>
|
|
137
|
+
</div>
|
|
138
|
+
<button onClick={onClose} className="text-muted-foreground hover:text-foreground transition-colors">
|
|
139
|
+
<XIcon size={14} />
|
|
140
|
+
</button>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
<div className="flex flex-col gap-4">
|
|
144
|
+
{/* API Key */}
|
|
145
|
+
<div>
|
|
146
|
+
<label className="block text-[10px] font-mono font-medium text-muted-foreground uppercase tracking-wider mb-1.5">
|
|
147
|
+
API Key
|
|
148
|
+
</label>
|
|
149
|
+
<div className="flex gap-2">
|
|
150
|
+
<div className="flex-1 relative">
|
|
151
|
+
<input
|
|
152
|
+
type={showKey ? 'text' : 'password'}
|
|
153
|
+
value={apiKey}
|
|
154
|
+
onChange={(e) => setApiKey(e.target.value)}
|
|
155
|
+
placeholder={meta.keyPrefix + '...'}
|
|
156
|
+
className="w-full text-sm border border-white/[0.06] rounded-md px-3 py-2 bg-black/20 font-mono text-foreground/80 placeholder:text-muted-foreground/50 focus:outline-none focus:border-[--cyan]/40 focus:ring-1 focus:ring-[--cyan]/20 transition-colors"
|
|
157
|
+
/>
|
|
158
|
+
<button
|
|
159
|
+
type="button"
|
|
160
|
+
onClick={() => setShowKey(!showKey)}
|
|
161
|
+
className="absolute right-2.5 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors"
|
|
162
|
+
>
|
|
163
|
+
{showKey ? <EyeOffIcon size={14} /> : <EyeIcon size={14} />}
|
|
164
|
+
</button>
|
|
165
|
+
</div>
|
|
166
|
+
<button
|
|
167
|
+
onClick={handleTest}
|
|
168
|
+
disabled={testing || !apiKey}
|
|
169
|
+
className="inline-flex items-center gap-1.5 rounded-md px-3 py-2 text-xs font-mono font-medium border border-white/[0.06] hover:bg-white/[0.04] hover:border-[--cyan]/30 hover:text-[--cyan] transition-colors disabled:opacity-50"
|
|
170
|
+
>
|
|
171
|
+
{testing ? <SpinnerIcon size={12} /> : 'Test'}
|
|
172
|
+
</button>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
|
|
176
|
+
{/* Model */}
|
|
177
|
+
<div>
|
|
178
|
+
<label className="block text-[10px] font-mono font-medium text-muted-foreground uppercase tracking-wider mb-1.5">
|
|
179
|
+
Model
|
|
180
|
+
</label>
|
|
181
|
+
{models.length > 0 ? (
|
|
182
|
+
<select
|
|
183
|
+
value={model}
|
|
184
|
+
onChange={(e) => setModel(e.target.value)}
|
|
185
|
+
className="w-full text-sm border border-white/[0.06] rounded-md px-3 py-2 bg-black/20 font-mono text-foreground/80 focus:outline-none focus:border-[--cyan]/40 focus:ring-1 focus:ring-[--cyan]/20 transition-colors appearance-none"
|
|
186
|
+
>
|
|
187
|
+
{models.map((m) => (
|
|
188
|
+
<option key={m} value={m}>{m}</option>
|
|
189
|
+
))}
|
|
190
|
+
</select>
|
|
191
|
+
) : (
|
|
192
|
+
<input
|
|
193
|
+
type="text"
|
|
194
|
+
value={model}
|
|
195
|
+
onChange={(e) => setModel(e.target.value)}
|
|
196
|
+
placeholder="model-name"
|
|
197
|
+
className="w-full text-sm border border-white/[0.06] rounded-md px-3 py-2 bg-black/20 font-mono text-foreground/80 placeholder:text-muted-foreground/50 focus:outline-none focus:border-[--cyan]/40 focus:ring-1 focus:ring-[--cyan]/20 transition-colors"
|
|
198
|
+
/>
|
|
199
|
+
)}
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
{/* Max Tokens */}
|
|
203
|
+
<div>
|
|
204
|
+
<label className="block text-[10px] font-mono font-medium text-muted-foreground uppercase tracking-wider mb-1.5">
|
|
205
|
+
Max Tokens
|
|
206
|
+
</label>
|
|
207
|
+
<input
|
|
208
|
+
type="number"
|
|
209
|
+
value={maxTokens}
|
|
210
|
+
onChange={(e) => setMaxTokens(e.target.value)}
|
|
211
|
+
className="w-32 text-sm border border-white/[0.06] rounded-md px-3 py-2 bg-black/20 font-mono text-foreground/80 focus:outline-none focus:border-[--cyan]/40 focus:ring-1 focus:ring-[--cyan]/20 transition-colors"
|
|
212
|
+
/>
|
|
213
|
+
</div>
|
|
214
|
+
|
|
215
|
+
{/* Base URL (custom only) */}
|
|
216
|
+
{provider === 'custom' && (
|
|
217
|
+
<div>
|
|
218
|
+
<label className="block text-[10px] font-mono font-medium text-muted-foreground uppercase tracking-wider mb-1.5">
|
|
219
|
+
Base URL
|
|
220
|
+
</label>
|
|
221
|
+
<input
|
|
222
|
+
type="text"
|
|
223
|
+
value={baseUrl}
|
|
224
|
+
onChange={(e) => setBaseUrl(e.target.value)}
|
|
225
|
+
placeholder="https://api.example.com/v1"
|
|
226
|
+
className="w-full text-sm border border-white/[0.06] rounded-md px-3 py-2 bg-black/20 font-mono text-foreground/80 placeholder:text-muted-foreground/50 focus:outline-none focus:border-[--cyan]/40 focus:ring-1 focus:ring-[--cyan]/20 transition-colors"
|
|
227
|
+
/>
|
|
228
|
+
</div>
|
|
229
|
+
)}
|
|
230
|
+
|
|
231
|
+
{/* Test result */}
|
|
232
|
+
{testResult && (
|
|
233
|
+
<div className="rounded-md border border-white/[0.04] bg-black/30 overflow-hidden">
|
|
234
|
+
<div className="flex items-center gap-1.5 px-2.5 py-1.5 border-b border-white/[0.04]">
|
|
235
|
+
<div className="flex items-center gap-1">
|
|
236
|
+
<div className="w-2 h-2 rounded-full bg-[#ff5f57]" />
|
|
237
|
+
<div className="w-2 h-2 rounded-full bg-[#febc2e]" />
|
|
238
|
+
<div className="w-2 h-2 rounded-full bg-[#28c840]" />
|
|
239
|
+
</div>
|
|
240
|
+
<span className="font-mono text-[9px] text-muted-foreground ml-1">connection test</span>
|
|
241
|
+
{testResult.error ? (
|
|
242
|
+
<XIcon size={10} className="text-[--destructive] ml-auto" />
|
|
243
|
+
) : (
|
|
244
|
+
<CheckIcon size={10} className="text-[--success] ml-auto" />
|
|
245
|
+
)}
|
|
246
|
+
</div>
|
|
247
|
+
<pre className="text-[11px] p-2.5 font-mono overflow-auto max-h-24 whitespace-pre-wrap break-words text-foreground/80">
|
|
248
|
+
{testResult.error ? `Error: ${testResult.error}` : `Connected. Response: ${testResult.response || 'OK'}`}
|
|
249
|
+
</pre>
|
|
250
|
+
</div>
|
|
251
|
+
)}
|
|
252
|
+
|
|
253
|
+
{/* Actions */}
|
|
254
|
+
<div className="flex items-center gap-2 pt-2">
|
|
255
|
+
<button
|
|
256
|
+
onClick={handleSave}
|
|
257
|
+
disabled={saving || !apiKey}
|
|
258
|
+
className="inline-flex items-center gap-1.5 rounded-md px-4 py-2 text-xs font-mono font-medium bg-[--cyan]/10 text-[--cyan] border border-[--cyan]/20 hover:bg-[--cyan] hover:text-[--primary-foreground] transition-colors disabled:opacity-50"
|
|
259
|
+
>
|
|
260
|
+
{saving ? <SpinnerIcon size={12} /> : <CheckIcon size={12} />}
|
|
261
|
+
Save
|
|
262
|
+
</button>
|
|
263
|
+
{!isActive && apiKey && (
|
|
264
|
+
<button
|
|
265
|
+
onClick={handleSetActive}
|
|
266
|
+
className="inline-flex items-center gap-1.5 rounded-md px-4 py-2 text-xs font-mono font-medium border border-white/[0.06] hover:bg-white/[0.04] hover:border-[--cyan]/30 hover:text-[--cyan] transition-colors"
|
|
267
|
+
>
|
|
268
|
+
Set as Active
|
|
269
|
+
</button>
|
|
270
|
+
)}
|
|
271
|
+
<button
|
|
272
|
+
onClick={onClose}
|
|
273
|
+
className="inline-flex items-center gap-1.5 rounded-md px-4 py-2 text-xs font-mono font-medium border border-white/[0.06] hover:bg-white/[0.04] transition-colors text-muted-foreground"
|
|
274
|
+
>
|
|
275
|
+
Cancel
|
|
276
|
+
</button>
|
|
277
|
+
</div>
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
</motion.div>
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ─── Stats Card ───────────────────────────────────────────────────────────────
|
|
285
|
+
|
|
286
|
+
function StatsCard({ label, value, active }) {
|
|
287
|
+
return (
|
|
288
|
+
<div className={`flex flex-col items-center justify-center p-4 rounded-lg border bg-[--card] ${active ? 'border-[--cyan]/20' : 'border-white/[0.06]'}`}>
|
|
289
|
+
<span className={`text-2xl font-semibold font-mono ${active ? 'text-[--cyan]' : 'text-foreground'}`}>{value}</span>
|
|
290
|
+
<span className="font-mono text-[10px] text-muted-foreground uppercase tracking-wider mt-1">{label}</span>
|
|
291
|
+
</div>
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ─── Main Page ────────────────────────────────────────────────────────────────
|
|
296
|
+
|
|
297
|
+
export function SettingsProvidersPage() {
|
|
298
|
+
const [loading, setLoading] = useState(true);
|
|
299
|
+
const [providers, setProviders] = useState({});
|
|
300
|
+
const [activeProviderInfo, setActiveProviderInfo] = useState(null);
|
|
301
|
+
const [editingProvider, setEditingProvider] = useState(null);
|
|
302
|
+
|
|
303
|
+
async function load() {
|
|
304
|
+
try {
|
|
305
|
+
const [provs, active] = await Promise.all([
|
|
306
|
+
getLlmProviders(),
|
|
307
|
+
getActiveProvider(),
|
|
308
|
+
]);
|
|
309
|
+
setProviders(provs || {});
|
|
310
|
+
setActiveProviderInfo(active || {});
|
|
311
|
+
} catch {}
|
|
312
|
+
setLoading(false);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
useEffect(() => { load(); }, []);
|
|
316
|
+
|
|
317
|
+
async function handleSave(provider, config) {
|
|
318
|
+
await saveLlmProvider(provider, config);
|
|
319
|
+
await load();
|
|
320
|
+
setEditingProvider(null);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
async function handleSetActive(provider, model) {
|
|
324
|
+
await setActiveProvider(provider, model);
|
|
325
|
+
await load();
|
|
326
|
+
setEditingProvider(null);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (loading) {
|
|
330
|
+
return (
|
|
331
|
+
<div className="flex flex-col gap-3">
|
|
332
|
+
{[...Array(4)].map((_, i) => (
|
|
333
|
+
<div key={i} className="h-16 animate-shimmer rounded-lg border border-white/[0.06] bg-[--card]" />
|
|
334
|
+
))}
|
|
335
|
+
</div>
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const configuredCount = Object.values(providers).filter((p) => p?.apiKey).length;
|
|
340
|
+
const providerList = ['anthropic', 'openai', 'google', 'custom'];
|
|
341
|
+
|
|
342
|
+
return (
|
|
343
|
+
<>
|
|
344
|
+
{/* Active provider banner */}
|
|
345
|
+
{activeProviderInfo?.provider && (
|
|
346
|
+
<div className="rounded-lg border border-[--cyan]/20 bg-[--cyan]/5 p-4 mb-6">
|
|
347
|
+
<div className="flex items-center gap-3">
|
|
348
|
+
<div className="shrink-0 rounded-md bg-[--cyan]/10 p-2">
|
|
349
|
+
<CpuIcon size={16} className="text-[--cyan]" />
|
|
350
|
+
</div>
|
|
351
|
+
<div className="flex-1 min-w-0">
|
|
352
|
+
<div className="flex items-center gap-2">
|
|
353
|
+
<span className="font-mono text-[10px] font-medium text-[--cyan] uppercase tracking-wider">Active Provider</span>
|
|
354
|
+
<div className="w-2 h-2 rounded-full bg-[--success] animate-pulse" />
|
|
355
|
+
</div>
|
|
356
|
+
<p className="text-sm font-mono font-medium mt-0.5">
|
|
357
|
+
{PROVIDER_META[activeProviderInfo.provider]?.name || activeProviderInfo.provider}
|
|
358
|
+
{activeProviderInfo.model && (
|
|
359
|
+
<span className="text-muted-foreground ml-2">/ {activeProviderInfo.model}</span>
|
|
360
|
+
)}
|
|
361
|
+
</p>
|
|
362
|
+
</div>
|
|
363
|
+
</div>
|
|
364
|
+
</div>
|
|
365
|
+
)}
|
|
366
|
+
|
|
367
|
+
{/* Stats */}
|
|
368
|
+
<div className="grid grid-cols-3 gap-3 mb-6">
|
|
369
|
+
<StatsCard label="Providers" value={providerList.length} />
|
|
370
|
+
<StatsCard label="Configured" value={configuredCount} />
|
|
371
|
+
<StatsCard label="Active" value={activeProviderInfo?.provider ? 1 : 0} active />
|
|
372
|
+
</div>
|
|
373
|
+
|
|
374
|
+
{/* Section Header */}
|
|
375
|
+
<div className="flex items-center gap-2 pb-2 mb-4">
|
|
376
|
+
<span className="font-mono text-[10px] font-medium text-[--cyan] uppercase tracking-wider">Configured Providers</span>
|
|
377
|
+
<span className="inline-flex items-center rounded-full bg-[--cyan]/10 px-2 py-0.5 text-[10px] font-mono font-medium text-[--cyan]">
|
|
378
|
+
{configuredCount}
|
|
379
|
+
</span>
|
|
380
|
+
</div>
|
|
381
|
+
|
|
382
|
+
{/* Provider Cards */}
|
|
383
|
+
<div className="flex flex-col gap-3">
|
|
384
|
+
{providerList.map((p, i) => (
|
|
385
|
+
<div key={p}>
|
|
386
|
+
<ProviderCard
|
|
387
|
+
provider={p}
|
|
388
|
+
config={providers[p]}
|
|
389
|
+
isActive={activeProviderInfo?.provider === p}
|
|
390
|
+
onEdit={() => setEditingProvider(editingProvider === p ? null : p)}
|
|
391
|
+
index={i}
|
|
392
|
+
/>
|
|
393
|
+
<AnimatePresence>
|
|
394
|
+
{editingProvider === p && (
|
|
395
|
+
<ProviderSetupForm
|
|
396
|
+
provider={p}
|
|
397
|
+
config={providers[p]}
|
|
398
|
+
isActive={activeProviderInfo?.provider === p}
|
|
399
|
+
onSave={handleSave}
|
|
400
|
+
onSetActive={handleSetActive}
|
|
401
|
+
onClose={() => setEditingProvider(null)}
|
|
402
|
+
/>
|
|
403
|
+
)}
|
|
404
|
+
</AnimatePresence>
|
|
405
|
+
</div>
|
|
406
|
+
))}
|
|
407
|
+
</div>
|
|
408
|
+
</>
|
|
409
|
+
);
|
|
410
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useState, useEffect } from "react";
|
|
4
|
-
import {
|
|
4
|
+
import { motion } from "framer-motion";
|
|
5
|
+
import { KeyIcon, CopyIcon, CheckIcon, TrashIcon, RefreshIcon, SpinnerIcon } from "./icons.js";
|
|
5
6
|
import { createNewApiKey, getApiKeys, deleteApiKey } from "../actions.js";
|
|
6
7
|
function timeAgo(ts) {
|
|
7
8
|
if (!ts) return "Never";
|
|
@@ -46,7 +47,7 @@ function CopyButton({ text }) {
|
|
|
46
47
|
"button",
|
|
47
48
|
{
|
|
48
49
|
onClick: handleCopy,
|
|
49
|
-
className: "inline-flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-xs font-medium border border-
|
|
50
|
+
className: "inline-flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-xs font-mono font-medium border border-white/[0.06] hover:bg-white/[0.04] hover:border-[--cyan]/30 hover:text-[--cyan] transition-colors",
|
|
50
51
|
children: [
|
|
51
52
|
copied ? /* @__PURE__ */ jsx(CheckIcon, { size: 14 }) : /* @__PURE__ */ jsx(CopyIcon, { size: 14 }),
|
|
52
53
|
copied ? "Copied" : "Copy"
|
|
@@ -54,11 +55,10 @@ function CopyButton({ text }) {
|
|
|
54
55
|
}
|
|
55
56
|
);
|
|
56
57
|
}
|
|
57
|
-
function
|
|
58
|
-
return /* @__PURE__ */ jsxs("div", { className: "pb-
|
|
59
|
-
/* @__PURE__ */ jsx("
|
|
60
|
-
description && /* @__PURE__ */ jsx("p", { className: "text-
|
|
61
|
-
children
|
|
58
|
+
function SectionHeader({ label, description }) {
|
|
59
|
+
return /* @__PURE__ */ jsxs("div", { className: "pb-2 mb-4", children: [
|
|
60
|
+
/* @__PURE__ */ jsx("span", { className: "font-mono text-[10px] font-medium text-[--cyan] uppercase tracking-wider", children: label }),
|
|
61
|
+
description && /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground mt-1 font-mono", children: description })
|
|
62
62
|
] });
|
|
63
63
|
}
|
|
64
64
|
function ApiKeySection() {
|
|
@@ -123,93 +123,118 @@ function ApiKeySection() {
|
|
|
123
123
|
handleCreate();
|
|
124
124
|
};
|
|
125
125
|
if (loading) {
|
|
126
|
-
return /* @__PURE__ */ jsx("div", { className: "h-
|
|
126
|
+
return /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-3", children: /* @__PURE__ */ jsx("div", { className: "h-16 animate-shimmer rounded-lg border border-white/[0.06] bg-[--card]" }) });
|
|
127
127
|
}
|
|
128
128
|
return /* @__PURE__ */ jsxs("div", { children: [
|
|
129
|
-
error && /* @__PURE__ */ jsx("
|
|
130
|
-
newKey && /* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-green-500/
|
|
131
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-
|
|
132
|
-
/* @__PURE__ */
|
|
129
|
+
error && /* @__PURE__ */ jsx("div", { className: "rounded-md border border-[--destructive]/20 bg-[--destructive]/5 px-3 py-2 mb-4", children: /* @__PURE__ */ jsx("p", { className: "text-xs font-mono text-[--destructive]", children: error }) }),
|
|
130
|
+
newKey && /* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-green-500/20 bg-green-500/5 p-4 mb-4", children: [
|
|
131
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 mb-2", children: [
|
|
132
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
|
|
133
|
+
/* @__PURE__ */ jsx("div", { className: "w-2 h-2 rounded-full bg-[#ff5f57]" }),
|
|
134
|
+
/* @__PURE__ */ jsx("div", { className: "w-2 h-2 rounded-full bg-[#febc2e]" }),
|
|
135
|
+
/* @__PURE__ */ jsx("div", { className: "w-2 h-2 rounded-full bg-[#28c840]" })
|
|
136
|
+
] }),
|
|
137
|
+
/* @__PURE__ */ jsx("span", { className: "font-mono text-[9px] text-green-500 ml-1", children: "new api key" }),
|
|
133
138
|
/* @__PURE__ */ jsx(
|
|
134
139
|
"button",
|
|
135
140
|
{
|
|
136
141
|
onClick: () => setNewKey(null),
|
|
137
|
-
className: "text-
|
|
142
|
+
className: "text-[10px] font-mono text-muted-foreground hover:text-foreground ml-auto",
|
|
138
143
|
children: "Dismiss"
|
|
139
144
|
}
|
|
140
145
|
)
|
|
141
146
|
] }),
|
|
147
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs font-mono text-green-500 mb-2", children: "Copy this key now. You won't be able to see it again." }),
|
|
142
148
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
143
|
-
/* @__PURE__ */ jsx("code", { className: "flex-1 rounded-md bg-
|
|
149
|
+
/* @__PURE__ */ jsx("code", { className: "flex-1 rounded-md bg-black/30 border border-white/[0.04] px-3 py-2 text-[11px] font-mono break-all select-all text-foreground/80", children: newKey }),
|
|
144
150
|
/* @__PURE__ */ jsx(CopyButton, { text: newKey })
|
|
145
151
|
] })
|
|
146
152
|
] }),
|
|
147
|
-
currentKey ? /* @__PURE__ */ jsx(
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
153
|
+
currentKey ? /* @__PURE__ */ jsx(
|
|
154
|
+
motion.div,
|
|
155
|
+
{
|
|
156
|
+
initial: { opacity: 0, y: 8 },
|
|
157
|
+
animate: { opacity: 1, y: 0 },
|
|
158
|
+
className: "rounded-lg border border-white/[0.06] bg-[--card] hover:border-[--cyan]/20 transition-colors",
|
|
159
|
+
children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 p-4", children: [
|
|
160
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 shrink-0", children: [
|
|
161
|
+
/* @__PURE__ */ jsx("div", { className: "w-2 h-2 rounded-full bg-[#ff5f57]" }),
|
|
162
|
+
/* @__PURE__ */ jsx("div", { className: "w-2 h-2 rounded-full bg-[#febc2e]" }),
|
|
163
|
+
/* @__PURE__ */ jsx("div", { className: "w-2 h-2 rounded-full bg-[#28c840]" })
|
|
154
164
|
] }),
|
|
155
|
-
/* @__PURE__ */
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
"
|
|
160
|
-
|
|
165
|
+
/* @__PURE__ */ jsx("div", { className: "shrink-0 rounded-md bg-[--cyan]/10 p-2", children: /* @__PURE__ */ jsx(KeyIcon, { size: 16, className: "text-[--cyan]" }) }),
|
|
166
|
+
/* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
|
|
167
|
+
/* @__PURE__ */ jsxs("code", { className: "text-sm font-mono text-foreground", children: [
|
|
168
|
+
currentKey.keyPrefix,
|
|
169
|
+
"..."
|
|
170
|
+
] }),
|
|
171
|
+
/* @__PURE__ */ jsxs("p", { className: "text-[10px] text-muted-foreground mt-0.5 font-mono", children: [
|
|
172
|
+
"Created ",
|
|
173
|
+
formatDate(currentKey.createdAt),
|
|
174
|
+
currentKey.lastUsedAt && /* @__PURE__ */ jsxs("span", { className: "ml-2", children: [
|
|
175
|
+
"\\u00b7 Last used ",
|
|
176
|
+
timeAgo(currentKey.lastUsedAt)
|
|
177
|
+
] })
|
|
161
178
|
] })
|
|
179
|
+
] }),
|
|
180
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 shrink-0", children: [
|
|
181
|
+
/* @__PURE__ */ jsx("div", { className: "w-2 h-2 rounded-full bg-[--success]" }),
|
|
182
|
+
/* @__PURE__ */ jsxs(
|
|
183
|
+
"button",
|
|
184
|
+
{
|
|
185
|
+
onClick: handleRegenerate,
|
|
186
|
+
disabled: creating,
|
|
187
|
+
className: `inline-flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-xs font-mono font-medium border transition-colors disabled:opacity-50 ${confirmRegenerate ? "border-yellow-500/30 text-yellow-500 hover:bg-yellow-500/10" : "border-white/[0.06] text-muted-foreground hover:bg-white/[0.04] hover:border-[--cyan]/30 hover:text-[--cyan]"}`,
|
|
188
|
+
children: [
|
|
189
|
+
creating ? /* @__PURE__ */ jsx(SpinnerIcon, { size: 12 }) : /* @__PURE__ */ jsx(RefreshIcon, { size: 12 }),
|
|
190
|
+
creating ? "Generating..." : confirmRegenerate ? "Confirm" : "Regenerate"
|
|
191
|
+
]
|
|
192
|
+
}
|
|
193
|
+
),
|
|
194
|
+
/* @__PURE__ */ jsxs(
|
|
195
|
+
"button",
|
|
196
|
+
{
|
|
197
|
+
onClick: handleDelete,
|
|
198
|
+
className: `inline-flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-xs font-mono font-medium border transition-colors ${confirmDelete ? "border-[--destructive]/30 text-[--destructive] hover:bg-[--destructive]/10" : "border-white/[0.06] text-muted-foreground hover:text-[--destructive] hover:border-[--destructive]/20"}`,
|
|
199
|
+
children: [
|
|
200
|
+
/* @__PURE__ */ jsx(TrashIcon, { size: 12 }),
|
|
201
|
+
confirmDelete ? "Confirm" : "Delete"
|
|
202
|
+
]
|
|
203
|
+
}
|
|
204
|
+
)
|
|
162
205
|
] })
|
|
163
206
|
] })
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
disabled: creating,
|
|
171
|
-
className: `inline-flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-xs font-medium border ${confirmRegenerate ? "border-yellow-500 text-yellow-600 hover:bg-yellow-500/10" : "border-border text-muted-foreground hover:bg-accent hover:text-foreground"} disabled:opacity-50`,
|
|
172
|
-
children: [
|
|
173
|
-
/* @__PURE__ */ jsx(RefreshIcon, { size: 12 }),
|
|
174
|
-
creating ? "Generating..." : confirmRegenerate ? "Confirm regenerate" : "Regenerate"
|
|
175
|
-
]
|
|
176
|
-
}
|
|
177
|
-
),
|
|
178
|
-
/* @__PURE__ */ jsxs(
|
|
179
|
-
"button",
|
|
180
|
-
{
|
|
181
|
-
onClick: handleDelete,
|
|
182
|
-
className: `inline-flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-xs font-medium border ${confirmDelete ? "border-destructive text-destructive hover:bg-destructive/10" : "border-border text-muted-foreground hover:text-destructive hover:border-destructive/50"}`,
|
|
183
|
-
children: [
|
|
184
|
-
/* @__PURE__ */ jsx(TrashIcon, { size: 12 }),
|
|
185
|
-
confirmDelete ? "Confirm delete" : "Delete"
|
|
186
|
-
]
|
|
187
|
-
}
|
|
188
|
-
)
|
|
189
|
-
] })
|
|
190
|
-
] }) }) : /* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-dashed bg-card p-6 flex flex-col items-center text-center", children: [
|
|
191
|
-
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground mb-3", children: "No API key configured" }),
|
|
192
|
-
/* @__PURE__ */ jsx(
|
|
207
|
+
}
|
|
208
|
+
) : /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center py-12 text-center rounded-lg border border-white/[0.06] bg-[--card]", children: [
|
|
209
|
+
/* @__PURE__ */ jsx("div", { className: "rounded-full bg-[--cyan]/10 p-4 mb-4", children: /* @__PURE__ */ jsx(KeyIcon, { size: 24, className: "text-[--cyan]" }) }),
|
|
210
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm font-mono font-medium mb-1", children: "No API key configured" }),
|
|
211
|
+
/* @__PURE__ */ jsx("p", { className: "text-[11px] text-muted-foreground font-mono mb-4", children: "Generate a key to authenticate external requests to /api endpoints." }),
|
|
212
|
+
/* @__PURE__ */ jsxs(
|
|
193
213
|
"button",
|
|
194
214
|
{
|
|
195
215
|
onClick: handleCreate,
|
|
196
216
|
disabled: creating,
|
|
197
|
-
className: "inline-flex items-center gap-
|
|
198
|
-
children:
|
|
217
|
+
className: "inline-flex items-center gap-1.5 rounded-md px-4 py-2 text-xs font-mono font-medium bg-[--cyan]/10 text-[--cyan] border border-[--cyan]/20 hover:bg-[--cyan] hover:text-[--primary-foreground] transition-colors disabled:opacity-50",
|
|
218
|
+
children: [
|
|
219
|
+
creating ? /* @__PURE__ */ jsx(SpinnerIcon, { size: 12 }) : /* @__PURE__ */ jsx(KeyIcon, { size: 12 }),
|
|
220
|
+
creating ? "Creating..." : "Create API Key"
|
|
221
|
+
]
|
|
199
222
|
}
|
|
200
223
|
)
|
|
201
224
|
] })
|
|
202
225
|
] });
|
|
203
226
|
}
|
|
204
227
|
function SettingsSecretsPage() {
|
|
205
|
-
return /* @__PURE__ */
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
228
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
229
|
+
/* @__PURE__ */ jsx(
|
|
230
|
+
SectionHeader,
|
|
231
|
+
{
|
|
232
|
+
label: "API Key",
|
|
233
|
+
description: "Authenticates external requests to /api endpoints. Pass via the x-api-key header."
|
|
234
|
+
}
|
|
235
|
+
),
|
|
236
|
+
/* @__PURE__ */ jsx(ApiKeySection, {})
|
|
237
|
+
] });
|
|
213
238
|
}
|
|
214
239
|
export {
|
|
215
240
|
SettingsSecretsPage
|