@benzsiangco/jarvis 1.0.2 → 1.1.0

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 (53) hide show
  1. package/dist/cli.js +478 -347
  2. package/dist/electron/main.js +160 -0
  3. package/dist/electron/preload.js +19 -0
  4. package/package.json +19 -6
  5. package/skills.md +147 -0
  6. package/src/agents/index.ts +248 -0
  7. package/src/brain/loader.ts +136 -0
  8. package/src/cli.ts +411 -0
  9. package/src/config/index.ts +363 -0
  10. package/src/core/executor.ts +222 -0
  11. package/src/core/plugins.ts +148 -0
  12. package/src/core/types.ts +217 -0
  13. package/src/electron/main.ts +192 -0
  14. package/src/electron/preload.ts +25 -0
  15. package/src/electron/types.d.ts +20 -0
  16. package/src/index.ts +12 -0
  17. package/src/providers/antigravity-loader.ts +233 -0
  18. package/src/providers/antigravity.ts +585 -0
  19. package/src/providers/index.ts +523 -0
  20. package/src/sessions/index.ts +194 -0
  21. package/src/tools/index.ts +436 -0
  22. package/src/tui/index.tsx +784 -0
  23. package/src/utils/auth-prompt.ts +394 -0
  24. package/src/utils/index.ts +180 -0
  25. package/src/utils/native-picker.ts +71 -0
  26. package/src/utils/skills.ts +99 -0
  27. package/src/utils/table-integration-examples.ts +617 -0
  28. package/src/utils/table-utils.ts +401 -0
  29. package/src/web/build-ui.ts +27 -0
  30. package/src/web/server.ts +674 -0
  31. package/src/web/ui/dist/.gitkeep +0 -0
  32. package/src/web/ui/dist/main.css +1 -0
  33. package/src/web/ui/dist/main.js +320 -0
  34. package/src/web/ui/dist/main.js.map +20 -0
  35. package/src/web/ui/index.html +46 -0
  36. package/src/web/ui/src/App.tsx +143 -0
  37. package/src/web/ui/src/Modules/Safety/GuardianModal.tsx +83 -0
  38. package/src/web/ui/src/components/Layout/ContextPanel.tsx +243 -0
  39. package/src/web/ui/src/components/Layout/Header.tsx +91 -0
  40. package/src/web/ui/src/components/Layout/ModelSelector.tsx +235 -0
  41. package/src/web/ui/src/components/Layout/SessionStats.tsx +369 -0
  42. package/src/web/ui/src/components/Layout/Sidebar.tsx +895 -0
  43. package/src/web/ui/src/components/Modules/Chat/ChatStage.tsx +620 -0
  44. package/src/web/ui/src/components/Modules/Chat/MessageItem.tsx +446 -0
  45. package/src/web/ui/src/components/Modules/Editor/CommandInspector.tsx +71 -0
  46. package/src/web/ui/src/components/Modules/Editor/DiffViewer.tsx +83 -0
  47. package/src/web/ui/src/components/Modules/Terminal/TabbedTerminal.tsx +202 -0
  48. package/src/web/ui/src/components/Settings/SettingsModal.tsx +935 -0
  49. package/src/web/ui/src/config/models.ts +70 -0
  50. package/src/web/ui/src/main.tsx +13 -0
  51. package/src/web/ui/src/store/agentStore.ts +41 -0
  52. package/src/web/ui/src/store/uiStore.ts +64 -0
  53. package/src/web/ui/src/types/index.ts +54 -0
