@bhooai/nexus-cli 2.0.7 → 2.0.9

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.
Files changed (51) hide show
  1. package/package.json +1 -1
  2. package/templates/base/apps/admin/package.json.ejs +3 -3
  3. package/templates/base/apps/admin/postcss.config.js +1 -1
  4. package/templates/base/apps/admin/tailwind.config.js +1 -0
  5. package/templates/base/apps/admin/src/App.tsx +0 -38
  6. package/templates/base/apps/admin/src/alertCenter.tsx +0 -151
  7. package/templates/base/apps/admin/src/api.ts +0 -474
  8. package/templates/base/apps/admin/src/assets/bhooai-nexus-logo.svg +0 -25
  9. package/templates/base/apps/admin/src/components/ThemeCentre.tsx +0 -201
  10. package/templates/base/apps/admin/src/components/shell/AppearancePanel.tsx +0 -37
  11. package/templates/base/apps/admin/src/components/shell/Dashboard.tsx +0 -441
  12. package/templates/base/apps/admin/src/components/shell/Login.tsx +0 -69
  13. package/templates/base/apps/admin/src/components/shell/NotifyPanel.tsx +0 -102
  14. package/templates/base/apps/admin/src/components/shell/RailPanel.tsx +0 -133
  15. package/templates/base/apps/admin/src/components/shell/SettingsDialog.tsx +0 -217
  16. package/templates/base/apps/admin/src/components/ui/ConfirmDialog.tsx +0 -38
  17. package/templates/base/apps/admin/src/components/ui/LintChecks.tsx +0 -54
  18. package/templates/base/apps/admin/src/components/ui/PageHead.tsx +0 -13
  19. package/templates/base/apps/admin/src/components/ui/PageHero.tsx +0 -15
  20. package/templates/base/apps/admin/src/icons.tsx +0 -104
  21. package/templates/base/apps/admin/src/index.css +0 -3539
  22. package/templates/base/apps/admin/src/lib/constants.ts +0 -94
  23. package/templates/base/apps/admin/src/lib/theme.ts +0 -80
  24. package/templates/base/apps/admin/src/lib/types.ts +0 -39
  25. package/templates/base/apps/admin/src/lib/utils.ts +0 -147
  26. package/templates/base/apps/admin/src/pages/Config.tsx +0 -108
  27. package/templates/base/apps/admin/src/pages/Databases.tsx +0 -122
  28. package/templates/base/apps/admin/src/pages/Environment.tsx +0 -69
  29. package/templates/base/apps/admin/src/pages/Logs.tsx +0 -270
  30. package/templates/base/apps/admin/src/pages/Monitoring.tsx +0 -88
  31. package/templates/base/apps/admin/src/pages/Overview.tsx +0 -108
  32. package/templates/base/apps/admin/src/pages/Payments.tsx +0 -68
  33. package/templates/base/apps/admin/src/pages/Plugins.tsx +0 -36
  34. package/templates/base/apps/admin/src/pages/Processes.tsx +0 -67
  35. package/templates/base/apps/admin/src/pages/Schema.tsx +0 -130
  36. package/templates/base/apps/admin/src/pages/Users.tsx +0 -144
  37. package/templates/base/apps/admin/src/pages/ai/AddProviderDialog.tsx +0 -150
  38. package/templates/base/apps/admin/src/pages/ai/AiAgents.tsx +0 -31
  39. package/templates/base/apps/admin/src/pages/ai/AiChatPlayground.tsx +0 -208
  40. package/templates/base/apps/admin/src/pages/ai/AiProviders.tsx +0 -276
  41. package/templates/base/apps/admin/src/pages/ai/AiSettings.tsx +0 -84
  42. package/templates/base/apps/admin/src/pages/ai/ProviderCard.tsx +0 -145
  43. package/templates/base/apps/admin/src/pages/ai/ProviderGroup.tsx +0 -50
  44. package/templates/base/apps/admin/src/pages/ai/ProviderKeyDialog.tsx +0 -66
  45. package/templates/base/apps/admin/src/pages/monitoring/RequestSeriesChart.tsx +0 -70
  46. package/templates/base/apps/admin/src/pages/overview/TrafficChart.tsx +0 -32
  47. package/templates/base/apps/admin/src/pages/payments/OrdersTable.tsx +0 -27
  48. package/templates/base/apps/admin/src/pages/payments/PaymentKeysDialog.tsx +0 -74
  49. package/templates/base/apps/admin/src/pages/payments/TestConsole.tsx +0 -173
  50. package/templates/base/apps/admin/src/pages/payments/TransactionsTable.tsx +0 -26
  51. package/templates/base/apps/admin/src/style.css +0 -6517
