@harbinger-ai/harbinger 0.1.3 → 0.1.5

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.
@@ -1,65 +1,121 @@
1
1
  "use client";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
- import { useState, useEffect } from "react";
3
+ import { useState, useEffect, useCallback } from "react";
4
4
  import { motion, AnimatePresence } from "framer-motion";
5
- import { CpuIcon, CheckIcon, SpinnerIcon, EyeIcon, EyeOffIcon, XIcon } from "./icons.js";
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";
6
19
  import {
7
20
  getLlmProviders,
8
21
  saveLlmProvider,
9
22
  setActiveProvider,
10
23
  testLlmConnection,
11
- getActiveProvider
24
+ getActiveProvider,
25
+ scanLocalProviders,
26
+ getLocalModels
12
27
  } 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: []
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
+ }
18
58
  };
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" }
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" }
24
66
  };
25
- function ProviderCard({ provider, config, isActive, onEdit, index }) {
26
- const meta = PROVIDER_META[provider];
27
- const hasKey = !!config?.apiKey;
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;
28
80
  return /* @__PURE__ */ jsx(
29
81
  motion.div,
30
82
  {
31
83
  initial: { opacity: 0, y: 8 },
32
84
  animate: { opacity: 1, y: 0 },
33
- transition: { duration: 0.25, delay: index * 0.05 },
85
+ transition: { duration: 0.25, delay: index * 0.04 },
34
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]"}`,
35
87
  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]" }) }),
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 }) }),
43
90
  /* @__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 })
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
+ ] })
46
100
  ] }),
47
101
  /* @__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" }),
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
+ ] }),
50
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" })
51
110
  ] })
52
111
  ] })
53
112
  }
54
113
  );
55
114
  }
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] || "");
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] || "");
61
118
  const [maxTokens, setMaxTokens] = useState(config?.maxTokens || 4096);
62
- const [baseUrl, setBaseUrl] = useState(config?.baseUrl || "");
63
119
  const [showKey, setShowKey] = useState(false);
64
120
  const [saving, setSaving] = useState(false);
65
121
  const [testing, setTesting] = useState(false);
@@ -67,7 +123,11 @@ function ProviderSetupForm({ provider, config, isActive, onSave, onSetActive, on
67
123
  async function handleSave() {
68
124
  setSaving(true);
69
125
  try {
70
- await onSave(provider, { apiKey, model, maxTokens: parseInt(maxTokens), baseUrl: baseUrl || void 0 });
126
+ const saveConfig = { model, maxTokens: parseInt(maxTokens) };
127
+ if (apiKey && !apiKey.includes("***")) {
128
+ saveConfig.apiKey = apiKey;
129
+ }
130
+ await onSave(provider, saveConfig);
71
131
  } finally {
72
132
  setSaving(false);
73
133
  }
@@ -84,10 +144,6 @@ function ProviderSetupForm({ provider, config, isActive, onSave, onSetActive, on
84
144
  setTesting(false);
85
145
  }
86
146
  }
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
147
  return /* @__PURE__ */ jsx(
92
148
  motion.div,
93
149
  {
@@ -96,19 +152,9 @@ function ProviderSetupForm({ provider, config, isActive, onSave, onSetActive, on
96
152
  exit: { opacity: 0, height: 0 },
97
153
  transition: { duration: 0.2 },
98
154
  className: "overflow-hidden",
99
- children: /* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-[--cyan]/20 bg-[--card] p-5 mt-3", children: [
155
+ children: /* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-[--cyan]/20 bg-[--card] p-5 mt-2", children: [
100
156
  /* @__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
- ] }),
157
+ /* @__PURE__ */ jsx(TrafficLights, { label: `${meta.name} Setup` }),
112
158
  /* @__PURE__ */ jsx("button", { onClick: onClose, className: "text-muted-foreground hover:text-foreground transition-colors", children: /* @__PURE__ */ jsx(XIcon, { size: 14 }) })
113
159
  ] }),
114
160
  /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-4", children: [
@@ -122,7 +168,7 @@ function ProviderSetupForm({ provider, config, isActive, onSave, onSetActive, on
122
168
  type: showKey ? "text" : "password",
123
169
  value: apiKey,
124
170
  onChange: (e) => setApiKey(e.target.value),
125
- placeholder: meta.keyPrefix + "...",
171
+ placeholder: config?.hasKey ? "(saved \u2014 enter new key to change)" : `${meta.keyPrefix}...`,
126
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"
127
173
  }
128
174
  ),
@@ -136,38 +182,230 @@ function ProviderSetupForm({ provider, config, isActive, onSave, onSetActive, on
136
182
  }
137
183
  )
138
184
  ] }),
139
- /* @__PURE__ */ jsx(
185
+ /* @__PURE__ */ jsxs(
140
186
  "button",
141
187
  {
142
188
  onClick: handleTest,
143
- disabled: testing || !apiKey,
189
+ disabled: testing || !apiKey && !config?.hasKey,
144
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",
145
- children: testing ? /* @__PURE__ */ jsx(SpinnerIcon, { size: 12 }) : "Test"
191
+ children: [
192
+ testing ? /* @__PURE__ */ jsx(SpinnerIcon, { size: 12 }) : /* @__PURE__ */ jsx(ZapIcon, { size: 12 }),
193
+ "Test"
194
+ ]
146
195
  }
147
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
148
201
  ] })
149
202
  ] }),
