@agentforge-ai/cli 0.3.2 → 0.4.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/.env.example +46 -6
- package/dist/default/README.md +89 -9
- package/dist/default/convex/schema.ts +248 -4
- package/dist/default/dashboard/app/components/DashboardLayout.tsx +245 -0
- package/dist/default/dashboard/app/components/ui/badge.tsx +26 -0
- package/dist/default/dashboard/app/components/ui/button.tsx +41 -0
- package/dist/default/dashboard/app/components/ui/card.tsx +44 -0
- package/dist/default/dashboard/app/components/ui/dialog.tsx +66 -0
- package/dist/default/dashboard/app/components/ui/input.tsx +21 -0
- package/dist/default/dashboard/app/components/ui/label.tsx +18 -0
- package/dist/default/dashboard/app/components/ui/select.tsx +75 -0
- package/dist/default/dashboard/app/components/ui/sheet.tsx +73 -0
- package/dist/default/dashboard/app/components/ui/switch.tsx +34 -0
- package/dist/default/dashboard/app/components/ui/table.tsx +60 -0
- package/dist/default/dashboard/app/components/ui/tabs.tsx +50 -0
- package/dist/default/dashboard/app/components/ui/tooltip.tsx +23 -0
- package/dist/default/dashboard/app/lib/utils.ts +6 -0
- package/dist/default/dashboard/app/main.tsx +35 -0
- package/dist/default/dashboard/app/routeTree.gen.ts +352 -0
- package/dist/default/dashboard/app/routes/__root.tsx +10 -0
- package/dist/default/dashboard/app/routes/agents.tsx +255 -0
- package/dist/default/dashboard/app/routes/chat.tsx +427 -0
- package/dist/default/dashboard/app/routes/connections.tsx +413 -0
- package/dist/default/dashboard/app/routes/cron.tsx +322 -0
- package/dist/default/dashboard/app/routes/files.tsx +203 -0
- package/dist/default/dashboard/app/routes/index.tsx +141 -0
- package/dist/default/dashboard/app/routes/projects.tsx +254 -0
- package/dist/default/dashboard/app/routes/sessions.tsx +272 -0
- package/dist/default/dashboard/app/routes/settings.tsx +583 -0
- package/dist/default/dashboard/app/routes/skills.tsx +252 -0
- package/dist/default/dashboard/app/routes/usage.tsx +181 -0
- package/dist/default/dashboard/app/styles/globals.css +93 -0
- package/dist/default/dashboard/index.html +13 -0
- package/dist/default/dashboard/package.json +36 -0
- package/dist/default/dashboard/postcss.config.js +6 -0
- package/dist/default/dashboard/tailwind.config.js +50 -0
- package/dist/default/dashboard/tsconfig.json +24 -0
- package/dist/default/dashboard/vite.config.ts +16 -0
- package/dist/default/package.json +8 -3
- package/dist/default/skills/skill-creator/SKILL.md +270 -0
- package/dist/default/skills/skill-creator/config.json +11 -0
- package/dist/default/skills/skill-creator/index.ts +392 -0
- package/dist/default/src/agent.ts +85 -5
- package/dist/index.js +1574 -10
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/templates/default/.env.example +46 -6
- package/templates/default/README.md +89 -9
- package/templates/default/convex/schema.ts +248 -4
- package/templates/default/dashboard/app/components/DashboardLayout.tsx +245 -0
- package/templates/default/dashboard/app/components/ui/badge.tsx +26 -0
- package/templates/default/dashboard/app/components/ui/button.tsx +41 -0
- package/templates/default/dashboard/app/components/ui/card.tsx +44 -0
- package/templates/default/dashboard/app/components/ui/dialog.tsx +66 -0
- package/templates/default/dashboard/app/components/ui/input.tsx +21 -0
- package/templates/default/dashboard/app/components/ui/label.tsx +18 -0
- package/templates/default/dashboard/app/components/ui/select.tsx +75 -0
- package/templates/default/dashboard/app/components/ui/sheet.tsx +73 -0
- package/templates/default/dashboard/app/components/ui/switch.tsx +34 -0
- package/templates/default/dashboard/app/components/ui/table.tsx +60 -0
- package/templates/default/dashboard/app/components/ui/tabs.tsx +50 -0
- package/templates/default/dashboard/app/components/ui/tooltip.tsx +23 -0
- package/templates/default/dashboard/app/lib/utils.ts +6 -0
- package/templates/default/dashboard/app/main.tsx +35 -0
- package/templates/default/dashboard/app/routeTree.gen.ts +352 -0
- package/templates/default/dashboard/app/routes/__root.tsx +10 -0
- package/templates/default/dashboard/app/routes/agents.tsx +255 -0
- package/templates/default/dashboard/app/routes/chat.tsx +427 -0
- package/templates/default/dashboard/app/routes/connections.tsx +413 -0
- package/templates/default/dashboard/app/routes/cron.tsx +322 -0
- package/templates/default/dashboard/app/routes/files.tsx +203 -0
- package/templates/default/dashboard/app/routes/index.tsx +141 -0
- package/templates/default/dashboard/app/routes/projects.tsx +254 -0
- package/templates/default/dashboard/app/routes/sessions.tsx +272 -0
- package/templates/default/dashboard/app/routes/settings.tsx +583 -0
- package/templates/default/dashboard/app/routes/skills.tsx +252 -0
- package/templates/default/dashboard/app/routes/usage.tsx +181 -0
- package/templates/default/dashboard/app/styles/globals.css +93 -0
- package/templates/default/dashboard/index.html +13 -0
- package/templates/default/dashboard/package.json +36 -0
- package/templates/default/dashboard/postcss.config.js +6 -0
- package/templates/default/dashboard/tailwind.config.js +50 -0
- package/templates/default/dashboard/tsconfig.json +24 -0
- package/templates/default/dashboard/vite.config.ts +16 -0
- package/templates/default/package.json +8 -3
- package/templates/default/skills/skill-creator/SKILL.md +270 -0
- package/templates/default/skills/skill-creator/config.json +11 -0
- package/templates/default/skills/skill-creator/index.ts +392 -0
- package/templates/default/src/agent.ts +85 -5
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
import { createFileRoute } from '@tanstack/react-router';
|
|
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";
|
|
6
|
+
|
|
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
|
+
export const Route = createFileRoute('/settings')({ component: SettingsPage });
|
|
21
|
+
|
|
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');
|
|
46
|
+
|
|
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]);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const handleDeleteApiKey = (id: string) => {
|
|
59
|
+
// deleteApiKey({ id });
|
|
60
|
+
setApiKeys(apiKeys.filter(key => key.id !== id));
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const handleProviderToggle = (id: string, enabled: boolean) => {
|
|
64
|
+
// updateProvider({ id, enabled });
|
|
65
|
+
setProviderSettings(providerSettings.map(p => p.id === id ? { ...p, enabled } : p));
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const handleSetDefaultProvider = (name: string) => {
|
|
69
|
+
// updateSettings({ defaultProvider: name });
|
|
70
|
+
setDefaultProvider(name);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<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>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
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
|
+
};
|
|
143
|
+
|
|
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
|
+
};
|
|
151
|
+
|
|
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
|
+
}
|
|
175
|
+
|
|
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('');
|
|
180
|
+
|
|
181
|
+
const handleAdd = () => {
|
|
182
|
+
if (newKey.trim()) {
|
|
183
|
+
onAdd(newProvider, newKey.trim());
|
|
184
|
+
setNewKey('');
|
|
185
|
+
setIsModalOpen(false);
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
|
|
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">
|
|
202
|
+
<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>
|
|
221
|
+
</div>
|
|
222
|
+
<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" />
|
|
225
|
+
</div>
|
|
226
|
+
</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
|
+
</div>
|
|
243
|
+
<Button variant="destructive" onClick={() => onDelete(key.id)}><Trash2 className="w-4 h-4" /></Button>
|
|
244
|
+
</div>
|
|
245
|
+
)) : (
|
|
246
|
+
<p className="text-muted-foreground text-center py-4">No API keys added yet.</p>
|
|
247
|
+
)}
|
|
248
|
+
</div>
|
|
249
|
+
</Card>
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
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>
|
|
279
|
+
</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'}`} />
|
|
447
|
+
</div>
|
|
448
|
+
<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>}
|
|
458
|
+
</div>
|
|
459
|
+
</div>
|
|
460
|
+
</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" />
|
|
471
|
+
</button>
|
|
472
|
+
</div>
|
|
473
|
+
</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
|
+
</div>
|
|
496
|
+
</Card>
|
|
497
|
+
)}
|
|
498
|
+
|
|
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" />
|
|
510
|
+
</div>
|
|
511
|
+
<div className="grid grid-cols-2 gap-3">
|
|
512
|
+
<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>
|
|
520
|
+
</div>
|
|
521
|
+
<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" />
|
|
524
|
+
</div>
|
|
525
|
+
</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>
|
|
530
|
+
</div>
|
|
531
|
+
</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 & 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>
|
|
582
|
+
);
|
|
583
|
+
}
|