@harbinger-ai/harbinger 0.1.2 → 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,337 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
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
+ const PROVIDER_MODELS = {
14
+ anthropic: ["claude-sonnet-4-20250514", "claude-opus-4-20250514", "claude-haiku-4-5-20251001", "claude-3-5-sonnet-20241022"],
15
+ openai: ["gpt-4o", "gpt-4o-mini", "gpt-4-turbo", "o1", "o1-mini", "o3-mini"],
16
+ google: ["gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.0-flash"],
17
+ custom: []
18
+ };
19
+ const PROVIDER_META = {
20
+ anthropic: { name: "Anthropic", keyPrefix: "sk-ant-", keyEnv: "ANTHROPIC_API_KEY" },
21
+ openai: { name: "OpenAI", keyPrefix: "sk-", keyEnv: "OPENAI_API_KEY" },
22
+ google: { name: "Google", keyPrefix: "AI", keyEnv: "GOOGLE_API_KEY" },
23
+ custom: { name: "Custom", keyPrefix: "", keyEnv: "CUSTOM_API_KEY" }
24
+ };
25
+ function ProviderCard({ provider, config, isActive, onEdit, index }) {
26
+ const meta = PROVIDER_META[provider];
27
+ const hasKey = !!config?.apiKey;
28
+ return /* @__PURE__ */ jsx(
29
+ motion.div,
30
+ {
31
+ initial: { opacity: 0, y: 8 },
32
+ animate: { opacity: 1, y: 0 },
33
+ transition: { duration: 0.25, delay: index * 0.05 },
34
+ className: `rounded-lg border bg-[--card] transition-all cursor-pointer hover:border-[--cyan]/20 ${isActive ? "border-[--cyan]/30 shadow-[0_0_15px_oklch(0.7_0.17_195/8%)]" : "border-white/[0.06]"}`,
35
+ onClick: onEdit,
36
+ children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 p-4", children: [
37
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 shrink-0", children: [
38
+ /* @__PURE__ */ jsx("div", { className: "w-2 h-2 rounded-full bg-[#ff5f57]" }),
39
+ /* @__PURE__ */ jsx("div", { className: "w-2 h-2 rounded-full bg-[#febc2e]" }),
40
+ /* @__PURE__ */ jsx("div", { className: "w-2 h-2 rounded-full bg-[#28c840]" })
41
+ ] }),
42
+ /* @__PURE__ */ jsx("div", { className: "shrink-0 rounded-md bg-[--cyan]/10 p-2", children: /* @__PURE__ */ jsx(CpuIcon, { size: 16, className: "text-[--cyan]" }) }),
43
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
44
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-mono font-medium", children: meta.name }),
45
+ config?.model && /* @__PURE__ */ jsx("p", { className: "text-[10px] font-mono text-muted-foreground mt-0.5 truncate", children: config.model })
46
+ ] }),
47
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 shrink-0", children: [
48
+ /* @__PURE__ */ jsx("div", { className: `w-2 h-2 rounded-full ${hasKey ? "bg-[--success]" : "bg-muted-foreground/40"}` }),
49
+ /* @__PURE__ */ jsx("span", { className: `text-[10px] font-mono ${hasKey ? "text-[--success]" : "text-muted-foreground"}`, children: hasKey ? "configured" : "no key" }),
50
+ isActive && /* @__PURE__ */ jsx("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", children: "active" })
51
+ ] })
52
+ ] })
53
+ }
54
+ );
55
+ }
56
+ function ProviderSetupForm({ provider, config, isActive, onSave, onSetActive, onClose }) {
57
+ const meta = PROVIDER_META[provider];
58
+ const models = PROVIDER_MODELS[provider];
59
+ const [apiKey, setApiKey] = useState(config?.apiKey || "");
60
+ const [model, setModel] = useState(config?.model || models[0] || "");
61
+ const [maxTokens, setMaxTokens] = useState(config?.maxTokens || 4096);
62
+ const [baseUrl, setBaseUrl] = useState(config?.baseUrl || "");
63
+ const [showKey, setShowKey] = useState(false);
64
+ const [saving, setSaving] = useState(false);
65
+ const [testing, setTesting] = useState(false);
66
+ const [testResult, setTestResult] = useState(null);
67
+ async function handleSave() {
68
+ setSaving(true);
69
+ try {
70
+ await onSave(provider, { apiKey, model, maxTokens: parseInt(maxTokens), baseUrl: baseUrl || void 0 });
71
+ } finally {
72
+ setSaving(false);
73
+ }
74
+ }
75
+ async function handleTest() {
76
+ setTesting(true);
77
+ setTestResult(null);
78
+ try {
79
+ const result = await testLlmConnection(provider);
80
+ setTestResult(result);
81
+ } catch (err) {
82
+ setTestResult({ error: err.message });
83
+ } finally {
84
+ setTesting(false);
85
+ }
86
+ }
87
+ async function handleSetActive() {
88
+ await onSetActive(provider, model);
89
+ }
90
+ const maskedKey = apiKey ? apiKey.slice(0, 8) + "\u2022".repeat(Math.max(0, apiKey.length - 12)) + apiKey.slice(-4) : "";
91
+ return /* @__PURE__ */ jsx(
92
+ motion.div,
93
+ {
94
+ initial: { opacity: 0, height: 0 },
95
+ animate: { opacity: 1, height: "auto" },
96
+ exit: { opacity: 0, height: 0 },
97
+ transition: { duration: 0.2 },
98
+ className: "overflow-hidden",
99
+ children: /* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-[--cyan]/20 bg-[--card] p-5 mt-3", children: [
100
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-4", children: [
101
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
102
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
103
+ /* @__PURE__ */ jsx("div", { className: "w-2 h-2 rounded-full bg-[#ff5f57]" }),
104
+ /* @__PURE__ */ jsx("div", { className: "w-2 h-2 rounded-full bg-[#febc2e]" }),
105
+ /* @__PURE__ */ jsx("div", { className: "w-2 h-2 rounded-full bg-[#28c840]" })
106
+ ] }),
107
+ /* @__PURE__ */ jsxs("span", { className: "font-mono text-[10px] font-medium text-[--cyan] uppercase tracking-wider", children: [
108
+ meta.name,
109
+ " Setup"
110
+ ] })
111
+ ] }),
112
+ /* @__PURE__ */ jsx("button", { onClick: onClose, className: "text-muted-foreground hover:text-foreground transition-colors", children: /* @__PURE__ */ jsx(XIcon, { size: 14 }) })
113
+ ] }),
114
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-4", children: [
115
+ /* @__PURE__ */ jsxs("div", { children: [
116
+ /* @__PURE__ */ jsx("label", { className: "block text-[10px] font-mono font-medium text-muted-foreground uppercase tracking-wider mb-1.5", children: "API Key" }),
117
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
118
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 relative", children: [
119
+ /* @__PURE__ */ jsx(
120
+ "input",
121
+ {
122
+ type: showKey ? "text" : "password",
123
+ value: apiKey,
124
+ onChange: (e) => setApiKey(e.target.value),
125
+ placeholder: meta.keyPrefix + "...",
126
+ 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"
127
+ }
128
+ ),
129
+ /* @__PURE__ */ jsx(
130
+ "button",
131
+ {
132
+ type: "button",
133
+ onClick: () => setShowKey(!showKey),
134
+ className: "absolute right-2.5 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors",
135
+ children: showKey ? /* @__PURE__ */ jsx(EyeOffIcon, { size: 14 }) : /* @__PURE__ */ jsx(EyeIcon, { size: 14 })
136
+ }
137
+ )
138
+ ] }),
139
+ /* @__PURE__ */ jsx(
140
+ "button",
141
+ {
142
+ onClick: handleTest,
143
+ disabled: testing || !apiKey,
144
+ 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",
145
+ children: testing ? /* @__PURE__ */ jsx(SpinnerIcon, { size: 12 }) : "Test"
146
+ }
147
+ )
148
+ ] })
149
+ ] }),
150
+ /* @__PURE__ */ jsxs("div", { children: [
151
+ /* @__PURE__ */ jsx("label", { className: "block text-[10px] font-mono font-medium text-muted-foreground uppercase tracking-wider mb-1.5", children: "Model" }),
152
+ models.length > 0 ? /* @__PURE__ */ jsx(
153
+ "select",
154
+ {
155
+ value: model,
156
+ onChange: (e) => setModel(e.target.value),
157
+ 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",
158
+ children: models.map((m) => /* @__PURE__ */ jsx("option", { value: m, children: m }, m))
159
+ }
160
+ ) : /* @__PURE__ */ jsx(
161
+ "input",
162
+ {
163
+ type: "text",
164
+ value: model,
165
+ onChange: (e) => setModel(e.target.value),
166
+ placeholder: "model-name",
167
+ 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"
168
+ }
169
+ )
170
+ ] }),
171
+ /* @__PURE__ */ jsxs("div", { children: [
172
+ /* @__PURE__ */ jsx("label", { className: "block text-[10px] font-mono font-medium text-muted-foreground uppercase tracking-wider mb-1.5", children: "Max Tokens" }),
173
+ /* @__PURE__ */ jsx(
174
+ "input",
175
+ {
176
+ type: "number",
177
+ value: maxTokens,
178
+ onChange: (e) => setMaxTokens(e.target.value),
179
+ 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"
180
+ }
181
+ )
182
+ ] }),
183
+ provider === "custom" && /* @__PURE__ */ jsxs("div", { children: [
184
+ /* @__PURE__ */ jsx("label", { className: "block text-[10px] font-mono font-medium text-muted-foreground uppercase tracking-wider mb-1.5", children: "Base URL" }),
185
+ /* @__PURE__ */ jsx(
186
+ "input",
187
+ {
188
+ type: "text",
189
+ value: baseUrl,
190
+ onChange: (e) => setBaseUrl(e.target.value),
191
+ placeholder: "https://api.example.com/v1",
192
+ 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"
193
+ }
194
+ )
195
+ ] }),
196
+ testResult && /* @__PURE__ */ jsxs("div", { className: "rounded-md border border-white/[0.04] bg-black/30 overflow-hidden", children: [
197
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 px-2.5 py-1.5 border-b border-white/[0.04]", children: [
198
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
199
+ /* @__PURE__ */ jsx("div", { className: "w-2 h-2 rounded-full bg-[#ff5f57]" }),
200
+ /* @__PURE__ */ jsx("div", { className: "w-2 h-2 rounded-full bg-[#febc2e]" }),
201
+ /* @__PURE__ */ jsx("div", { className: "w-2 h-2 rounded-full bg-[#28c840]" })
202
+ ] }),
203
+ /* @__PURE__ */ jsx("span", { className: "font-mono text-[9px] text-muted-foreground ml-1", children: "connection test" }),
204
+ testResult.error ? /* @__PURE__ */ jsx(XIcon, { size: 10, className: "text-[--destructive] ml-auto" }) : /* @__PURE__ */ jsx(CheckIcon, { size: 10, className: "text-[--success] ml-auto" })
205
+ ] }),
206
+ /* @__PURE__ */ jsx("pre", { className: "text-[11px] p-2.5 font-mono overflow-auto max-h-24 whitespace-pre-wrap break-words text-foreground/80", children: testResult.error ? `Error: ${testResult.error}` : `Connected. Response: ${testResult.response || "OK"}` })
207
+ ] }),
208
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 pt-2", children: [
209
+ /* @__PURE__ */ jsxs(
210
+ "button",
211
+ {
212
+ onClick: handleSave,
213
+ disabled: saving || !apiKey,
214
+ 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",
215
+ children: [
216
+ saving ? /* @__PURE__ */ jsx(SpinnerIcon, { size: 12 }) : /* @__PURE__ */ jsx(CheckIcon, { size: 12 }),
217
+ "Save"
218
+ ]
219
+ }
220
+ ),
221
+ !isActive && apiKey && /* @__PURE__ */ jsx(
222
+ "button",
223
+ {
224
+ onClick: handleSetActive,
225
+ 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",
226
+ children: "Set as Active"
227
+ }
228
+ ),
229
+ /* @__PURE__ */ jsx(
230
+ "button",
231
+ {
232
+ onClick: onClose,
233
+ 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",
234
+ children: "Cancel"
235
+ }
236
+ )
237
+ ] })
238
+ ] })
239
+ ] })
240
+ }
241
+ );
242
+ }
243
+ function StatsCard({ label, value, active }) {
244
+ return /* @__PURE__ */ jsxs("div", { className: `flex flex-col items-center justify-center p-4 rounded-lg border bg-[--card] ${active ? "border-[--cyan]/20" : "border-white/[0.06]"}`, children: [
245
+ /* @__PURE__ */ jsx("span", { className: `text-2xl font-semibold font-mono ${active ? "text-[--cyan]" : "text-foreground"}`, children: value }),
246
+ /* @__PURE__ */ jsx("span", { className: "font-mono text-[10px] text-muted-foreground uppercase tracking-wider mt-1", children: label })
247
+ ] });
248
+ }
249
+ function SettingsProvidersPage() {
250
+ const [loading, setLoading] = useState(true);
251
+ const [providers, setProviders] = useState({});
252
+ const [activeProviderInfo, setActiveProviderInfo] = useState(null);
253
+ const [editingProvider, setEditingProvider] = useState(null);
254
+ async function load() {
255
+ try {
256
+ const [provs, active] = await Promise.all([
257
+ getLlmProviders(),
258
+ getActiveProvider()
259
+ ]);
260
+ setProviders(provs || {});
261
+ setActiveProviderInfo(active || {});
262
+ } catch {
263
+ }
264
+ setLoading(false);
265
+ }
266
+ useEffect(() => {
267
+ load();
268
+ }, []);
269
+ async function handleSave(provider, config) {
270
+ await saveLlmProvider(provider, config);
271
+ await load();
272
+ setEditingProvider(null);
273
+ }
274
+ async function handleSetActive(provider, model) {
275
+ await setActiveProvider(provider, model);
276
+ await load();
277
+ setEditingProvider(null);
278
+ }
279
+ if (loading) {
280
+ return /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-3", children: [...Array(4)].map((_, i) => /* @__PURE__ */ jsx("div", { className: "h-16 animate-shimmer rounded-lg border border-white/[0.06] bg-[--card]" }, i)) });
281
+ }
282
+ const configuredCount = Object.values(providers).filter((p) => p?.apiKey).length;
283
+ const providerList = ["anthropic", "openai", "google", "custom"];
284
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
285
+ activeProviderInfo?.provider && /* @__PURE__ */ jsx("div", { className: "rounded-lg border border-[--cyan]/20 bg-[--cyan]/5 p-4 mb-6", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
286
+ /* @__PURE__ */ jsx("div", { className: "shrink-0 rounded-md bg-[--cyan]/10 p-2", children: /* @__PURE__ */ jsx(CpuIcon, { size: 16, className: "text-[--cyan]" }) }),
287
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
288
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
289
+ /* @__PURE__ */ jsx("span", { className: "font-mono text-[10px] font-medium text-[--cyan] uppercase tracking-wider", children: "Active Provider" }),
290
+ /* @__PURE__ */ jsx("div", { className: "w-2 h-2 rounded-full bg-[--success] animate-pulse" })
291
+ ] }),
292
+ /* @__PURE__ */ jsxs("p", { className: "text-sm font-mono font-medium mt-0.5", children: [
293
+ PROVIDER_META[activeProviderInfo.provider]?.name || activeProviderInfo.provider,
294
+ activeProviderInfo.model && /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground ml-2", children: [
295
+ "/ ",
296
+ activeProviderInfo.model
297
+ ] })
298
+ ] })
299
+ ] })
300
+ ] }) }),
301
+ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-3 gap-3 mb-6", children: [
302
+ /* @__PURE__ */ jsx(StatsCard, { label: "Providers", value: providerList.length }),
303
+ /* @__PURE__ */ jsx(StatsCard, { label: "Configured", value: configuredCount }),
304
+ /* @__PURE__ */ jsx(StatsCard, { label: "Active", value: activeProviderInfo?.provider ? 1 : 0, active: true })
305
+ ] }),
306
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 pb-2 mb-4", children: [
307
+ /* @__PURE__ */ jsx("span", { className: "font-mono text-[10px] font-medium text-[--cyan] uppercase tracking-wider", children: "Configured Providers" }),
308
+ /* @__PURE__ */ jsx("span", { className: "inline-flex items-center rounded-full bg-[--cyan]/10 px-2 py-0.5 text-[10px] font-mono font-medium text-[--cyan]", children: configuredCount })
309
+ ] }),
310
+ /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-3", children: providerList.map((p, i) => /* @__PURE__ */ jsxs("div", { children: [
311
+ /* @__PURE__ */ jsx(
312
+ ProviderCard,
313
+ {
314
+ provider: p,
315
+ config: providers[p],
316
+ isActive: activeProviderInfo?.provider === p,
317
+ onEdit: () => setEditingProvider(editingProvider === p ? null : p),
318
+ index: i
319
+ }
320
+ ),
321
+ /* @__PURE__ */ jsx(AnimatePresence, { children: editingProvider === p && /* @__PURE__ */ jsx(
322
+ ProviderSetupForm,
323
+ {
324
+ provider: p,
325
+ config: providers[p],
326
+ isActive: activeProviderInfo?.provider === p,
327
+ onSave: handleSave,
328
+ onSetActive: handleSetActive,
329
+ onClose: () => setEditingProvider(null)
330
+ }
331
+ ) })
332
+ ] }, p)) })
333
+ ] });
334
+ }
335
+ export {
336
+ SettingsProvidersPage
337
+ };