150
203
  /* @__PURE__ */ jsxs("div", { children: [
151
204
  /* @__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(
205
+ /* @__PURE__ */ jsx(
153
206
  "select",
154
207
  {
155
208
  value: model,
156
209
  onChange: (e) => setModel(e.target.value),
157
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",
158
- children: models.map((m) => /* @__PURE__ */ jsx("option", { value: m, children: m }, m))
211
+ children: meta.models.map((m) => /* @__PURE__ */ jsx("option", { value: m, children: m }, m))
159
212
  }
160
- ) : /* @__PURE__ */ jsx(
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(
161
338
  "input",
162
339
  {
163
340
  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"
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"
168
345
  }
169
346
  )
170
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
+ ] }),
171
409
  /* @__PURE__ */ jsxs("div", { children: [
172
410
  /* @__PURE__ */ jsx("label", { className: "block text-[10px] font-mono font-medium text-muted-foreground uppercase tracking-wider mb-1.5", children: "Max Tokens" }),
173
411
  /* @__PURE__ */ jsx(
@@ -176,11 +414,111 @@ function ProviderSetupForm({ provider, config, isActive, onSave, onSetActive, on
176
414
  type: "number",
177
415
  value: maxTokens,
178
416
  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"
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"
180
461
  }
181
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)" })
182
517
  ] }),
183
- provider === "custom" && /* @__PURE__ */ jsxs("div", { children: [
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: [
184
522
  /* @__PURE__ */ jsx("label", { className: "block text-[10px] font-mono font-medium text-muted-foreground uppercase tracking-wider mb-1.5", children: "Base URL" }),
185
523
  /* @__PURE__ */ jsx(
186
524
  "input",
@@ -189,39 +527,89 @@ function ProviderSetupForm({ provider, config, isActive, onSave, onSetActive, on
189
527
  value: baseUrl,
190
528
  onChange: (e) => setBaseUrl(e.target.value),
191
529
  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"
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"
193
531
  }
194
532
  )
195
533
  ] }),
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"}` })
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
+ ] })
207
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 }),
208
584
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 pt-2", children: [
209
585
  /* @__PURE__ */ jsxs(
210
586
  "button",
211
587
  {
212
588
  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",
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",
215
591
  children: [
216
592
  saving ? /* @__PURE__ */ jsx(SpinnerIcon, { size: 12 }) : /* @__PURE__ */ jsx(CheckIcon, { size: 12 }),
217
- "Save"
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"
218
606
  ]
219
607
  }
220
608
  ),
221
- !isActive && apiKey && /* @__PURE__ */ jsx(
609
+ !isActive && model && /* @__PURE__ */ jsx(
222
610
  "button",
223
611
  {
224
- onClick: handleSetActive,
612
+ onClick: () => onSetActive("custom", model),
225
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",
226
614
  children: "Set as Active"
227
615
  }
@@ -240,10 +628,28 @@ function ProviderSetupForm({ provider, config, isActive, onSave, onSetActive, on
240
628
  }
241
629
  );
242
630
  }
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 })
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 })
247
653
  ] });
248
654
  }
249
655
  function SettingsProvidersPage() {
@@ -251,7 +657,9 @@ function SettingsProvidersPage() {
251
657
  const [providers, setProviders] = useState({});
252
658
  const [activeProviderInfo, setActiveProviderInfo] = useState(null);
253
659
  const [editingProvider, setEditingProvider] = useState(null);
254
- async function load() {
660
+ const [scanning, setScanning] = useState(false);
661
+ const [localDetected, setLocalDetected] = useState({});
662
+ const load = useCallback(async () => {
255
663
  try {
256
664
  const [provs, active] = await Promise.all([
257
665
  getLlmProviders(),
@@ -262,9 +670,23 @@ function SettingsProvidersPage() {
262
670
  } catch {
263
671
  }
264
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);
265
686
  }
266
687
  useEffect(() => {
267
688
  load();
689
+ handleScan();
268
690
  }, []);
269
691
  async function handleSave(provider, config) {
270
692
  await saveLlmProvider(provider, config);
@@ -279,57 +701,170 @@ function SettingsProvidersPage() {
279
701
  if (loading) {
280
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)) });
281
703
  }
282
- const configuredCount = Object.values(providers).filter((p) => p?.apiKey).length;
283
- const providerList = ["anthropic", "openai", "google", "custom"];
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);
284
708
  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
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
+ ] })
297
740
  ] })
298
741
  ] })
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 })
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" })
305
749
  ] }),
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 })
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
+ ] })
309
758
  ] }),
310
- /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-3", children: providerList.map((p, i) => /* @__PURE__ */ jsxs("div", { children: [
759
+ /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-2 mb-8", children: cloudList.map((p, i) => /* @__PURE__ */ jsxs("div", { children: [
311
760
  /* @__PURE__ */ jsx(
312
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,
313
774
  {
314
775
  provider: p,
776
+ meta: CLOUD_PROVIDERS[p],
315
777
  config: providers[p],
316
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],
317
821
  onEdit: () => setEditingProvider(editingProvider === p ? null : p),
318
822
  index: i
319
823
  }
320
824
  ),
321
825
  /* @__PURE__ */ jsx(AnimatePresence, { children: editingProvider === p && /* @__PURE__ */ jsx(
322
- ProviderSetupForm,
826
+ LocalSetupForm,
323
827
  {
324
828
  provider: p,
829
+ meta: LOCAL_PROVIDERS[p],
325
830
  config: providers[p],
831
+ detected: localDetected[p],
326
832
  isActive: activeProviderInfo?.provider === p,
327
833
  onSave: handleSave,
328
834
  onSetActive: handleSetActive,
329
835
  onClose: () => setEditingProvider(null)
330
836
  }
331
837
  ) })
332
- ] }, p)) })
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
+ ] })
333
868
  ] });
334
869
  }
335
870
  export {