@agentforge-ai/cli 0.4.0 → 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 +11 -0
- 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 +5 -2
- package/dist/default/src/agent.ts +42 -2
- package/dist/index.js +135 -22
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/templates/default/.env.example +11 -0
- 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 +5 -2
- package/templates/default/src/agent.ts +42 -2
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { createFileRoute } from '@tanstack/react-router';
|
|
2
|
+
import { DashboardLayout } from '../components/DashboardLayout';
|
|
3
|
+
import { useState, useMemo } from 'react';
|
|
4
|
+
// import { useQuery, useMutation } from 'convex/react';
|
|
5
|
+
// import { api } from '../../convex/_generated/api';
|
|
6
|
+
import { Sparkles, Download, Trash2, Plus, Search, Filter, X } from 'lucide-react';
|
|
7
|
+
|
|
8
|
+
// --- Mock Data and Types ---
|
|
9
|
+
type SkillCategory = 'Tools' | 'Knowledge' | 'Workflows' | 'Integrations';
|
|
10
|
+
|
|
11
|
+
interface Skill {
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
description: string;
|
|
15
|
+
category: SkillCategory;
|
|
16
|
+
version: string;
|
|
17
|
+
isInstalled: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const mockSkills: Skill[] = [
|
|
21
|
+
{ id: '1', name: 'Web Scraper', description: 'Extracts data from websites using CSS selectors.', category: 'Tools', version: '1.2.0', isInstalled: true },
|
|
22
|
+
{ id: '2', name: 'Sentiment Analysis', description: 'Analyzes text to determine emotional tone.', category: 'Knowledge', version: '2.0.1', isInstalled: false },
|
|
23
|
+
{ id: '3', name: 'Daily Report', description: 'Generates a daily summary of activities.', category: 'Workflows', version: '1.0.0', isInstalled: true },
|
|
24
|
+
{ id: '4', name: 'GitHub Integration', description: 'Connects to GitHub to manage repositories.', category: 'Integrations', version: '1.5.3', isInstalled: false },
|
|
25
|
+
{ id: '5', name: 'Image Resizer', description: 'Resizes images to specified dimensions.', category: 'Tools', version: '1.1.0', isInstalled: false },
|
|
26
|
+
{ id: '6', name: 'Company Financials', description: 'Provides financial data for public companies.', category: 'Knowledge', version: '3.1.0', isInstalled: true },
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const CATEGORIES: SkillCategory[] = ['Tools', 'Knowledge', 'Workflows', 'Integrations'];
|
|
30
|
+
|
|
31
|
+
// --- Components ---
|
|
32
|
+
|
|
33
|
+
function CreateSkillDialog({ open, onOpenChange, onCreateSkill }: { open: boolean; onOpenChange: (open: boolean) => void; onCreateSkill: (skill: Omit<Skill, 'id' | 'isInstalled'>) => void; }) {
|
|
34
|
+
const [name, setName] = useState('');
|
|
35
|
+
const [description, setDescription] = useState('');
|
|
36
|
+
const [category, setCategory] = useState<SkillCategory>('Tools');
|
|
37
|
+
const [version, setVersion] = useState('1.0.0');
|
|
38
|
+
|
|
39
|
+
const handleSubmit = (e: React.FormEvent) => {
|
|
40
|
+
e.preventDefault();
|
|
41
|
+
if (!name || !description || !version) return;
|
|
42
|
+
onCreateSkill({ name, description, category, version });
|
|
43
|
+
onOpenChange(false);
|
|
44
|
+
setName('');
|
|
45
|
+
setDescription('');
|
|
46
|
+
setCategory('Tools');
|
|
47
|
+
setVersion('1.0.0');
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
if (!open) return null;
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div className="fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4">
|
|
54
|
+
<div className="bg-card p-6 rounded-lg shadow-xl w-full max-w-md border border-border" onClick={(e) => e.stopPropagation()}>
|
|
55
|
+
<div className="flex justify-between items-center mb-4">
|
|
56
|
+
<h2 className="text-xl font-semibold text-foreground">Create Custom Skill</h2>
|
|
57
|
+
<button onClick={() => onOpenChange(false)} className="text-muted-foreground hover:text-foreground"><X size={20} /></button>
|
|
58
|
+
</div>
|
|
59
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
60
|
+
<div>
|
|
61
|
+
<label htmlFor="name" className="block text-sm font-medium text-muted-foreground mb-1">Skill Name</label>
|
|
62
|
+
<input id="name" value={name} onChange={(e) => setName(e.target.value)} required className="w-full bg-background border border-border rounded-md px-3 py-2 text-foreground focus:outline-none focus:ring-2 focus:ring-primary" />
|
|
63
|
+
</div>
|
|
64
|
+
<div>
|
|
65
|
+
<label htmlFor="description" className="block text-sm font-medium text-muted-foreground mb-1">Description</label>
|
|
66
|
+
<textarea id="description" value={description} onChange={(e) => setDescription(e.target.value)} required rows={3} className="w-full bg-background border border-border rounded-md px-3 py-2 text-foreground focus:outline-none focus:ring-2 focus:ring-primary"></textarea>
|
|
67
|
+
</div>
|
|
68
|
+
<div className="grid grid-cols-2 gap-4">
|
|
69
|
+
<div>
|
|
70
|
+
<label htmlFor="category" className="block text-sm font-medium text-muted-foreground mb-1">Category</label>
|
|
71
|
+
<select id="category" value={category} onChange={(e) => setCategory(e.target.value as SkillCategory)} className="w-full bg-background border border-border rounded-md px-3 py-2 text-foreground focus:outline-none focus:ring-2 focus:ring-primary">
|
|
72
|
+
{CATEGORIES.map(cat => <option key={cat} value={cat}>{cat}</option>)}
|
|
73
|
+
</select>
|
|
74
|
+
</div>
|
|
75
|
+
<div>
|
|
76
|
+
<label htmlFor="version" className="block text-sm font-medium text-muted-foreground mb-1">Version</label>
|
|
77
|
+
<input id="version" value={version} onChange={(e) => setVersion(e.target.value)} required className="w-full bg-background border border-border rounded-md px-3 py-2 text-foreground focus:outline-none focus:ring-2 focus:ring-primary" />
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
<div className="flex justify-end gap-2 pt-4">
|
|
81
|
+
<button type="button" onClick={() => onOpenChange(false)} className="px-4 py-2 rounded-md border border-border text-foreground hover:bg-zinc-800 transition-colors">Cancel</button>
|
|
82
|
+
<button type="submit" className="px-4 py-2 rounded-md bg-primary text-primary-foreground hover:bg-primary/90 transition-colors">Create Skill</button>
|
|
83
|
+
</div>
|
|
84
|
+
</form>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function SkillCard({ skill, onToggleInstall }: { skill: Skill; onToggleInstall: (id: string) => void; }) {
|
|
91
|
+
// const uninstallSkill = useMutation(api.skills.uninstall);
|
|
92
|
+
// const installSkill = useMutation(api.skills.install);
|
|
93
|
+
|
|
94
|
+
const handleToggle = () => {
|
|
95
|
+
onToggleInstall(skill.id);
|
|
96
|
+
// if (skill.isInstalled) {
|
|
97
|
+
// uninstallSkill({ id: skill.id });
|
|
98
|
+
// } else {
|
|
99
|
+
// installSkill({ id: skill.id });
|
|
100
|
+
// }
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<div className="bg-card border border-border rounded-lg p-4 flex flex-col h-full transition-shadow hover:shadow-lg hover:shadow-primary/10">
|
|
105
|
+
<div className="flex-grow">
|
|
106
|
+
<div className="flex items-start justify-between mb-2">
|
|
107
|
+
<div className="flex items-center gap-3">
|
|
108
|
+
<div className="bg-primary/10 p-2 rounded-md">
|
|
109
|
+
<Sparkles className="text-primary" size={20} />
|
|
110
|
+
</div>
|
|
111
|
+
<div>
|
|
112
|
+
<h3 className="font-semibold text-foreground">{skill.name}</h3>
|
|
113
|
+
<p className="text-xs text-muted-foreground">v{skill.version}</p>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
<span className="text-xs bg-zinc-800 border border-zinc-700 text-muted-foreground px-2 py-0.5 rounded-full whitespace-nowrap">{skill.category}</span>
|
|
117
|
+
</div>
|
|
118
|
+
<p className="text-sm text-muted-foreground mt-3">
|
|
119
|
+
{skill.description}
|
|
120
|
+
</p>
|
|
121
|
+
</div>
|
|
122
|
+
<button
|
|
123
|
+
onClick={handleToggle}
|
|
124
|
+
className={`mt-4 w-full flex items-center justify-center gap-2 px-4 py-2 rounded-md text-sm font-medium transition-colors ${skill.isInstalled ? 'bg-destructive/10 text-destructive hover:bg-destructive/20' : 'bg-primary/10 text-primary hover:bg-primary/20'}`}>
|
|
125
|
+
{skill.isInstalled ? <Trash2 size={16} /> : <Download size={16} />}
|
|
126
|
+
{skill.isInstalled ? 'Uninstall' : 'Install'}
|
|
127
|
+
</button>
|
|
128
|
+
</div>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export const Route = createFileRoute('/skills')({ component: SkillsPage });
|
|
133
|
+
|
|
134
|
+
function SkillsPage() {
|
|
135
|
+
const [skills, setSkills] = useState(mockSkills);
|
|
136
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
137
|
+
const [selectedCategories, setSelectedCategories] = useState<SkillCategory[]>([]);
|
|
138
|
+
const [isCreateDialogOpen, setCreateDialogOpen] = useState(false);
|
|
139
|
+
|
|
140
|
+
// const allSkillsQuery = useQuery(api.skills.list);
|
|
141
|
+
// const createSkillMutation = useMutation(api.skills.create);
|
|
142
|
+
// const isLoading = allSkillsQuery === undefined;
|
|
143
|
+
// const skillsData = allSkillsQuery || [];
|
|
144
|
+
|
|
145
|
+
const handleToggleCategory = (category: SkillCategory) => {
|
|
146
|
+
setSelectedCategories(prev =>
|
|
147
|
+
prev.includes(category)
|
|
148
|
+
? prev.filter(c => c !== category)
|
|
149
|
+
: [...prev, category]
|
|
150
|
+
);
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const handleCreateSkill = (newSkillData: Omit<Skill, 'id' | 'isInstalled'>) => {
|
|
154
|
+
const newSkill: Skill = {
|
|
155
|
+
...newSkillData,
|
|
156
|
+
id: `custom-${Date.now()}`,
|
|
157
|
+
isInstalled: false,
|
|
158
|
+
};
|
|
159
|
+
setSkills(prev => [newSkill, ...prev]);
|
|
160
|
+
// createSkillMutation(newSkillData).catch(console.error);
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const handleToggleInstall = (id: string) => {
|
|
164
|
+
setSkills(prev => prev.map(s => s.id === id ? { ...s, isInstalled: !s.isInstalled } : s));
|
|
165
|
+
// Note: The actual mutation would be called in SkillCard
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const filteredSkills = useMemo(() => {
|
|
169
|
+
// Replace `skills` with `skillsData` when using Convex
|
|
170
|
+
return skills.filter(skill =>
|
|
171
|
+
skill.name.toLowerCase().includes(searchQuery.toLowerCase()) &&
|
|
172
|
+
(selectedCategories.length === 0 || selectedCategories.includes(skill.category))
|
|
173
|
+
);
|
|
174
|
+
}, [skills, searchQuery, selectedCategories]); // Replace `skills` with `skillsData`
|
|
175
|
+
|
|
176
|
+
const isLoading = false; // When using Convex, this would be `allSkillsQuery === undefined`
|
|
177
|
+
|
|
178
|
+
return (
|
|
179
|
+
<DashboardLayout>
|
|
180
|
+
<div className="p-4 sm:p-6 lg:p-8 bg-background text-foreground min-h-screen">
|
|
181
|
+
<header className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 mb-6">
|
|
182
|
+
<div>
|
|
183
|
+
<h1 className="text-2xl font-bold tracking-tight">Skills Marketplace</h1>
|
|
184
|
+
<p className="text-muted-foreground">Discover, install, and create new capabilities for your agents.</p>
|
|
185
|
+
</div>
|
|
186
|
+
<button onClick={() => setCreateDialogOpen(true)} className="flex items-center justify-center gap-2 px-4 py-2 rounded-md text-sm font-medium bg-primary text-primary-foreground hover:bg-primary/90 transition-colors">
|
|
187
|
+
<Plus size={16} /> Create Skill
|
|
188
|
+
</button>
|
|
189
|
+
</header>
|
|
190
|
+
|
|
191
|
+
<div className="mb-6 space-y-4">
|
|
192
|
+
<div className="relative">
|
|
193
|
+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" size={20} />
|
|
194
|
+
<input
|
|
195
|
+
type="text"
|
|
196
|
+
placeholder="Search skills by name..."
|
|
197
|
+
value={searchQuery}
|
|
198
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
199
|
+
className="w-full bg-card border border-border rounded-md pl-10 pr-4 py-2 focus:outline-none focus:ring-2 focus:ring-primary"
|
|
200
|
+
/>
|
|
201
|
+
</div>
|
|
202
|
+
<div className="flex items-center gap-2 flex-wrap">
|
|
203
|
+
<Filter size={16} className="text-muted-foreground" />
|
|
204
|
+
<span className="text-sm font-medium text-muted-foreground">Filter by:</span>
|
|
205
|
+
{CATEGORIES.map(category => (
|
|
206
|
+
<button
|
|
207
|
+
key={category}
|
|
208
|
+
onClick={() => handleToggleCategory(category)}
|
|
209
|
+
className={`px-3 py-1 text-sm rounded-full border transition-colors ${selectedCategories.includes(category) ? 'bg-primary text-primary-foreground border-primary' : 'bg-card hover:bg-zinc-800 border-border'}`}>
|
|
210
|
+
{category}
|
|
211
|
+
</button>
|
|
212
|
+
))}
|
|
213
|
+
{selectedCategories.length > 0 && (
|
|
214
|
+
<button onClick={() => setSelectedCategories([])} className="text-sm text-muted-foreground hover:text-foreground underline">Clear</button>
|
|
215
|
+
)}
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
|
|
219
|
+
{isLoading ? (
|
|
220
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
|
221
|
+
{[...Array(8)].map((_, i) => (
|
|
222
|
+
<div key={i} className="bg-card border border-border rounded-lg p-4 animate-pulse">
|
|
223
|
+
<div className="h-8 bg-zinc-800 rounded w-12 mb-3"></div>
|
|
224
|
+
<div className="h-5 bg-zinc-800 rounded w-3/4 mb-2"></div>
|
|
225
|
+
<div className="h-4 bg-zinc-800 rounded w-1/4 mb-4"></div>
|
|
226
|
+
<div className="h-4 bg-zinc-800 rounded w-full mb-1"></div>
|
|
227
|
+
<div className="h-4 bg-zinc-800 rounded w-5/6"></div>
|
|
228
|
+
<div className="h-10 bg-zinc-800 rounded mt-4"></div>
|
|
229
|
+
</div>
|
|
230
|
+
))}
|
|
231
|
+
</div>
|
|
232
|
+
) : filteredSkills.length > 0 ? (
|
|
233
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
|
234
|
+
{filteredSkills.map(skill => (
|
|
235
|
+
<SkillCard key={skill.id} skill={skill} onToggleInstall={handleToggleInstall} />
|
|
236
|
+
))}
|
|
237
|
+
</div>
|
|
238
|
+
) : (
|
|
239
|
+
<div className="text-center py-16 border-2 border-dashed border-border rounded-lg bg-card">
|
|
240
|
+
<Sparkles className="mx-auto h-12 w-12 text-muted-foreground" />
|
|
241
|
+
<h3 className="mt-2 text-lg font-medium text-foreground">No Skills Found</h3>
|
|
242
|
+
<p className="mt-1 text-sm text-muted-foreground">Your search or filter criteria did not match any skills. Try a different query.</p>
|
|
243
|
+
<button onClick={() => { setSearchQuery(''); setSelectedCategories([]); }} className="mt-4 px-4 py-2 rounded-md text-sm font-medium bg-primary text-primary-foreground hover:bg-primary/90 transition-colors">
|
|
244
|
+
Clear Filters
|
|
245
|
+
</button>
|
|
246
|
+
</div>
|
|
247
|
+
)}
|
|
248
|
+
</div>
|
|
249
|
+
<CreateSkillDialog open={isCreateDialogOpen} onOpenChange={setCreateDialogOpen} onCreateSkill={handleCreateSkill} />
|
|
250
|
+
</DashboardLayout>
|
|
251
|
+
);
|
|
252
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
+
import { DashboardLayout } from "../components/DashboardLayout";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { BarChart3, DollarSign, Cpu, Activity, TrendingUp, Calendar, Bot, Zap } from "lucide-react";
|
|
5
|
+
|
|
6
|
+
export const Route = createFileRoute("/usage")({ component: UsagePage });
|
|
7
|
+
|
|
8
|
+
interface UsageRecord {
|
|
9
|
+
id: string; agentName: string; provider: string; model: string;
|
|
10
|
+
promptTokens: number; completionTokens: number; totalTokens: number;
|
|
11
|
+
cost: number; timestamp: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function UsagePage() {
|
|
15
|
+
const [dateRange, setDateRange] = useState<"7d" | "30d" | "90d">("30d");
|
|
16
|
+
|
|
17
|
+
const [records] = useState<UsageRecord[]>([
|
|
18
|
+
{ id: "u1", agentName: "Customer Support", provider: "openai", model: "gpt-4o", promptTokens: 12500, completionTokens: 8200, totalTokens: 20700, cost: 0.62, timestamp: Date.now() - 3600000 },
|
|
19
|
+
{ id: "u2", agentName: "Code Review", provider: "anthropic", model: "claude-3.5-sonnet", promptTokens: 45000, completionTokens: 15000, totalTokens: 60000, cost: 1.35, timestamp: Date.now() - 7200000 },
|
|
20
|
+
{ id: "u3", agentName: "Data Analyst", provider: "openrouter", model: "mixtral-8x7b", promptTokens: 8000, completionTokens: 4500, totalTokens: 12500, cost: 0.08, timestamp: Date.now() - 14400000 },
|
|
21
|
+
{ id: "u4", agentName: "Customer Support", provider: "openai", model: "gpt-4o-mini", promptTokens: 6000, completionTokens: 3200, totalTokens: 9200, cost: 0.09, timestamp: Date.now() - 28800000 },
|
|
22
|
+
{ id: "u5", agentName: "Research Agent", provider: "google", model: "gemini-pro", promptTokens: 22000, completionTokens: 11000, totalTokens: 33000, cost: 0.17, timestamp: Date.now() - 43200000 },
|
|
23
|
+
{ id: "u6", agentName: "Code Review", provider: "anthropic", model: "claude-3.5-sonnet", promptTokens: 38000, completionTokens: 12000, totalTokens: 50000, cost: 1.13, timestamp: Date.now() - 86400000 },
|
|
24
|
+
{ id: "u7", agentName: "Writer Agent", provider: "openai", model: "gpt-4o", promptTokens: 15000, completionTokens: 20000, totalTokens: 35000, cost: 1.05, timestamp: Date.now() - 172800000 },
|
|
25
|
+
{ id: "u8", agentName: "Data Analyst", provider: "xai", model: "grok-2", promptTokens: 10000, completionTokens: 5000, totalTokens: 15000, cost: 0.30, timestamp: Date.now() - 259200000 },
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
const totalTokens = records.reduce((s, r) => s + r.totalTokens, 0);
|
|
29
|
+
const totalCost = records.reduce((s, r) => s + r.cost, 0);
|
|
30
|
+
const uniqueAgents = new Set(records.map((r) => r.agentName)).size;
|
|
31
|
+
const totalSessions = records.length;
|
|
32
|
+
|
|
33
|
+
// Cost by provider
|
|
34
|
+
const costByProvider: Record<string, number> = {};
|
|
35
|
+
records.forEach((r) => { costByProvider[r.provider] = (costByProvider[r.provider] || 0) + r.cost; });
|
|
36
|
+
const maxProviderCost = Math.max(...Object.values(costByProvider), 1);
|
|
37
|
+
const providerColors: Record<string, string> = { openai: "bg-green-500", anthropic: "bg-orange-500", openrouter: "bg-blue-500", google: "bg-yellow-500", xai: "bg-purple-500" };
|
|
38
|
+
|
|
39
|
+
// Top agents by usage
|
|
40
|
+
const agentUsage: Record<string, { tokens: number; cost: number }> = {};
|
|
41
|
+
records.forEach((r) => {
|
|
42
|
+
if (!agentUsage[r.agentName]) agentUsage[r.agentName] = { tokens: 0, cost: 0 };
|
|
43
|
+
agentUsage[r.agentName].tokens += r.totalTokens;
|
|
44
|
+
agentUsage[r.agentName].cost += r.cost;
|
|
45
|
+
});
|
|
46
|
+
const topAgents = Object.entries(agentUsage).sort((a, b) => b[1].tokens - a[1].tokens);
|
|
47
|
+
const maxAgentTokens = topAgents.length > 0 ? topAgents[0][1].tokens : 1;
|
|
48
|
+
|
|
49
|
+
// Tokens over time (last 7 days)
|
|
50
|
+
const days = Array.from({ length: 7 }, (_, i) => {
|
|
51
|
+
const d = new Date(); d.setDate(d.getDate() - (6 - i));
|
|
52
|
+
return { label: d.toLocaleDateString("en-US", { weekday: "short" }), date: d.toDateString() };
|
|
53
|
+
});
|
|
54
|
+
const tokensByDay = days.map((day) => {
|
|
55
|
+
const dayTokens = records.filter((r) => new Date(r.timestamp).toDateString() === day.date).reduce((s, r) => s + r.totalTokens, 0);
|
|
56
|
+
return { ...day, tokens: dayTokens };
|
|
57
|
+
});
|
|
58
|
+
const maxDayTokens = Math.max(...tokensByDay.map((d) => d.tokens), 1);
|
|
59
|
+
|
|
60
|
+
const formatTokens = (n: number) => {
|
|
61
|
+
if (n >= 1000000) return (n / 1000000).toFixed(1) + "M";
|
|
62
|
+
if (n >= 1000) return (n / 1000).toFixed(1) + "K";
|
|
63
|
+
return n.toString();
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<DashboardLayout>
|
|
68
|
+
<div className="space-y-6">
|
|
69
|
+
<div className="flex items-center justify-between">
|
|
70
|
+
<div><h1 className="text-3xl font-bold">Usage & Metrics</h1><p className="text-muted-foreground mt-1">Monitor token usage, costs, and agent performance</p></div>
|
|
71
|
+
<div className="flex items-center gap-1 bg-card border rounded-lg p-1">
|
|
72
|
+
{(["7d", "30d", "90d"] as const).map((range) => (
|
|
73
|
+
<button key={range} onClick={() => setDateRange(range)} className={`px-3 py-1.5 text-sm rounded-md transition-colors ${dateRange === range ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:text-foreground"}`}>
|
|
74
|
+
{range === "7d" ? "7 Days" : range === "30d" ? "30 Days" : "90 Days"}
|
|
75
|
+
</button>
|
|
76
|
+
))}
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
{/* Summary Cards */}
|
|
81
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
82
|
+
{[
|
|
83
|
+
{ label: "Total Tokens", value: formatTokens(totalTokens), icon: Zap, color: "text-blue-400", sub: `${records.length} requests` },
|
|
84
|
+
{ label: "Total Cost", value: `$${totalCost.toFixed(2)}`, icon: DollarSign, color: "text-green-400", sub: `Avg $${(totalCost / Math.max(records.length, 1)).toFixed(3)}/req` },
|
|
85
|
+
{ label: "Active Agents", value: uniqueAgents.toString(), icon: Bot, color: "text-purple-400", sub: "Using LLM providers" },
|
|
86
|
+
{ label: "Total Requests", value: totalSessions.toString(), icon: Activity, color: "text-orange-400", sub: "In selected period" },
|
|
87
|
+
].map((stat) => (
|
|
88
|
+
<div key={stat.label} className="bg-card border rounded-lg p-5">
|
|
89
|
+
<div className="flex items-center justify-between mb-3">
|
|
90
|
+
<span className="text-sm text-muted-foreground">{stat.label}</span>
|
|
91
|
+
<stat.icon className={`h-5 w-5 ${stat.color}`} />
|
|
92
|
+
</div>
|
|
93
|
+
<p className="text-2xl font-bold">{stat.value}</p>
|
|
94
|
+
<p className="text-xs text-muted-foreground mt-1">{stat.sub}</p>
|
|
95
|
+
</div>
|
|
96
|
+
))}
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
{/* Charts Row */}
|
|
100
|
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
101
|
+
{/* Tokens Over Time */}
|
|
102
|
+
<div className="bg-card border rounded-lg p-5">
|
|
103
|
+
<h3 className="text-sm font-medium mb-4 flex items-center gap-2"><BarChart3 className="h-4 w-4 text-muted-foreground" />Tokens Over Time</h3>
|
|
104
|
+
<div className="flex items-end gap-2 h-40">
|
|
105
|
+
{tokensByDay.map((day) => (
|
|
106
|
+
<div key={day.label} className="flex-1 flex flex-col items-center gap-1">
|
|
107
|
+
<span className="text-xs text-muted-foreground">{day.tokens > 0 ? formatTokens(day.tokens) : ""}</span>
|
|
108
|
+
<div className="w-full bg-primary/20 rounded-t-sm relative" style={{ height: `${Math.max((day.tokens / maxDayTokens) * 120, 4)}px` }}>
|
|
109
|
+
<div className="absolute inset-0 bg-primary rounded-t-sm" style={{ height: "100%" }} />
|
|
110
|
+
</div>
|
|
111
|
+
<span className="text-xs text-muted-foreground">{day.label}</span>
|
|
112
|
+
</div>
|
|
113
|
+
))}
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
{/* Cost by Provider */}
|
|
118
|
+
<div className="bg-card border rounded-lg p-5">
|
|
119
|
+
<h3 className="text-sm font-medium mb-4 flex items-center gap-2"><DollarSign className="h-4 w-4 text-muted-foreground" />Cost by Provider</h3>
|
|
120
|
+
<div className="space-y-3">
|
|
121
|
+
{Object.entries(costByProvider).sort((a, b) => b[1] - a[1]).map(([provider, cost]) => (
|
|
122
|
+
<div key={provider} className="space-y-1">
|
|
123
|
+
<div className="flex items-center justify-between text-sm">
|
|
124
|
+
<span className="capitalize">{provider}</span>
|
|
125
|
+
<span className="font-medium">${cost.toFixed(2)}</span>
|
|
126
|
+
</div>
|
|
127
|
+
<div className="w-full h-2 bg-accent rounded-full overflow-hidden">
|
|
128
|
+
<div className={`h-full rounded-full ${providerColors[provider] || "bg-primary"}`} style={{ width: `${(cost / maxProviderCost) * 100}%` }} />
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
))}
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
{/* Top Agents */}
|
|
137
|
+
<div className="bg-card border rounded-lg p-5">
|
|
138
|
+
<h3 className="text-sm font-medium mb-4 flex items-center gap-2"><TrendingUp className="h-4 w-4 text-muted-foreground" />Top Agents by Usage</h3>
|
|
139
|
+
<div className="space-y-3">
|
|
140
|
+
{topAgents.map(([name, data], i) => (
|
|
141
|
+
<div key={name} className="flex items-center gap-4">
|
|
142
|
+
<span className="text-sm text-muted-foreground w-6 text-right">#{i + 1}</span>
|
|
143
|
+
<div className="flex-1">
|
|
144
|
+
<div className="flex items-center justify-between text-sm mb-1">
|
|
145
|
+
<span className="font-medium">{name}</span>
|
|
146
|
+
<span className="text-muted-foreground">{formatTokens(data.tokens)} tokens · ${data.cost.toFixed(2)}</span>
|
|
147
|
+
</div>
|
|
148
|
+
<div className="w-full h-1.5 bg-accent rounded-full overflow-hidden">
|
|
149
|
+
<div className="h-full bg-primary rounded-full" style={{ width: `${(data.tokens / maxAgentTokens) * 100}%` }} />
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
))}
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
{/* Recent Usage Records */}
|
|
158
|
+
<div className="bg-card border rounded-lg overflow-hidden">
|
|
159
|
+
<div className="px-5 py-4 border-b"><h3 className="text-sm font-medium flex items-center gap-2"><Calendar className="h-4 w-4 text-muted-foreground" />Recent Usage Records</h3></div>
|
|
160
|
+
<table className="w-full text-sm">
|
|
161
|
+
<thead><tr className="border-b text-muted-foreground text-left"><th className="px-5 py-3 font-medium">Agent</th><th className="px-5 py-3 font-medium">Provider</th><th className="px-5 py-3 font-medium">Model</th><th className="px-5 py-3 font-medium text-right">Prompt</th><th className="px-5 py-3 font-medium text-right">Completion</th><th className="px-5 py-3 font-medium text-right">Total</th><th className="px-5 py-3 font-medium text-right">Cost</th><th className="px-5 py-3 font-medium">Time</th></tr></thead>
|
|
162
|
+
<tbody>
|
|
163
|
+
{records.map((r) => (
|
|
164
|
+
<tr key={r.id} className="border-b hover:bg-accent/50">
|
|
165
|
+
<td className="px-5 py-3 font-medium">{r.agentName}</td>
|
|
166
|
+
<td className="px-5 py-3"><span className="capitalize px-2 py-0.5 bg-accent rounded text-xs">{r.provider}</span></td>
|
|
167
|
+
<td className="px-5 py-3 text-muted-foreground">{r.model}</td>
|
|
168
|
+
<td className="px-5 py-3 text-right text-muted-foreground">{r.promptTokens.toLocaleString()}</td>
|
|
169
|
+
<td className="px-5 py-3 text-right text-muted-foreground">{r.completionTokens.toLocaleString()}</td>
|
|
170
|
+
<td className="px-5 py-3 text-right font-medium">{r.totalTokens.toLocaleString()}</td>
|
|
171
|
+
<td className="px-5 py-3 text-right font-medium text-green-400">${r.cost.toFixed(3)}</td>
|
|
172
|
+
<td className="px-5 py-3 text-muted-foreground">{new Date(r.timestamp).toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit" })}</td>
|
|
173
|
+
</tr>
|
|
174
|
+
))}
|
|
175
|
+
</tbody>
|
|
176
|
+
</table>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
</DashboardLayout>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
@tailwind base;
|
|
2
|
+
@tailwind components;
|
|
3
|
+
@tailwind utilities;
|
|
4
|
+
|
|
5
|
+
@layer base {
|
|
6
|
+
/* Default to dark theme (OpenClaw-style) */
|
|
7
|
+
:root {
|
|
8
|
+
--background: 240 10% 3.9%;
|
|
9
|
+
--foreground: 0 0% 98%;
|
|
10
|
+
--card: 240 10% 5.9%;
|
|
11
|
+
--card-foreground: 0 0% 98%;
|
|
12
|
+
--popover: 240 10% 5.9%;
|
|
13
|
+
--popover-foreground: 0 0% 98%;
|
|
14
|
+
--primary: 0 72.2% 50.6%;
|
|
15
|
+
--primary-foreground: 0 0% 98%;
|
|
16
|
+
--secondary: 240 3.7% 15.9%;
|
|
17
|
+
--secondary-foreground: 0 0% 98%;
|
|
18
|
+
--muted: 240 3.7% 15.9%;
|
|
19
|
+
--muted-foreground: 240 5% 64.9%;
|
|
20
|
+
--accent: 240 3.7% 15.9%;
|
|
21
|
+
--accent-foreground: 0 0% 98%;
|
|
22
|
+
--destructive: 0 62.8% 30.6%;
|
|
23
|
+
--destructive-foreground: 0 0% 98%;
|
|
24
|
+
--border: 240 3.7% 15.9%;
|
|
25
|
+
--input: 240 3.7% 15.9%;
|
|
26
|
+
--ring: 0 72.2% 50.6%;
|
|
27
|
+
--radius: 0.5rem;
|
|
28
|
+
--sidebar: 240 10% 4.5%;
|
|
29
|
+
--sidebar-foreground: 240 5% 64.9%;
|
|
30
|
+
--sidebar-active: 0 72.2% 50.6%;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.light {
|
|
34
|
+
--background: 0 0% 100%;
|
|
35
|
+
--foreground: 240 10% 3.9%;
|
|
36
|
+
--card: 0 0% 100%;
|
|
37
|
+
--card-foreground: 240 10% 3.9%;
|
|
38
|
+
--popover: 0 0% 100%;
|
|
39
|
+
--popover-foreground: 240 10% 3.9%;
|
|
40
|
+
--primary: 0 72.2% 50.6%;
|
|
41
|
+
--primary-foreground: 0 0% 98%;
|
|
42
|
+
--secondary: 240 4.8% 95.9%;
|
|
43
|
+
--secondary-foreground: 240 5.9% 10%;
|
|
44
|
+
--muted: 240 4.8% 95.9%;
|
|
45
|
+
--muted-foreground: 240 3.8% 46.1%;
|
|
46
|
+
--accent: 240 4.8% 95.9%;
|
|
47
|
+
--accent-foreground: 240 5.9% 10%;
|
|
48
|
+
--destructive: 0 84.2% 60.2%;
|
|
49
|
+
--destructive-foreground: 0 0% 98%;
|
|
50
|
+
--border: 240 5.9% 90%;
|
|
51
|
+
--input: 240 5.9% 90%;
|
|
52
|
+
--ring: 0 72.2% 50.6%;
|
|
53
|
+
--sidebar: 0 0% 98%;
|
|
54
|
+
--sidebar-foreground: 240 3.8% 46.1%;
|
|
55
|
+
--sidebar-active: 0 72.2% 50.6%;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@layer base {
|
|
60
|
+
* {
|
|
61
|
+
@apply border-border;
|
|
62
|
+
}
|
|
63
|
+
html {
|
|
64
|
+
@apply antialiased;
|
|
65
|
+
color-scheme: dark;
|
|
66
|
+
}
|
|
67
|
+
body {
|
|
68
|
+
@apply bg-background text-foreground;
|
|
69
|
+
font-feature-settings: "rlig" 1, "calt" 1;
|
|
70
|
+
}
|
|
71
|
+
/* Custom scrollbar */
|
|
72
|
+
::-webkit-scrollbar {
|
|
73
|
+
width: 6px;
|
|
74
|
+
height: 6px;
|
|
75
|
+
}
|
|
76
|
+
::-webkit-scrollbar-track {
|
|
77
|
+
background: transparent;
|
|
78
|
+
}
|
|
79
|
+
::-webkit-scrollbar-thumb {
|
|
80
|
+
background: hsl(var(--muted));
|
|
81
|
+
border-radius: 3px;
|
|
82
|
+
}
|
|
83
|
+
::-webkit-scrollbar-thumb:hover {
|
|
84
|
+
background: hsl(var(--muted-foreground));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
@layer utilities {
|
|
89
|
+
.sidebar-bg {
|
|
90
|
+
background-color: hsl(var(--sidebar));
|
|
91
|
+
color: hsl(var(--sidebar-foreground));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>AgentForge Dashboard</title>
|
|
7
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="root"></div>
|
|
11
|
+
<script type="module" src="/app/main.tsx"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agentforge-dashboard",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite --host",
|
|
8
|
+
"build": "vite build",
|
|
9
|
+
"preview": "vite preview"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@radix-ui/react-dialog": "^1.1.0",
|
|
13
|
+
"@radix-ui/react-select": "^2.1.0",
|
|
14
|
+
"@radix-ui/react-switch": "^1.1.0",
|
|
15
|
+
"@radix-ui/react-tabs": "^1.1.0",
|
|
16
|
+
"@radix-ui/react-tooltip": "^1.1.0",
|
|
17
|
+
"@tanstack/react-router": "^1.120.20",
|
|
18
|
+
"clsx": "^2.1.0",
|
|
19
|
+
"convex": "^1.17.0",
|
|
20
|
+
"lucide-react": "^0.400.0",
|
|
21
|
+
"react": "^18.3.0",
|
|
22
|
+
"react-dom": "^18.3.0",
|
|
23
|
+
"tailwind-merge": "^2.3.0"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/react": "^18.3.0",
|
|
27
|
+
"@types/react-dom": "^18.3.0",
|
|
28
|
+
"@vitejs/plugin-react": "^4.3.0",
|
|
29
|
+
"autoprefixer": "^10.4.0",
|
|
30
|
+
"postcss": "^8.4.0",
|
|
31
|
+
"tailwindcss": "^3.4.0",
|
|
32
|
+
"typescript": "^5.5.0",
|
|
33
|
+
"vite": "^6.0.0",
|
|
34
|
+
"vite-tsconfig-paths": "^5.1.0"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** @type {import('tailwindcss').Config} */
|
|
2
|
+
export default {
|
|
3
|
+
darkMode: ["class"],
|
|
4
|
+
content: ["./app/**/*.{ts,tsx}"],
|
|
5
|
+
theme: {
|
|
6
|
+
extend: {
|
|
7
|
+
colors: {
|
|
8
|
+
border: "hsl(var(--border))",
|
|
9
|
+
input: "hsl(var(--input))",
|
|
10
|
+
ring: "hsl(var(--ring))",
|
|
11
|
+
background: "hsl(var(--background))",
|
|
12
|
+
foreground: "hsl(var(--foreground))",
|
|
13
|
+
primary: {
|
|
14
|
+
DEFAULT: "hsl(var(--primary))",
|
|
15
|
+
foreground: "hsl(var(--primary-foreground))",
|
|
16
|
+
},
|
|
17
|
+
secondary: {
|
|
18
|
+
DEFAULT: "hsl(var(--secondary))",
|
|
19
|
+
foreground: "hsl(var(--secondary-foreground))",
|
|
20
|
+
},
|
|
21
|
+
destructive: {
|
|
22
|
+
DEFAULT: "hsl(var(--destructive))",
|
|
23
|
+
foreground: "hsl(var(--destructive-foreground))",
|
|
24
|
+
},
|
|
25
|
+
muted: {
|
|
26
|
+
DEFAULT: "hsl(var(--muted))",
|
|
27
|
+
foreground: "hsl(var(--muted-foreground))",
|
|
28
|
+
},
|
|
29
|
+
accent: {
|
|
30
|
+
DEFAULT: "hsl(var(--accent))",
|
|
31
|
+
foreground: "hsl(var(--accent-foreground))",
|
|
32
|
+
},
|
|
33
|
+
popover: {
|
|
34
|
+
DEFAULT: "hsl(var(--popover))",
|
|
35
|
+
foreground: "hsl(var(--popover-foreground))",
|
|
36
|
+
},
|
|
37
|
+
card: {
|
|
38
|
+
DEFAULT: "hsl(var(--card))",
|
|
39
|
+
foreground: "hsl(var(--card-foreground))",
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
borderRadius: {
|
|
43
|
+
lg: "var(--radius)",
|
|
44
|
+
md: "calc(var(--radius) - 2px)",
|
|
45
|
+
sm: "calc(var(--radius) - 4px)",
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
plugins: [require("tailwindcss-animate")],
|
|
50
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["ES2023", "DOM", "DOM.Iterable"],
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"moduleResolution": "bundler",
|
|
8
|
+
"allowImportingTsExtensions": true,
|
|
9
|
+
"resolveJsonModule": true,
|
|
10
|
+
"isolatedModules": true,
|
|
11
|
+
"noEmit": true,
|
|
12
|
+
"jsx": "react-jsx",
|
|
13
|
+
"strict": true,
|
|
14
|
+
"noUnusedLocals": false,
|
|
15
|
+
"noUnusedParameters": false,
|
|
16
|
+
"noFallthroughCasesInSwitch": true,
|
|
17
|
+
"baseUrl": ".",
|
|
18
|
+
"paths": {
|
|
19
|
+
"~/*": ["./app/*"]
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"include": ["app", "app.config.ts"],
|
|
23
|
+
"exclude": ["node_modules", "dist", ".vinxi"]
|
|
24
|
+
}
|