@@ -0,0 +1,935 @@
1
+ import React, { useState, useEffect, useCallback } from 'react';
2
+ import {
3
+ X, Shield, Zap, Globe, Cpu, User, Check,
4
+ RefreshCw, Save, Terminal as LucideTerminal, FileText, Bot, Code, Settings, Plus, Trash2
5
+ } from 'lucide-react';
6
+ import { motion, AnimatePresence } from 'framer-motion';
7
+
8
+ interface SettingsModalProps {
9
+ onClose: () => void;
10
+ }
11
+
12
+ const cn = (...classes: any[]) => classes.filter(Boolean).join(' ');
13
+
14
+ import { useAgentStore, type Persona } from '../../store/agentStore';
15
+ import { useUIStore } from '../../store/uiStore';
16
+
17
+ const personas = [
18
+ { id: 'it-expert', name: 'IT Expert', icon: Cpu, description: 'Deep technical knowledge, professional architecture focus.' },
19
+ { id: 'hacker', name: 'Hacker', icon: Shield, description: 'Efficiency, automation, and security-centric perspective.' },
20
+ { id: 'designer', name: 'Graphic Designer', icon: FileText, description: 'Visual aesthetic, user experience, and creative flow.' },
21
+ { id: 'minimalist', name: 'Minimalist', icon: User, description: 'Concise, helpful, and standard professional tone.' },
22
+ ];
23
+
24
+ export function SettingsModal({ onClose }: SettingsModalProps) {
25
+ const { persona, setPersona } = useAgentStore();
26
+ const [activeTab, setActiveTab] = useState<'brain' | 'general' | 'intelligence' | 'security' | 'accounts' | 'personality' | 'config' | 'tools'>('personality');
27
+ const [systemPrompt, setSystemPrompt] = useState('');
28
+ const [skillsContent, setSkillsContent] = useState('');
29
+ const [brainContent, setBrainContent] = useState('');
30
+ const [selectedPersona, setSelectedPersona] = useState<Persona>(persona);
31
+ const [isSaving, setIsSaving] = useState(false);
32
+ const [authProviders, setAuthProviders] = useState<any[]>([]);
33
+ const [accounts, setAccounts] = useState<any[]>([]);
34
+ const [workdir, setWorkdir] = useState('');
35
+ const [activeKeySetup, setActiveKeySetup] = useState<string | null>(null);
36
+ const [activeModeSelection, setActiveModeSelection] = useState<string | null>(null);
37
+ const [accountToDelete, setAccountToDelete] = useState<string | null>(null);
38
+ const [tempApiKey, setTempApiKey] = useState('');
39
+ const [jarvisConfig, setJarvisConfig] = useState<any>(null);
40
+ const [editingConfig, setEditingConfig] = useState(false);
41
+ const [configJson, setConfigJson] = useState('');
42
+ const [oauthConfig, setOauthConfig] = useState<any>(null);
43
+ const [editingOauth, setEditingOauth] = useState(false);
44
+ const [tempCallbackUrl, setTempCallbackUrl] = useState('');
45
+
46
+ const refreshAuthStatus = async () => {
47
+ try {
48
+ const res = await fetch('/api/auth/status');
49
+ const data = await res.json();
50
+ setAuthProviders(data.providers || []);
51
+ setAccounts(data.accounts || []);
52
+ } catch (e) { console.error(e); }
53
+ };
54
+
55
+ useEffect(() => {
56
+ fetch('/api/config').then(r => r.json()).then(data => {
57
+ setSystemPrompt(data.instructions || '');
58
+ setWorkdir(data.workdir || '');
59
+ setJarvisConfig(data);
60
+ setConfigJson(JSON.stringify(data, null, 2));
61
+ });
62
+ fetch('/api/skills').then(r => r.json()).then(data => setSkillsContent(data.content || ''));
63
+ fetch('/api/brain').then(r => r.json()).then(data => setBrainContent(data.content || ''));
64
+ fetch('/api/oauth/config').then(r => r.json()).then(data => {
65
+ setOauthConfig(data);
66
+ setTempCallbackUrl(data.callbackUrl || '');
67
+ });
68
+ refreshAuthStatus();
69
+ }, []);
70
+
71
+ const handleSetupKey = async (providerId: string) => {
72
+ // If provider supports both (like google/antigravity or openai), show mode selection
73
+ if (providerId === 'google' || providerId === 'openai') {
74
+ setActiveModeSelection(providerId);
75
+ return;
76
+ }
77
+
78
+ setActiveKeySetup(providerId);
79
+ setTempApiKey('');
80
+ };
81
+
82
+ const handleLoginFlow = async (providerId: string) => {
83
+ try {
84
+ const res = await fetch('/api/auth/antigravity/login', { method: 'POST' });
85
+ const data = await res.json();
86
+
87
+ if (data.url) {
88
+ // Open the auth URL in a new tab
89
+ window.open(data.url, '_blank');
90
+ } else {
91
+ console.error('Failed to initiate login flow');
92
+ }
93
+
94
+ setActiveModeSelection(null);
95
+ } catch (e) { console.error(e); }
96
+ };
97
+
98
+ const handleManualKeyFlow = (providerId: string) => {
99
+ setActiveModeSelection(null);
100
+ setActiveKeySetup(providerId);
101
+ setTempApiKey('');
102
+ };
103
+
104
+ const handleSaveKey = async () => {
105
+ if (!activeKeySetup || !tempApiKey) return;
106
+
107
+ try {
108
+ await fetch('/api/auth/key', {
109
+ method: 'POST',
110
+ headers: { 'Content-Type': 'application/json' },
111
+ body: JSON.stringify({ providerId: activeKeySetup, apiKey: tempApiKey })
112
+ });
113
+ refreshAuthStatus();
114
+ setActiveKeySetup(null);
115
+ setTempApiKey('');
116
+ } catch (e) { console.error(e); }
117
+ };
118
+
119
+ const handleDeleteAccount = async (id: string) => {
120
+ setAccountToDelete(id);
121
+ };
122
+
123
+ const confirmDeleteAccount = async () => {
124
+ if (!accountToDelete) return;
125
+ try {
126
+ await fetch(`/api/auth/account/${accountToDelete}`, { method: 'DELETE' });
127
+ refreshAuthStatus();
128
+ setAccountToDelete(null);
129
+ } catch (e) { console.error(e); }
130
+ };
131
+
132
+ const handleSaveConfig = async () => {
133
+ try {
134
+ const parsedConfig = JSON.parse(configJson);
135
+ await fetch('/api/config/full', {
136
+ method: 'POST',
137
+ headers: { 'Content-Type': 'application/json' },
138
+ body: JSON.stringify(parsedConfig)
139
+ });
140
+ setJarvisConfig(parsedConfig);
141
+ setEditingConfig(false);
142
+ } catch (e) {
143
+ console.error('Failed to save config:', e);
144
+ }
145
+ };
146
+
147
+ const handleSave = async () => {
148
+ setIsSaving(true);
149
+ try {
150
+ setPersona(selectedPersona);
151
+
152
+ // Save brain
153
+ await fetch('/api/brain', {
154
+ method: 'POST',
155
+ headers: { 'Content-Type': 'application/json' },
156
+ body: JSON.stringify({ content: brainContent })
157
+ });
158
+
159
+ await fetch('/api/config/full', {
160
+ method: 'POST',
161
+ headers: { 'Content-Type': 'application/json' },
162
+ body: JSON.stringify({
163
+ instructions: systemPrompt,
164
+ persona: selectedPersona,
165
+ })
166
+ });
167
+
168
+ await fetch('/api/skills', {
169
+ method: 'POST',
170
+ headers: { 'Content-Type': 'application/json' },
171
+ body: JSON.stringify({ content: skillsContent })
172
+ });
173
+
174
+ setIsSaving(false);
175
+ onClose();
176
+ } catch (e) {
177
+ console.error(e);
178
+ setIsSaving(false);
179
+ }
180
+ };
181
+
182
+ return (
183
+ <motion.div
184
+ initial={{ opacity: 0 }}
185
+ animate={{ opacity: 1 }}
186
+ exit={{ opacity: 0 }}
187
+ className="fixed inset-0 z-[100] flex items-center justify-center p-0 md:p-8 bg-black/80 backdrop-blur-sm"
188
+ >
189
+ <motion.div
190
+ initial={{ scale: 0.95, opacity: 0, y: 20 }}
191
+ animate={{ scale: 1, opacity: 1, y: 0 }}
192
+ exit={{ scale: 0.95, opacity: 0, y: 20 }}
193
+ className="w-full max-w-5xl h-full md:h-auto md:max-h-[85vh] bg-zinc-950 border-0 md:border border-zinc-800 rounded-none md:rounded-3xl shadow-2xl flex flex-col overflow-hidden ring-0 md:ring-1 ring-white/5"
194
+ >
195
+ <div className="h-16 border-b border-zinc-900 flex items-center justify-between px-6 md:px-8 shrink-0 bg-zinc-900/30">
196
+ <div className="flex items-center gap-4">
197
+ <div className="w-8 h-8 rounded bg-zinc-900 border border-zinc-800 flex items-center justify-center text-zinc-500">
198
+ <Settings size={18} />
199
+ </div>
200
+ <h2 className="text-lg font-black uppercase tracking-tighter italic text-white">System Configuration</h2>
201
+ </div>
202
+ <button onClick={onClose} className="p-2 hover:bg-zinc-800 rounded-xl text-zinc-500 hover:text-white transition-all">
203
+ <X size={20} />
204
+ </button>
205
+ </div>
206
+
207
+ <div className="flex-1 flex flex-col md:flex-row min-h-0">
208
+ {/* Sidebar / Tabs */}
209
+ <div className="w-full md:w-64 border-b md:border-b-0 md:border-r border-zinc-900 p-2 md:p-4 shrink-0 bg-zinc-900/10 overflow-x-auto md:overflow-visible">
210
+ <div className="flex flex-row md:flex-col gap-1 md:gap-1 min-w-max md:min-w-0">
211
+ { [
212
+ { id: 'brain', label: 'Brain', icon: Cpu },
213
+ { id: 'intelligence', label: 'Intelligence', icon: Bot },
214
+ { id: 'personality', label: 'Persona', icon: User },
215
+ { id: 'general', label: 'Environment', icon: Globe },
216
+ { id: 'security', label: 'Safety', icon: Shield },
217
+ { id: 'accounts', label: 'Accounts', icon: User },
218
+ { id: 'tools', label: 'Tools', icon: Code },
219
+ { id: 'config', label: 'Config', icon: FileText },
220
+ ].map(tab => (
221
+ <button
222
+ key={tab.id}
223
+ onClick={() => setActiveTab(tab.id as any)}
224
+ className={`flex items-center gap-3 px-4 py-2.5 md:p-3 rounded-xl text-sm font-bold transition-all whitespace-nowrap ${
225
+ activeTab === tab.id ? 'bg-zinc-900 text-white shadow-sm ring-1 ring-white/5' : 'text-zinc-500 hover:bg-zinc-900/50 hover:text-zinc-300'
226
+ }`}
227
+ >
228
+ <tab.icon size={16} />
229
+ <span>{tab.label}</span>
230
+ </button>
231
+ ))}
232
+ </div>
233
+ </div>
234
+
235
+ {/* Main Content Area */}
236
+ <div className="flex-1 overflow-y-auto custom-scrollbar p-4 md:p-10 bg-zinc-950">
237
+
238
+ {activeTab === 'brain' && (
239
+ <div className="space-y-8 animate-in fade-in slide-in-from-right-4 duration-500 pb-10">
240
+ <div className="flex items-center justify-between border-b border-zinc-900 pb-6">
241
+ <div>
242
+ <h3 className="text-xl font-black text-white italic tracking-tighter">JARVIS Core Brain</h3>
243
+ <p className="text-xs text-zinc-600 font-medium mt-1">
244
+ Define JARVIS's fundamental identity, personality, and capabilities.
245
+ </p>
246
+ </div>
247
+ </div>
248
+
249
+ <div className="space-y-4">
250
+ <label className="text-[10px] font-black uppercase tracking-widest text-zinc-500 flex items-center gap-2">
251
+ <FileText size={12} className="text-cyan-500" />
252
+ JARVIS.md - Core Identity
253
+ </label>
254
+ <p className="text-xs text-zinc-600 font-medium opacity-60">
255
+ This is JARVIS's primary knowledge base. Everything here becomes part of the system prompt.
256
+ Use markdown for formatting. Changes apply to all new conversations.
257
+ </p>
258
+
259
+ <textarea
260
+ value={brainContent}
261
+ onChange={(e) => setBrainContent(e.target.value)}
262
+ placeholder="# JARVIS - Just A Rather Very Intelligent System
263
+
264
+ You are JARVIS, an advanced AI coding assistant...
265
+
266
+ ## Core Principles
267
+ - Be precise and thorough
268
+ - Explain your reasoning
269
+ - Prioritize security
270
+
271
+ ## Capabilities
272
+ - Full-stack development
273
+ - DevOps automation
274
+ - Security analysis"
275
+ className="w-full h-[500px] bg-zinc-950 border border-zinc-800 rounded-2xl p-5 text-sm font-mono text-zinc-300 focus:outline-none focus:border-cyan-500/30 transition-all shadow-inner custom-scrollbar"
276
+ spellCheck={false}
277
+ />
278
+
279
+ <div className="flex items-center gap-3 p-4 bg-cyan-950/10 border border-cyan-900/30 rounded-2xl">
280
+ <div className="w-8 h-8 rounded-lg bg-cyan-900/30 flex items-center justify-center text-cyan-500">
281
+ <Zap size={16} />
282
+ </div>
283
+ <div className="flex-1">
284
+ <p className="text-xs font-bold text-cyan-400">Live System Prompt</p>
285
+ <p className="text-[10px] text-zinc-500 mt-0.5">
286
+ Changes here will be included in every AI conversation. Combine with Skills for modular knowledge.
287
+ </p>
288
+ </div>
289
+ </div>
290
+ </div>
291
+ </div>
292
+ )}
293
+
294
+ {activeTab === 'intelligence' && (
295
+ <div className="space-y-12 animate-in fade-in slide-in-from-right-4 duration-500">
296
+ <div className="space-y-4">
297
+ <label className="text-[10px] font-black uppercase tracking-widest text-zinc-500 flex items-center gap-2">
298
+ <Zap size={12} className="text-cyan-500" />
299
+ Master System Prompt
300
+ </label>
301
+ <p className="text-xs text-zinc-600 font-medium opacity-60">The core identity and instructions that guide JARVIS's behavior.</p>
302
+ <textarea
303
+ value={systemPrompt}
304
+ onChange={(e) => setSystemPrompt(e.target.value)}
305
+ className="w-full h-48 bg-zinc-900 border border-zinc-800 rounded-2xl p-5 text-sm font-mono text-zinc-300 focus:outline-none focus:border-cyan-500/30 transition-all shadow-inner custom-scrollbar"
306
+ />
307
+ </div>
308
+
309
+ <div className="space-y-4 pb-10">
310
+ <label className="text-[10px] font-black uppercase tracking-widest text-zinc-500 flex items-center gap-2">
311
+ <Code size={12} className="text-cyan-500" />
312
+ Skills Registry (skills.md)
313
+ </label>
314
+ <p className="text-xs text-zinc-600 font-medium opacity-60">Define modular capabilities and instructions the agent can use.</p>
315
+ <textarea
316
+ value={skillsContent}
317
+ onChange={(e) => setSkillsContent(e.target.value)}
318
+ className="w-full h-96 bg-zinc-950 border border-zinc-800 border-dashed rounded-2xl p-5 text-xs font-mono text-zinc-400 focus:outline-none focus:border-cyan-500/30 transition-all custom-scrollbar"
319
+ />
320
+ </div>
321
+ </div>
322
+ )}
323
+
324
+ {activeTab === 'personality' && (
325
+ <div className="space-y-8 animate-in fade-in slide-in-from-right-4 duration-500">
326
+ <p className="text-[10px] font-black uppercase tracking-widest text-zinc-500">Select Core Persona</p>
327
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
328
+ {personas.map(p => (
329
+ <button
330
+ key={p.id}
331
+ onClick={() => setSelectedPersona(p.id as Persona)}
332
+ className={`p-6 rounded-2xl border text-left transition-all ${
333
+ selectedPersona === p.id ? 'bg-zinc-900 border-cyan-500/30 shadow-lg shadow-cyan-950/20' : 'bg-zinc-900/30 border-zinc-900 hover:border-zinc-800'
334
+ }`}
335
+ >
336
+ <p className={`text-xs font-black uppercase mb-1 ${selectedPersona === p.id ? 'text-cyan-500' : 'text-zinc-500'}`}>{p.name}</p>
337
+ <p className="text-xs text-zinc-400 font-medium opacity-70 italic">{p.description}</p>
338
+ </button>
339
+ ))}
340
+ </div>
341
+ </div>
342
+ )}
343
+
344
+ {activeTab === 'security' && (
345
+ <div className="space-y-10 animate-in fade-in slide-in-from-right-4 duration-500 pb-10">
346
+ <div className="flex items-center justify-between border-b border-zinc-900 pb-6">
347
+ <h3 className="text-xl font-black text-white italic tracking-tighter">Guardian Safety Protocols</h3>
348
+ </div>
349
+
350
+ <div className="space-y-4">
351
+ <label className="text-[10px] font-black uppercase tracking-widest text-zinc-500 flex items-center gap-2">
352
+ Restricted Filesystem Paths
353
+ </label>
354
+ <p className="text-xs text-zinc-600 font-medium opacity-60 italic">Jarvis is strictly forbidden to read or write in these directories.</p>
355
+ <div className="space-y-2">
356
+ {['/etc', '/windows/system32', '.env', 'credentials.json'].map(path => (
357
+ <div key={path} className="flex items-center justify-between p-4 bg-zinc-900/30 border border-zinc-900 rounded-2xl group hover:border-red-950/50 transition-all">
358
+ <span className="text-xs font-mono text-zinc-400">{path}</span>
359
+ <button className="text-[10px] font-black uppercase text-zinc-700 hover:text-red-500 transition-all opacity-0 group-hover:opacity-100">Remove</button>
360
+ </div>
361
+ ))}
362
+ <button className="w-full py-4 border border-zinc-900 border-dashed rounded-2xl text-[10px] font-black uppercase tracking-widest text-zinc-700 hover:text-zinc-500 hover:border-zinc-800 transition-all flex items-center justify-center gap-2">
363
+ <Plus size={14} /> Add Restricted Path
364
+ </button>
365
+ </div>
366
+ </div>
367
+
368
+ <div className="space-y-4">
369
+ <label className="text-[10px] font-black uppercase tracking-widest text-zinc-500">Tool Permission Matrix</label>
370
+ <div className="grid grid-cols-1 gap-2">
371
+ {[
372
+ { name: 'bash', risk: 'high', mode: 'ask' },
373
+ { name: 'edit', risk: 'high', mode: 'ask' },
374
+ { name: 'read', risk: 'low', mode: 'allow' },
375
+ { name: 'grep', risk: 'low', mode: 'allow' },
376
+ ].map(tool => (
377
+ <div key={tool.name} className="flex items-center justify-between p-4 bg-zinc-900/30 border border-zinc-900 rounded-2xl">
378
+ <div className="flex items-center gap-3">
379
+ <div className={`w-2 h-2 rounded-full ${tool.risk === 'high' ? 'bg-red-500 animate-pulse' : 'bg-emerald-500'}`} />
380
+ <span className="text-xs font-bold text-zinc-200 font-mono italic">{tool.name}</span>
381
+ </div>
382
+ <select className="bg-zinc-950 border border-zinc-800 rounded-lg px-3 py-1.5 text-[10px] font-black uppercase text-zinc-400 focus:outline-none focus:border-cyan-500/50">
383
+ <option value="allow" selected={tool.mode === 'allow'}>Allow</option>
384
+ <option value="ask" selected={tool.mode === 'ask'}>Always Ask</option>
385
+ <option value="deny" selected={tool.mode === 'deny'}>Deny</option>
386
+ </select>
387
+ </div>
388
+ ))}
389
+ </div>
390
+ </div>
391
+ </div>
392
+ )}
393
+
394
+ {activeTab === 'accounts' && (
395
+ <div className="space-y-8 animate-in fade-in slide-in-from-right-4 duration-500 pb-10">
396
+ <div className="flex items-center justify-between border-b border-zinc-900 pb-6">
397
+ <h3 className="text-xl font-black text-white italic tracking-tighter">Connected Accounts Pool</h3>
398
+ <button
399
+ onClick={() => handleSetupKey('google')}
400
+ className="flex items-center gap-2 px-6 py-2 bg-white text-black rounded-xl text-[10px] font-black uppercase tracking-widest hover:bg-zinc-200 transition-all"
401
+ >
402
+ <Plus size={14} /> Link Google
403
+ </button>
404
+ </div>
405
+
406
+ {/* Active Accounts List */}
407
+ <div className="space-y-3">
408
+ {accounts.length > 0 ? accounts.map((acc: any) => (
409
+ <div key={acc.account.id} className="flex items-center justify-between p-5 bg-zinc-900/50 border border-zinc-800 rounded-[1.5rem] group hover:border-zinc-700 transition-all">
410
+ <div className="flex items-center gap-4">
411
+ <div className="w-10 h-10 rounded-xl bg-purple-600/10 flex items-center justify-center text-purple-500">
412
+ <User size={20} />
413
+ </div>
414
+ <div>
415
+ <p className="font-bold text-zinc-200">{acc.account.email || acc.account.id}</p>
416
+ <div className="flex items-center gap-2 mt-0.5">
417
+ <span className={`text-[8px] font-black uppercase px-1.5 py-0.5 rounded border ${acc.status === 'available' ? 'bg-emerald-500/10 text-emerald-500 border-emerald-500/20' : 'bg-red-500/10 text-red-500 border-red-500/20'}`}>
418
+ {acc.status}
419
+ </span>
420
+ <span className="text-[9px] text-zinc-600 font-medium italic">Provider: {acc.account.quotaType || 'antigravity'}</span>
421
+ </div>
422
+ </div>
423
+ </div>
424
+ <button
425
+ onClick={() => handleDeleteAccount(acc.account.id)}
426
+ className="p-3 text-zinc-700 hover:text-red-500 hover:bg-red-500/10 rounded-xl transition-all opacity-0 group-hover:opacity-100"
427
+ >
428
+ <Trash2 size={16} />
429
+ </button>
430
+ </div>
431
+ )) : (
432
+ <div className="p-10 text-center border border-dashed border-zinc-900 rounded-[2rem]">
433
+ <p className="text-xs text-zinc-600 italic">No active accounts in the pool...</p>
434
+ </div>
435
+ )}
436
+ </div>
437
+
438
+ <div className="pt-8 border-t border-zinc-900">
439
+ <h4 className="text-[10px] font-black uppercase tracking-widest text-zinc-500 mb-6">Available AI Providers</h4>
440
+ <div className="grid grid-cols-1 gap-4">
441
+ {authProviders.map((p: any) => (
442
+ <div key={p.id} className="group relative">
443
+ <div className="flex items-center justify-between p-6 bg-zinc-900/30 border border-zinc-900 rounded-[2rem] group-hover:border-zinc-700 transition-all">
444
+ <div className="flex items-center gap-5">
445
+ <div className={`w-12 h-12 rounded-2xl flex items-center justify-center shadow-inner ${p.isAvailable ? 'bg-cyan-600/10 text-cyan-500 ring-1 ring-cyan-500/20' : 'bg-zinc-800 text-zinc-600'}`}>
446
+ <Bot size={24} />
447
+ </div>
448
+ <div>
449
+ <div className="flex items-center gap-2">
450
+ <p className="font-bold text-zinc-200">{p.name}</p>
451
+ {p.isAvailable && <span className="w-1.5 h-1.5 rounded-full bg-emerald-500 shadow-[0_0_8px_rgba(16,185,129,0.5)]" />}
452
+ </div>
453
+ <div className="flex flex-col mt-1">
454
+ <p className="text-[10px] font-mono text-zinc-600 uppercase tracking-widest">{p.envVar}</p>
455
+ {p.maskedKey && (
456
+ <p className="text-[10px] font-mono text-emerald-500/70 mt-0.5 flex items-center gap-1">
457
+ <span className="w-1 h-1 rounded-full bg-emerald-500/50"></span>
458
+ {p.maskedKey}
459
+ </p>
460
+ )}
461
+ </div>
462
+ </div>
463
+ </div>
464
+
465
+ <div className="flex items-center gap-4">
466
+ <div className="text-right mr-4 hidden md:block">
467
+ <p className="text-[10px] font-black uppercase text-zinc-700">Usage Status</p>
468
+ <p className="text-xs font-mono text-zinc-400">Stable</p>
469
+ </div>
470
+ <button
471
+ onClick={() => handleSetupKey(p.id)}
472
+ className={cn(
473
+ "px-8 py-3 rounded-2xl text-[10px] font-black uppercase tracking-widest transition-all",
474
+ p.isAvailable ? "bg-zinc-800 text-zinc-300 hover:bg-zinc-700" : "bg-cyan-600 text-white hover:bg-cyan-500"
475
+ )}
476
+ >
477
+ {p.isAvailable ? 'Update Key' : 'Setup Key'}
478
+ </button>
479
+ </div>
480
+ </div>
481
+ </div>
482
+ ))}
483
+ </div>
484
+ </div>
485
+
486
+ <div className="p-8 rounded-[2.5rem] bg-zinc-900/50 border border-zinc-800 border-dashed">
487
+ <div className="flex flex-col items-center text-center space-y-4">
488
+ <div className="w-12 h-12 rounded-full bg-zinc-950 flex items-center justify-center text-zinc-700">
489
+ <RefreshCw size={24} />
490
+ </div>
491
+ <div>
492
+ <p className="text-sm font-bold text-zinc-400">Rate-Limit Guardian Active</p>
493
+ <p className="text-xs text-zinc-600 max-w-xs mt-1 italic">Jarvis will automatically cycle accounts within the pool if a provider hits a rate limit.</p>
494
+ </div>
495
+ </div>
496
+ </div>
497
+ </div>
498
+ )}
499
+
500
+ {activeTab === 'config' && (
501
+ <div className="space-y-8 animate-in fade-in slide-in-from-right-4 duration-500 pb-10">
502
+ <div className="flex items-center justify-between border-b border-zinc-900 pb-6">
503
+ <h3 className="text-xl font-black text-white italic tracking-tighter">JARVIS Configuration</h3>
504
+ {!editingConfig && (
505
+ <button
506
+ onClick={() => setEditingConfig(true)}
507
+ className="flex items-center gap-2 px-6 py-2 bg-cyan-600 text-white rounded-xl text-[10px] font-black uppercase tracking-widest hover:bg-cyan-500 transition-all"
508
+ >
509
+ Edit Config
510
+ </button>
511
+ )}
512
+ </div>
513
+
514
+ <div className="space-y-4">
515
+ <label className="text-[10px] font-black uppercase tracking-widest text-zinc-500 flex items-center gap-2">
516
+ <FileText size={12} className="text-cyan-500" />
517
+ Configuration JSON
518
+ </label>
519
+ <p className="text-xs text-zinc-600 font-medium opacity-60">
520
+ View and edit the complete JARVIS configuration. Be careful when modifying this directly.
521
+ </p>
522
+
523
+ {editingConfig ? (
524
+ <div className="space-y-4">
525
+ <textarea
526
+ value={configJson}
527
+ onChange={(e) => setConfigJson(e.target.value)}
528
+ className="w-full h-96 bg-zinc-950 border border-zinc-800 border-dashed rounded-2xl p-5 text-xs font-mono text-zinc-400 focus:outline-none focus:border-cyan-500/30 transition-all custom-scrollbar"
529
+ spellCheck={false}
530
+ />
531
+ <div className="flex items-center gap-3">
532
+ <button
533
+ onClick={() => {
534
+ setConfigJson(JSON.stringify(jarvisConfig, null, 2));
535
+ setEditingConfig(false);
536
+ }}
537
+ className="px-6 py-2 bg-zinc-900 text-zinc-500 rounded-xl text-[10px] font-black uppercase tracking-widest hover:bg-zinc-800 hover:text-zinc-300 transition-all"
538
+ >
539
+ Cancel
540
+ </button>
541
+ <button
542
+ onClick={handleSaveConfig}
543
+ className="px-6 py-2 bg-cyan-600 text-white rounded-xl text-[10px] font-black uppercase tracking-widest hover:bg-cyan-500 transition-all"
544
+ >
545
+ Save Config
546
+ </button>
547
+ </div>
548
+ </div>
549
+ ) : (
550
+ <pre className="w-full h-96 bg-zinc-950 border border-zinc-800 rounded-2xl p-5 text-xs font-mono text-zinc-400 overflow-auto custom-scrollbar">
551
+ {JSON.stringify(jarvisConfig, null, 2)}
552
+ </pre>
553
+ )}
554
+ </div>
555
+ </div>
556
+ )}
557
+
558
+ {activeTab === 'tools' && (
559
+ <div className="space-y-8 animate-in fade-in slide-in-from-right-4 duration-500 pb-10">
560
+ <div className="flex items-center justify-between border-b border-zinc-900 pb-6">
561
+ <div>
562
+ <h3 className="text-xl font-black text-white italic tracking-tighter">Extension Tools</h3>
563
+ <p className="text-xs text-zinc-600 font-medium mt-1">
564
+ Manage custom tools and extensions that JARVIS can use
565
+ </p>
566
+ </div>
567
+ <button className="flex items-center gap-2 px-6 py-2 bg-zinc-900 text-zinc-600 rounded-xl text-[10px] font-black uppercase tracking-widest opacity-50 cursor-not-allowed">
568
+ <Plus size={14} />
569
+ Install Tool
570
+ </button>
571
+ </div>
572
+
573
+ {/* Built-in Tools */}
574
+ <div className="space-y-4">
575
+ <h4 className="text-[10px] font-black uppercase tracking-widest text-zinc-500">
576
+ Built-in Tools (12)
577
+ </h4>
578
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
579
+ {['bash', 'read', 'write', 'edit', 'glob', 'grep', 'list', 'webfetch', 'task', 'question', 'todoread', 'todowrite'].map(tool => (
580
+ <div key={tool} className="flex items-center justify-between p-4 bg-zinc-900/30 border border-zinc-800 rounded-2xl">
581
+ <div className="flex items-center gap-3">
582
+ <div className="w-8 h-8 rounded-lg bg-zinc-950 flex items-center justify-center text-zinc-500">
583
+ <LucideTerminal size={16} />
584
+ </div>
585
+ <div>
586
+ <p className="text-sm font-bold text-zinc-200 font-mono">{tool}</p>
587
+ <p className="text-[10px] text-zinc-600">Core tool</p>
588
+ </div>
589
+ </div>
590
+ <span className="w-2 h-2 rounded-full bg-emerald-500" />
591
+ </div>
592
+ ))}
593
+ </div>
594
+ </div>
595
+
596
+ {/* Extension Tools (Coming Soon) */}
597
+ <div className="space-y-4">
598
+ <h4 className="text-[10px] font-black uppercase tracking-widest text-zinc-500">
599
+ Extension Tools
600
+ </h4>
601
+ <div className="p-10 text-center border border-dashed border-zinc-900 rounded-[2rem] bg-zinc-900/20">
602
+ <div className="w-12 h-12 rounded-full bg-zinc-950 flex items-center justify-center text-zinc-700 mx-auto mb-4">
603
+ <Code size={24} />
604
+ </div>
605
+ <p className="text-sm font-bold text-zinc-500 mb-2">Extension System Coming Soon</p>
606
+ <p className="text-xs text-zinc-600 max-w-md mx-auto italic">
607
+ Install custom tools like SSH, Git integration, Docker management, and more.
608
+ </p>
609
+ </div>
610
+ </div>
611
+
612
+ {/* Planned Extensions Preview */}
613
+ <div className="p-6 bg-gradient-to-br from-cyan-950/20 to-purple-950/20 border border-cyan-900/30 rounded-[2rem]">
614
+ <h4 className="text-[10px] font-black uppercase tracking-widest text-cyan-500 mb-4">
615
+ Planned Extensions
616
+ </h4>
617
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-3">
618
+ {[
619
+ { name: 'SSH Manager', desc: 'Execute commands on remote servers', icon: '🔐' },
620
+ { name: 'Git Advanced', desc: 'Enhanced git operations', icon: '🌿' },
621
+ { name: 'Docker Tools', desc: 'Container management', icon: '🐳' },
622
+ ].map(ext => (
623
+ <div key={ext.name} className="p-4 bg-zinc-900/50 border border-zinc-800 rounded-xl">
624
+ <div className="text-2xl mb-2">{ext.icon}</div>
625
+ <p className="text-xs font-bold text-zinc-300 mb-1">{ext.name}</p>
626
+ <p className="text-[10px] text-zinc-600">{ext.desc}</p>
627
+ </div>
628
+ ))}
629
+ </div>
630
+ </div>
631
+
632
+ {/* OAuth Callback Configuration */}
633
+ <div className="pt-8 border-t border-zinc-900">
634
+ <div className="flex items-center justify-between mb-6">
635
+ <div>
636
+ <h4 className="text-[10px] font-black uppercase tracking-widest text-zinc-500 mb-2">
637
+ OAuth Callback Configuration
638
+ </h4>
639
+ <p className="text-xs text-zinc-600 font-medium">
640
+ Configure OAuth callback URL for container deployments and custom domains
641
+ </p>
642
+ </div>
643
+ {!editingOauth && (
644
+ <button
645
+ onClick={() => {
646
+ setEditingOauth(true);
647
+ setTempCallbackUrl(oauthConfig?.callbackUrl || '');
648
+ }}
649
+ className="px-6 py-2 bg-zinc-900 text-zinc-400 rounded-xl text-[10px] font-black uppercase tracking-widest hover:bg-zinc-800 hover:text-zinc-200 transition-all"
650
+ >
651
+ Edit OAuth
652
+ </button>
653
+ )}
654
+ </div>
655
+
656
+ {editingOauth ? (
657
+ <div className="space-y-4">
658
+ <div className="space-y-2">
659
+ <label className="text-[10px] font-bold text-zinc-400 uppercase">
660
+ Callback URL (Optional)
661
+ </label>
662
+ <input
663
+ type="text"
664
+ value={tempCallbackUrl}
665
+ onChange={(e) => setTempCallbackUrl(e.target.value)}
666
+ placeholder="http://localhost:51121/oauth-callback"
667
+ className="w-full bg-zinc-950 border border-zinc-800 rounded-2xl p-4 text-sm font-mono text-zinc-200 focus:outline-none focus:border-cyan-500/50 transition-all placeholder:text-zinc-700"
668
+ />
669
+ <p className="text-[10px] text-zinc-600 italic">
670
+ Leave empty for default (localhost:51121)
671
+ </p>
672
+ </div>
673
+
674
+ <div className="p-4 bg-zinc-900/50 border border-zinc-800 rounded-2xl space-y-2">
675
+ <p className="text-[10px] font-black uppercase text-zinc-500">Examples:</p>
676
+ <div className="space-y-1 font-mono text-[10px] text-zinc-600">
677
+ <div className="flex items-center gap-2">
678
+ <span className="text-zinc-700">•</span>
679
+ <code className="text-cyan-500">http://localhost:51121/oauth-callback</code>
680
+ <span className="text-zinc-700">(Default)</span>
681
+ </div>
682
+ <div className="flex items-center gap-2">
683
+ <span className="text-zinc-700">•</span>
684
+ <code className="text-purple-500">https://jarvis.yourdomain.com/oauth-callback</code>
685
+ <span className="text-zinc-700">(Cloudflare Tunnel)</span>
686
+ </div>
687
+ <div className="flex items-center gap-2">
688
+ <span className="text-zinc-700">•</span>
689
+ <code className="text-emerald-500">http://100.64.1.2:51121/oauth-callback</code>
690
+ <span className="text-zinc-700">(Tailscale)</span>
691
+ </div>
692
+ </div>
693
+ </div>
694
+
695
+ <div className="flex items-center gap-3">
696
+ <button
697
+ onClick={() => {
698
+ setTempCallbackUrl(oauthConfig?.callbackUrl || '');
699
+ setEditingOauth(false);
700
+ }}
701
+ className="px-6 py-2 bg-zinc-900 text-zinc-500 rounded-xl text-[10px] font-black uppercase tracking-widest hover:bg-zinc-800 hover:text-zinc-300 transition-all"
702
+ >
703
+ Cancel
704
+ </button>
705
+ <button
706
+ onClick={async () => {
707
+ try {
708
+ const res = await fetch('/api/oauth/config', {
709
+ method: 'POST',
710
+ headers: { 'Content-Type': 'application/json' },
711
+ body: JSON.stringify({
712
+ callbackUrl: tempCallbackUrl.trim() || null
713
+ })
714
+ });
715
+ const data = await res.json();
716
+ if (data.success) {
717
+ setOauthConfig({ ...oauthConfig, callbackUrl: tempCallbackUrl.trim() || null, currentUrl: tempCallbackUrl.trim() || 'http://localhost:51121/oauth-callback' });
718
+ setEditingOauth(false);
719
+ }
720
+ } catch (e) {
721
+ console.error('Failed to save OAuth config:', e);
722
+ }
723
+ }}
724
+ className="px-6 py-2 bg-cyan-600 text-white rounded-xl text-[10px] font-black uppercase tracking-widest hover:bg-cyan-500 transition-all"
725
+ >
726
+ Save OAuth Config
727
+ </button>
728
+ {oauthConfig?.callbackUrl && (
729
+ <button
730
+ onClick={async () => {
731
+ try {
732
+ await fetch('/api/oauth/config', { method: 'DELETE' });
733
+ setOauthConfig({ ...oauthConfig, callbackUrl: null, currentUrl: 'http://localhost:51121/oauth-callback' });
734
+ setTempCallbackUrl('');
735
+ setEditingOauth(false);
736
+ } catch (e) {
737
+ console.error('Failed to reset OAuth config:', e);
738
+ }
739
+ }}
740
+ className="px-6 py-2 text-red-500 rounded-xl text-[10px] font-black uppercase tracking-widest hover:bg-red-500/10 transition-all"
741
+ >
742
+ Reset to Default
743
+ </button>
744
+ )}
745
+ </div>
746
+ </div>
747
+ ) : (
748
+ <div className="p-6 bg-zinc-900/30 border border-zinc-800 rounded-2xl">
749
+ <div className="flex items-center justify-between">
750
+ <div>
751
+ <p className="text-xs font-bold text-zinc-400 mb-1">Current Callback URL</p>
752
+ <code className="text-sm font-mono text-cyan-500">
753
+ {oauthConfig?.currentUrl || 'http://localhost:51121/oauth-callback'}
754
+ </code>
755
+ </div>
756
+ <div className={`w-2 h-2 rounded-full ${oauthConfig?.callbackUrl ? 'bg-cyan-500' : 'bg-zinc-700'}`} />
757
+ </div>
758
+ </div>
759
+ )}
760
+ </div>
761
+ </div>
762
+ )}
763
+ </div>
764
+ </div>
765
+
766
+ <div className="h-auto py-4 md:h-24 border-t border-zinc-900 px-6 md:px-10 flex flex-col md:flex-row items-center justify-between gap-4 bg-zinc-900/50 backdrop-blur shrink-0">
767
+ <div className="flex items-center gap-6 w-full md:w-auto justify-center md:justify-start">
768
+ <div className="flex items-center gap-2">
769
+ <div className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse" />
770
+ <span className="text-[9px] font-black uppercase text-zinc-700 tracking-widest">Core Synchronized</span>
771
+ </div>
772
+ </div>
773
+ <div className="flex items-center gap-3 w-full md:w-auto">
774
+ <button onClick={onClose} className="flex-1 md:flex-none px-6 py-3 md:px-8 md:py-3 text-[10px] font-black uppercase tracking-widest text-zinc-600 hover:text-white transition-all italic border border-transparent hover:border-zinc-800 rounded-xl">Discard</button>
775
+ <button
776
+ onClick={handleSave}
777
+ disabled={isSaving}
778
+ className="flex-1 md:flex-none flex items-center justify-center gap-3 px-8 py-3 md:px-12 md:py-4 bg-white text-black rounded-xl md:rounded-2xl text-[10px] font-black uppercase tracking-[0.2em] hover:bg-zinc-200 transition-all active:scale-95 shadow-2xl whitespace-nowrap"
779
+ >
780
+ {isSaving ? <RefreshCw size={14} className="animate-spin" /> : <Save size={14} />}
781
+ <span>Commit</span>
782
+ </button>
783
+ </div>
784
+ </div>
785
+ </motion.div>
786
+
787
+ <AnimatePresence>
788
+ {/* Mode Selection Modal (Login vs API Key) */}
789
+ {activeModeSelection && (
790
+ <motion.div
791
+ initial={{ opacity: 0 }}
792
+ animate={{ opacity: 1 }}
793
+ exit={{ opacity: 0 }}
794
+ className="fixed inset-0 z-[200] flex items-center justify-center p-4 bg-black/60 backdrop-blur-md"
795
+ >
796
+ <motion.div
797
+ initial={{ scale: 0.9, y: 10 }}
798
+ animate={{ scale: 1, y: 0 }}
799
+ exit={{ scale: 0.9, y: 10 }}
800
+ className="w-full max-w-lg bg-zinc-950 border border-zinc-800 rounded-3xl p-8 shadow-2xl ring-1 ring-white/10"
801
+ >
802
+ <h3 className="text-xl font-black text-white italic tracking-tighter mb-2">Connect {activeModeSelection}</h3>
803
+ <p className="text-xs text-zinc-500 font-medium mb-8">Choose how you want to authenticate with this provider.</p>
804
+
805
+ <div className="grid grid-cols-2 gap-4">
806
+ <button
807
+ onClick={() => handleLoginFlow(activeModeSelection)}
808
+ className="flex flex-col items-center justify-center gap-3 p-6 rounded-2xl bg-zinc-900 border border-zinc-800 hover:bg-zinc-800 hover:border-zinc-700 transition-all group"
809
+ >
810
+ <div className="w-10 h-10 rounded-full bg-cyan-900/20 text-cyan-500 flex items-center justify-center group-hover:scale-110 transition-transform">
811
+ <User size={20} />
812
+ </div>
813
+ <div className="text-center">
814
+ <p className="text-xs font-black uppercase text-zinc-200">Browser Login</p>
815
+ <p className="text-[10px] text-zinc-500 mt-1">Interactive OAuth flow</p>
816
+ </div>
817
+ </button>
818
+
819
+ <button
820
+ onClick={() => handleManualKeyFlow(activeModeSelection)}
821
+ className="flex flex-col items-center justify-center gap-3 p-6 rounded-2xl bg-zinc-900 border border-zinc-800 hover:bg-zinc-800 hover:border-zinc-700 transition-all group"
822
+ >
823
+ <div className="w-10 h-10 rounded-full bg-purple-900/20 text-purple-500 flex items-center justify-center group-hover:scale-110 transition-transform">
824
+ <Code size={20} />
825
+ </div>
826
+ <div className="text-center">
827
+ <p className="text-xs font-black uppercase text-zinc-200">API Key</p>
828
+ <p className="text-[10px] text-zinc-500 mt-1">Manual credential entry</p>
829
+ </div>
830
+ </button>
831
+ </div>
832
+
833
+ <div className="flex justify-center mt-8">
834
+ <button
835
+ onClick={() => setActiveModeSelection(null)}
836
+ className="text-[10px] font-black uppercase tracking-widest text-zinc-600 hover:text-zinc-400 transition-all"
837
+ >
838
+ Cancel
839
+ </button>
840
+ </div>
841
+ </motion.div>
842
+ </motion.div>
843
+ )}
844
+
845
+ {/* Delete Confirmation Modal */}
846
+ {accountToDelete && (
847
+ <motion.div
848
+ initial={{ opacity: 0 }}
849
+ animate={{ opacity: 1 }}
850
+ exit={{ opacity: 0 }}
851
+ className="fixed inset-0 z-[200] flex items-center justify-center p-4 bg-black/60 backdrop-blur-md"
852
+ >
853
+ <motion.div
854
+ initial={{ scale: 0.9, y: 10 }}
855
+ animate={{ scale: 1, y: 0 }}
856
+ exit={{ scale: 0.9, y: 10 }}
857
+ className="w-full max-w-sm bg-zinc-950 border border-zinc-800 rounded-3xl p-6 shadow-2xl ring-1 ring-white/10"
858
+ >
859
+ <div className="flex flex-col items-center text-center space-y-4">
860
+ <div className="w-12 h-12 rounded-full bg-red-500/10 text-red-500 flex items-center justify-center mb-2">
861
+ <Trash2 size={20} />
862
+ </div>
863
+ <h3 className="text-lg font-black text-white italic tracking-tighter">Remove Account?</h3>
864
+ <p className="text-xs text-zinc-500 font-medium px-4">This will permanently remove <span className="text-zinc-300 font-mono">{accountToDelete}</span> from the pool.</p>
865
+
866
+ <div className="flex items-center gap-3 w-full pt-4">
867
+ <button
868
+ onClick={() => setAccountToDelete(null)}
869
+ className="flex-1 py-3 rounded-xl bg-zinc-900 text-zinc-500 text-[10px] font-black uppercase tracking-widest hover:bg-zinc-800 hover:text-zinc-300 transition-all"
870
+ >
871
+ Cancel
872
+ </button>
873
+ <button
874
+ onClick={confirmDeleteAccount}
875
+ className="flex-1 py-3 rounded-xl bg-red-600 text-white text-[10px] font-black uppercase tracking-widest hover:bg-red-500 transition-all shadow-lg shadow-red-900/20"
876
+ >
877
+ Confirm
878
+ </button>
879
+ </div>
880
+ </div>
881
+ </motion.div>
882
+ </motion.div>
883
+ )}
884
+
885
+ {activeKeySetup && (
886
+ <motion.div
887
+ initial={{ opacity: 0 }}
888
+ animate={{ opacity: 1 }}
889
+ exit={{ opacity: 0 }}
890
+ className="fixed inset-0 z-[200] flex items-center justify-center p-4 bg-black/60 backdrop-blur-md"
891
+ >
892
+ <motion.div
893
+ initial={{ scale: 0.9, y: 10 }}
894
+ animate={{ scale: 1, y: 0 }}
895
+ exit={{ scale: 0.9, y: 10 }}
896
+ className="w-full max-w-lg bg-zinc-950 border border-zinc-800 rounded-3xl p-8 shadow-2xl ring-1 ring-white/10"
897
+ >
898
+ <h3 className="text-xl font-black text-white italic tracking-tighter mb-2">Connect {activeKeySetup}</h3>
899
+ <p className="text-xs text-zinc-500 font-medium mb-6">
900
+ {activeKeySetup === 'local'
901
+ ? 'Enter the Base URL of your local inference server (Ollama or LM Studio).'
902
+ : 'Enter your API key below to enable this provider.'}
903
+ </p>
904
+
905
+ <input
906
+ type={activeKeySetup === 'local' ? 'text' : 'password'}
907
+ value={tempApiKey}
908
+ onChange={(e) => setTempApiKey(e.target.value)}
909
+ placeholder={activeKeySetup === 'local' ? 'http://localhost:1234/v1' : `sk-...`}
910
+ className="w-full bg-zinc-900 border border-zinc-800 rounded-2xl p-4 text-sm font-mono text-zinc-200 focus:outline-none focus:border-cyan-500/50 transition-all mb-6 placeholder:text-zinc-700"
911
+ autoFocus
912
+ />
913
+
914
+ <div className="flex items-center justify-end gap-3">
915
+ <button
916
+ onClick={() => setActiveKeySetup(null)}
917
+ className="px-6 py-3 rounded-xl text-[10px] font-black uppercase tracking-widest text-zinc-500 hover:text-zinc-300 hover:bg-zinc-900 transition-all"
918
+ >
919
+ Cancel
920
+ </button>
921
+ <button
922
+ onClick={handleSaveKey}
923
+ disabled={!tempApiKey}
924
+ className="px-8 py-3 rounded-xl bg-cyan-600 text-white text-[10px] font-black uppercase tracking-widest hover:bg-cyan-500 transition-all disabled:opacity-50 disabled:cursor-not-allowed shadow-lg shadow-cyan-900/20"
925
+ >
926
+ {activeKeySetup === 'local' ? 'Save URL' : 'Save Credential'}
927
+ </button>
928
+ </div>
929
+ </motion.div>
930
+ </motion.div>
931
+ )}
932
+ </AnimatePresence>
933
+ </motion.div>
934
+ );
935
+ }