@agentforge-ai/cli 0.4.3 → 0.5.1

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 (67) hide show
  1. package/dist/default/convex/agents.ts +204 -0
  2. package/dist/default/convex/apiKeys.ts +133 -0
  3. package/dist/default/convex/cronJobs.ts +224 -0
  4. package/dist/default/convex/files.ts +103 -0
  5. package/dist/default/convex/folders.ts +110 -0
  6. package/dist/default/convex/heartbeat.ts +371 -0
  7. package/dist/default/convex/logs.ts +66 -0
  8. package/dist/default/convex/mastraIntegration.ts +185 -0
  9. package/dist/default/convex/mcpConnections.ts +127 -0
  10. package/dist/default/convex/messages.ts +90 -0
  11. package/dist/default/convex/projects.ts +114 -0
  12. package/dist/default/convex/schema.ts +150 -83
  13. package/dist/default/convex/sessions.ts +174 -0
  14. package/dist/default/convex/settings.ts +79 -0
  15. package/dist/default/convex/skills.ts +178 -0
  16. package/dist/default/convex/threads.ts +100 -0
  17. package/dist/default/convex/usage.ts +195 -0
  18. package/dist/default/convex/vault.ts +397 -0
  19. package/dist/default/dashboard/app/main.tsx +7 -3
  20. package/dist/default/dashboard/app/routes/agents.tsx +103 -161
  21. package/dist/default/dashboard/app/routes/chat.tsx +163 -317
  22. package/dist/default/dashboard/app/routes/connections.tsx +247 -386
  23. package/dist/default/dashboard/app/routes/cron.tsx +127 -286
  24. package/dist/default/dashboard/app/routes/files.tsx +184 -167
  25. package/dist/default/dashboard/app/routes/index.tsx +63 -96
  26. package/dist/default/dashboard/app/routes/projects.tsx +106 -225
  27. package/dist/default/dashboard/app/routes/sessions.tsx +87 -253
  28. package/dist/default/dashboard/app/routes/settings.tsx +316 -532
  29. package/dist/default/dashboard/app/routes/skills.tsx +329 -216
  30. package/dist/default/dashboard/app/routes/usage.tsx +107 -150
  31. package/dist/default/dashboard/tsconfig.json +3 -2
  32. package/dist/default/dashboard/vite.config.ts +6 -0
  33. package/dist/index.js +256 -49
  34. package/dist/index.js.map +1 -1
  35. package/package.json +1 -1
  36. package/templates/default/convex/agents.ts +204 -0
  37. package/templates/default/convex/apiKeys.ts +133 -0
  38. package/templates/default/convex/cronJobs.ts +224 -0
  39. package/templates/default/convex/files.ts +103 -0
  40. package/templates/default/convex/folders.ts +110 -0
  41. package/templates/default/convex/heartbeat.ts +371 -0
  42. package/templates/default/convex/logs.ts +66 -0
  43. package/templates/default/convex/mastraIntegration.ts +185 -0
  44. package/templates/default/convex/mcpConnections.ts +127 -0
  45. package/templates/default/convex/messages.ts +90 -0
  46. package/templates/default/convex/projects.ts +114 -0
  47. package/templates/default/convex/schema.ts +150 -83
  48. package/templates/default/convex/sessions.ts +174 -0
  49. package/templates/default/convex/settings.ts +79 -0
  50. package/templates/default/convex/skills.ts +178 -0
  51. package/templates/default/convex/threads.ts +100 -0
  52. package/templates/default/convex/usage.ts +195 -0
  53. package/templates/default/convex/vault.ts +397 -0
  54. package/templates/default/dashboard/app/main.tsx +7 -3
  55. package/templates/default/dashboard/app/routes/agents.tsx +103 -161
  56. package/templates/default/dashboard/app/routes/chat.tsx +163 -317
  57. package/templates/default/dashboard/app/routes/connections.tsx +247 -386
  58. package/templates/default/dashboard/app/routes/cron.tsx +127 -286
  59. package/templates/default/dashboard/app/routes/files.tsx +184 -167
  60. package/templates/default/dashboard/app/routes/index.tsx +63 -96
  61. package/templates/default/dashboard/app/routes/projects.tsx +106 -225
  62. package/templates/default/dashboard/app/routes/sessions.tsx +87 -253
  63. package/templates/default/dashboard/app/routes/settings.tsx +316 -532
  64. package/templates/default/dashboard/app/routes/skills.tsx +329 -216
  65. package/templates/default/dashboard/app/routes/usage.tsx +107 -150
  66. package/templates/default/dashboard/tsconfig.json +3 -2
  67. package/templates/default/dashboard/vite.config.ts +6 -0
@@ -1,583 +1,367 @@
1
1
  import { createFileRoute } from '@tanstack/react-router';
2
2
  import { DashboardLayout } from '../components/DashboardLayout';
3
- import React, { useState } from 'react';
4
- // import { useQuery, useMutation } from "convex/react";
5
- // import { api } from "../../convex/_generated/api";
3
+ import { useState } from 'react';
4
+ import { useQuery, useMutation } from 'convex/react';
5
+ import { api } from '@convex/_generated/api';
6
+ import { Key, Plus, Trash2, Eye, EyeOff, Check, X, Shield, AlertTriangle, ExternalLink, Settings } from 'lucide-react';
6
7
 
7
- import * as Tabs from '@radix-ui/react-tabs';
8
- import * as Dialog from '@radix-ui/react-dialog';
9
- import * as Select from '@radix-ui/react-select';
10
- import * as Switch from '@radix-ui/react-switch';
11
- import { Settings, Key, Palette, Shield, Download, Upload, AlertTriangle, X, Plus, Trash2, ChevronDown, Lock, Eye, EyeOff, Clock, Activity, ShieldCheck } from 'lucide-react';
12
-
13
- // Mock data and types
14
- const AVAILABLE_PROVIDERS = ["OpenAI", "Anthropic", "OpenRouter", "Google", "xAI"];
15
-
16
- type ApiKey = { id: string; provider: string; maskedKey: string; createdAt: string };
17
- type ProviderSetting = { id: string; name: string; enabled: boolean };
18
-
19
- // --- Main Component --- //
20
8
  export const Route = createFileRoute('/settings')({ component: SettingsPage });
21
9
 
