@harbinger-ai/harbinger 0.1.2 → 0.1.4
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/bounty/actions.js +45 -0
- package/lib/chat/actions.js +715 -0
- package/lib/chat/components/agents-page.js +918 -0
- package/lib/chat/components/agents-page.jsx +869 -0
- package/lib/chat/components/app-sidebar.js +17 -1
- package/lib/chat/components/app-sidebar.jsx +19 -1
- package/lib/chat/components/icons.js +145 -0
- package/lib/chat/components/icons.jsx +171 -0
- package/lib/chat/components/index.js +2 -0
- package/lib/chat/components/mcp-page.js +383 -55
- package/lib/chat/components/mcp-page.jsx +404 -101
- package/lib/chat/components/page-layout.js +41 -2
- package/lib/chat/components/page-layout.jsx +40 -2
- 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 +872 -0
- package/lib/chat/components/settings-providers-page.jsx +917 -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 +554 -96
- package/lib/chat/components/targets-page.jsx +464 -114
- package/lib/mcp/actions.js +120 -0
- package/lib/mcp/registry.js +164 -0
- package/package.json +1 -1
|
@@ -0,0 +1,872 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState, useEffect, useCallback } from "react";
|
|
4
|
+
import { motion, AnimatePresence } from "framer-motion";
|
|
5
|
+
import {
|
|
6
|
+
CpuIcon,
|
|
7
|
+
CheckIcon,
|
|
8
|
+
SpinnerIcon,
|
|
9
|
+
EyeIcon,
|
|
10
|
+
EyeOffIcon,
|
|
11
|
+
XIcon,
|
|
12
|
+
RefreshIcon,
|
|
13
|
+
WifiIcon,
|
|
14
|
+
ServerIcon,
|
|
15
|
+
ScanIcon,
|
|
16
|
+
GlobeIcon,
|
|
17
|
+
ZapIcon
|
|
18
|
+
} from "./icons.js";
|
|
19
|
+
import {
|
|
20
|
+
getLlmProviders,
|
|
21
|
+
saveLlmProvider,
|
|
22
|
+
setActiveProvider,
|
|
23
|
+
testLlmConnection,
|
|
24
|
+
getActiveProvider,
|
|
25
|
+
scanLocalProviders,
|
|
26
|
+
getLocalModels
|
|
27
|
+
} from "../actions.js";
|
|
28
|
+
const CLOUD_PROVIDERS = {
|
|
29
|
+
anthropic: {
|
|
30
|
+
name: "Anthropic",
|
|
31
|
+
icon: "A",
|
|
32
|
+
color: "#d4a574",
|
|
33
|
+
keyPrefix: "sk-ant-",
|
|
34
|
+
keyEnv: "ANTHROPIC_API_KEY",
|
|
35
|
+
models: [
|
|
36
|
+
"claude-sonnet-4-20250514",
|
|
37
|
+
"claude-opus-4-20250514",
|
|
38
|
+
"claude-haiku-4-5-20251001",
|
|
39
|
+
"claude-3-5-sonnet-20241022"
|
|
40
|
+
]
|
|
41
|
+
},
|
|
42
|
+
openai: {
|
|
43
|
+
name: "OpenAI",
|
|
44
|
+
icon: "O",
|
|
45
|
+
color: "#74aa9c",
|
|
46
|
+
keyPrefix: "sk-",
|
|
47
|
+
keyEnv: "OPENAI_API_KEY",
|
|
48
|
+
models: ["gpt-4o", "gpt-4o-mini", "gpt-4-turbo", "o1", "o1-mini", "o3-mini"]
|
|
49
|
+
},
|
|
50
|
+
google: {
|
|
51
|
+
name: "Google",
|
|
52
|
+
icon: "G",
|
|
53
|
+
color: "#4285f4",
|
|
54
|
+
keyPrefix: "AI",
|
|
55
|
+
keyEnv: "GOOGLE_API_KEY",
|
|
56
|
+
models: ["gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.0-flash"]
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
const LOCAL_PROVIDERS = {
|
|
60
|
+
ollama: { name: "Ollama", icon: "\u{1F999}", defaultUrl: "http://localhost:11434" },
|
|
61
|
+
lmstudio: { name: "LM Studio", icon: "\u{1F52C}", defaultUrl: "http://localhost:1234/v1" },
|
|
62
|
+
localai: { name: "LocalAI", icon: "\u{1F916}", defaultUrl: "http://localhost:8080/v1" },
|
|
63
|
+
llamacpp: { name: "llama.cpp", icon: "\u{1F527}", defaultUrl: "http://localhost:8081" },
|
|
64
|
+
vllm: { name: "vLLM", icon: "\u26A1", defaultUrl: "http://localhost:8000/v1" },
|
|
65
|
+
jan: { name: "Jan", icon: "\u{1F310}", defaultUrl: "http://localhost:1337/v1" }
|
|
66
|
+
};
|
|
67
|
+
function TrafficLights({ label }) {
|
|
68
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
69
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
|
|
70
|
+
/* @__PURE__ */ jsx("div", { className: "w-2 h-2 rounded-full bg-[#ff5f57]" }),
|
|
71
|
+
/* @__PURE__ */ jsx("div", { className: "w-2 h-2 rounded-full bg-[#febc2e]" }),
|
|
72
|
+
/* @__PURE__ */ jsx("div", { className: "w-2 h-2 rounded-full bg-[#28c840]" })
|
|
73
|
+
] }),
|
|
74
|
+
label && /* @__PURE__ */ jsx("span", { className: "font-mono text-[10px] font-medium text-[--cyan] uppercase tracking-wider ml-1", children: label })
|
|
75
|
+
] });
|
|
76
|
+
}
|
|
77
|
+
function ProviderCard({ id, meta, config, isActive, isLocal, detected, onEdit, index }) {
|
|
78
|
+
const hasKey = !isLocal && (config?.hasKey || !!config?.apiKey);
|
|
79
|
+
const isDetected = isLocal && detected;
|
|
80
|
+
return /* @__PURE__ */ jsx(
|
|
81
|
+
motion.div,
|
|
82
|
+
{
|
|
83
|
+
initial: { opacity: 0, y: 8 },
|
|
84
|
+
animate: { opacity: 1, y: 0 },
|
|
85
|
+
transition: { duration: 0.25, delay: index * 0.04 },
|
|
86
|
+
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]"}`,
|
|
87
|
+
onClick: onEdit,
|
|
88
|
+
children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 p-3.5", children: [
|
|
89
|
+
/* @__PURE__ */ jsx("div", { className: `shrink-0 w-9 h-9 rounded-md flex items-center justify-center text-sm font-bold ${isLocal ? "bg-emerald-500/10 text-emerald-400" : "bg-[--cyan]/10 text-[--cyan]"}`, children: isLocal ? /* @__PURE__ */ jsx("span", { className: "text-base", children: meta.icon }) : /* @__PURE__ */ jsx("span", { className: "font-mono", children: meta.icon }) }),
|
|
90
|
+
/* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
|
|
91
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
92
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm font-mono font-medium", children: meta.name }),
|
|
93
|
+
isLocal && /* @__PURE__ */ jsx("span", { className: "inline-flex items-center rounded-full bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 px-1.5 py-0 text-[9px] font-mono", children: "local" })
|
|
94
|
+
] }),
|
|
95
|
+
config?.model && /* @__PURE__ */ jsx("p", { className: "text-[10px] font-mono text-muted-foreground mt-0.5 truncate", children: config.model }),
|
|
96
|
+
isLocal && detected?.models?.length > 0 && !config?.model && /* @__PURE__ */ jsxs("p", { className: "text-[10px] font-mono text-muted-foreground mt-0.5", children: [
|
|
97
|
+
detected.models.length,
|
|
98
|
+
" model(s) available"
|
|
99
|
+
] })
|
|
100
|
+
] }),
|
|
101
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 shrink-0", children: [
|
|
102
|
+
isLocal ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
103
|
+
/* @__PURE__ */ jsx("div", { className: `w-2 h-2 rounded-full ${isDetected ? "bg-emerald-400 animate-pulse" : "bg-muted-foreground/40"}` }),
|
|
104
|
+
/* @__PURE__ */ jsx("span", { className: `text-[10px] font-mono ${isDetected ? "text-emerald-400" : "text-muted-foreground"}`, children: isDetected ? "running" : "not found" })
|
|
105
|
+
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
106
|
+
/* @__PURE__ */ jsx("div", { className: `w-2 h-2 rounded-full ${hasKey ? "bg-[--success]" : "bg-muted-foreground/40"}` }),
|
|
107
|
+
/* @__PURE__ */ jsx("span", { className: `text-[10px] font-mono ${hasKey ? "text-[--success]" : "text-muted-foreground"}`, children: hasKey ? "configured" : "no key" })
|
|
108
|
+
] }),
|
|
109
|
+
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" })
|
|
110
|
+
] })
|
|
111
|
+
] })
|
|
112
|
+
}
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
function CloudSetupForm({ provider, meta, config, isActive, onSave, onSetActive, onClose }) {
|
|
116
|
+
const [apiKey, setApiKey] = useState("");
|
|
117
|
+
const [model, setModel] = useState(config?.model || meta.models[0] || "");
|
|
118
|
+
const [maxTokens, setMaxTokens] = useState(config?.maxTokens || 4096);
|
|
119
|
+
const [showKey, setShowKey] = useState(false);
|
|
120
|
+
const [saving, setSaving] = useState(false);
|
|
121
|
+
const [testing, setTesting] = useState(false);
|
|
122
|
+
const [testResult, setTestResult] = useState(null);
|
|
123
|
+
async function handleSave() {
|
|
124
|
+
setSaving(true);
|
|
125
|
+
try {
|
|
126
|
+
const saveConfig = { model, maxTokens: parseInt(maxTokens) };
|
|
127
|
+
if (apiKey && !apiKey.includes("***")) {
|
|
128
|
+
saveConfig.apiKey = apiKey;
|
|
129
|
+
}
|
|
130
|
+
await onSave(provider, saveConfig);
|
|
131
|
+
} finally {
|
|
132
|
+
setSaving(false);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async function handleTest() {
|
|
136
|
+
setTesting(true);
|
|
137
|
+
setTestResult(null);
|
|
138
|
+
try {
|
|
139
|
+
const result = await testLlmConnection(provider);
|
|
140
|
+
setTestResult(result);
|
|
141
|
+
} catch (err) {
|
|
142
|
+
setTestResult({ error: err.message });
|
|
143
|
+
} finally {
|
|
144
|
+
setTesting(false);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return /* @__PURE__ */ jsx(
|
|
148
|
+
motion.div,
|
|
149
|
+
{
|
|
150
|
+
initial: { opacity: 0, height: 0 },
|
|
151
|
+
animate: { opacity: 1, height: "auto" },
|
|
152
|
+
exit: { opacity: 0, height: 0 },
|
|
153
|
+
transition: { duration: 0.2 },
|
|
154
|
+
className: "overflow-hidden",
|
|
155
|
+
children: /* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-[--cyan]/20 bg-[--card] p-5 mt-2", children: [
|
|
156
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-4", children: [
|
|
157
|
+
/* @__PURE__ */ jsx(TrafficLights, { label: `${meta.name} Setup` }),
|
|
158
|
+
/* @__PURE__ */ jsx("button", { onClick: onClose, className: "text-muted-foreground hover:text-foreground transition-colors", children: /* @__PURE__ */ jsx(XIcon, { size: 14 }) })
|
|
159
|
+
] }),
|
|
160
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-4", children: [
|
|
161
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
162
|
+
/* @__PURE__ */ jsx("label", { className: "block text-[10px] font-mono font-medium text-muted-foreground uppercase tracking-wider mb-1.5", children: "API Key" }),
|
|
163
|
+
/* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
|
|
164
|
+
/* @__PURE__ */ jsxs("div", { className: "flex-1 relative", children: [
|
|
165
|
+
/* @__PURE__ */ jsx(
|
|
166
|
+
"input",
|
|
167
|
+
{
|
|
168
|
+
type: showKey ? "text" : "password",
|
|
169
|
+
value: apiKey,
|
|
170
|
+
onChange: (e) => setApiKey(e.target.value),
|
|
171
|
+
placeholder: config?.hasKey ? "(saved \u2014 enter new key to change)" : `${meta.keyPrefix}...`,
|
|
172
|
+
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"
|
|
173
|
+
}
|
|
174
|
+
),
|
|
175
|
+
/* @__PURE__ */ jsx(
|
|
176
|
+
"button",
|
|
177
|
+
{
|
|
178
|
+
type: "button",
|
|
179
|
+
onClick: () => setShowKey(!showKey),
|
|
180
|
+
className: "absolute right-2.5 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors",
|
|
181
|
+
children: showKey ? /* @__PURE__ */ jsx(EyeOffIcon, { size: 14 }) : /* @__PURE__ */ jsx(EyeIcon, { size: 14 })
|
|
182
|
+
}
|
|
183
|
+
)
|
|
184
|
+
] }),
|
|
185
|
+
/* @__PURE__ */ jsxs(
|
|
186
|
+
"button",
|
|
187
|
+
{
|
|
188
|
+
onClick: handleTest,
|
|
189
|
+
disabled: testing || !apiKey && !config?.hasKey,
|
|
190
|
+
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",
|
|
191
|
+
children: [
|
|
192
|
+
testing ? /* @__PURE__ */ jsx(SpinnerIcon, { size: 12 }) : /* @__PURE__ */ jsx(ZapIcon, { size: 12 }),
|
|
193
|
+
"Test"
|
|
194
|
+
]
|
|
195
|
+
}
|
|
196
|
+
)
|
|
197
|
+
] }),
|
|
198
|
+
config?.hasKey && /* @__PURE__ */ jsxs("p", { className: "text-[10px] font-mono text-muted-foreground/60 mt-1", children: [
|
|
199
|
+
"Key saved: ",
|
|
200
|
+
config.apiKey
|
|
201
|
+
] })
|
|
202
|
+
] }),
|
|
203
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
204
|
+
/* @__PURE__ */ jsx("label", { className: "block text-[10px] font-mono font-medium text-muted-foreground uppercase tracking-wider mb-1.5", children: "Model" }),
|
|
205
|
+
/* @__PURE__ */ jsx(
|
|
206
|
+
"select",
|
|
207
|
+
{
|
|
208
|
+
value: model,
|
|
209
|
+
onChange: (e) => setModel(e.target.value),
|
|
210
|
+
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",
|
|
211
|
+
children: meta.models.map((m) => /* @__PURE__ */ jsx("option", { value: m, children: m }, m))
|
|
212
|
+
}
|
|
213
|
+
)
|
|
214
|
+
] }),
|
|
215
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
216
|
+
/* @__PURE__ */ jsx("label", { className: "block text-[10px] font-mono font-medium text-muted-foreground uppercase tracking-wider mb-1.5", children: "Max Tokens" }),
|
|
217
|
+
/* @__PURE__ */ jsx(
|
|
218
|
+
"input",
|
|
219
|
+
{
|
|
220
|
+
type: "number",
|
|
221
|
+
value: maxTokens,
|
|
222
|
+
onChange: (e) => setMaxTokens(e.target.value),
|
|
223
|
+
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"
|
|
224
|
+
}
|
|
225
|
+
)
|
|
226
|
+
] }),
|
|
227
|
+
/* @__PURE__ */ jsx(TestResultBox, { result: testResult }),
|
|
228
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 pt-2", children: [
|
|
229
|
+
/* @__PURE__ */ jsxs(
|
|
230
|
+
"button",
|
|
231
|
+
{
|
|
232
|
+
onClick: handleSave,
|
|
233
|
+
disabled: saving,
|
|
234
|
+
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",
|
|
235
|
+
children: [
|
|
236
|
+
saving ? /* @__PURE__ */ jsx(SpinnerIcon, { size: 12 }) : /* @__PURE__ */ jsx(CheckIcon, { size: 12 }),
|
|
237
|
+
"Save"
|
|
238
|
+
]
|
|
239
|
+
}
|
|
240
|
+
),
|
|
241
|
+
!isActive && (config?.hasKey || apiKey) && /* @__PURE__ */ jsx(
|
|
242
|
+
"button",
|
|
243
|
+
{
|
|
244
|
+
onClick: () => onSetActive(provider, model),
|
|
245
|
+
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",
|
|
246
|
+
children: "Set as Active"
|
|
247
|
+
}
|
|
248
|
+
),
|
|
249
|
+
/* @__PURE__ */ jsx(
|
|
250
|
+
"button",
|
|
251
|
+
{
|
|
252
|
+
onClick: onClose,
|
|
253
|
+
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",
|
|
254
|
+
children: "Cancel"
|
|
255
|
+
}
|
|
256
|
+
)
|
|
257
|
+
] })
|
|
258
|
+
] })
|
|
259
|
+
] })
|
|
260
|
+
}
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
function LocalSetupForm({ provider, meta, config, detected, isActive, onSave, onSetActive, onClose }) {
|
|
264
|
+
const detectedModels = detected?.models || [];
|
|
265
|
+
const [model, setModel] = useState(config?.model || detectedModels[0]?.id || "");
|
|
266
|
+
const [customModel, setCustomModel] = useState("");
|
|
267
|
+
const [baseUrl, setBaseUrl] = useState(config?.baseUrl || detected?.baseUrl || meta.defaultUrl || "");
|
|
268
|
+
const [maxTokens, setMaxTokens] = useState(config?.maxTokens || 4096);
|
|
269
|
+
const [saving, setSaving] = useState(false);
|
|
270
|
+
const [testing, setTesting] = useState(false);
|
|
271
|
+
const [testResult, setTestResult] = useState(null);
|
|
272
|
+
const [refreshing, setRefreshing] = useState(false);
|
|
273
|
+
const [models, setModels] = useState(detectedModels);
|
|
274
|
+
async function handleRefreshModels() {
|
|
275
|
+
setRefreshing(true);
|
|
276
|
+
try {
|
|
277
|
+
const fetched = await getLocalModels(baseUrl);
|
|
278
|
+
setModels(fetched);
|
|
279
|
+
if (fetched.length > 0 && !model) setModel(fetched[0].id);
|
|
280
|
+
} catch {
|
|
281
|
+
}
|
|
282
|
+
setRefreshing(false);
|
|
283
|
+
}
|
|
284
|
+
async function handleSave() {
|
|
285
|
+
setSaving(true);
|
|
286
|
+
try {
|
|
287
|
+
const selectedModel = customModel || model;
|
|
288
|
+
await onSave(provider, {
|
|
289
|
+
model: selectedModel,
|
|
290
|
+
maxTokens: parseInt(maxTokens),
|
|
291
|
+
baseUrl,
|
|
292
|
+
apiKey: "not-needed"
|
|
293
|
+
});
|
|
294
|
+
} finally {
|
|
295
|
+
setSaving(false);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
async function handleTest() {
|
|
299
|
+
setTesting(true);
|
|
300
|
+
setTestResult(null);
|
|
301
|
+
try {
|
|
302
|
+
const result = await testLlmConnection(provider);
|
|
303
|
+
setTestResult(result);
|
|
304
|
+
} catch (err) {
|
|
305
|
+
setTestResult({ error: err.message });
|
|
306
|
+
} finally {
|
|
307
|
+
setTesting(false);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return /* @__PURE__ */ jsx(
|
|
311
|
+
motion.div,
|
|
312
|
+
{
|
|
313
|
+
initial: { opacity: 0, height: 0 },
|
|
314
|
+
animate: { opacity: 1, height: "auto" },
|
|
315
|
+
exit: { opacity: 0, height: 0 },
|
|
316
|
+
transition: { duration: 0.2 },
|
|
317
|
+
className: "overflow-hidden",
|
|
318
|
+
children: /* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-emerald-500/20 bg-[--card] p-5 mt-2", children: [
|
|
319
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-4", children: [
|
|
320
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
321
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
|
|
322
|
+
/* @__PURE__ */ jsx("div", { className: "w-2 h-2 rounded-full bg-[#ff5f57]" }),
|
|
323
|
+
/* @__PURE__ */ jsx("div", { className: "w-2 h-2 rounded-full bg-[#febc2e]" }),
|
|
324
|
+
/* @__PURE__ */ jsx("div", { className: "w-2 h-2 rounded-full bg-[#28c840]" })
|
|
325
|
+
] }),
|
|
326
|
+
/* @__PURE__ */ jsxs("span", { className: "font-mono text-[10px] font-medium text-emerald-400 uppercase tracking-wider ml-1", children: [
|
|
327
|
+
meta.name,
|
|
328
|
+
" Setup"
|
|
329
|
+
] }),
|
|
330
|
+
detected && /* @__PURE__ */ jsx("span", { className: "inline-flex items-center rounded-full bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 px-1.5 py-0 text-[9px] font-mono", children: "detected" })
|
|
331
|
+
] }),
|
|
332
|
+
/* @__PURE__ */ jsx("button", { onClick: onClose, className: "text-muted-foreground hover:text-foreground transition-colors", children: /* @__PURE__ */ jsx(XIcon, { size: 14 }) })
|
|
333
|
+
] }),
|
|
334
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-4", children: [
|
|
335
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
336
|
+
/* @__PURE__ */ jsx("label", { className: "block text-[10px] font-mono font-medium text-muted-foreground uppercase tracking-wider mb-1.5", children: "Base URL" }),
|
|
337
|
+
/* @__PURE__ */ jsx(
|
|
338
|
+
"input",
|
|
339
|
+
{
|
|
340
|
+
type: "text",
|
|
341
|
+
value: baseUrl,
|
|
342
|
+
onChange: (e) => setBaseUrl(e.target.value),
|
|
343
|
+
placeholder: meta.defaultUrl,
|
|
344
|
+
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-emerald-500/40 focus:ring-1 focus:ring-emerald-500/20 transition-colors"
|
|
345
|
+
}
|
|
346
|
+
)
|
|
347
|
+
] }),
|
|
348
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
349
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-1.5", children: [
|
|
350
|
+
/* @__PURE__ */ jsx("label", { className: "text-[10px] font-mono font-medium text-muted-foreground uppercase tracking-wider", children: "Model" }),
|
|
351
|
+
/* @__PURE__ */ jsxs(
|
|
352
|
+
"button",
|
|
353
|
+
{
|
|
354
|
+
onClick: handleRefreshModels,
|
|
355
|
+
disabled: refreshing,
|
|
356
|
+
className: "inline-flex items-center gap-1 text-[10px] font-mono text-muted-foreground hover:text-emerald-400 transition-colors disabled:opacity-50",
|
|
357
|
+
children: [
|
|
358
|
+
refreshing ? /* @__PURE__ */ jsx(SpinnerIcon, { size: 10 }) : /* @__PURE__ */ jsx(RefreshIcon, { size: 10 }),
|
|
359
|
+
"Scan Models"
|
|
360
|
+
]
|
|
361
|
+
}
|
|
362
|
+
)
|
|
363
|
+
] }),
|
|
364
|
+
models.length > 0 ? /* @__PURE__ */ jsx(
|
|
365
|
+
"select",
|
|
366
|
+
{
|
|
367
|
+
value: model,
|
|
368
|
+
onChange: (e) => {
|
|
369
|
+
setModel(e.target.value);
|
|
370
|
+
setCustomModel("");
|
|
371
|
+
},
|
|
372
|
+
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-emerald-500/40 focus:ring-1 focus:ring-emerald-500/20 transition-colors appearance-none",
|
|
373
|
+
children: models.map((m) => /* @__PURE__ */ jsxs("option", { value: m.id, children: [
|
|
374
|
+
m.name,
|
|
375
|
+
m.paramSize ? ` (${m.paramSize})` : "",
|
|
376
|
+
m.family ? ` \u2014 ${m.family}` : ""
|
|
377
|
+
] }, m.id))
|
|
378
|
+
}
|
|
379
|
+
) : /* @__PURE__ */ jsx(
|
|
380
|
+
"input",
|
|
381
|
+
{
|
|
382
|
+
type: "text",
|
|
383
|
+
value: customModel,
|
|
384
|
+
onChange: (e) => setCustomModel(e.target.value),
|
|
385
|
+
placeholder: "model-name (or click Scan Models)",
|
|
386
|
+
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-emerald-500/40 focus:ring-1 focus:ring-emerald-500/20 transition-colors"
|
|
387
|
+
}
|
|
388
|
+
),
|
|
389
|
+
models.length > 0 && /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-1.5 mt-2", children: [
|
|
390
|
+
models.slice(0, 8).map((m) => /* @__PURE__ */ jsx(
|
|
391
|
+
"button",
|
|
392
|
+
{
|
|
393
|
+
onClick: () => {
|
|
394
|
+
setModel(m.id);
|
|
395
|
+
setCustomModel("");
|
|
396
|
+
},
|
|
397
|
+
className: `inline-flex items-center rounded-full px-2 py-0.5 text-[9px] font-mono border transition-colors ${model === m.id ? "bg-emerald-500/15 text-emerald-400 border-emerald-500/30" : "bg-white/[0.02] text-muted-foreground border-white/[0.06] hover:border-emerald-500/20"}`,
|
|
398
|
+
children: m.name
|
|
399
|
+
},
|
|
400
|
+
m.id
|
|
401
|
+
)),
|
|
402
|
+
models.length > 8 && /* @__PURE__ */ jsxs("span", { className: "text-[9px] font-mono text-muted-foreground self-center", children: [
|
|
403
|
+
"+",
|
|
404
|
+
models.length - 8,
|
|
405
|
+
" more"
|
|
406
|
+
] })
|
|
407
|
+
] })
|
|
408
|
+
] }),
|
|
409
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
410
|
+
/* @__PURE__ */ jsx("label", { className: "block text-[10px] font-mono font-medium text-muted-foreground uppercase tracking-wider mb-1.5", children: "Max Tokens" }),
|
|
411
|
+
/* @__PURE__ */ jsx(
|
|
412
|
+
"input",
|
|
413
|
+
{
|
|
414
|
+
type: "number",
|
|
415
|
+
value: maxTokens,
|
|
416
|
+
onChange: (e) => setMaxTokens(e.target.value),
|
|
417
|
+
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-emerald-500/40 focus:ring-1 focus:ring-emerald-500/20 transition-colors"
|
|
418
|
+
}
|
|
419
|
+
)
|
|
420
|
+
] }),
|
|
421
|
+
/* @__PURE__ */ jsx(TestResultBox, { result: testResult }),
|
|
422
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 pt-2", children: [
|
|
423
|
+
/* @__PURE__ */ jsxs(
|
|
424
|
+
"button",
|
|
425
|
+
{
|
|
426
|
+
onClick: handleSave,
|
|
427
|
+
disabled: saving || !model && !customModel,
|
|
428
|
+
className: "inline-flex items-center gap-1.5 rounded-md px-4 py-2 text-xs font-mono font-medium bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 hover:bg-emerald-500 hover:text-white transition-colors disabled:opacity-50",
|
|
429
|
+
children: [
|
|
430
|
+
saving ? /* @__PURE__ */ jsx(SpinnerIcon, { size: 12 }) : /* @__PURE__ */ jsx(CheckIcon, { size: 12 }),
|
|
431
|
+
"Save"
|
|
432
|
+
]
|
|
433
|
+
}
|
|
434
|
+
),
|
|
435
|
+
/* @__PURE__ */ jsxs(
|
|
436
|
+
"button",
|
|
437
|
+
{
|
|
438
|
+
onClick: handleTest,
|
|
439
|
+
disabled: testing,
|
|
440
|
+
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-emerald-500/30 hover:text-emerald-400 transition-colors disabled:opacity-50",
|
|
441
|
+
children: [
|
|
442
|
+
testing ? /* @__PURE__ */ jsx(SpinnerIcon, { size: 12 }) : /* @__PURE__ */ jsx(ZapIcon, { size: 12 }),
|
|
443
|
+
"Test"
|
|
444
|
+
]
|
|
445
|
+
}
|
|
446
|
+
),
|
|
447
|
+
!isActive && (model || customModel) && /* @__PURE__ */ jsx(
|
|
448
|
+
"button",
|
|
449
|
+
{
|
|
450
|
+
onClick: () => onSetActive(provider, customModel || model),
|
|
451
|
+
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",
|
|
452
|
+
children: "Set as Active"
|
|
453
|
+
}
|
|
454
|
+
),
|
|
455
|
+
/* @__PURE__ */ jsx(
|
|
456
|
+
"button",
|
|
457
|
+
{
|
|
458
|
+
onClick: onClose,
|
|
459
|
+
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",
|
|
460
|
+
children: "Cancel"
|
|
461
|
+
}
|
|
462
|
+
)
|
|
463
|
+
] })
|
|
464
|
+
] })
|
|
465
|
+
] })
|
|
466
|
+
}
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
function CustomSetupForm({ config, isActive, onSave, onSetActive, onClose }) {
|
|
470
|
+
const [apiKey, setApiKey] = useState("");
|
|
471
|
+
const [model, setModel] = useState(config?.model || "");
|
|
472
|
+
const [baseUrl, setBaseUrl] = useState(config?.baseUrl || "");
|
|
473
|
+
const [maxTokens, setMaxTokens] = useState(config?.maxTokens || 4096);
|
|
474
|
+
const [showKey, setShowKey] = useState(false);
|
|
475
|
+
const [saving, setSaving] = useState(false);
|
|
476
|
+
const [testing, setTesting] = useState(false);
|
|
477
|
+
const [testResult, setTestResult] = useState(null);
|
|
478
|
+
async function handleSave() {
|
|
479
|
+
setSaving(true);
|
|
480
|
+
try {
|
|
481
|
+
const saveConfig = { model, maxTokens: parseInt(maxTokens), baseUrl };
|
|
482
|
+
if (apiKey && !apiKey.includes("***")) saveConfig.apiKey = apiKey;
|
|
483
|
+
await onSave("custom", saveConfig);
|
|
484
|
+
} finally {
|
|
485
|
+
setSaving(false);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
async function handleTest() {
|
|
489
|
+
setTesting(true);
|
|
490
|
+
setTestResult(null);
|
|
491
|
+
try {
|
|
492
|
+
const result = await testLlmConnection("custom");
|
|
493
|
+
setTestResult(result);
|
|
494
|
+
} catch (err) {
|
|
495
|
+
setTestResult({ error: err.message });
|
|
496
|
+
} finally {
|
|
497
|
+
setTesting(false);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return /* @__PURE__ */ jsx(
|
|
501
|
+
motion.div,
|
|
502
|
+
{
|
|
503
|
+
initial: { opacity: 0, height: 0 },
|
|
504
|
+
animate: { opacity: 1, height: "auto" },
|
|
505
|
+
exit: { opacity: 0, height: 0 },
|
|
506
|
+
transition: { duration: 0.2 },
|
|
507
|
+
className: "overflow-hidden",
|
|
508
|
+
children: /* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-purple-500/20 bg-[--card] p-5 mt-2", children: [
|
|
509
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-4", children: [
|
|
510
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
511
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
|
|
512
|
+
/* @__PURE__ */ jsx("div", { className: "w-2 h-2 rounded-full bg-[#ff5f57]" }),
|
|
513
|
+
/* @__PURE__ */ jsx("div", { className: "w-2 h-2 rounded-full bg-[#febc2e]" }),
|
|
514
|
+
/* @__PURE__ */ jsx("div", { className: "w-2 h-2 rounded-full bg-[#28c840]" })
|
|
515
|
+
] }),
|
|
516
|
+
/* @__PURE__ */ jsx("span", { className: "font-mono text-[10px] font-medium text-purple-400 uppercase tracking-wider ml-1", children: "Custom Provider (OpenAI-Compatible)" })
|
|
517
|
+
] }),
|
|
518
|
+
/* @__PURE__ */ jsx("button", { onClick: onClose, className: "text-muted-foreground hover:text-foreground transition-colors", children: /* @__PURE__ */ jsx(XIcon, { size: 14 }) })
|
|
519
|
+
] }),
|
|
520
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-4", children: [
|
|
521
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
522
|
+
/* @__PURE__ */ jsx("label", { className: "block text-[10px] font-mono font-medium text-muted-foreground uppercase tracking-wider mb-1.5", children: "Base URL" }),
|
|
523
|
+
/* @__PURE__ */ jsx(
|
|
524
|
+
"input",
|
|
525
|
+
{
|
|
526
|
+
type: "text",
|
|
527
|
+
value: baseUrl,
|
|
528
|
+
onChange: (e) => setBaseUrl(e.target.value),
|
|
529
|
+
placeholder: "https://api.example.com/v1",
|
|
530
|
+
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-purple-500/40 focus:ring-1 focus:ring-purple-500/20 transition-colors"
|
|
531
|
+
}
|
|
532
|
+
)
|
|
533
|
+
] }),
|
|
534
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
535
|
+
/* @__PURE__ */ jsx("label", { className: "block text-[10px] font-mono font-medium text-muted-foreground uppercase tracking-wider mb-1.5", children: "API Key" }),
|
|
536
|
+
/* @__PURE__ */ jsxs("div", { className: "flex-1 relative", children: [
|
|
537
|
+
/* @__PURE__ */ jsx(
|
|
538
|
+
"input",
|
|
539
|
+
{
|
|
540
|
+
type: showKey ? "text" : "password",
|
|
541
|
+
value: apiKey,
|
|
542
|
+
onChange: (e) => setApiKey(e.target.value),
|
|
543
|
+
placeholder: config?.hasKey ? "(saved)" : "sk-... (or leave empty if not needed)",
|
|
544
|
+
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-purple-500/40 focus:ring-1 focus:ring-purple-500/20 transition-colors"
|
|
545
|
+
}
|
|
546
|
+
),
|
|
547
|
+
/* @__PURE__ */ jsx(
|
|
548
|
+
"button",
|
|
549
|
+
{
|
|
550
|
+
type: "button",
|
|
551
|
+
onClick: () => setShowKey(!showKey),
|
|
552
|
+
className: "absolute right-2.5 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors",
|
|
553
|
+
children: showKey ? /* @__PURE__ */ jsx(EyeOffIcon, { size: 14 }) : /* @__PURE__ */ jsx(EyeIcon, { size: 14 })
|
|
554
|
+
}
|
|
555
|
+
)
|
|
556
|
+
] })
|
|
557
|
+
] }),
|
|
558
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
559
|
+
/* @__PURE__ */ jsx("label", { className: "block text-[10px] font-mono font-medium text-muted-foreground uppercase tracking-wider mb-1.5", children: "Model" }),
|
|
560
|
+
/* @__PURE__ */ jsx(
|
|
561
|
+
"input",
|
|
562
|
+
{
|
|
563
|
+
type: "text",
|
|
564
|
+
value: model,
|
|
565
|
+
onChange: (e) => setModel(e.target.value),
|
|
566
|
+
placeholder: "model-name",
|
|
567
|
+
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-purple-500/40 focus:ring-1 focus:ring-purple-500/20 transition-colors"
|
|
568
|
+
}
|
|
569
|
+
)
|
|
570
|
+
] }),
|
|
571
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
572
|
+
/* @__PURE__ */ jsx("label", { className: "block text-[10px] font-mono font-medium text-muted-foreground uppercase tracking-wider mb-1.5", children: "Max Tokens" }),
|
|
573
|
+
/* @__PURE__ */ jsx(
|
|
574
|
+
"input",
|
|
575
|
+
{
|
|
576
|
+
type: "number",
|
|
577
|
+
value: maxTokens,
|
|
578
|
+
onChange: (e) => setMaxTokens(e.target.value),
|
|
579
|
+
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-purple-500/40 focus:ring-1 focus:ring-purple-500/20 transition-colors"
|
|
580
|
+
}
|
|
581
|
+
)
|
|
582
|
+
] }),
|
|
583
|
+
/* @__PURE__ */ jsx(TestResultBox, { result: testResult }),
|
|
584
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 pt-2", children: [
|
|
585
|
+
/* @__PURE__ */ jsxs(
|
|
586
|
+
"button",
|
|
587
|
+
{
|
|
588
|
+
onClick: handleSave,
|
|
589
|
+
disabled: saving,
|
|
590
|
+
className: "inline-flex items-center gap-1.5 rounded-md px-4 py-2 text-xs font-mono font-medium bg-purple-500/10 text-purple-400 border border-purple-500/20 hover:bg-purple-500 hover:text-white transition-colors disabled:opacity-50",
|
|
591
|
+
children: [
|
|
592
|
+
saving ? /* @__PURE__ */ jsx(SpinnerIcon, { size: 12 }) : /* @__PURE__ */ jsx(CheckIcon, { size: 12 }),
|
|
593
|
+
" Save"
|
|
594
|
+
]
|
|
595
|
+
}
|
|
596
|
+
),
|
|
597
|
+
/* @__PURE__ */ jsxs(
|
|
598
|
+
"button",
|
|
599
|
+
{
|
|
600
|
+
onClick: handleTest,
|
|
601
|
+
disabled: testing,
|
|
602
|
+
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-purple-500/30 hover:text-purple-400 transition-colors disabled:opacity-50",
|
|
603
|
+
children: [
|
|
604
|
+
testing ? /* @__PURE__ */ jsx(SpinnerIcon, { size: 12 }) : /* @__PURE__ */ jsx(ZapIcon, { size: 12 }),
|
|
605
|
+
" Test"
|
|
606
|
+
]
|
|
607
|
+
}
|
|
608
|
+
),
|
|
609
|
+
!isActive && model && /* @__PURE__ */ jsx(
|
|
610
|
+
"button",
|
|
611
|
+
{
|
|
612
|
+
onClick: () => onSetActive("custom", model),
|
|
613
|
+
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",
|
|
614
|
+
children: "Set as Active"
|
|
615
|
+
}
|
|
616
|
+
),
|
|
617
|
+
/* @__PURE__ */ jsx(
|
|
618
|
+
"button",
|
|
619
|
+
{
|
|
620
|
+
onClick: onClose,
|
|
621
|
+
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",
|
|
622
|
+
children: "Cancel"
|
|
623
|
+
}
|
|
624
|
+
)
|
|
625
|
+
] })
|
|
626
|
+
] })
|
|
627
|
+
] })
|
|
628
|
+
}
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
function TestResultBox({ result }) {
|
|
632
|
+
if (!result) return null;
|
|
633
|
+
return /* @__PURE__ */ jsxs("div", { className: "rounded-md border border-white/[0.04] bg-black/30 overflow-hidden", children: [
|
|
634
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 px-2.5 py-1.5 border-b border-white/[0.04]", children: [
|
|
635
|
+
/* @__PURE__ */ jsx(TrafficLights, {}),
|
|
636
|
+
/* @__PURE__ */ jsx("span", { className: "font-mono text-[9px] text-muted-foreground ml-1", children: "connection test" }),
|
|
637
|
+
result.error ? /* @__PURE__ */ jsx(XIcon, { size: 10, className: "text-[--destructive] ml-auto" }) : /* @__PURE__ */ jsx(CheckIcon, { size: 10, className: "text-[--success] ml-auto" })
|
|
638
|
+
] }),
|
|
639
|
+
/* @__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: result.error ? `Error: ${result.error}` : `Connected. ${result.response || "OK"}` })
|
|
640
|
+
] });
|
|
641
|
+
}
|
|
642
|
+
function StatsCard({ label, value, color = "cyan" }) {
|
|
643
|
+
const colors = {
|
|
644
|
+
cyan: { border: "border-[--cyan]/20", text: "text-[--cyan]" },
|
|
645
|
+
emerald: { border: "border-emerald-500/20", text: "text-emerald-400" },
|
|
646
|
+
purple: { border: "border-purple-500/20", text: "text-purple-400" },
|
|
647
|
+
default: { border: "border-white/[0.06]", text: "text-foreground" }
|
|
648
|
+
};
|
|
649
|
+
const c = colors[color] || colors.default;
|
|
650
|
+
return /* @__PURE__ */ jsxs("div", { className: `flex flex-col items-center justify-center p-3 rounded-lg border bg-[--card] ${c.border}`, children: [
|
|
651
|
+
/* @__PURE__ */ jsx("span", { className: `text-xl font-semibold font-mono ${c.text}`, children: value }),
|
|
652
|
+
/* @__PURE__ */ jsx("span", { className: "font-mono text-[9px] text-muted-foreground uppercase tracking-wider mt-0.5", children: label })
|
|
653
|
+
] });
|
|
654
|
+
}
|
|
655
|
+
function SettingsProvidersPage() {
|
|
656
|
+
const [loading, setLoading] = useState(true);
|
|
657
|
+
const [providers, setProviders] = useState({});
|
|
658
|
+
const [activeProviderInfo, setActiveProviderInfo] = useState(null);
|
|
659
|
+
const [editingProvider, setEditingProvider] = useState(null);
|
|
660
|
+
const [scanning, setScanning] = useState(false);
|
|
661
|
+
const [localDetected, setLocalDetected] = useState({});
|
|
662
|
+
const load = useCallback(async () => {
|
|
663
|
+
try {
|
|
664
|
+
const [provs, active] = await Promise.all([
|
|
665
|
+
getLlmProviders(),
|
|
666
|
+
getActiveProvider()
|
|
667
|
+
]);
|
|
668
|
+
setProviders(provs || {});
|
|
669
|
+
setActiveProviderInfo(active || {});
|
|
670
|
+
} catch {
|
|
671
|
+
}
|
|
672
|
+
setLoading(false);
|
|
673
|
+
}, []);
|
|
674
|
+
async function handleScan() {
|
|
675
|
+
setScanning(true);
|
|
676
|
+
try {
|
|
677
|
+
const results = await scanLocalProviders();
|
|
678
|
+
const detected = {};
|
|
679
|
+
for (const r of results) {
|
|
680
|
+
detected[r.id] = r;
|
|
681
|
+
}
|
|
682
|
+
setLocalDetected(detected);
|
|
683
|
+
} catch {
|
|
684
|
+
}
|
|
685
|
+
setScanning(false);
|
|
686
|
+
}
|
|
687
|
+
useEffect(() => {
|
|
688
|
+
load();
|
|
689
|
+
handleScan();
|
|
690
|
+
}, []);
|
|
691
|
+
async function handleSave(provider, config) {
|
|
692
|
+
await saveLlmProvider(provider, config);
|
|
693
|
+
await load();
|
|
694
|
+
setEditingProvider(null);
|
|
695
|
+
}
|
|
696
|
+
async function handleSetActive(provider, model) {
|
|
697
|
+
await setActiveProvider(provider, model);
|
|
698
|
+
await load();
|
|
699
|
+
setEditingProvider(null);
|
|
700
|
+
}
|
|
701
|
+
if (loading) {
|
|
702
|
+
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)) });
|
|
703
|
+
}
|
|
704
|
+
const cloudCount = Object.keys(CLOUD_PROVIDERS).filter((p) => providers[p]?.hasKey).length;
|
|
705
|
+
const localCount = Object.keys(localDetected).length;
|
|
706
|
+
const cloudList = Object.keys(CLOUD_PROVIDERS);
|
|
707
|
+
const localList = Object.keys(LOCAL_PROVIDERS);
|
|
708
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
709
|
+
activeProviderInfo?.provider && /* @__PURE__ */ jsx(
|
|
710
|
+
motion.div,
|
|
711
|
+
{
|
|
712
|
+
initial: { opacity: 0, y: -8 },
|
|
713
|
+
animate: { opacity: 1, y: 0 },
|
|
714
|
+
className: "rounded-lg border border-[--cyan]/20 bg-[--cyan]/5 p-4 mb-6",
|
|
715
|
+
children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
716
|
+
/* @__PURE__ */ jsx("div", { className: "shrink-0 rounded-md bg-[--cyan]/10 p-2", children: /* @__PURE__ */ jsx(CpuIcon, { size: 16, className: "text-[--cyan]" }) }),
|
|
717
|
+
/* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
|
|
718
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
719
|
+
/* @__PURE__ */ jsx("span", { className: "font-mono text-[10px] font-medium text-[--cyan] uppercase tracking-wider", children: "Active Provider" }),
|
|
720
|
+
/* @__PURE__ */ jsx("div", { className: "w-2 h-2 rounded-full bg-[--success] animate-pulse" })
|
|
721
|
+
] }),
|
|
722
|
+
/* @__PURE__ */ jsxs("p", { className: "text-sm font-mono font-medium mt-0.5", children: [
|
|
723
|
+
CLOUD_PROVIDERS[activeProviderInfo.provider]?.name || LOCAL_PROVIDERS[activeProviderInfo.provider]?.name || activeProviderInfo.provider,
|
|
724
|
+
activeProviderInfo.model && /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground ml-2", children: [
|
|
725
|
+
"/ ",
|
|
726
|
+
activeProviderInfo.model
|
|
727
|
+
] })
|
|
728
|
+
] }),
|
|
729
|
+
activeProviderInfo.keyLoaded === false && /* @__PURE__ */ jsx("p", { className: "text-[10px] font-mono text-amber-400 mt-1", children: "Warning: API key not loaded in process. Save the provider config or restart the server." })
|
|
730
|
+
] }),
|
|
731
|
+
/* @__PURE__ */ jsxs("div", { className: "text-right shrink-0", children: [
|
|
732
|
+
/* @__PURE__ */ jsxs("p", { className: "text-[9px] font-mono text-muted-foreground/50", children: [
|
|
733
|
+
"env: ",
|
|
734
|
+
activeProviderInfo.envProvider || "not set"
|
|
735
|
+
] }),
|
|
736
|
+
/* @__PURE__ */ jsxs("p", { className: "text-[9px] font-mono text-muted-foreground/50", children: [
|
|
737
|
+
"model: ",
|
|
738
|
+
activeProviderInfo.envModel || "default"
|
|
739
|
+
] })
|
|
740
|
+
] })
|
|
741
|
+
] })
|
|
742
|
+
}
|
|
743
|
+
),
|
|
744
|
+
/* @__PURE__ */ jsxs("div", { className: "grid grid-cols-4 gap-3 mb-6", children: [
|
|
745
|
+
/* @__PURE__ */ jsx(StatsCard, { label: "Cloud", value: cloudList.length }),
|
|
746
|
+
/* @__PURE__ */ jsx(StatsCard, { label: "Configured", value: cloudCount, color: "cyan" }),
|
|
747
|
+
/* @__PURE__ */ jsx(StatsCard, { label: "Local", value: localCount, color: "emerald" }),
|
|
748
|
+
/* @__PURE__ */ jsx(StatsCard, { label: "Active", value: activeProviderInfo?.provider ? 1 : 0, color: "purple" })
|
|
749
|
+
] }),
|
|
750
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 pb-2 mb-3", children: [
|
|
751
|
+
/* @__PURE__ */ jsx(GlobeIcon, { size: 12, className: "text-[--cyan]" }),
|
|
752
|
+
/* @__PURE__ */ jsx("span", { className: "font-mono text-[10px] font-medium text-[--cyan] uppercase tracking-wider", children: "Cloud Providers" }),
|
|
753
|
+
/* @__PURE__ */ jsxs("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: [
|
|
754
|
+
cloudCount,
|
|
755
|
+
"/",
|
|
756
|
+
cloudList.length
|
|
757
|
+
] })
|
|
758
|
+
] }),
|
|
759
|
+
/* @__PURE__ */ jsx("div", { className: "flex flex-col gap-2 mb-8", children: cloudList.map((p, i) => /* @__PURE__ */ jsxs("div", { children: [
|
|
760
|
+
/* @__PURE__ */ jsx(
|
|
761
|
+
ProviderCard,
|
|
762
|
+
{
|
|
763
|
+
id: p,
|
|
764
|
+
meta: CLOUD_PROVIDERS[p],
|
|
765
|
+
config: providers[p],
|
|
766
|
+
isActive: activeProviderInfo?.provider === p,
|
|
767
|
+
isLocal: false,
|
|
768
|
+
onEdit: () => setEditingProvider(editingProvider === p ? null : p),
|
|
769
|
+
index: i
|
|
770
|
+
}
|
|
771
|
+
),
|
|
772
|
+
/* @__PURE__ */ jsx(AnimatePresence, { children: editingProvider === p && /* @__PURE__ */ jsx(
|
|
773
|
+
CloudSetupForm,
|
|
774
|
+
{
|
|
775
|
+
provider: p,
|
|
776
|
+
meta: CLOUD_PROVIDERS[p],
|
|
777
|
+
config: providers[p],
|
|
778
|
+
isActive: activeProviderInfo?.provider === p,
|
|
779
|
+
onSave: handleSave,
|
|
780
|
+
onSetActive: handleSetActive,
|
|
781
|
+
onClose: () => setEditingProvider(null)
|
|
782
|
+
}
|
|
783
|
+
) })
|
|
784
|
+
] }, p)) }),
|
|
785
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between pb-2 mb-3", children: [
|
|
786
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
787
|
+
/* @__PURE__ */ jsx(ServerIcon, { size: 12, className: "text-emerald-400" }),
|
|
788
|
+
/* @__PURE__ */ jsx("span", { className: "font-mono text-[10px] font-medium text-emerald-400 uppercase tracking-wider", children: "Local Providers" }),
|
|
789
|
+
/* @__PURE__ */ jsxs("span", { className: "inline-flex items-center rounded-full bg-emerald-500/10 px-2 py-0.5 text-[10px] font-mono font-medium text-emerald-400", children: [
|
|
790
|
+
localCount,
|
|
791
|
+
" detected"
|
|
792
|
+
] })
|
|
793
|
+
] }),
|
|
794
|
+
/* @__PURE__ */ jsxs(
|
|
795
|
+
"button",
|
|
796
|
+
{
|
|
797
|
+
onClick: handleScan,
|
|
798
|
+
disabled: scanning,
|
|
799
|
+
className: "inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-[10px] font-mono font-medium border border-emerald-500/20 text-emerald-400 hover:bg-emerald-500/10 transition-colors disabled:opacity-50",
|
|
800
|
+
children: [
|
|
801
|
+
scanning ? /* @__PURE__ */ jsx(SpinnerIcon, { size: 10 }) : /* @__PURE__ */ jsx(ScanIcon, { size: 10 }),
|
|
802
|
+
scanning ? "Scanning..." : "Scan Network"
|
|
803
|
+
]
|
|
804
|
+
}
|
|
805
|
+
)
|
|
806
|
+
] }),
|
|
807
|
+
/* @__PURE__ */ jsx("div", { className: "flex flex-col gap-2 mb-8", children: localList.sort((a, b) => {
|
|
808
|
+
const aD = localDetected[a] ? 1 : 0;
|
|
809
|
+
const bD = localDetected[b] ? 1 : 0;
|
|
810
|
+
return bD - aD;
|
|
811
|
+
}).map((p, i) => /* @__PURE__ */ jsxs("div", { children: [
|
|
812
|
+
/* @__PURE__ */ jsx(
|
|
813
|
+
ProviderCard,
|
|
814
|
+
{
|
|
815
|
+
id: p,
|
|
816
|
+
meta: LOCAL_PROVIDERS[p],
|
|
817
|
+
config: providers[p],
|
|
818
|
+
isActive: activeProviderInfo?.provider === p,
|
|
819
|
+
isLocal: true,
|
|
820
|
+
detected: localDetected[p],
|
|
821
|
+
onEdit: () => setEditingProvider(editingProvider === p ? null : p),
|
|
822
|
+
index: i
|
|
823
|
+
}
|
|
824
|
+
),
|
|
825
|
+
/* @__PURE__ */ jsx(AnimatePresence, { children: editingProvider === p && /* @__PURE__ */ jsx(
|
|
826
|
+
LocalSetupForm,
|
|
827
|
+
{
|
|
828
|
+
provider: p,
|
|
829
|
+
meta: LOCAL_PROVIDERS[p],
|
|
830
|
+
config: providers[p],
|
|
831
|
+
detected: localDetected[p],
|
|
832
|
+
isActive: activeProviderInfo?.provider === p,
|
|
833
|
+
onSave: handleSave,
|
|
834
|
+
onSetActive: handleSetActive,
|
|
835
|
+
onClose: () => setEditingProvider(null)
|
|
836
|
+
}
|
|
837
|
+
) })
|
|
838
|
+
] }, p)) }),
|
|
839
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 pb-2 mb-3", children: [
|
|
840
|
+
/* @__PURE__ */ jsx(WifiIcon, { size: 12, className: "text-purple-400" }),
|
|
841
|
+
/* @__PURE__ */ jsx("span", { className: "font-mono text-[10px] font-medium text-purple-400 uppercase tracking-wider", children: "Custom Provider" }),
|
|
842
|
+
/* @__PURE__ */ jsx("span", { className: "text-[9px] font-mono text-muted-foreground", children: "(OpenAI-Compatible)" })
|
|
843
|
+
] }),
|
|
844
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
|
|
845
|
+
/* @__PURE__ */ jsx(
|
|
846
|
+
ProviderCard,
|
|
847
|
+
{
|
|
848
|
+
id: "custom",
|
|
849
|
+
meta: { name: "Custom", icon: "\u2699", color: "#a78bfa" },
|
|
850
|
+
config: providers.custom,
|
|
851
|
+
isActive: activeProviderInfo?.provider === "custom",
|
|
852
|
+
isLocal: false,
|
|
853
|
+
onEdit: () => setEditingProvider(editingProvider === "custom" ? null : "custom"),
|
|
854
|
+
index: 0
|
|
855
|
+
}
|
|
856
|
+
),
|
|
857
|
+
/* @__PURE__ */ jsx(AnimatePresence, { children: editingProvider === "custom" && /* @__PURE__ */ jsx(
|
|
858
|
+
CustomSetupForm,
|
|
859
|
+
{
|
|
860
|
+
config: providers.custom,
|
|
861
|
+
isActive: activeProviderInfo?.provider === "custom",
|
|
862
|
+
onSave: handleSave,
|
|
863
|
+
onSetActive: handleSetActive,
|
|
864
|
+
onClose: () => setEditingProvider(null)
|
|
865
|
+
}
|
|
866
|
+
) })
|
|
867
|
+
] })
|
|
868
|
+
] });
|
|
869
|
+
}
|
|
870
|
+
export {
|
|
871
|
+
SettingsProvidersPage
|
|
872
|
+
};
|