@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.
- package/dist/default/convex/agents.ts +204 -0
- package/dist/default/convex/apiKeys.ts +133 -0
- package/dist/default/convex/cronJobs.ts +224 -0
- package/dist/default/convex/files.ts +103 -0
- package/dist/default/convex/folders.ts +110 -0
- package/dist/default/convex/heartbeat.ts +371 -0
- package/dist/default/convex/logs.ts +66 -0
- package/dist/default/convex/mastraIntegration.ts +185 -0
- package/dist/default/convex/mcpConnections.ts +127 -0
- package/dist/default/convex/messages.ts +90 -0
- package/dist/default/convex/projects.ts +114 -0
- package/dist/default/convex/schema.ts +150 -83
- package/dist/default/convex/sessions.ts +174 -0
- package/dist/default/convex/settings.ts +79 -0
- package/dist/default/convex/skills.ts +178 -0
- package/dist/default/convex/threads.ts +100 -0
- package/dist/default/convex/usage.ts +195 -0
- package/dist/default/convex/vault.ts +397 -0
- package/dist/default/dashboard/app/main.tsx +7 -3
- package/dist/default/dashboard/app/routes/agents.tsx +103 -161
- package/dist/default/dashboard/app/routes/chat.tsx +163 -317
- package/dist/default/dashboard/app/routes/connections.tsx +247 -386
- package/dist/default/dashboard/app/routes/cron.tsx +127 -286
- package/dist/default/dashboard/app/routes/files.tsx +184 -167
- package/dist/default/dashboard/app/routes/index.tsx +63 -96
- package/dist/default/dashboard/app/routes/projects.tsx +106 -225
- package/dist/default/dashboard/app/routes/sessions.tsx +87 -253
- package/dist/default/dashboard/app/routes/settings.tsx +316 -532
- package/dist/default/dashboard/app/routes/skills.tsx +329 -216
- package/dist/default/dashboard/app/routes/usage.tsx +107 -150
- package/dist/default/dashboard/tsconfig.json +3 -2
- package/dist/default/dashboard/vite.config.ts +6 -0
- package/dist/index.js +256 -49
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/templates/default/convex/agents.ts +204 -0
- package/templates/default/convex/apiKeys.ts +133 -0
- package/templates/default/convex/cronJobs.ts +224 -0
- package/templates/default/convex/files.ts +103 -0
- package/templates/default/convex/folders.ts +110 -0
- package/templates/default/convex/heartbeat.ts +371 -0
- package/templates/default/convex/logs.ts +66 -0
- package/templates/default/convex/mastraIntegration.ts +185 -0
- package/templates/default/convex/mcpConnections.ts +127 -0
- package/templates/default/convex/messages.ts +90 -0
- package/templates/default/convex/projects.ts +114 -0
- package/templates/default/convex/schema.ts +150 -83
- package/templates/default/convex/sessions.ts +174 -0
- package/templates/default/convex/settings.ts +79 -0
- package/templates/default/convex/skills.ts +178 -0
- package/templates/default/convex/threads.ts +100 -0
- package/templates/default/convex/usage.ts +195 -0
- package/templates/default/convex/vault.ts +397 -0
- package/templates/default/dashboard/app/main.tsx +7 -3
- package/templates/default/dashboard/app/routes/agents.tsx +103 -161
- package/templates/default/dashboard/app/routes/chat.tsx +163 -317
- package/templates/default/dashboard/app/routes/connections.tsx +247 -386
- package/templates/default/dashboard/app/routes/cron.tsx +127 -286
- package/templates/default/dashboard/app/routes/files.tsx +184 -167
- package/templates/default/dashboard/app/routes/index.tsx +63 -96
- package/templates/default/dashboard/app/routes/projects.tsx +106 -225
- package/templates/default/dashboard/app/routes/sessions.tsx +87 -253
- package/templates/default/dashboard/app/routes/settings.tsx +316 -532
- package/templates/default/dashboard/app/routes/skills.tsx +329 -216
- package/templates/default/dashboard/app/routes/usage.tsx +107 -150
- package/templates/default/dashboard/tsconfig.json +3 -2
- 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
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
64
|
-
|
|
65
|
-
|
|
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="
|
|
76
|
-
<div
|
|
77
|
-
<
|
|
78
|
-
<
|
|
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
|
-
<
|
|
85
|
-
<
|
|
86
|
-
<
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
<
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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">
|
|
204
|
-
<
|
|
205
|
-
<
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
</
|
|
209
|
-
|
|
210
|
-
|
|
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">
|
|
224
|
-
<input type="
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
|
450
|
-
<
|
|
451
|
-
<
|
|
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
|
|
462
|
-
<
|
|
463
|
-
|
|
464
|
-
|
|
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
|
-
|
|
497
|
-
)}
|
|
319
|
+
)}
|
|
498
320
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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="
|
|
329
|
+
<div className="p-6 space-y-4">
|
|
512
330
|
<div>
|
|
513
|
-
<label className="block text-sm font-medium mb-1">
|
|
514
|
-
<
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
<
|
|
519
|
-
|
|
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">
|
|
523
|
-
<input type="
|
|
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
|
-
<
|
|
528
|
-
<
|
|
529
|
-
|
|
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
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
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
|
}
|