22
- function SettingsPage() {
23
- // const settings = useQuery(api.settings.get);
24
- // const updateSettings = useMutation(api.settings.update);
25
- // const apiKeys = useQuery(api.apiKeys.list) || [];
26
- // const addApiKey = useMutation(api.apiKeys.create);
27
- // const deleteApiKey = useMutation(api.apiKeys.delete);
28
- // const providerSettings = useQuery(api.providers.list) || [];
29
- // const updateProvider = useMutation(api.providers.update);
30
-
31
- // Local state for UI development
32
- const [generalSettings, setGeneralSettings] = useState({ appName: 'AgentForge', defaultModel: 'gpt-4.1-mini', defaultProvider: 'OpenAI' });
33
- const [apiKeys, setApiKeys] = useState<ApiKey[]>([
34
- { id: '1', provider: 'OpenAI', maskedKey: 'sk-******************1234', createdAt: new Date().toISOString() },
35
- { id: '2', provider: 'Google', maskedKey: 'go-******************5678', createdAt: new Date().toISOString() },
36
- ]);
37
- const [providerSettings, setProviderSettings] = useState<ProviderSetting[]>([
38
- { id: '1', name: 'OpenAI', enabled: true },
39
- { id: '2', name: 'Anthropic', enabled: false },
40
- { id: '3', name: 'OpenRouter', enabled: true },
41
- { id: '4', name: 'Google', enabled: true },
42
- { id: '5', name: 'xAI', enabled: false },
43
- ]);
44
- const [appearance, setAppearance] = useState({ theme: 'dark' });
45
- const [defaultProvider, setDefaultProvider] = useState('OpenAI');
10
+ // ─── AI Provider Definitions ─────────────────────────────────────
11
+ const AI_PROVIDERS = [
12
+ {
13
+ id: 'openai',
14
+ name: 'OpenAI',
15
+ description: 'GPT-4o, GPT-4.1 Mini, DALL-E, Whisper',
16
+ prefix: 'sk-',
17
+ docsUrl: 'https://platform.openai.com/api-keys',
18
+ color: 'bg-green-500',
19
+ },
20
+ {
21
+ id: 'anthropic',
22
+ name: 'Anthropic',
23
+ description: 'Claude 3.5 Sonnet, Claude 3 Haiku, Claude 3 Opus',
24
+ prefix: 'sk-ant-',
25
+ docsUrl: 'https://console.anthropic.com/settings/keys',
26
+ color: 'bg-orange-500',
27
+ },
28
+ {
29
+ id: 'openrouter',
30
+ name: 'OpenRouter',
31
+ description: 'Multi-model routing — access 200+ models through one API',
32
+ prefix: 'sk-or-',
33
+ docsUrl: 'https://openrouter.ai/keys',
34
+ color: 'bg-purple-500',
35
+ },
36
+ {
37
+ id: 'google',
38
+ name: 'Google AI',
39
+ description: 'Gemini 2.5 Flash, Gemini 1.5 Pro',
40
+ prefix: 'AIza',
41
+ docsUrl: 'https://aistudio.google.com/apikey',
42
+ color: 'bg-blue-500',
43
+ },
44
+ {
45
+ id: 'xai',
46
+ name: 'xAI',
47
+ description: 'Grok 4, Grok 3',
48
+ prefix: 'xai-',
49
+ docsUrl: 'https://console.x.ai/',
50
+ color: 'bg-gray-500',
51
+ },
52
+ {
53
+ id: 'groq',
54
+ name: 'Groq',
55
+ description: 'Ultra-fast inference — Llama, Mixtral, Gemma',
56
+ prefix: 'gsk_',
57
+ docsUrl: 'https://console.groq.com/keys',
58
+ color: 'bg-red-500',
59
+ },
60
+ {
61
+ id: 'together',
62
+ name: 'Together AI',
63
+ description: 'Open-source models — Llama, Mistral, Code Llama',
64
+ prefix: '',
65
+ docsUrl: 'https://api.together.xyz/settings/api-keys',
66
+ color: 'bg-indigo-500',
67
+ },
68
+ {
69
+ id: 'perplexity',
70
+ name: 'Perplexity',
71
+ description: 'Sonar — real-time web-grounded AI search',
72
+ prefix: 'pplx-',
73
+ docsUrl: 'https://www.perplexity.ai/settings/api',
74
+ color: 'bg-teal-500',
75
+ },
76
+ ];
46
77
 
47
- const handleAddApiKey = (provider: string, key: string) => {
48
- const newKey: ApiKey = {
49
- id: (apiKeys.length + 1).toString(),
50
- provider,
51
- maskedKey: `${key.substring(0, 5)}******************${key.substring(key.length - 4)}`,
52
- createdAt: new Date().toISOString(),
53
- };
54
- // addApiKey({ provider, key });
55
- setApiKeys([...apiKeys, newKey]);
78
+ function SettingsPage() {
79
+ const apiKeys = useQuery(api.apiKeys.list, {}) ?? [];
80
+ const vaultSecrets = useQuery(api.vault.list, {}) ?? [];
81
+ const createApiKey = useMutation(api.apiKeys.create);
82
+ const removeApiKey = useMutation(api.apiKeys.remove);
83
+ const toggleApiKey = useMutation(api.apiKeys.toggleActive);
84
+ const storeVaultSecret = useMutation(api.vault.store);
85
+ const removeVaultSecret = useMutation(api.vault.remove);
86
+
87
+ const [tab, setTab] = useState<'providers' | 'vault' | 'general'>('providers');
88
+ const [addingProvider, setAddingProvider] = useState<typeof AI_PROVIDERS[0] | null>(null);
89
+ const [newKeyName, setNewKeyName] = useState('');
90
+ const [newKeyValue, setNewKeyValue] = useState('');
91
+ const [showKey, setShowKey] = useState<Record<string, boolean>>({});
92
+ const [addingVaultSecret, setAddingVaultSecret] = useState(false);
93
+ const [vaultForm, setVaultForm] = useState({ name: '', category: 'api_key', provider: '', value: '' });
94
+
95
+ const keysByProvider = apiKeys.reduce((acc: Record<string, any[]>, key: any) => {
96
+ if (!acc[key.provider]) acc[key.provider] = [];
97
+ acc[key.provider].push(key);
98
+ return acc;
99
+ }, {} as Record<string, any[]>);
100
+
101
+ const handleAddKey = async () => {
102
+ if (!addingProvider || !newKeyValue.trim()) return;
103
+ await createApiKey({
104
+ provider: addingProvider.id,
105
+ keyName: newKeyName || `${addingProvider.name} Key`,
106
+ encryptedKey: newKeyValue,
107
+ });
108
+ setAddingProvider(null);
109
+ setNewKeyName('');
110
+ setNewKeyValue('');
56
111
  };
57
112
 
58
- const handleDeleteApiKey = (id: string) => {
59
- // deleteApiKey({ id });
60
- setApiKeys(apiKeys.filter(key => key.id !== id));
113
+ const handleDeleteKey = async (id: any) => {
114
+ if (confirm('Delete this API key? This cannot be undone.')) {
115
+ await removeApiKey({ id });
116
+ }
61
117
  };
62
118
 
63
- const handleProviderToggle = (id: string, enabled: boolean) => {
64
- // updateProvider({ id, enabled });
65
- setProviderSettings(providerSettings.map(p => p.id === id ? { ...p, enabled } : p));
119
+ const handleAddVaultSecret = async () => {
120
+ if (!vaultForm.name || !vaultForm.value) return;
121
+ await storeVaultSecret(vaultForm);
122
+ setAddingVaultSecret(false);
123
+ setVaultForm({ name: '', category: 'api_key', provider: '', value: '' });
66
124
  };
67
125
 
68
- const handleSetDefaultProvider = (name: string) => {
69
- // updateSettings({ defaultProvider: name });
70
- setDefaultProvider(name);
71
- }
72
-
73
126
  return (
74
127
  <DashboardLayout>
75
- <div className="p-8 bg-background text-foreground">
76
- <div className="flex items-center mb-6">
77
- <Settings className="w-8 h-8 mr-4 text-primary" />
78
- <div>
79
- <h1 className="text-3xl font-bold">Configuration</h1>
80
- <p className="text-muted-foreground">Manage your application settings and preferences.</p>
81
- </div>
128
+ <div className="space-y-6">
129
+ <div>
130
+ <h1 className="text-3xl font-bold">Settings</h1>
131
+ <p className="text-muted-foreground">Manage AI provider keys, secrets, and workspace configuration.</p>
82
132
  </div>
83
133
 
84
- <Tabs.Root defaultValue="general" className="w-full">
85
- <Tabs.List className="flex border-b border-border mb-6">
86
- <TabTrigger value="general" icon={<Settings />}>General</TabTrigger>
87
- <TabTrigger value="apiKeys" icon={<Key />}>API Keys</TabTrigger>
88
- <TabTrigger value="providers" icon={<Shield />}>Providers</TabTrigger>
89
- <TabTrigger value="appearance" icon={<Palette />}>Appearance</TabTrigger>
90
- <TabTrigger value="vault" icon={<Lock />}>Vault</TabTrigger>
91
- <TabTrigger value="advanced" icon={<AlertTriangle />}>Advanced</TabTrigger>
92
- </Tabs.List>
93
-
94
- <Tabs.Content value="general"><GeneralTab settings={generalSettings} setSettings={setGeneralSettings} /></Tabs.Content>
95
- <Tabs.Content value="apiKeys"><ApiKeysTab keys={apiKeys} onAdd={handleAddApiKey} onDelete={handleDeleteApiKey} /></Tabs.Content>
96
- <Tabs.Content value="providers"><ProvidersTab providers={providerSettings} defaultProvider={defaultProvider} onToggle={handleProviderToggle} onSetDefault={handleSetDefaultProvider} /></Tabs.Content>
97
- <Tabs.Content value="vault"><VaultTab /></Tabs.Content>
98
- <Tabs.Content value="appearance"><AppearanceTab appearance={appearance} setAppearance={setAppearance} /></Tabs.Content>
99
- <Tabs.Content value="advanced"><AdvancedTab /></Tabs.Content>
100
- </Tabs.Root>
101
- </div>
102
- </DashboardLayout>
103
- );
104
- }
105
-
106
- // --- Tab Components --- //
107
-
108
- const TabTrigger = ({ children, value, icon }: { children: React.ReactNode, value: string, icon: React.ReactNode }) => (
109
- <Tabs.Trigger
110
- value={value}
111
- className="flex items-center px-4 py-2 text-sm font-medium text-muted-foreground data-[state=active]:text-primary data-[state=active]:border-b-2 data-[state=active]:border-primary focus:outline-none focus:ring-2 focus:ring-primary/50 rounded-t-md transition-colors"
112
- >
113
- {React.cloneElement(icon as React.ReactElement, { className: 'w-4 h-4 mr-2' })}
114
- {children}
115
- </Tabs.Trigger>
116
- );
117
-
118
- const Card = ({ children, className }: { children: React.ReactNode, className?: string }) => (
119
- <div className={`bg-card border border-border rounded-lg p-6 ${className}`}>
120
- {children}
121
- </div>
122
- );
123
-
124
- const CardHeader = ({ title, description }: { title: string, description: string }) => (
125
- <div className="mb-6">
126
- <h3 className="text-xl font-semibold">{title}</h3>
127
- <p className="text-muted-foreground mt-1">{description}</p>
128
- </div>
129
- );
130
-
131
- const Button = ({ children, variant = 'primary', ...props }: { children: React.ReactNode, variant?: 'primary' | 'destructive' | 'secondary', [key: string]: any }) => {
132
- const variants = {
133
- primary: 'bg-primary text-primary-foreground hover:bg-primary/90',
134
- destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
135
- secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/90 border border-border',
136
- };
137
- return (
138
- <button className={`px-4 py-2 rounded-md text-sm font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed ${variants[variant]}`} {...props}>
139
- {children}
140
- </button>
141
- );
142
- };
134
+ <div className="flex gap-1 bg-muted p-1 rounded-lg w-fit">
135
+ <button onClick={() => setTab('providers')} className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${tab === 'providers' ? 'bg-card shadow-sm' : 'text-muted-foreground hover:text-foreground'}`}>
136
+ <Key className="w-4 h-4 inline mr-2" />AI Providers
137
+ </button>
138
+ <button onClick={() => setTab('vault')} className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${tab === 'vault' ? 'bg-card shadow-sm' : 'text-muted-foreground hover:text-foreground'}`}>
139
+ <Shield className="w-4 h-4 inline mr-2" />Secure Vault
140
+ </button>
141
+ <button onClick={() => setTab('general')} className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${tab === 'general' ? 'bg-card shadow-sm' : 'text-muted-foreground hover:text-foreground'}`}>
142
+ <Settings className="w-4 h-4 inline mr-2" />General
143
+ </button>
144
+ </div>
143
145
 
144
- function GeneralTab({ settings, setSettings }: { settings: any, setSettings: any }) {
145
- const [isSaving, setIsSaving] = useState(false);
146
- const handleSave = () => {
147
- setIsSaving(true);
148
- // updateSettings(settings);
149
- setTimeout(() => setIsSaving(false), 1000);
150
- };
146
+ {/* AI Providers Tab */}
147
+ {tab === 'providers' && (
148
+ <div className="space-y-6">
149
+ <div className="bg-blue-900/20 border border-blue-700/30 rounded-lg p-4 flex items-start gap-3">
150
+ <AlertTriangle className="w-5 h-5 text-blue-400 flex-shrink-0 mt-0.5" />
151
+ <div>
152
+ <p className="text-sm text-foreground font-medium">API keys are stored encrypted in Convex</p>
153
+ <p className="text-xs text-muted-foreground mt-1">Keys are encrypted at rest and only decrypted when making API calls. You can also manage keys via the CLI with <code className="bg-muted px-1 rounded">agentforge vault set</code>.</p>
154
+ </div>
155
+ </div>
151
156
 
152
- return (
153
- <Card>
154
- <CardHeader title="General Settings" description="Configure the core settings for your application." />
155
- <div className="space-y-4">
156
- <div>
157
- <label className="block text-sm font-medium mb-1">App Name</label>
158
- <input type="text" value={settings.appName} onChange={e => setSettings({...settings, appName: e.target.value})} className="w-full bg-background border border-border rounded-md px-3 py-2" />
159
- </div>
160
- <div>
161
- <label className="block text-sm font-medium mb-1">Default Model</label>
162
- <input type="text" value={settings.defaultModel} onChange={e => setSettings({...settings, defaultModel: e.target.value})} className="w-full bg-background border border-border rounded-md px-3 py-2" />
163
- </div>
164
- <div>
165
- <label className="block text-sm font-medium mb-1">Default Provider</label>
166
- <input type="text" value={settings.defaultProvider} onChange={e => setSettings({...settings, defaultProvider: e.target.value})} className="w-full bg-background border border-border rounded-md px-3 py-2" />
167
- </div>
168
- </div>
169
- <div className="mt-6 flex justify-end">
170
- <Button onClick={handleSave} disabled={isSaving}>{isSaving ? 'Saving...' : 'Save Changes'}</Button>
171
- </div>
172
- </Card>
173
- );
174
- }
157
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
158
+ {AI_PROVIDERS.map(provider => {
159
+ const keys = keysByProvider[provider.id] || [];
160
+ const hasKey = keys.length > 0;
161
+ return (
162
+ <div key={provider.id} className={`bg-card border rounded-lg p-5 shadow-sm ${hasKey ? 'border-green-700/30' : 'border-border'}`}>
163
+ <div className="flex items-start justify-between mb-3">
164
+ <div className="flex items-center gap-3">
165
+ <div className={`w-3 h-3 rounded-full ${provider.color}`} />
166
+ <div>
167
+ <h3 className="font-semibold text-foreground">{provider.name}</h3>
168
+ <p className="text-xs text-muted-foreground">{provider.description}</p>
169
+ </div>
170
+ </div>
171
+ <a href={provider.docsUrl} target="_blank" rel="noopener noreferrer" className="text-xs text-primary hover:underline flex items-center gap-1">
172
+ Get Key <ExternalLink className="w-3 h-3" />
173
+ </a>
174
+ </div>
175
+
176
+ {keys.length > 0 ? (
177
+ <div className="space-y-2 mb-3">
178
+ {keys.map((key: any) => (
179
+ <div key={key._id} className="flex items-center justify-between bg-background rounded-lg px-3 py-2 border border-border">
180
+ <div className="flex items-center gap-2 min-w-0">
181
+ <span className={`w-2 h-2 rounded-full ${key.isActive ? 'bg-green-500' : 'bg-muted-foreground'}`} />
182
+ <span className="text-sm truncate">{key.keyName}</span>
183
+ <span className="text-xs font-mono text-muted-foreground">
184
+ {showKey[key._id] ? key.encryptedKey : key.encryptedKey.substring(0, 8) + '...'}
185
+ </span>
186
+ </div>
187
+ <div className="flex items-center gap-1">
188
+ <button onClick={() => setShowKey(prev => ({ ...prev, [key._id]: !prev[key._id] }))} className="p-1 rounded hover:bg-muted">
189
+ {showKey[key._id] ? <EyeOff className="w-3.5 h-3.5 text-muted-foreground" /> : <Eye className="w-3.5 h-3.5 text-muted-foreground" />}
190
+ </button>
191
+ <button onClick={() => toggleApiKey({ id: key._id })} className="p-1 rounded hover:bg-muted">
192
+ {key.isActive ? <Check className="w-3.5 h-3.5 text-green-500" /> : <X className="w-3.5 h-3.5 text-muted-foreground" />}
193
+ </button>
194
+ <button onClick={() => handleDeleteKey(key._id)} className="p-1 rounded hover:bg-destructive/10">
195
+ <Trash2 className="w-3.5 h-3.5 text-destructive" />
196
+ </button>
197
+ </div>
198
+ </div>
199
+ ))}
200
+ </div>
201
+ ) : null}
202
+
203
+ <button onClick={() => { setAddingProvider(provider); setNewKeyName(`${provider.name} Key`); setNewKeyValue(''); }} className={`w-full px-3 py-2 rounded-lg text-sm flex items-center justify-center gap-2 ${hasKey ? 'bg-muted text-muted-foreground hover:text-foreground' : 'bg-primary text-primary-foreground hover:bg-primary/90'}`}>
204
+ <Plus className="w-4 h-4" /> {hasKey ? 'Add Another Key' : 'Add API Key'}
205
+ </button>
206
+ </div>
207
+ );
208
+ })}
209
+ </div>
210
+ </div>
211
+ )}
175
212
 
176
- function ApiKeysTab({ keys, onAdd, onDelete }: { keys: ApiKey[], onAdd: (p: string, k: string) => void, onDelete: (id: string) => void }) {
177
- const [isModalOpen, setIsModalOpen] = useState(false);
178
- const [newProvider, setNewProvider] = useState(AVAILABLE_PROVIDERS[0]);
179
- const [newKey, setNewKey] = useState('');
213
+ {/* Vault Tab */}
214
+ {tab === 'vault' && (
215
+ <div className="space-y-6">
216
+ <div className="flex justify-between items-center">
217
+ <p className="text-sm text-muted-foreground">Encrypted secrets stored in the Secure Vault. Secrets detected in chat are automatically stored here.</p>
218
+ <button onClick={() => setAddingVaultSecret(true)} className="bg-primary text-primary-foreground px-4 py-2 rounded-lg text-sm hover:bg-primary/90 flex items-center gap-2">
219
+ <Plus className="w-4 h-4" /> Add Secret
220
+ </button>
221
+ </div>
180
222
 
181
- const handleAdd = () => {
182
- if (newKey.trim()) {
183
- onAdd(newProvider, newKey.trim());
184
- setNewKey('');
185
- setIsModalOpen(false);
186
- }
187
- };
223
+ {vaultSecrets.length === 0 ? (
224
+ <div className="text-center py-16 bg-card border border-border rounded-lg">
225
+ <Shield className="w-16 h-16 text-muted-foreground/30 mx-auto mb-4" />
226
+ <h3 className="text-lg font-semibold mb-2">Vault is empty</h3>
227
+ <p className="text-muted-foreground">Secrets will appear here when detected in chat or added manually.</p>
228
+ </div>
229
+ ) : (
230
+ <div className="bg-card border border-border rounded-lg overflow-hidden">
231
+ <table className="w-full text-sm">
232
+ <thead className="bg-muted/50">
233
+ <tr>
234
+ <th className="text-left px-4 py-3 font-medium text-muted-foreground">Name</th>
235
+ <th className="text-left px-4 py-3 font-medium text-muted-foreground">Category</th>
236
+ <th className="text-left px-4 py-3 font-medium text-muted-foreground">Provider</th>
237
+ <th className="text-left px-4 py-3 font-medium text-muted-foreground">Value</th>
238
+ <th className="text-left px-4 py-3 font-medium text-muted-foreground">Created</th>
239
+ <th className="text-right px-4 py-3 font-medium text-muted-foreground">Actions</th>
240
+ </tr>
241
+ </thead>
242
+ <tbody>
243
+ {vaultSecrets.map((secret: any) => (
244
+ <tr key={secret._id} className="border-t border-border hover:bg-muted/30">
245
+ <td className="px-4 py-3 font-medium">{secret.name}</td>
246
+ <td className="px-4 py-3"><span className="text-xs bg-muted px-2 py-0.5 rounded">{secret.category}</span></td>
247
+ <td className="px-4 py-3 text-muted-foreground">{secret.provider || '—'}</td>
248
+ <td className="px-4 py-3 font-mono text-xs text-muted-foreground">{secret.maskedValue}</td>
249
+ <td className="px-4 py-3 text-xs text-muted-foreground">{new Date(secret.createdAt).toLocaleDateString()}</td>
250
+ <td className="px-4 py-3 text-right">
251
+ <button onClick={() => { if (confirm('Delete this secret?')) removeVaultSecret({ id: secret._id }); }} className="p-1.5 rounded hover:bg-destructive/10"><Trash2 className="w-4 h-4 text-destructive" /></button>
252
+ </td>
253
+ </tr>
254
+ ))}
255
+ </tbody>
256
+ </table>
257
+ </div>
258
+ )}
259
+ </div>
260
+ )}
188
261
 
189
- return (
190
- <Card>
191
- <div className="flex justify-between items-start">
192
- <CardHeader title="API Keys" description="Manage API keys for third-party providers." />
193
- <Dialog.Root open={isModalOpen} onOpenChange={setIsModalOpen}>
194
- <Dialog.Trigger asChild>
195
- <Button><Plus className="w-4 h-4 mr-2" />Add API Key</Button>
196
- </Dialog.Trigger>
197
- <Dialog.Portal>
198
- <Dialog.Overlay className="fixed inset-0 bg-black/50" />
199
- <Dialog.Content className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-card border border-border rounded-lg p-6 w-[400px] shadow-lg">
200
- <Dialog.Title className="text-lg font-semibold">Add New API Key</Dialog.Title>
201
- <div className="space-y-4 mt-4">
262
+ {/* General Tab */}
263
+ {tab === 'general' && (
264
+ <div className="space-y-6">
265
+ <div className="bg-card border border-border rounded-lg p-6">
266
+ <h3 className="text-lg font-semibold mb-4">Workspace Configuration</h3>
267
+ <div className="space-y-4">
202
268
  <div>
203
- <label className="block text-sm font-medium mb-1">Provider</label>
204
- <Select.Root value={newProvider} onValueChange={setNewProvider}>
205
- <Select.Trigger className="w-full flex justify-between items-center bg-background border border-border rounded-md px-3 py-2">
206
- <Select.Value />
207
- <Select.Icon><ChevronDown className="w-4 h-4" /></Select.Icon>
208
- </Select.Trigger>
209
- <Select.Portal>
210
- <Select.Content className="bg-card border border-border rounded-md shadow-lg">
211
- <Select.Viewport className="p-2">
212
- {AVAILABLE_PROVIDERS.map(p => (
213
- <Select.Item key={p} value={p} className="px-3 py-2 rounded-md hover:bg-primary/20 cursor-pointer focus:outline-none">
214
- <Select.ItemText>{p}</Select.ItemText>
215
- </Select.Item>
216
- ))}
217
- </Select.Viewport>
218
- </Select.Content>
219
- </Select.Portal>
220
- </Select.Root>
269
+ <label className="block text-sm font-medium mb-1">Default Model</label>
270
+ <select className="w-full max-w-sm bg-background border border-border rounded-md px-3 py-2 text-sm">
271
+ <option value="gpt-4.1-mini">gpt-4.1-mini (OpenAI)</option>
272
+ <option value="claude-3.5-sonnet">claude-3.5-sonnet (Anthropic)</option>
273
+ <option value="gemini-2.5-flash">gemini-2.5-flash (Google)</option>
274
+ <option value="openrouter/auto">auto (OpenRouter)</option>
275
+ </select>
276
+ <p className="text-xs text-muted-foreground mt-1">Used when creating new agents without specifying a model.</p>
221
277
  </div>
222
278
  <div>
223
- <label className="block text-sm font-medium mb-1">API Key</label>
224
- <input type="password" value={newKey} onChange={e => setNewKey(e.target.value)} className="w-full bg-background border border-border rounded-md px-3 py-2" />
279
+ <label className="block text-sm font-medium mb-1">Default Temperature</label>
280
+ <input type="number" defaultValue={0.7} step={0.1} min={0} max={2} className="w-full max-w-sm bg-background border border-border rounded-md px-3 py-2 text-sm" />
225
281
  </div>
226
282
  </div>
227
- <div className="mt-6 flex justify-end space-x-2">
228
- <Dialog.Close asChild><Button variant="secondary">Cancel</Button></Dialog.Close>
229
- <Button onClick={handleAdd}>Add Key</Button>
230
- </div>
231
- <Dialog.Close asChild className="absolute top-4 right-4"><button><X className="w-4 h-4" /></button></Dialog.Close>
232
- </Dialog.Content>
233
- </Dialog.Portal>
234
- </Dialog.Root>
235
- </div>
236
- <div className="mt-4 space-y-3">
237
- {keys.length > 0 ? keys.map(key => (
238
- <div key={key.id} className="flex items-center justify-between bg-background/50 p-3 rounded-md border border-border">
239
- <div>
240
- <span className="font-semibold">{key.provider}</span>
241
- <p className="text-sm text-muted-foreground font-mono">{key.maskedKey}</p>
242
283
  </div>
243
- <Button variant="destructive" onClick={() => onDelete(key.id)}><Trash2 className="w-4 h-4" /></Button>
244
284
  </div>
245
- )) : (
246
- <p className="text-muted-foreground text-center py-4">No API keys added yet.</p>
247
285
  )}
248
- </div>
249
- </Card>
250
- );
251
- }
252
286
 
253
- function ProvidersTab({ providers, defaultProvider, onToggle, onSetDefault }: { providers: ProviderSetting[], defaultProvider: string, onToggle: (id: string, e: boolean) => void, onSetDefault: (name: string) => void }) {
254
- return (
255
- <Card>
256
- <CardHeader title="Providers" description="Enable or disable providers and set a default." />
257
- <div className="space-y-3">
258
- {providers.map(provider => (
259
- <div key={provider.id} className="flex items-center justify-between bg-background/50 p-3 rounded-md border border-border">
260
- <span className="font-semibold">{provider.name}</span>
261
- <div className="flex items-center space-x-4">
262
- <Button
263
- variant={defaultProvider === provider.name ? 'primary' : 'secondary'}
264
- onClick={() => onSetDefault(provider.name)}
265
- disabled={!provider.enabled}
266
- >
267
- {defaultProvider === provider.name ? 'Default' : 'Set as Default'}
268
- </Button>
269
- <div className="flex items-center space-x-2">
270
- <label htmlFor={`switch-${provider.id}`} className="text-sm">{provider.enabled ? 'Enabled' : 'Disabled'}</label>
271
- <Switch.Root
272
- id={`switch-${provider.id}`}
273
- checked={provider.enabled}
274
- onCheckedChange={(checked) => onToggle(provider.id, checked)}
275
- className="w-[42px] h-[25px] bg-gray-600 rounded-full relative data-[state=checked]:bg-primary outline-none cursor-pointer"
276
- >
277
- <Switch.Thumb className="block w-[21px] h-[21px] bg-white rounded-full shadow-sm transition-transform duration-100 translate-x-0.5 data-[state=checked]:translate-x-[19px]" />
278
- </Switch.Root>
287
+ {/* Add Key Modal */}
288
+ {addingProvider && (
289
+ <div className="fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4">
290
+ <div className="bg-card border border-border rounded-lg shadow-xl w-full max-w-md">
291
+ <div className="flex justify-between items-center p-4 border-b border-border">
292
+ <h2 className="text-lg font-bold">Add {addingProvider.name} API Key</h2>
293
+ <button onClick={() => setAddingProvider(null)} className="text-muted-foreground hover:text-foreground"><X className="h-5 w-5" /></button>
279
294
  </div>
280
- </div>
281
- </div>
282
- ))}
283
- </div>
284
- </Card>
285
- );
286
- }
287
-
288
- function AppearanceTab({ appearance, setAppearance }: { appearance: any, setAppearance: any }) {
289
- const toggleTheme = () => {
290
- const newTheme = appearance.theme === 'dark' ? 'light' : 'dark';
291
- setAppearance({ ...appearance, theme: newTheme });
292
- // In a real app, you'd also do:
293
- // document.documentElement.classList.toggle('dark', newTheme === 'dark');
294
- };
295
-
296
- return (
297
- <Card>
298
- <CardHeader title="Appearance" description="Customize the look and feel of the application." />
299
- <div className="flex items-center justify-between">
300
- <span className="font-medium">Theme</span>
301
- <div className="flex items-center space-x-2">
302
- <span>Light</span>
303
- <Switch.Root
304
- checked={appearance.theme === 'dark'}
305
- onCheckedChange={toggleTheme}
306
- className="w-[42px] h-[25px] bg-gray-600 rounded-full relative data-[state=checked]:bg-primary outline-none cursor-pointer"
307
- >
308
- <Switch.Thumb className="block w-[21px] h-[21px] bg-white rounded-full shadow-sm transition-transform duration-100 translate-x-0.5 data-[state=checked]:translate-x-[19px]" />
309
- </Switch.Root>
310
- <span>Dark</span>
311
- </div>
312
- </div>
313
- </Card>
314
- );
315
- }
316
-
317
- // ============================================================
318
- // VAULT TAB - Secure secrets management
319
- // ============================================================
320
- type VaultEntry = {
321
- id: string;
322
- name: string;
323
- category: string;
324
- provider: string;
325
- maskedValue: string;
326
- isActive: boolean;
327
- accessCount: number;
328
- lastAccessedAt: string | null;
329
- createdAt: string;
330
- };
331
-
332
- type AuditLogEntry = {
333
- id: string;
334
- action: string;
335
- source: string;
336
- timestamp: string;
337
- secretName: string;
338
- };
339
-
340
- function VaultTab() {
341
- const [entries, setEntries] = useState<VaultEntry[]>([
342
- { id: '1', name: 'OpenAI API Key', category: 'api_key', provider: 'openai', maskedValue: 'sk-pro...abc123', isActive: true, accessCount: 47, lastAccessedAt: new Date(Date.now() - 3600000).toISOString(), createdAt: new Date(Date.now() - 86400000 * 7).toISOString() },
343
- { id: '2', name: 'Anthropic API Key', category: 'api_key', provider: 'anthropic', maskedValue: 'sk-ant...xyz789', isActive: true, accessCount: 12, lastAccessedAt: new Date(Date.now() - 7200000).toISOString(), createdAt: new Date(Date.now() - 86400000 * 3).toISOString() },
344
- { id: '3', name: 'GitHub Token (auto-captured)', category: 'token', provider: 'github', maskedValue: 'ghp_ab...ef1234', isActive: true, accessCount: 3, lastAccessedAt: new Date(Date.now() - 86400000).toISOString(), createdAt: new Date(Date.now() - 86400000).toISOString() },
345
- { id: '4', name: 'Stripe Test Key', category: 'api_key', provider: 'stripe', maskedValue: 'sk_tes...9876ab', isActive: false, accessCount: 0, lastAccessedAt: null, createdAt: new Date(Date.now() - 86400000 * 14).toISOString() },
346
- ]);
347
-
348
- const [auditLog] = useState<AuditLogEntry[]>([
349
- { id: '1', action: 'accessed', source: 'agent', timestamp: new Date(Date.now() - 3600000).toISOString(), secretName: 'OpenAI API Key' },
350
- { id: '2', action: 'auto_captured', source: 'chat', timestamp: new Date(Date.now() - 86400000).toISOString(), secretName: 'GitHub Token' },
351
- { id: '3', action: 'created', source: 'dashboard', timestamp: new Date(Date.now() - 86400000 * 3).toISOString(), secretName: 'Anthropic API Key' },
352
- { id: '4', action: 'updated', source: 'dashboard', timestamp: new Date(Date.now() - 86400000 * 5).toISOString(), secretName: 'OpenAI API Key' },
353
- ]);
354
-
355
- const [isAddModalOpen, setIsAddModalOpen] = useState(false);
356
- const [newSecret, setNewSecret] = useState({ name: '', category: 'api_key', provider: '', value: '' });
357
- const [showAuditLog, setShowAuditLog] = useState(false);
358
-
359
- const handleAddSecret = () => {
360
- if (!newSecret.name.trim() || !newSecret.value.trim()) return;
361
- const masked = newSecret.value.length > 12
362
- ? newSecret.value.substring(0, 6) + '...' + newSecret.value.substring(newSecret.value.length - 4)
363
- : newSecret.value.substring(0, 3) + '...' + newSecret.value.substring(newSecret.value.length - 3);
364
- setEntries(prev => [{
365
- id: Date.now().toString(),
366
- name: newSecret.name,
367
- category: newSecret.category,
368
- provider: newSecret.provider,
369
- maskedValue: masked,
370
- isActive: true,
371
- accessCount: 0,
372
- lastAccessedAt: null,
373
- createdAt: new Date().toISOString(),
374
- }, ...prev]);
375
- setNewSecret({ name: '', category: 'api_key', provider: '', value: '' });
376
- setIsAddModalOpen(false);
377
- };
378
-
379
- const handleToggle = (id: string) => {
380
- setEntries(prev => prev.map(e => e.id === id ? { ...e, isActive: !e.isActive } : e));
381
- };
382
-
383
- const handleDelete = (id: string) => {
384
- if (confirm('Are you sure? This will permanently delete this secret.')) {
385
- setEntries(prev => prev.filter(e => e.id !== id));
386
- }
387
- };
388
-
389
- const categoryColors: Record<string, string> = {
390
- api_key: 'bg-blue-900/30 text-blue-400 border-blue-700/40',
391
- token: 'bg-purple-900/30 text-purple-400 border-purple-700/40',
392
- secret: 'bg-yellow-900/30 text-yellow-400 border-yellow-700/40',
393
- credential: 'bg-orange-900/30 text-orange-400 border-orange-700/40',
394
- };
395
-
396
- const actionColors: Record<string, string> = {
397
- created: 'text-green-400',
398
- accessed: 'text-blue-400',
399
- updated: 'text-yellow-400',
400
- deleted: 'text-red-400',
401
- auto_captured: 'text-purple-400',
402
- };
403
-
404
- return (
405
- <div className="space-y-6">
406
- {/* Vault Header Stats */}
407
- <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
408
- <div className="bg-card border border-border rounded-lg p-4">
409
- <div className="flex items-center gap-2 text-muted-foreground text-sm mb-1"><Lock className="w-4 h-4" /> Total Secrets</div>
410
- <p className="text-2xl font-bold">{entries.length}</p>
411
- </div>
412
- <div className="bg-card border border-border rounded-lg p-4">
413
- <div className="flex items-center gap-2 text-muted-foreground text-sm mb-1"><ShieldCheck className="w-4 h-4" /> Active</div>
414
- <p className="text-2xl font-bold text-green-400">{entries.filter(e => e.isActive).length}</p>
415
- </div>
416
- <div className="bg-card border border-border rounded-lg p-4">
417
- <div className="flex items-center gap-2 text-muted-foreground text-sm mb-1"><Activity className="w-4 h-4" /> Total Accesses</div>
418
- <p className="text-2xl font-bold">{entries.reduce((sum, e) => sum + e.accessCount, 0)}</p>
419
- </div>
420
- <div className="bg-card border border-border rounded-lg p-4">
421
- <div className="flex items-center gap-2 text-muted-foreground text-sm mb-1"><Shield className="w-4 h-4" /> Auto-Captured</div>
422
- <p className="text-2xl font-bold text-purple-400">{entries.filter(e => e.name.includes('auto-captured')).length}</p>
423
- </div>
424
- </div>
425
-
426
- {/* Vault Entries */}
427
- <Card>
428
- <div className="flex justify-between items-start mb-6">
429
- <div>
430
- <h3 className="text-xl font-semibold flex items-center gap-2"><Lock className="w-5 h-5 text-primary" /> Secure Vault</h3>
431
- <p className="text-muted-foreground mt-1">Encrypted storage for API keys, tokens, and secrets. Values are never exposed in the UI or database.</p>
432
- </div>
433
- <div className="flex gap-2">
434
- <Button variant="secondary" onClick={() => setShowAuditLog(!showAuditLog)}>
435
- <Clock className="w-4 h-4 mr-2" />{showAuditLog ? 'Hide' : 'Show'} Audit Log
436
- </Button>
437
- <Button onClick={() => setIsAddModalOpen(true)}><Plus className="w-4 h-4 mr-2" />Add Secret</Button>
438
- </div>
439
- </div>
440
-
441
- <div className="space-y-3">
442
- {entries.map(entry => (
443
- <div key={entry.id} className={`flex items-center justify-between p-4 rounded-lg border ${entry.isActive ? 'bg-background/50 border-border' : 'bg-background/20 border-border/50 opacity-60'}`}>
444
- <div className="flex items-center gap-4">
445
- <div className={`w-10 h-10 rounded-lg flex items-center justify-center ${entry.isActive ? 'bg-primary/20' : 'bg-muted'}`}>
446
- <Lock className={`w-5 h-5 ${entry.isActive ? 'text-primary' : 'text-muted-foreground'}`} />
295
+ <div className="p-6 space-y-4">
296
+ <div>
297
+ <label className="block text-sm font-medium mb-1">Key Name</label>
298
+ <input type="text" value={newKeyName} onChange={(e) => setNewKeyName(e.target.value)} className="w-full bg-background border border-border rounded-md px-3 py-2 text-sm" placeholder="e.g. Production Key" />
447
299
  </div>
448
300
  <div>
449
- <div className="flex items-center gap-2">
450
- <p className="font-medium">{entry.name}</p>
451
- <span className={`text-xs px-2 py-0.5 rounded-full border ${categoryColors[entry.category] || 'bg-muted text-muted-foreground'}`}>{entry.category}</span>
452
- {entry.provider && <span className="text-xs text-muted-foreground">{entry.provider}</span>}
453
- </div>
454
- <div className="flex items-center gap-3 mt-1">
455
- <code className="text-xs text-muted-foreground font-mono bg-background px-2 py-0.5 rounded">{entry.maskedValue}</code>
456
- <span className="text-xs text-muted-foreground">{entry.accessCount} accesses</span>
457
- {entry.lastAccessedAt && <span className="text-xs text-muted-foreground">Last: {new Date(entry.lastAccessedAt).toLocaleDateString()}</span>}
301
+ <div className="flex items-center justify-between mb-1">
302
+ <label className="text-sm font-medium">API Key</label>
303
+ <a href={addingProvider.docsUrl} target="_blank" rel="noopener noreferrer" className="text-xs text-primary hover:underline flex items-center gap-1">Get key <ExternalLink className="w-3 h-3" /></a>
458
304
  </div>
305
+ <input type="password" value={newKeyValue} onChange={(e) => setNewKeyValue(e.target.value)} className="w-full bg-background border border-border rounded-md px-3 py-2 text-sm font-mono" placeholder={`${addingProvider.prefix}xxxxxxxxxxxxxxxxxxxx`} />
459
306
  </div>
307
+ {newKeyValue && !newKeyValue.startsWith(addingProvider.prefix) && addingProvider.prefix && (
308
+ <p className="text-xs text-yellow-500 flex items-center gap-1"><AlertTriangle className="w-3.5 h-3.5" /> Key should start with "{addingProvider.prefix}"</p>
309
+ )}
460
310
  </div>
461
- <div className="flex items-center gap-2">
462
- <Switch.Root
463
- checked={entry.isActive}
464
- onCheckedChange={() => handleToggle(entry.id)}
465
- className="w-[42px] h-[25px] bg-gray-600 rounded-full relative data-[state=checked]:bg-primary outline-none cursor-pointer"
466
- >
467
- <Switch.Thumb className="block w-[21px] h-[21px] bg-white rounded-full shadow-sm transition-transform duration-100 translate-x-0.5 data-[state=checked]:translate-x-[19px]" />
468
- </Switch.Root>
469
- <button onClick={() => handleDelete(entry.id)} className="p-2 rounded-md hover:bg-destructive/20 text-muted-foreground hover:text-destructive">
470
- <Trash2 className="w-4 h-4" />
311
+ <div className="p-4 border-t border-border flex justify-end gap-2">
312
+ <button onClick={() => setAddingProvider(null)} className="px-4 py-2 rounded-lg bg-muted text-muted-foreground text-sm">Cancel</button>
313
+ <button onClick={handleAddKey} disabled={!newKeyValue.trim()} className="px-4 py-2 rounded-lg bg-primary text-primary-foreground text-sm hover:bg-primary/90 disabled:opacity-50 flex items-center gap-2">
314
+ <Key className="w-4 h-4" /> Save Key
471
315
  </button>
472
316
  </div>
473
317
  </div>
474
- ))}
475
- </div>
476
- </Card>
477
-
478
- {/* Audit Log */}
479
- {showAuditLog && (
480
- <Card>
481
- <CardHeader title="Audit Log" description="Track all vault access and modifications." />
482
- <div className="space-y-2">
483
- {auditLog.map(log => (
484
- <div key={log.id} className="flex items-center justify-between py-2 px-3 rounded-md bg-background/50 border border-border">
485
- <div className="flex items-center gap-3">
486
- <span className={`text-sm font-medium capitalize ${actionColors[log.action] || 'text-foreground'}`}>{log.action.replace('_', ' ')}</span>
487
- <span className="text-sm text-foreground">{log.secretName}</span>
488
- </div>
489
- <div className="flex items-center gap-3 text-xs text-muted-foreground">
490
- <span className="px-2 py-0.5 bg-card rounded border border-border">{log.source}</span>
491
- <span>{new Date(log.timestamp).toLocaleString()}</span>
492
- </div>
493
- </div>
494
- ))}
495
318
  </div>
496
- </Card>
497
- )}
319
+ )}
498
320
 
499
- {/* Add Secret Modal */}
500
- <Dialog.Root open={isAddModalOpen} onOpenChange={setIsAddModalOpen}>
501
- <Dialog.Portal>
502
- <Dialog.Overlay className="fixed inset-0 bg-black/50" />
503
- <Dialog.Content className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-card border border-border rounded-lg p-6 w-[450px] shadow-lg">
504
- <Dialog.Title className="text-lg font-semibold flex items-center gap-2"><Lock className="w-5 h-5 text-primary" /> Add Secret to Vault</Dialog.Title>
505
- <p className="text-sm text-muted-foreground mt-1">The value will be encrypted before storage. It will never be visible again.</p>
506
- <div className="space-y-4 mt-4">
507
- <div>
508
- <label className="block text-sm font-medium mb-1">Name</label>
509
- <input type="text" value={newSecret.name} onChange={e => setNewSecret({...newSecret, name: e.target.value})} placeholder="e.g., OpenAI Production Key" className="w-full bg-background border border-border rounded-md px-3 py-2" />
321
+ {/* Add Vault Secret Modal */}
322
+ {addingVaultSecret && (
323
+ <div className="fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4">
324
+ <div className="bg-card border border-border rounded-lg shadow-xl w-full max-w-md">
325
+ <div className="flex justify-between items-center p-4 border-b border-border">
326
+ <h2 className="text-lg font-bold">Add Secret to Vault</h2>
327
+ <button onClick={() => setAddingVaultSecret(false)} className="text-muted-foreground hover:text-foreground"><X className="h-5 w-5" /></button>
510
328
  </div>
511
- <div className="grid grid-cols-2 gap-3">
329
+ <div className="p-6 space-y-4">
512
330
  <div>
513
- <label className="block text-sm font-medium mb-1">Category</label>
514
- <select value={newSecret.category} onChange={e => setNewSecret({...newSecret, category: e.target.value})} className="w-full bg-background border border-border rounded-md px-3 py-2">
515
- <option value="api_key">API Key</option>
516
- <option value="token">Token</option>
517
- <option value="secret">Secret</option>
518
- <option value="credential">Credential</option>
519
- </select>
331
+ <label className="block text-sm font-medium mb-1">Name</label>
332
+ <input type="text" value={vaultForm.name} onChange={(e) => setVaultForm(prev => ({ ...prev, name: e.target.value }))} className="w-full bg-background border border-border rounded-md px-3 py-2 text-sm" placeholder="e.g. Database Password" />
333
+ </div>
334
+ <div className="grid grid-cols-2 gap-4">
335
+ <div>
336
+ <label className="block text-sm font-medium mb-1">Category</label>
337
+ <select value={vaultForm.category} onChange={(e) => setVaultForm(prev => ({ ...prev, category: e.target.value }))} className="w-full bg-background border border-border rounded-md px-3 py-2 text-sm">
338
+ <option value="api_key">API Key</option>
339
+ <option value="token">Token</option>
340
+ <option value="credential">Credential</option>
341
+ <option value="certificate">Certificate</option>
342
+ <option value="other">Other</option>
343
+ </select>
344
+ </div>
345
+ <div>
346
+ <label className="block text-sm font-medium mb-1">Provider</label>
347
+ <input type="text" value={vaultForm.provider} onChange={(e) => setVaultForm(prev => ({ ...prev, provider: e.target.value }))} className="w-full bg-background border border-border rounded-md px-3 py-2 text-sm" placeholder="e.g. aws" />
348
+ </div>
520
349
  </div>
521
350
  <div>
522
- <label className="block text-sm font-medium mb-1">Provider</label>
523
- <input type="text" value={newSecret.provider} onChange={e => setNewSecret({...newSecret, provider: e.target.value})} placeholder="e.g., openai" className="w-full bg-background border border-border rounded-md px-3 py-2" />
351
+ <label className="block text-sm font-medium mb-1">Secret Value</label>
352
+ <input type="password" value={vaultForm.value} onChange={(e) => setVaultForm(prev => ({ ...prev, value: e.target.value }))} className="w-full bg-background border border-border rounded-md px-3 py-2 text-sm font-mono" placeholder="Enter the secret value" />
524
353
  </div>
525
354
  </div>
526
- <div>
527
- <label className="block text-sm font-medium mb-1">Secret Value</label>
528
- <input type="password" value={newSecret.value} onChange={e => setNewSecret({...newSecret, value: e.target.value})} placeholder="Enter the secret value" className="w-full bg-background border border-border rounded-md px-3 py-2 font-mono" />
529
- <p className="text-xs text-muted-foreground mt-1">This value will be encrypted with AES-256-GCM. You will only see a masked version after saving.</p>
355
+ <div className="p-4 border-t border-border flex justify-end gap-2">
356
+ <button onClick={() => setAddingVaultSecret(false)} className="px-4 py-2 rounded-lg bg-muted text-muted-foreground text-sm">Cancel</button>
357
+ <button onClick={handleAddVaultSecret} disabled={!vaultForm.name || !vaultForm.value} className="px-4 py-2 rounded-lg bg-primary text-primary-foreground text-sm hover:bg-primary/90 disabled:opacity-50 flex items-center gap-2">
358
+ <Shield className="w-4 h-4" /> Store Secret
359
+ </button>
530
360
  </div>
531
361
  </div>
532
- <div className="mt-6 flex justify-end space-x-2">
533
- <Dialog.Close asChild><Button variant="secondary">Cancel</Button></Dialog.Close>
534
- <Button onClick={handleAddSecret}><Lock className="w-4 h-4 mr-2" />Encrypt &amp; Store</Button>
535
- </div>
536
- <Dialog.Close asChild className="absolute top-4 right-4"><button><X className="w-4 h-4" /></button></Dialog.Close>
537
- </Dialog.Content>
538
- </Dialog.Portal>
539
- </Dialog.Root>
540
- </div>
541
- );
542
- }
543
-
544
- function AdvancedTab() {
545
- const [isResetModalOpen, setIsResetModalOpen] = useState(false);
546
-
547
- const handleReset = () => {
548
- console.log("Resetting all settings...");
549
- // Call mutation to reset settings
550
- setIsResetModalOpen(false);
551
- };
552
-
553
- return (
554
- <div className="space-y-6">
555
- <Card>
556
- <CardHeader title="Configuration Management" description="Export or import your application configuration." />
557
- <div className="flex space-x-4">
558
- <Button variant="secondary"><Upload className="w-4 h-4 mr-2" />Import Configuration</Button>
559
- <Button variant="secondary"><Download className="w-4 h-4 mr-2" />Export Configuration</Button>
560
- </div>
561
- </Card>
562
- <Card className="border-destructive">
563
- <CardHeader title="Danger Zone" description="These actions are irreversible. Please proceed with caution." />
564
- <Dialog.Root open={isResetModalOpen} onOpenChange={setIsResetModalOpen}>
565
- <Dialog.Trigger asChild>
566
- <Button variant="destructive">Reset All Settings</Button>
567
- </Dialog.Trigger>
568
- <Dialog.Portal>
569
- <Dialog.Overlay className="fixed inset-0 bg-black/50" />
570
- <Dialog.Content className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-card border border-border rounded-lg p-6 w-[400px] shadow-lg">
571
- <Dialog.Title className="text-lg font-semibold flex items-center"><AlertTriangle className="w-5 h-5 mr-2 text-destructive"/>Confirm Reset</Dialog.Title>
572
- <p className="mt-2 text-muted-foreground">Are you sure you want to reset all settings? This will erase all API keys, provider configurations, and general settings. This action cannot be undone.</p>
573
- <div className="mt-6 flex justify-end space-x-2">
574
- <Dialog.Close asChild><Button variant="secondary">Cancel</Button></Dialog.Close>
575
- <Button variant="destructive" onClick={handleReset}>Yes, Reset Everything</Button>
576
- </div>
577
- </Dialog.Content>
578
- </Dialog.Portal>
579
- </Dialog.Root>
580
- </Card>
581
- </div>
362
+ </div>
363
+ )}
364
+ </div>
365
+ </DashboardLayout>
582
366
  );
583
367
  }