@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.
@@ -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 { KeyIcon, CopyIcon, CheckIcon, TrashIcon, RefreshIcon } from "./icons.js";
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-border bg-background text-muted-foreground hover:bg-accent hover:text-foreground",
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 Section({ title, description, children }) {
58
- return /* @__PURE__ */ jsxs("div", { className: "pb-8 mb-8 border-b border-border last:border-b-0 last:pb-0 last:mb-0", children: [
59
- /* @__PURE__ */ jsx("h2", { className: "text-base font-medium mb-1", children: title }),
60
- description && /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground mb-4", children: description }),
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-14 animate-pulse rounded-md bg-border/50" });
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("p", { className: "text-sm text-destructive mb-4", children: error }),
130
- newKey && /* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-green-500/30 bg-green-500/5 p-4 mb-4", children: [
131
- /* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-3 mb-2", children: [
132
- /* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-green-600 dark:text-green-400", children: "API key created \u2014 copy it now. You won't be able to see it again." }),
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-xs text-muted-foreground hover:text-foreground shrink-0",
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-muted px-3 py-2 text-xs font-mono break-all select-all", children: newKey }),
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("div", { className: "rounded-lg border bg-card p-4", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
148
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
149
- /* @__PURE__ */ jsx("div", { className: "shrink-0 rounded-md bg-muted p-2", children: /* @__PURE__ */ jsx(KeyIcon, { size: 16 }) }),
150
- /* @__PURE__ */ jsxs("div", { children: [
151
- /* @__PURE__ */ jsxs("code", { className: "text-sm font-mono", children: [
152
- currentKey.keyPrefix,
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__ */ jsxs("p", { className: "text-xs text-muted-foreground mt-0.5", children: [
156
- "Created ",
157
- formatDate(currentKey.createdAt),
158
- currentKey.lastUsedAt && /* @__PURE__ */ jsxs("span", { className: "ml-2", children: [
159
- "\xB7 Last used ",
160
- timeAgo(currentKey.lastUsedAt)
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
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
166
- /* @__PURE__ */ jsxs(
167
- "button",
168
- {
169
- onClick: handleRegenerate,
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-2 rounded-md px-3 py-2 text-sm font-medium bg-foreground text-background hover:bg-foreground/90 disabled:opacity-50 disabled:pointer-events-none",
198
- children: creating ? "Creating..." : "Create API key"
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__ */ jsx("div", { children: /* @__PURE__ */ jsx(
206
- Section,
207
- {
208
- title: "API Key",
209
- description: "Authenticates external requests to /api endpoints. Pass via the x-api-key header.",
210
- children: /* @__PURE__ */ jsx(ApiKeySection, {})
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