@@ -1,208 +0,0 @@
1
- import React, { useEffect, useRef, useState } from 'react';
2
- import { Icon } from '../../icons.js';
3
- import {
4
- getAdminConfig, getAiStatus, getAiModels, aiChatStream, getAiProviders,
5
- type AdminConfig, type AiStatus, type AiModelInfo, type AiChatMessage, type AiProviderView,
6
- } from '../../api.js';
7
- import { useAdminAlert } from '../../alertCenter.js';
8
-
9
- export function AiChatPlayground() {
10
- const [config, setConfig] = useState<AdminConfig | null>(null);
11
- const [providers, setProviders] = useState<AiProviderView[]>([]);
12
- const [aiStatus, setAiStatus] = useState<AiStatus | null>(null);
13
- const [models, setModels] = useState<AiModelInfo[]>([]);
14
- const [model, setModel] = useState('');
15
- const [provider, setProvider] = useState('');
16
- const [systemPrompt, setSystemPrompt] = useState('You are a helpful assistant.');
17
- const [input, setInput] = useState('');
18
- const [messages, setMessages] = useState<AiChatMessage[]>([]);
19
- const [streaming, setStreaming] = useState(false);
20
- const [streamText, setStreamText] = useState('');
21
- const [msg, setMsg] = useAdminAlert('ai-playground');
22
- const [modelsLoaded, setModelsLoaded] = useState(false);
23
- const [loadedFor, setLoadedFor] = useState('');
24
- const loadModelsRef = useRef<(p: string) => Promise<void>>(async () => {});
25
- const checkAiRef = useRef<() => Promise<void>>(async () => {});
26
-
27
- useEffect(() => {
28
- getAdminConfig().then((c) => { setConfig(c); const a = c?.config?.ai as { schemaModel?: string; defaultProvider?: string } | undefined; setModel((x) => x || (a?.schemaModel ?? 'llama3:latest')); }).catch(() => {});
29
- getAiProviders().then((r) => {
30
- setProviders(r.providers ?? []);
31
- // Auto-select the first enabled provider, then load its models.
32
- const firstEnabled = (r.providers ?? []).find((p) => p.enabled);
33
- if (firstEnabled) {
34
- setProvider(firstEnabled.id);
35
- setModel(firstEnabled.defaultModel ?? 'llama3:latest');
36
- // Load its models once the provider state lands.
37
- setTimeout(() => { void loadModelsRef.current(firstEnabled.id); }, 0);
38
- } else {
39
- // No enabled provider — fall back to the AI server's own detection.
40
- void checkAiRef.current();
41
- }
42
- }).catch(() => { void checkAiRef.current(); });
43
- }, []);
44
-
45
- const checkAi = async () => {
46
- try {
47
- const s = await getAiStatus();
48
- setAiStatus(s);
49
- // Auto-select ollama if it's the only provider available on the AI server
50
- if (s.ollama?.ok && s.ollama.detail?.models?.length && !provider) {
51
- setProvider('ollama');
52
- setModel(s.ollama.detail.models[0] ?? 'llama3:latest');
53
- }
54
- // Load models for the current (or resolved) provider.
55
- const prov = provider || (s.autoResolvesTo ?? 'ollama');
56
- await loadModels(prov);
57
- } catch { /* non-fatal */ }
58
- };
59
-
60
- const loadModels = async (p: string) => {
61
- setProvider(p);
62
- // Find the provider's default model
63
- const pv = providers.find((x) => x.id === p);
64
- if (pv?.defaultModel || modelsLoaded) setModel((cur) => pv?.defaultModel ?? cur);
65
- try {
66
- const m = await getAiModels(p);
67
- setModels(m?.data ?? []);
68
- setModelsLoaded(true);
69
- setLoadedFor(p);
70
- const err = m?.error;
71
- if (err) setMsg(`Models unavailable for "${p}": ${err}`);
72
- else setMsg('');
73
- } catch (e: any) {
74
- setModels([]);
75
- setModelsLoaded(false);
76
- setLoadedFor('');
77
- setMsg(`Could not load models for "${p}": ${e?.message ?? e}`);
78
- }
79
- };
80
- loadModelsRef.current = loadModels;
81
- checkAiRef.current = () => checkAi();
82
-
83
- const send = async () => {
84
- if (!input.trim() || streaming) return;
85
- // Resolve the actual provider to send to the AI server
86
- const effectiveProvider = provider || aiStatus?.autoResolvesTo || 'ollama';
87
- const effectiveModel = model || models[0]?.id || 'llama3:latest';
88
-
89
- const userMsg: AiChatMessage = { role: 'user', content: input.trim() };
90
- const chatMsgs: AiChatMessage[] = [];
91
- if (systemPrompt.trim()) chatMsgs.push({ role: 'system', content: systemPrompt.trim() });
92
- chatMsgs.push(...messages, userMsg);
93
- setMessages((m) => [...m, userMsg]);
94
- setInput('');
95
- setStreaming(true);
96
- setStreamText('');
97
- setMsg('');
98
- let acc = '';
99
- try {
100
- await aiChatStream(effectiveModel, chatMsgs, effectiveProvider, (chunk) => {
101
- acc += chunk;
102
- setStreamText(acc);
103
- });
104
- if (!acc) { setMsg('AI returned an empty response. Check that the model is available and the AI server is running.'); }
105
- else { setMessages((m) => [...m, { role: 'assistant', content: acc }]); }
106
- setStreamText('');
107
- } catch (e: any) {
108
- const errMsg = String(e?.message ?? e);
109
- setMsg(`Chat failed: ${errMsg}`);
110
- if (acc) setMessages((m) => [...m, { role: 'assistant', content: acc }]);
111
- setStreamText('');
112
- } finally {
113
- setStreaming(false);
114
- }
115
- };
116
-
117
- const clearChat = () => { setMessages([]); setStreamText(''); setMsg(''); };
118
- const sendOnEnter = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); } };
119
-
120
- const modelOptions = models.map((m) => m.id);
121
- const enabledProviders = providers.filter((p) => p.enabled);
122
- const ai = config?.config?.ai as { serverUrl: string; timeoutMs: number; defaultProvider: string; schemaModel: string } | undefined;
123
-
124
- // Build provider options: enabled config providers + any from AI server status
125
- const providerOptions: { id: string; label: string }[] = [];
126
- if (enabledProviders.length) {
127
- enabledProviders.forEach((p) => providerOptions.push({ id: p.id, label: p.label }));
128
- } else {
129
- // Fall back to AI server's known providers
130
- if (aiStatus?.aiServer?.ok) providerOptions.push({ id: 'ollama', label: 'Ollama' });
131
- if (aiStatus?.openai?.ok) providerOptions.push({ id: 'openai', label: 'OpenAI' });
132
- }
133
-
134
- return (
135
- <div className="space-y-4">
136
-
137
- <section className="glass-card space-y-4">
138
- <div className="flex items-center justify-between">
139
- <h3 className="glass-h3">Chat playground</h3>
140
- <div className="flex gap-2">
141
- <button onClick={checkAi} disabled={streaming} className="glass-chip-btn"><Icon name="refresh" size={12} /> refresh models</button>
142
- <button onClick={clearChat} disabled={streaming} className="glass-chip-btn">clear</button>
143
- </div>
144
- </div>
145
-
146
- {/* Controls */}
147
- <div className="flex flex-wrap items-center gap-3">
148
- <label className="text-xs text-slate-400">Provider
149
- <select className="glass-input w-44 !py-1.5 text-sm" value={provider} onChange={(e) => loadModels(e.target.value)}>
150
- {!provider && <option value="" className="bg-slate-900">— select —</option>}
151
- {providerOptions.map((p) => <option key={p.id} value={p.id} className="bg-slate-900">{p.label}</option>)}
152
- </select>
153
- </label>
154
- <label className="text-xs text-slate-400">Model
155
- <input className="glass-input w-52 !py-1.5 font-mono text-sm" value={model} onChange={(e) => setModel(e.target.value)} placeholder="e.g. llama3:latest" />
156
- </label>
157
- <select
158
- className="glass-input w-48 !py-1.5 text-sm"
159
- value=""
160
- onChange={(e) => { const v = e.target.value; if (v) setModel(v); }}
161
- disabled={!modelOptions.length}
162
- title={modelOptions.length ? `Available models for "${loadedFor || provider}"` : 'No models — click refresh models'}
163
- >
164
- <option value="" className="bg-slate-900">{modelOptions.length ? `${modelOptions.length} available for "${loadedFor || provider}"` : 'no models loaded'}</option>
165
- {modelOptions.map((m) => <option key={m} value={m} className="bg-slate-900">{m}</option>)}
166
- </select>
167
- </div>
168
-
169
- {!provider && (
170
- <p className="text-xs text-amber-400"><Icon name="warning" size={12} className="mb-0.5 mr-1 inline" /> Select a provider above. If no providers are listed, go to the AI Providers tab and enable one, or click "refresh models" to detect the AI server's available providers.</p>
171
- )}
172
-
173
- {/* System prompt */}
174
- <div>
175
- <label className="text-xs text-slate-400">System prompt</label>
176
- <textarea className="glass-code mt-1 h-16 w-full p-2 text-sm" value={systemPrompt} onChange={(e) => setSystemPrompt(e.target.value)} placeholder="Set the assistant's behaviour…" />
177
- </div>
178
-
179
- {/* Chat messages */}
180
- <div className="glass-code max-h-96 min-h-[200px] space-y-3 overflow-y-auto p-3 text-sm">
181
- {messages.length === 0 && !streamText && <p className="text-slate-500">Start a conversation by typing below.</p>}
182
- {messages.map((m, i) => (
183
- <div key={i} className={`flex ${m.role === 'user' ? 'justify-end' : 'justify-start'}`}>
184
- <div className={`max-w-[80%] rounded-xl px-3 py-2 ${m.role === 'user' ? 'bg-indigo-500/20 text-slate-100' : m.role === 'assistant' ? 'bg-emerald-500/15 text-slate-100' : 'bg-slate-700/30 text-slate-400'}`}>
185
- <span className="mb-1 block text-[10px] uppercase tracking-wider opacity-60">{m.role}</span>
186
- <span className="whitespace-pre-wrap">{m.content}</span>
187
- </div>
188
- </div>
189
- ))}
190
- {streaming && (
191
- <div className="flex justify-start">
192
- <div className="max-w-[80%] rounded-xl bg-emerald-500/15 px-3 py-2 text-slate-100">
193
- <span className="mb-1 block text-[10px] uppercase tracking-wider opacity-60">assistant <span className="animate-pulse">…</span></span>
194
- <span className="whitespace-pre-wrap">{streamText}<span className="animate-pulse">â–‹</span></span>
195
- </div>
196
- </div>
197
- )}
198
- </div>
199
-
200
- {/* Input */}
201
- <div className="flex gap-2">
202
- <textarea className="glass-code flex-1 h-12 p-2 text-sm" value={input} onChange={(e) => setInput(e.target.value)} onKeyDown={sendOnEnter} placeholder="Type a message… (Enter to send, Shift+Enter for newline)" disabled={streaming} />
203
- <button onClick={send} disabled={streaming || !input.trim()} className="glass-btn-primary shrink-0">{streaming ? '…' : 'Send'}</button>
204
- </div>
205
- </section>
206
- </div>
207
- );
208
- }
@@ -1,276 +0,0 @@
1
- import React, { useEffect, useState } from 'react';
2
- import { Icon } from '../../icons.js';
3
- import { SafeGotoLink } from '@bhooai/nexus-safe-goto';
4
- import {
5
- getAiProviders, updateAiProvider, addAiProvider, deleteAiProvider, testAiProvider,
6
- type AiProviderView, type AiProviderPersistence, type AiProviderTestResult,
7
- } from '../../api.js';
8
- import { useAdminAlert } from '../../alertCenter.js';
9
- import type { ToggleStyle, ProviderFilter } from '../../lib/types.js';
10
- import { BUILTIN_PROVIDER_IDS } from '../../lib/constants.js';
11
- import { ProviderGroup } from './ProviderGroup.js';
12
- import { AddProviderDialog } from './AddProviderDialog.js';
13
- import { ProviderKeyDialog } from './ProviderKeyDialog.js';
14
-
15
- export function AiProviders({ toggle }: { toggle: ToggleStyle }) {
16
- const [providers, setProviders] = useState<AiProviderView[]>([]);
17
- const [busy, setBusy] = useState(false);
18
- const [msg, setMsg] = useAdminAlert('ai-providers');
19
- const [showAdd, setShowAdd] = useState(false);
20
- const [newProvider, setNewProvider] = useState({ id: '', label: '', baseUrl: '', apiKey: '', defaultModel: '' });
21
- const [query, setQuery] = useState('');
22
- const [filter, setFilter] = useState<ProviderFilter>('enabled');
23
- const [testingId, setTestingId] = useState<string | null>(null);
24
- const [testResults, setTestResults] = useState<Record<string, AiProviderTestResult>>({});
25
- const [keyEditorProvider, setKeyEditorProvider] = useState<AiProviderView | null>(null);
26
- const [keyDraft, setKeyDraft] = useState('');
27
- const [switchAnim, setSwitchAnim] = useState<Record<string, 'out' | 'in' | null>>({});
28
-
29
- useEffect(() => { loadProviders(); }, []);
30
-
31
- const wait = (ms: number) => new Promise<void>((resolve) => window.setTimeout(resolve, ms));
32
-
33
- const loadProviders = async () => {
34
- try { const r = await getAiProviders(); setProviders(r.providers ?? []); } catch { /* non-fatal */ }
35
- };
36
-
37
- const persistenceNote = (persistence?: AiProviderPersistence): string => {
38
- if (!persistence) return '';
39
- const missing = [
40
- !persistence.runtime ? 'nexus.runtime.json' : '',
41
- !persistence.database ? 'MongoDB' : '',
42
- ].filter(Boolean);
43
- return missing.length ? ` Persistence failed: ${missing.join(' and ')}.` : '';
44
- };
45
-
46
- const openKeyEditor = (provider: AiProviderView) => {
47
- setKeyEditorProvider(provider);
48
- setKeyDraft('');
49
- };
50
-
51
- const closeKeyEditor = () => {
52
- if (busy) return;
53
- setKeyEditorProvider(null);
54
- setKeyDraft('');
55
- };
56
-
57
- const saveKeyFromEditor = async () => {
58
- if (!keyEditorProvider || !keyDraft.trim()) return;
59
- const saved = await saveApiKey(keyEditorProvider, keyDraft);
60
- if (saved) {
61
- setKeyEditorProvider(null);
62
- setKeyDraft('');
63
- }
64
- };
65
-
66
- const toggleProvider = async (p: AiProviderView) => {
67
- setMsg('');
68
- setBusy(true);
69
- setSwitchAnim((prev) => ({ ...prev, [p.id]: 'out' }));
70
- await wait(280);
71
- try {
72
- const result = await updateAiProvider(p.id, { enabled: !p.enabled });
73
- setProviders((current) => current.map((item) => item.id === p.id ? result.provider : item));
74
- setSwitchAnim((prev) => ({ ...prev, [p.id]: 'in' }));
75
- setMsg(`${p.label} ${result.provider.enabled ? 'enabled' : 'disabled'}.${persistenceNote(result.persistence)}`);
76
- }
77
- catch (e: any) {
78
- setSwitchAnim((prev) => { const next = { ...prev }; delete next[p.id]; return next; });
79
- setMsg(`Failed: ${e?.message ?? e}`);
80
- }
81
- finally {
82
- setBusy(false);
83
- await wait(420);
84
- setSwitchAnim((prev) => { const next = { ...prev }; delete next[p.id]; return next; });
85
- }
86
- };
87
-
88
- const saveApiKey = async (p: AiProviderView, key: string): Promise<boolean> => {
89
- if (!key.trim()) return false;
90
- setBusy(true);
91
- try {
92
- const result = await updateAiProvider(p.id, { apiKey: key.trim() });
93
- // Update only the provider that changed. Refreshing the whole collection
94
- // here can replace the visible 20-provider list with a stale/empty read
95
- // while the backend is persisting the key.
96
- setProviders((current) => current.map((item) => item.id === p.id ? result.provider : item));
97
- setMsg(`${p.label} API key saved.${persistenceNote(result.persistence)}`);
98
- return true;
99
- } catch (e: any) {
100
- setMsg(`Failed: ${e?.message ?? e}`);
101
- return false;
102
- } finally { setBusy(false); }
103
- };
104
-
105
- const saveDefaultModel = async (p: AiProviderView, m: string) => {
106
- setBusy(true);
107
- try {
108
- const result = await updateAiProvider(p.id, { defaultModel: m.trim() });
109
- setProviders((current) => current.map((item) => item.id === p.id ? result.provider : item));
110
- setMsg(`${p.label} model saved.${persistenceNote(result.persistence)}`);
111
- }
112
- catch (e: any) { setMsg(`Failed: ${e?.message ?? e}`); }
113
- finally { setBusy(false); }
114
- };
115
-
116
- const addProvider = async () => {
117
- if (!newProvider.id.trim() || !newProvider.label.trim() || !newProvider.baseUrl.trim()) { setMsg('id, label and baseUrl are required'); return; }
118
- setBusy(true);
119
- try {
120
- const result = await addAiProvider({ id: newProvider.id.trim().toLowerCase(), label: newProvider.label.trim(), baseUrl: newProvider.baseUrl.trim(), apiKey: newProvider.apiKey.trim() || undefined, defaultModel: newProvider.defaultModel.trim() || undefined, enabled: true });
121
- setProviders((current) => [...current, result.provider]);
122
- setShowAdd(false);
123
- setNewProvider({ id: '', label: '', baseUrl: '', apiKey: '', defaultModel: '' });
124
- setMsg(`Provider added.${persistenceNote(result.persistence)}`);
125
- } catch (e: any) { setMsg(`Failed: ${e?.message ?? e}`); }
126
- finally { setBusy(false); }
127
- };
128
-
129
- const removeProvider = async (id: string) => {
130
- setBusy(true);
131
- try {
132
- const result = await deleteAiProvider(id);
133
- setProviders((current) => current.filter((item) => item.id !== id));
134
- setTestResults((prev) => { const next = { ...prev }; delete next[id]; return next; });
135
- setMsg(`Provider removed.${persistenceNote(result.persistence)}`);
136
- }
137
- catch (e: any) { setMsg(`Failed: ${e?.message ?? e}`); }
138
- finally { setBusy(false); }
139
- };
140
-
141
- const testProvider = async (p: AiProviderView) => {
142
- setTestingId(p.id);
143
- setMsg('');
144
- try {
145
- const result = await testAiProvider(p.id);
146
- setTestResults((prev) => ({ ...prev, [p.id]: result }));
147
- if (result.ok) setMsg(`${p.label}: reachable — ${result.modelCount ?? 0} model(s).`);
148
- else setMsg(`${p.label}: ${result.error ?? 'unreachable'}.`);
149
- } catch (e: any) {
150
- setTestResults((prev) => ({ ...prev, [p.id]: { ok: false, provider: p.id, error: String(e?.message ?? e), checkedAt: new Date().toISOString() } }));
151
- setMsg(`${p.label}: ${e?.message ?? e}`);
152
- } finally {
153
- setTestingId(null);
154
- }
155
- };
156
-
157
- // Search + filter logic.
158
- const matchesQuery = (p: AiProviderView) => {
159
- if (!query.trim()) return true;
160
- const q = query.toLowerCase();
161
- return p.id.toLowerCase().includes(q) || p.label.toLowerCase().includes(q);
162
- };
163
-
164
- const enabled = providers.filter((p) => matchesQuery(p) && p.enabled && BUILTIN_PROVIDER_IDS.has(p.id));
165
- const disabled = providers.filter((p) => matchesQuery(p) && !p.enabled && BUILTIN_PROVIDER_IDS.has(p.id));
166
- const custom = providers.filter((p) => matchesQuery(p) && !BUILTIN_PROVIDER_IDS.has(p.id));
167
-
168
- const groups: Array<{ id: string; label: string; icon: string; items: AiProviderView[] }> = [];
169
- if (filter === 'all' || filter === 'enabled') {
170
- if (enabled.length) groups.push({ id: 'enabled', label: 'Enabled', icon: '●', items: enabled });
171
- }
172
- if (filter === 'all' || filter === 'disabled') {
173
- if (disabled.length) groups.push({ id: 'disabled', label: 'Disabled', icon: 'â—‹', items: disabled });
174
- }
175
- if (filter === 'all' || filter === 'custom') {
176
- if (custom.length) groups.push({ id: 'custom', label: 'Custom', icon: '✚', items: custom });
177
- }
178
-
179
- const filterPills: Array<[ProviderFilter, string, number]> = [
180
- ['all', 'All', providers.length],
181
- ['enabled', 'Enabled', enabled.length],
182
- ['disabled', 'Disabled', disabled.length],
183
- ['custom', 'Custom', custom.length],
184
- ];
185
-
186
- return (
187
- <div className="space-y-4">
188
-
189
- {/* Header */}
190
- <section className="glass-card space-y-3">
191
- <div className="flex items-center justify-between">
192
- <h3 className="glass-h3">AI providers ({providers.length})</h3>
193
- <div className="flex gap-2">
194
- <button onClick={loadProviders} disabled={busy} className="glass-chip-btn">refresh</button>
195
- <button onClick={() => setShowAdd(true)} disabled={busy} className="glass-chip-btn">+ add provider</button>
196
- </div>
197
- </div>
198
-
199
- {/* Filter bar */}
200
- <div className="provider-filter-bar">
201
- <div className="relative flex-1 sm:w-64">
202
- <span className="pointer-events-none absolute left-2.5 top-1/2 -translate-y-1/2 text-slate-500"><Icon name="search" size={13} /></span>
203
- <input
204
- className="glass-input !py-1.5 text-sm w-full sm:w-64 pl-8"
205
- placeholder="Search providers…"
206
- type="search"
207
- autoComplete="off"
208
- autoCapitalize="none"
209
- autoCorrect="off"
210
- spellCheck={false}
211
- value={query}
212
- onChange={(e) => setQuery(e.target.value)}
213
- />
214
- </div>
215
- <div className="flex flex-wrap gap-1.5">
216
- {filterPills.map(([id, label, count]) => (
217
- <button
218
- key={id}
219
- onClick={() => setFilter(id)}
220
- className={filter === id ? 'glass-btn-primary !py-1 !px-2.5 text-xs' : 'glass-chip-btn'}
221
- >
222
- {label} <span className="ml-1 opacity-60">{count}</span>
223
- </button>
224
- ))}
225
- </div>
226
- </div>
227
-
228
- {/* Groups */}
229
- {groups.length === 0 && (
230
- <p className="text-xs text-slate-500 py-4 text-center">
231
- {providers.length === 0 ? 'No providers configured.' : 'No providers match your search/filter.'}
232
- </p>
233
- )}
234
- {groups.map((g) => (
235
- <ProviderGroup
236
- key={g.id}
237
- label={g.label}
238
- icon={g.icon}
239
- items={g.items}
240
- toggle={toggle}
241
- switchAnim={switchAnim}
242
- busy={busy}
243
- testingId={testingId}
244
- testResults={testResults}
245
- onToggle={toggleProvider}
246
- onSaveModel={saveDefaultModel}
247
- onRemove={removeProvider}
248
- onTest={testProvider}
249
- onOpenKey={openKeyEditor}
250
- />
251
- ))}
252
- </section>
253
-
254
- {/* Add provider modal */}
255
- {showAdd && (
256
- <AddProviderDialog
257
- value={newProvider}
258
- onChange={setNewProvider}
259
- onSubmit={addProvider}
260
- onCancel={() => setShowAdd(false)}
261
- busy={busy}
262
- />
263
- )}
264
- {keyEditorProvider && (
265
- <ProviderKeyDialog
266
- provider={keyEditorProvider}
267
- value={keyDraft}
268
- onChange={setKeyDraft}
269
- onSave={saveKeyFromEditor}
270
- onCancel={closeKeyEditor}
271
- busy={busy}
272
- />
273
- )}
274
- </div>
275
- );
276
- }
@@ -1,84 +0,0 @@
1
- import React, { useEffect, useState } from 'react';
2
- import { getAdminConfig, getAiStatus, type AdminConfig, type AiStatus } from '../../api.js';
3
- import { useAdminAlert } from '../../alertCenter.js';
4
-
5
- export function AiSettings() {
6
- const [config, setConfig] = useState<AdminConfig | null>(null);
7
- const [aiStatus, setAiStatus] = useState<AiStatus | null>(null);
8
- const [aiErr, setAiErr] = useState('');
9
- const [msg, setMsg] = useAdminAlert('ai-settings');
10
-
11
- useEffect(() => {
12
- getAdminConfig().then((c) => setConfig(c)).catch(() => {});
13
- checkAi();
14
- }, []);
15
-
16
- const checkAi = async () => {
17
- setAiErr('');
18
- try { setAiStatus(await getAiStatus()); }
19
- catch (e: any) { setAiErr(String(e?.message ?? e)); }
20
- };
21
-
22
- const ai = config?.config?.ai as { serverUrl: string; timeoutMs: number; defaultProvider: string; schemaModel: string } | undefined;
23
-
24
- return (
25
- <div className="space-y-4">
26
-
27
- <section className="glass-card space-y-3">
28
- <h3 className="glass-h3">AI configuration</h3>
29
- {ai ? (
30
- <div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
31
- {([
32
- ['Server URL', ai.serverUrl],
33
- ['Timeout', `${(ai.timeoutMs / 1000).toFixed(0)}s`],
34
- ['Default provider', ai.defaultProvider],
35
- ['Schema model', ai.schemaModel],
36
- ] as const).map(([label, value]) => (
37
- <div key={label} className="glass rounded-xl px-3 py-2">
38
- <span className="admin-overline">{label}</span>
39
- <code className="block mt-1 truncate font-mono text-sm text-slate-200">{value}</code>
40
- </div>
41
- ))}
42
- </div>
43
- ) : (
44
- <p className="text-xs text-slate-500">Loading settings…</p>
45
- )}
46
- </section>
47
-
48
- <section className="glass-card space-y-3">
49
- <div className="flex items-center justify-between">
50
- <h3 className="glass-h3">AI server health</h3>
51
- <button onClick={checkAi} className="glass-chip-btn">re-check</button>
52
- </div>
53
- {aiErr && <p className="text-xs text-rose-400">{aiErr}</p>}
54
- <div className="grid grid-cols-1 gap-3 md:grid-cols-3">
55
- {aiStatus ? (
56
- <>
57
- {([
58
- ['AI server', aiStatus.aiServer, aiStatus.aiServer.detail ? `health: ${aiStatus.aiServer.detail.status ?? 'unknown'}` : undefined],
59
- ['Ollama', aiStatus.ollama, aiStatus.ollama.detail?.modelCount != null ? `${aiStatus.ollama.detail.modelCount} model(s)` : undefined],
60
- ] as const).map(([label, s, detail]) => (
61
- <div key={label} className="glass flex items-center gap-2 rounded-xl px-3 py-2 text-sm">
62
- <span className={s.ok ? 'dot-ok' : 'dot-bad'} />
63
- <span className="font-medium text-slate-200">{label}</span>
64
- <span className={`text-xs ${s.ok ? 'text-emerald-400' : 'text-rose-400'}`}>{s.ok ? (detail ?? 'ok') : (s.error ?? 'down')}</span>
65
- </div>
66
- ))}
67
- </>
68
- ) : (
69
- <p className="col-span-3 text-xs text-slate-500">No status yet{aiErr ? '' : ' — run a check'}.</p>
70
- )}
71
- </div>
72
- {aiStatus && <p className="text-xs text-slate-500">"auto" currently resolves to <span className="font-mono text-slate-300">{aiStatus.autoResolvesTo}</span> on the AI server.</p>}
73
- {aiStatus?.aiServer?.detail?.providers && (
74
- <div className="flex flex-wrap gap-2">
75
- <span className="text-xs text-slate-500">Available providers on AI server:</span>
76
- {aiStatus.aiServer.detail.providers.map((p) => (
77
- <span key={p} className="glass-chip text-xs">{p}</span>
78
- ))}
79
- </div>
80
- )}
81
- </section>
82
- </div>
83
- );
84
- }